From 74a55b604a476952e83f6d9f1dea66fa774b4422 Mon Sep 17 00:00:00 2001 From: dpiercey Date: Fri, 1 Dec 2023 09:00:38 -0700 Subject: [PATCH] fix: use inline script to ensure adaptive asset load order --- .changeset/hungry-dodos-judge.md | 5 ++ package-lock.json | 1 + package.json | 1 + src/plugins/build-web.ts | 106 ++++++++++------------------ src/utils/prepare-arc-entry-html.ts | 75 ++++++++++++++++++++ 5 files changed, 118 insertions(+), 70 deletions(-) create mode 100644 .changeset/hungry-dodos-judge.md create mode 100644 src/utils/prepare-arc-entry-html.ts diff --git a/.changeset/hungry-dodos-judge.md b/.changeset/hungry-dodos-judge.md new file mode 100644 index 0000000..1c3bca1 --- /dev/null +++ b/.changeset/hungry-dodos-judge.md @@ -0,0 +1,5 @@ +--- +"arc-vite": patch +--- + +Use inline script to load adaptive assets in correct order instead of appending to chunk file. diff --git a/package-lock.json b/package-lock.json index eff1ff9..6adcb50 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "arc-resolver": "^3.0.0", "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", "htmlparser2": "^9.0.0" }, "devDependencies": { diff --git a/package.json b/package.json index 5932976..b174328 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "dependencies": { "arc-resolver": "^3.0.0", "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", "htmlparser2": "^9.0.0" }, "devDependencies": { diff --git a/src/plugins/build-web.ts b/src/plugins/build-web.ts index e547aa6..d0efa9f 100644 --- a/src/plugins/build-web.ts +++ b/src/plugins/build-web.ts @@ -15,8 +15,8 @@ import { } from "../utils/manifest"; import { type Matches } from "../utils/matches"; import { type InternalPluginOptions } from "../utils/options"; +import { prepareArcEntryHTML } from "../utils/prepare-arc-entry-html"; import { stripEntryScript } from "../utils/strip-entry-script"; -import { toPosix } from "../utils/to-posix"; import { decodeArcVirtualMatch, getVirtualMatches, @@ -27,26 +27,25 @@ const arcPrefix = "\0arc-"; const arcJsSuffix = ".mjs"; const arcInitPrefix = `${arcPrefix}init:`; const arcProxyPrefix = `${arcPrefix}proxy:`; -const emptyScriptReg = /^[\s;]+$/; const arcChunkFileNameReg = /(.+)\.arc(?:\.(.+))?\.html$/; export function pluginBuildWeb({ runtimeId, flagSets, store, }: InternalPluginOptions): Plugin[] { - const globalIds = new Map(); - const adaptiveImporters = new Map>(); - const adaptiveMatchesForId = new Map(); - const bindingsByAdaptiveId = new Map | true>(); - const metaForAdaptiveChunk = new Map< + const apply: Plugin["apply"] = (config, { command }) => + command === "build" && !config.build?.ssr; + let globalIds = new Map(); + let adaptiveImporters = new Map>(); + let adaptiveMatchesForId = new Map(); + let bindingsByAdaptiveId = new Map | true>(); + let metaForAdaptiveChunk = new Map< string, { entryId: string; adaptiveImports: Map; } >(); - const apply: Plugin["apply"] = (config, { command }) => - command === "build" && !config.build?.ssr; let proxyModuleId = 0; let initModuleId = 0; let basePath = "/"; @@ -63,11 +62,11 @@ export function pluginBuildWeb({ }, closeBundle() { proxyModuleId = initModuleId = 0; - globalIds.clear(); - adaptiveImporters.clear(); - adaptiveMatchesForId.clear(); - bindingsByAdaptiveId.clear(); - metaForAdaptiveChunk.clear(); + globalIds = new Map(); + adaptiveImporters = new Map(); + adaptiveMatchesForId = new Map(); + bindingsByAdaptiveId = new Map(); + metaForAdaptiveChunk = new Map(); }, async resolveId(source, importer, options) { if (importer) { @@ -383,59 +382,34 @@ export function pluginBuildWeb({ return null; }, - transformIndexHtml(html, { chunk }) { - if (!chunk?.facadeModuleId) return; - if (arcChunkFileNameReg.test(chunk.facadeModuleId)) { - return [ - { - injectTo: "head-prepend", - tag: "script", - children: `${runtimeId}={}`, - }, - ]; - } - - return stripEntryScript(basePath, chunk.fileName, html); - }, - generateBundle(_, bundle) { - const facadeModuleIdToEntryName = new Map(); - for (const fileName in bundle) { - const chunk = bundle[fileName]; - if ( - chunk.type === "chunk" && - chunk.isEntry && - chunk.facadeModuleId && - !arcChunkFileNameReg.test(chunk.facadeModuleId) && - !emptyScriptReg.test(chunk.code) - ) { - facadeModuleIdToEntryName.set(chunk.facadeModuleId, chunk.fileName); - } - } + transformIndexHtml(html, { chunk, bundle }) { + if (!bundle || !chunk?.facadeModuleId) return; + const adaptiveChunkMeta = metaForAdaptiveChunk.get( + chunk.facadeModuleId, + ); - for (const fileName in bundle) { - const chunk = bundle[fileName]; - if ( - chunk.type === "chunk" && - chunk.isEntry && - chunk.facadeModuleId && - arcChunkFileNameReg.test(chunk.facadeModuleId) - ) { - const adaptiveChunkMeta = metaForAdaptiveChunk.get( - chunk.facadeModuleId, - ); - if (adaptiveChunkMeta) { - const originalEntryName = facadeModuleIdToEntryName.get( - adaptiveChunkMeta.entryId, + if (adaptiveChunkMeta) { + for (const fileName in bundle) { + const curChunk = bundle[fileName]; + if ( + curChunk.type === "chunk" && + curChunk.isEntry && + curChunk.facadeModuleId === adaptiveChunkMeta.entryId + ) { + return prepareArcEntryHTML( + basePath, + runtimeId, + html, + curChunk, + chunk, ); - if (originalEntryName) { - chunk.imports.push(originalEntryName); - chunk.code += `;import ${JSON.stringify( - toRelativeImport(chunk.fileName, originalEntryName), - )}`; - } } } + + return; } + + return stripEntryScript(basePath, chunk.fileName, html); }, }, { @@ -586,11 +560,3 @@ function decodeArcInitId(id: string) { : path.join(adaptiveImport, "..", relativeAdaptedImport); return [adaptiveImport, adaptedImport]; } - -function toRelativeImport(from: string, to: string) { - const relative = path.relative(path.dirname(toPosix(from)), toPosix(to)); - if (relative[0] !== ".") { - return `./${relative}`; - } - return relative; -} diff --git a/src/utils/prepare-arc-entry-html.ts b/src/utils/prepare-arc-entry-html.ts new file mode 100644 index 0000000..d9bed0e --- /dev/null +++ b/src/utils/prepare-arc-entry-html.ts @@ -0,0 +1,75 @@ +import toHTML from "dom-serializer"; +import { type Node, Element, Text } from "domhandler"; +import { parseDocument, DomUtils, ElementType } from "htmlparser2"; +import type { Rollup } from "vite"; + +const { isTag, filter, appendChild, prepend, removeElement } = DomUtils; +const parserOptions = { decodeEntities: false, encodeEntities: false }; +const emptyScriptReg = /^[\s;]+$/; + +export function prepareArcEntryHTML( + basePath: string, + runtimeId: string, + html: string, + originalChunk: Rollup.OutputChunk, + adaptedChunk: Rollup.OutputChunk, +) { + const dom = parseDocument(html, parserOptions); + const originalChunkIsEmpty = emptyScriptReg.test(originalChunk.code); + const adaptedChunkIsEmpty = emptyScriptReg.test(adaptedChunk.code); + + for (const script of filter(isModule, dom) as Element[]) { + if (stripBasePath(basePath, script.attribs.src) === adaptedChunk.fileName) { + if (originalChunkIsEmpty && adaptedChunkIsEmpty) { + removeElement(script); + } else if (originalChunkIsEmpty) { + prepend( + script, + new Element( + "script", + {}, + [new Text(`${runtimeId}={}`)], + ElementType.Script, + ), + ); + } else if (adaptedChunkIsEmpty) { + script.attribs.src = basePath + originalChunk.fileName; + } else { + delete script.attribs.src; + prepend( + script, + new Element( + "script", + {}, + [new Text(`${runtimeId}={}`)], + ElementType.Script, + ), + ); + appendChild( + script, + new Text( + `import ${JSON.stringify( + basePath + adaptedChunk.fileName, + )}\nimport ${JSON.stringify(basePath + originalChunk.fileName)}`, + ), + ); + } + } + } + + return toHTML(dom, parserOptions); +} + +function isModule(node: Node): node is Element { + return ( + isTag(node) && + node.tagName === "script" && + node.attribs.type === "module" && + !!node.attribs.src + ); +} + +function stripBasePath(basePath: string, path: string) { + if (path.startsWith(basePath)) return path.slice(basePath.length); + return path; +}