diff --git a/.changeset/puny-pens-cry.md b/.changeset/puny-pens-cry.md new file mode 100644 index 0000000000..eb03c5506b --- /dev/null +++ b/.changeset/puny-pens-cry.md @@ -0,0 +1,12 @@ +--- +"@lynx-js/web-worker-runtime": patch +"@lynx-js/web-constants": patch +"@lynx-js/web-core": patch +--- + +feat: support load bts chunk from remote address + +- re-support chunk splitting +- support lynx.requireModule with a json file +- support lynx.requireModule, lynx.requireModuleAsync with a remote url +- support to add a breakpoint in chrome after reloading the web page diff --git a/packages/web-platform/web-constants/src/types/LynxModule.ts b/packages/web-platform/web-constants/src/types/LynxModule.ts index 8c879c7c05..356bb36ca5 100644 --- a/packages/web-platform/web-constants/src/types/LynxModule.ts +++ b/packages/web-platform/web-constants/src/types/LynxModule.ts @@ -41,6 +41,47 @@ export interface LynxTemplate { appType: 'card' | 'lazy'; } +export type BTSChunkEntry = ( + postMessage: undefined, + module: { exports: unknown }, + exports: unknown, + lynxCoreInject: unknown, + Card: unknown, + setTimeout: unknown, + setInterval: unknown, + clearInterval: unknown, + clearTimeout: unknown, + NativeModules: unknown, + Component: unknown, + ReactLynx: unknown, + nativeAppId: unknown, + Behavior: unknown, + LynxJSBI: unknown, + lynx: unknown, + // BOM API + window: unknown, + document: unknown, + frames: unknown, + location: unknown, + navigator: unknown, + localStorage: unknown, + history: unknown, + Caches: unknown, + screen: unknown, + alert: unknown, + confirm: unknown, + prompt: unknown, + fetch: unknown, + XMLHttpRequest: unknown, + webkit: unknown, + Reporter: unknown, + print: unknown, + global: unknown, + // Lynx API + requestAnimationFrame: unknown, + cancelAnimationFrame: unknown, +) => unknown; + export interface LynxJSModule { exports?: (lynx_runtime: any) => unknown; } diff --git a/packages/web-platform/web-constants/src/types/NativeApp.ts b/packages/web-platform/web-constants/src/types/NativeApp.ts index b8da11ed7d..3d105bb216 100644 --- a/packages/web-platform/web-constants/src/types/NativeApp.ts +++ b/packages/web-platform/web-constants/src/types/NativeApp.ts @@ -123,11 +123,14 @@ export interface NativeApp { cancelAnimationFrame: (id: number) => void; + readScript: (sourceURL: string, entryName?: string) => string; + loadScript: (sourceURL: string, entryName?: string) => BundleInitReturnObj; loadScriptAsync( sourceURL: string, callback: (message: string | null, exports?: BundleInitReturnObj) => void, + entryName?: string, ): void; nativeModuleProxy: Record; diff --git a/packages/web-platform/web-constants/src/utils/generateTemplate.ts b/packages/web-platform/web-constants/src/utils/generateTemplate.ts index 684c1fe880..55f323e6be 100644 --- a/packages/web-platform/web-constants/src/utils/generateTemplate.ts +++ b/packages/web-platform/web-constants/src/utils/generateTemplate.ts @@ -5,69 +5,27 @@ import type { LynxTemplate } from '../types/LynxModule.js'; const currentSupportedTemplateVersion = 2; -const globalDisallowedVars = ['navigator', 'postMessage']; +const globalDisallowedVars = ['navigator', 'postMessage', 'window']; type templateUpgrader = (template: LynxTemplate) => LynxTemplate; const templateUpgraders: templateUpgrader[] = [ (template) => { - const defaultInjectStr = [ - 'Card', - 'setTimeout', - 'setInterval', - 'clearInterval', - 'clearTimeout', - 'NativeModules', - 'Component', - 'ReactLynx', - 'nativeAppId', - 'Behavior', - 'LynxJSBI', - 'lynx', - - // BOM API - 'window', - 'document', - 'frames', - 'location', - 'navigator', - 'localStorage', - 'history', - 'Caches', - 'screen', - 'alert', - 'confirm', - 'prompt', - 'fetch', - 'XMLHttpRequest', - '__WebSocket__', // We would provide `WebSocket` using `ProvidePlugin` - 'webkit', - 'Reporter', - 'print', - 'global', - - // Lynx API - 'requestAnimationFrame', - 'cancelAnimationFrame', - ].join(','); template.appType = template.appType ?? (template.lepusCode.root.startsWith( '(function (globDynamicComponentEntry', ) ? 'lazy' : 'card'); - /** - * The template version 1 has no module wrapper for bts code - */ - template.manifest = Object.fromEntries( - Object.entries(template.manifest).map(([key, value]) => [ - key, - `module.exports={init: (lynxCoreInject) => { var {${defaultInjectStr}} = lynxCoreInject.tt; var module = {exports:{}}; var exports=module.exports; ${value}\n return module.exports; } }`, - ]), - ) as typeof template.manifest; template.version = 2; + template.lepusCode = Object.fromEntries( + Object.entries(template.lepusCode).filter(([_, content]) => + typeof content === 'string' + ), + ) as typeof template.lepusCode; return template; }, ]; const generateModuleContent = ( + fileName: string, content: string, eager: boolean, appType: 'card' | 'lazy', @@ -90,6 +48,8 @@ const generateModuleContent = ( appType !== 'card' ? 'module.exports=\n' : '', content, '\n})()', + '\n//# sourceURL=', + fileName, ].join(''); async function generateJavascriptUrl>( @@ -97,23 +57,25 @@ async function generateJavascriptUrl>( createJsModuleUrl: (content: string, name: string) => Promise, eager: boolean, appType: 'card' | 'lazy', - templateName?: string, + templateName: string, ): Promise { - const processEntry = async ([name, content]: [string, string]) => [ - name, - await createJsModuleUrl( - generateModuleContent( - content, - eager, - appType, - ), - `${templateName}-${name.replaceAll('/', '')}.js`, - ), - ]; return Promise.all( (Object.entries(obj).filter(([_, content]) => typeof content === 'string' - ) as [string, string][]).map(processEntry), + ) as [string, string][]).map(async ([name, content]) => { + return [ + name, + await createJsModuleUrl( + generateModuleContent( + `${templateName}/${name.replaceAll('/', '_')}.js`, + content, + eager, + appType, + ), + `${templateName}-${name.replaceAll('/', '_')}.js`, + ), + ]; + }), ).then( Object.fromEntries, ); @@ -124,7 +86,7 @@ export async function generateTemplate( createJsModuleUrl: | ((content: string, name: string) => Promise) | ((content: string) => string), - templateName?: string, + templateName: string, ): Promise { template.version = template.version ?? 1; if (template.version > currentSupportedTemplateVersion) { @@ -148,12 +110,5 @@ export async function generateTemplate( template.appType!, templateName, ), - manifest: await generateJavascriptUrl( - template.manifest, - createJsModuleUrl as (content: string, name: string) => Promise, - false, - template.appType!, - templateName, - ), }; } diff --git a/packages/web-platform/web-core/src/utils/loadTemplate.ts b/packages/web-platform/web-core/src/utils/loadTemplate.ts index 68d9811c4a..64fb3d60fa 100644 --- a/packages/web-platform/web-core/src/utils/loadTemplate.ts +++ b/packages/web-platform/web-core/src/utils/loadTemplate.ts @@ -35,6 +35,7 @@ export function createTemplateLoader( const decodedTemplate = await generateTemplate( template, createJsModuleUrl, + encodeURIComponent(url), ); resolve(decodedTemplate); } catch (e) { diff --git a/packages/web-platform/web-tests/resources/web-core.main-thread.json b/packages/web-platform/web-tests/resources/web-core.main-thread.json index cb46efccd7..be442e1ef5 100644 --- a/packages/web-platform/web-tests/resources/web-core.main-thread.json +++ b/packages/web-platform/web-tests/resources/web-core.main-thread.json @@ -8,7 +8,8 @@ "manifest": { "/app-service.js": "globalThis.runtime = lynxCoreInject.tt; globalThis.__lynx_worker_type = 'background'", "/manifest-chunk.js": "module.exports = 'hello';", - "/manifest-chunk2.js": "module.exports = 'world';" + "/manifest-chunk2.js": "module.exports = 'world';", + "/json": "{}" }, "customSections": {}, "cardType": "react", diff --git a/packages/web-platform/web-tests/tests/react.spec.ts b/packages/web-platform/web-tests/tests/react.spec.ts index 1781054284..cc99ff912e 100644 --- a/packages/web-platform/web-tests/tests/react.spec.ts +++ b/packages/web-platform/web-tests/tests/react.spec.ts @@ -1872,7 +1872,7 @@ test.describe('reactlynx3 tests', () => { test( 'config-splitchunk-split-by-experience', async ({ page }, { title }) => { - test.skip(true, 'incorrectly implemented test case'); + test.skip(isSSR, 'incorrectly implemented test case'); await goto(page, title, undefined, true); await wait(1500); const target = page.locator('#target'); @@ -1882,7 +1882,7 @@ test.describe('reactlynx3 tests', () => { test( 'config-splitchunk-split-by-module', async ({ page }, { title }) => { - test.skip(true, 'incorrectly implemented test case'); + test.skip(isSSR, 'incorrectly implemented test case'); await goto(page, title, undefined, true); await wait(1500); const target = page.locator('#target'); diff --git a/packages/web-platform/web-tests/tests/web-core.test.ts b/packages/web-platform/web-tests/tests/web-core.test.ts index f7934e1adb..0cccb52af0 100644 --- a/packages/web-platform/web-tests/tests/web-core.test.ts +++ b/packages/web-platform/web-tests/tests/web-core.test.ts @@ -204,6 +204,24 @@ test.describe('web core tests', () => { expect(success).toBe(true); expect(fail).toBe(false); }); + + test('api-nativeApp-readScript', async ({ page, browserName }) => { + // firefox dose not support this. + test.skip(browserName === 'firefox'); + await goto(page); + const mainWorker = await getMainThreadWorker(page); + await mainWorker.evaluate(() => { + globalThis.runtime.renderPage = () => {}; + }); + await wait(3000); + const backWorker = await getBackgroundThreadWorker(page); + const jsonContent = await backWorker.evaluate(() => { + const nativeApp = globalThis.runtime.lynx.getNativeApp(); + return nativeApp.readScript('json'); + }); + await wait(100); + expect(jsonContent).toBe('{}'); + }); test('registerDataProcessor-as-global-var-update', async ({ page, browserName }) => { await goto(page); const mainWorker = await getMainThreadWorker(page); diff --git a/packages/web-platform/web-worker-runtime/src/backgroundThread/background-apis/createChunkLoading.ts b/packages/web-platform/web-worker-runtime/src/backgroundThread/background-apis/createChunkLoading.ts new file mode 100644 index 0000000000..82dc42b874 --- /dev/null +++ b/packages/web-platform/web-worker-runtime/src/backgroundThread/background-apis/createChunkLoading.ts @@ -0,0 +1,177 @@ +// Copyright 2023 The Lynx Authors. All rights reserved. +// Licensed under the Apache License Version 2.0 that can be found in the +// LICENSE file in the root directory of this source tree. + +import type { + NativeApp, + LynxTemplate, + BTSChunkEntry, + BundleInitReturnObj, +} from '@lynx-js/web-constants'; + +export function createChunkLoading(initialTemplate: LynxTemplate): { + readScript: NativeApp['readScript']; + loadScript: NativeApp['loadScript']; + loadScriptAsync: NativeApp['loadScriptAsync']; + templateCache: Map; +} { + const templateCache = new Map([[ + '__Card__', + initialTemplate, + ]]); + const readScript: NativeApp['readScript'] = ( + sourceURL, + entryName = '__Card__', + ) => { + const jsContentInTemplate = templateCache.get(entryName!) + ?.manifest[`/${sourceURL}`]; + if (jsContentInTemplate) return jsContentInTemplate; + const xhr = new XMLHttpRequest(); + xhr.open('GET', sourceURL, false); + xhr.send(null); + if (xhr.status === 200) { + return xhr.responseText; + } + throw new Error(`Failed to load ${sourceURL}, status: ${xhr.status}`); + }; + + const readScriptAsync: ( + sourceURL: string, + entryName: string | undefined, + ) => Promise = async (sourceURL, entryName = '__Card__') => { + const jsContentInTemplate = templateCache.get(entryName!) + ?.manifest[`/${sourceURL}`]; + if (jsContentInTemplate) return jsContentInTemplate; + return new Promise((resolve, reject) => { + fetch(sourceURL).then((response) => { + if (response.ok) { + response.text().then((text) => resolve(text), reject); + } else { + reject( + new Error( + `Failed to load ${sourceURL}, status: ${response.status}`, + ), + ); + } + }, reject); + }); + }; + const createBundleInitReturnObj = ( + jsContent: string, + fileName: string, + ): BundleInitReturnObj => { + const foo = new Function( + 'postMessage', + 'module', + 'exports', + 'lynxCoreInject', + 'Card', + 'setTimeout', + 'setInterval', + 'clearInterval', + 'clearTimeout', + 'NativeModules', + 'Component', + 'ReactLynx', + 'nativeAppId', + 'Behavior', + 'LynxJSBI', + 'lynx', + // BOM API + 'window', + 'document', + 'frames', + 'location', + 'navigator', + 'localStorage', + 'history', + 'Caches', + 'screen', + 'alert', + 'confirm', + 'prompt', + 'fetch', + 'XMLHttpRequest', + 'webkit', + 'Reporter', + 'print', + 'global', + // Lynx API + 'requestAnimationFrame', + 'cancelAnimationFrame', + [ + jsContent, + '\n//# sourceURL=', + fileName, + ].join(''), + ) as BTSChunkEntry; + return { + init(lynxCoreInject) { + const module = { exports: {} }; + const tt = lynxCoreInject.tt as any; + foo( + undefined, + module, + module.exports, + lynxCoreInject, + tt.Card, + tt.setTimeout, + tt.setInterval, + tt.clearInterval, + tt.clearTimeout, + tt.NativeModules, + tt.Component, + tt.ReactLynx, + tt.nativeAppId, + tt.Behavior, + tt.LynxJSBI, + tt.lynx, + // BOM API + tt.window, + tt.document, + tt.frames, + tt.location, + tt.navigator, + tt.localStorage, + tt.history, + tt.Caches, + tt.screen, + tt.alert, + tt.confirm, + tt.prompt, + tt.fetch, + tt.XMLHttpRequest, + tt.webkit, + tt.Reporter, + tt.print, + tt.global, + tt.requestAnimationFrame, + tt.cancelAnimationFrame, + ); + return module.exports; + }, + }; + }; + return { + readScript, + loadScript: (sourceURL, entryName = '__Card__') => { + const jsContent = readScript(sourceURL, entryName); + return createBundleInitReturnObj( + jsContent, + `${encodeURIComponent(entryName)}/${sourceURL}`, + ); + }, + loadScriptAsync: async (sourceURL, callback, entryName = '__Card__') => { + readScriptAsync(sourceURL, entryName).then((jsContent) => { + callback( + null, + createBundleInitReturnObj( + jsContent, + `${encodeURIComponent(entryName)}/${sourceURL}`, + ), + ); + }); + }, + templateCache, + }; +} diff --git a/packages/web-platform/web-worker-runtime/src/backgroundThread/background-apis/createNativeApp.ts b/packages/web-platform/web-worker-runtime/src/backgroundThread/background-apis/createNativeApp.ts index 4fd0164f66..1464433c71 100644 --- a/packages/web-platform/web-worker-runtime/src/backgroundThread/background-apis/createNativeApp.ts +++ b/packages/web-platform/web-worker-runtime/src/backgroundThread/background-apis/createNativeApp.ts @@ -7,13 +7,11 @@ import { setNativePropsEndpoint, triggerComponentEventEndpoint, selectComponentEndpoint, - type BundleInitReturnObj, type NativeApp, type LynxCrossThreadContext, type BackMainThreadContextConfig, I18nResource, reportErrorEndpoint, - type LynxTemplate, queryComponentEndpoint, updateBTSTemplateCacheEndpoint, } from '@lynx-js/web-constants'; @@ -30,6 +28,7 @@ import type { TimingSystem } from './createTimingSystem.js'; import { registerUpdateGlobalPropsHandler } from './crossThreadHandlers/registerUpdateGlobalPropsHandler.js'; import { registerUpdateI18nResource } from './crossThreadHandlers/registerUpdateI18nResource.js'; import { createGetPathInfo } from './crossThreadHandlers/createGetPathInfo.js'; +import { createChunkLoading } from './createChunkLoading.js'; let nativeAppCount = 0; const sharedData: Record = {}; @@ -67,13 +66,9 @@ export async function createNativeApp( queryComponentEndpoint, ); const reportError = uiThreadRpc.createCall(reportErrorEndpoint); - const createBundleInitReturnObj = (): BundleInitReturnObj => { - const ret = globalThis.module.exports ?? globalThis.__bundle__holder; - globalThis.module.exports = null; - globalThis.__bundle__holder = null; - return ret as unknown as BundleInitReturnObj; - }; - const templateCache = new Map([['__Card__', template]]); + const { templateCache, loadScript, loadScriptAsync, readScript } = + createChunkLoading(template); + mainThreadRpc.registerHandler( updateBTSTemplateCacheEndpoint, (url, template) => { @@ -94,36 +89,9 @@ export async function createNativeApp( mainThreadRpc, nativeModulesMap, ), - loadScriptAsync: function( - sourceURL: string, - callback: (message: string | null, exports?: BundleInitReturnObj) => void, - entryName?: string, - ): void { - entryName = entryName ?? '__Card__'; - const manifestUrl = templateCache.get(entryName!) - ?.manifest[`/${sourceURL}`]; - if (manifestUrl) sourceURL = manifestUrl; - else throw Error(`Cannot find ${sourceURL} in manifest`); - globalThis.module.exports = null; - globalThis.__bundle__holder = null; - import( - /* webpackIgnore: true */ - sourceURL - ).catch(callback).then(async () => { - callback(null, createBundleInitReturnObj()); - }); - }, - loadScript: (sourceURL: string, entryName?: string) => { - entryName = entryName ?? '__Card__'; - const manifestUrl = templateCache.get(entryName!) - ?.manifest[`/${sourceURL}`]; - if (manifestUrl) sourceURL = manifestUrl; - else throw Error(`Cannot find ${sourceURL} in manifest`); - globalThis.module.exports = null; - globalThis.__bundle__holder = null; - importScripts(sourceURL); - return createBundleInitReturnObj(); - }, + readScript, + loadScriptAsync, + loadScript, requestAnimationFrame(cb: FrameRequestCallback) { return requestAnimationFrame(cb); },