Skip to content

feat: Svelte native HMR compatibility for NativeScript 8 #85

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Feb 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion packages/svelte-hmr/lib/make-hot.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,9 @@ const renderApplyHmr = ({
emitCss,
imports = [
`import * as ${globalName} from ${JSON.stringify(hotApiImport)}`,
`import { adapter as ${importAdapterName} } from ${JSON.stringify(adapterImport)}`,
`import { adapter as ${importAdapterName} } from ${JSON.stringify(
adapterImport
)}`,
],
}) =>
// this silly formatting keeps all original characters in their position,
Expand Down
144 changes: 26 additions & 118 deletions packages/svelte-hmr/runtime/svelte-native/proxy-adapter-native.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,65 +47,11 @@ const getNavTransition = ({ transition }) => {
return transition ? { animated: true, transition } : { animated: false }
}

// copied from TNS FrameBase.replacePage
//
// it is not public but there is a comment in there indicating it is for
// HMR (probably their own core HMR though)
//
// NOTE this "worked" in TNS 5, but not anymore in TNS 6: updated version bellow
//
// eslint-disable-next-line no-unused-vars
const replacePage_tns5 = (frame, newPageElement, hotOptions) => {
const currentBackstackEntry = frame._currentEntry
frame.navigationType = 2
frame.performNavigation({
isBackNavigation: false,
entry: {
resolvedPage: newPageElement.nativeView,
//
// entry: currentBackstackEntry.entry,
entry: Object.assign(
currentBackstackEntry.entry,
getNavTransition(hotOptions)
),
navDepth: currentBackstackEntry.navDepth,
fragmentTag: currentBackstackEntry.fragmentTag,
frameId: currentBackstackEntry.frameId,
},
})
}

// Updated for TNS v6
//
// https://github.com/NativeScript/NativeScript/blob/6.1.1/tns-core-modules/ui/frame/frame-common.ts#L656
const replacePage = (frame, newPageElement) => {
const currentBackstackEntry = frame._currentEntry
const newPage = newPageElement.nativeView
const newBackstackEntry = {
entry: currentBackstackEntry.entry,
resolvedPage: newPage,
navDepth: currentBackstackEntry.navDepth,
fragmentTag: currentBackstackEntry.fragmentTag,
frameId: currentBackstackEntry.frameId,
}
const navigationContext = {
entry: newBackstackEntry,
isBackNavigation: false,
navigationType: 2 /* NavigationType replace */,
}
frame._navigationQueue.push(navigationContext)
frame._processNextNavigationEntry()
}

export const adapter = class ProxyAdapterNative extends ProxyAdapterDom {
constructor(instance) {
super(instance)

this.nativePageElement = null
this.originalNativeView = null
this.navigatedFromHandler = null

this.relayNativeNavigatedFrom = this.relayNativeNavigatedFrom.bind(this)
}

dispose() {
Expand All @@ -120,31 +66,6 @@ export const adapter = class ProxyAdapterNative extends ProxyAdapterDom {
}
}

// svelte-native uses navigateFrom event + e.isBackNavigation to know
// when to $destroy the component -- but we don't want our proxy instance
// destroyed when we renavigate to the same page for navigation purposes!
interceptPageNavigation(pageElement) {
const originalNativeView = pageElement.nativeView
const { on } = originalNativeView
const ownOn = originalNativeView.hasOwnProperty('on')
// tricks svelte-native into giving us its handler
originalNativeView.on = function(type, handler) {
if (type === 'navigatedFrom') {
this.navigatedFromHandler = handler
if (ownOn) {
originalNativeView.on = on
} else {
delete originalNativeView.on
}
} else {
//some other handler wireup, we will just pass it on.
if (on) {
on(type, handler)
}
}
}
}

afterMount(target, anchor) {
// nativePageElement needs to be updated each time (only for page
// components, native component that are not pages follow normal flow)
Expand All @@ -168,7 +89,6 @@ export const adapter = class ProxyAdapterNative extends ProxyAdapterDom {
target.firstChild.tagName == 'page'
if (isNativePage) {
const nativePageElement = target.firstChild
this.interceptPageNavigation(nativePageElement)
this.nativePageElement = nativePageElement
} else {
// try to protect against components changing from page to no-page
Expand Down Expand Up @@ -216,6 +136,15 @@ export const adapter = class ProxyAdapterNative extends ProxyAdapterDom {
throw new Error('Failed to create updated page')
}
const isFirstPage = !frame.canGoBack()
const nativeView = newPageElement.nativeView
const navigationEntry = Object.assign(
{},
{
create: () => nativeView,
clearHistory: true,
},
getNavTransition(hotOptions)
)

if (isFirstPage) {
// NOTE not so sure of bellow with the new NS6 method for replace
Expand All @@ -233,19 +162,9 @@ export const adapter = class ProxyAdapterNative extends ProxyAdapterDom {
//
// Fortunately, we can overwrite history in this case.
//
const nativeView = newPageElement.nativeView
frame.navigate(
Object.assign(
{},
{
create: () => nativeView,
clearHistory: true,
},
getNavTransition(hotOptions)
)
)
frame.navigate(navigationEntry)
} else {
replacePage(frame, newPageElement, hotOptions)
frame.replacePage(navigationEntry)
}
} else {
const backEntry = frame.backStack.find(
Expand Down Expand Up @@ -295,42 +214,31 @@ export const adapter = class ProxyAdapterNative extends ProxyAdapterDom {
const {
instance: { refreshComponent },
} = this
const { nativePageElement, relayNativeNavigatedFrom } = this
const oldNativeView = nativePageElement.nativeView
const { nativePageElement: oldNativePageElement } = this
const oldNativeView = oldNativePageElement.nativeView
// rerender
const target = document.createElement('fragment')

// not using conservative for now, since there's nothing in place here to
// leverage it (yet?) -- and it might be easier to miss breakages in native
// only code paths
refreshComponent(target, null)

// this.nativePageElement is updated in afterMount, triggered by proxy / hooks
const newPageElement = this.nativePageElement
// update event proxy
oldNativeView.off('navigatedFrom', relayNativeNavigatedFrom)
nativePageElement.nativeView.on('navigatedFrom', relayNativeNavigatedFrom)
return newPageElement
}

relayNativeNavigatedFrom({ isBackNavigation }) {
const { originalNativeView, navigatedFromHandler } = this
if (!isBackNavigation) {
return
}
if (originalNativeView) {
const { off } = originalNativeView
const ownOff = originalNativeView.hasOwnProperty('off')
originalNativeView.off = function() {
this.navigatedFromHandler = null
if (ownOff) {
originalNativeView.off = off
} else {
delete originalNativeView.off
}
}
}
if (navigatedFromHandler) {
return navigatedFromHandler.apply(this, arguments)
// svelte-native uses navigateFrom event + e.isBackNavigation to know when to $destroy the component.
// To keep that behaviour after refresh, we move event handler from old native view to the new one using
// __navigateFromHandler property that svelte-native provides us with.
const navigateFromHandler = oldNativeView.__navigateFromHandler
if (navigateFromHandler) {
oldNativeView.off('navigatedFrom', navigateFromHandler)
newPageElement.nativeView.on('navigatedFrom', navigateFromHandler)
newPageElement.nativeView.__navigateFromHandler = navigateFromHandler
delete oldNativeView.__navigateFromHandler
}

return newPageElement
}

renderError(err /* , target, anchor */) {
Expand Down