Skip to content

Commit

Permalink
fix: use inline script to ensure adaptive asset load order
Browse files Browse the repository at this point in the history
  • Loading branch information
DylanPiercey committed Dec 1, 2023
1 parent 44751aa commit 74a55b6
Show file tree
Hide file tree
Showing 5 changed files with 118 additions and 70 deletions.
5 changes: 5 additions & 0 deletions .changeset/hungry-dodos-judge.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"arc-vite": patch
---

Use inline script to load adaptive assets in correct order instead of appending to chunk file.
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"dependencies": {
"arc-resolver": "^3.0.0",
"dom-serializer": "^2.0.0",
"domhandler": "^5.0.3",
"htmlparser2": "^9.0.0"
},
"devDependencies": {
Expand Down
106 changes: 36 additions & 70 deletions src/plugins/build-web.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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<string, string>();
const adaptiveImporters = new Map<string, Map<string, string>>();
const adaptiveMatchesForId = new Map<string, Matches>();
const bindingsByAdaptiveId = new Map<string, Set<string> | true>();
const metaForAdaptiveChunk = new Map<
const apply: Plugin["apply"] = (config, { command }) =>
command === "build" && !config.build?.ssr;
let globalIds = new Map<string, string>();
let adaptiveImporters = new Map<string, Map<string, string>>();
let adaptiveMatchesForId = new Map<string, Matches>();
let bindingsByAdaptiveId = new Map<string, Set<string> | true>();
let metaForAdaptiveChunk = new Map<
string,
{
entryId: string;
adaptiveImports: Map<string, string>;
}
>();
const apply: Plugin["apply"] = (config, { command }) =>
command === "build" && !config.build?.ssr;
let proxyModuleId = 0;
let initModuleId = 0;
let basePath = "/";
Expand All @@ -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) {
Expand Down Expand Up @@ -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<string, string>();
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);
},
},
{
Expand Down Expand Up @@ -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;
}
75 changes: 75 additions & 0 deletions src/utils/prepare-arc-entry-html.ts
Original file line number Diff line number Diff line change
@@ -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;
}

0 comments on commit 74a55b6

Please sign in to comment.