Skip to content

Instantly share code, notes, and snippets.

@mizchi
Created October 19, 2023 08:11
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mizchi/8643b1fb7b6ae9dd6938620d33bf7786 to your computer and use it in GitHub Desktop.
Save mizchi/8643b1fb7b6ae9dd6938620d33bf7786 to your computer and use it in GitHub Desktop.
import { test, expect } from 'vitest';
import { build, splitVendorChunkPlugin } from 'vite';
import path from 'path';
import { OutputAsset, OutputBundle, OutputChunk, RollupOutput } from 'rollup';
import ts from 'typescript';
const root = path.join(__dirname, "__fixtures")
test("nested", async () => {
const a = 'export default Math.random()';
const b = `export default () => import('data:text/javascript;base64,${btoa(a)}')`;
const bmod = await import(`data:text/javascript;base64,${btoa(b)}`);
const amod = await import(`data:text/javascript;base64,${btoa(a)}`);
expect(await bmod.default()).not.toBe(await amod.default);
});
// const DYNAMIC_IMPORT_REGEX = /import\s*\((['"])([^'"]*?)['"]\)/g;
const DYNAMIC_IMPORT_REGEX = /import\s*\((['"])(?!data\:)([^'"]*?)['"]\)/g;
const STATIC_IMPORT_REGEX = /import\s+(?<expr>.*?)+from\s*['"](?!data\:)(?<specifier>[^'"]*?)['"]/g;
// const STATIC_IMPORT_WITHOUT_FROM_REGEX = /import\s*['"](?<specifier>[^'"]*?)['"]/g;
const SCRIPT_TAG = /\<script(?<before>[^>]*?) src=['"](?<specifier>[^'"]*)['"](?<after>[^>]*?)\>\<\/script\>/g;
test("ok", async () => {
const out = await build({
configFile: false,
root,
mode: "preview",
build: {
// assetsInlineLimit: Infinity,
modulePreload: false,
// assetsInlineLimit: 0,
// {
// polyfill: false,
// },
cssCodeSplit: false,
target: 'esnext',
minify: false,
cssMinify: false,
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes('node_modules')) {
return 'vendor';
}
}
}
}
// write: false,
},
plugins: [
splitVendorChunkPlugin(),
// virtualEntry(target),
// viteSingleFile({
// useRecommendedBuildConfig: false,
// removeViteModuleLoader: true,
// }),
{
name: 'single',
enforce: 'post',
async generateBundle(opts, bundle, ...args) {
// const result = await toSingle(bundle as OutputBundle);
// // console.log(result);
// this.emitFile({
// type: 'asset',
// fileName: 'single.html',
// source: result,
// })
},
// transformIndexHtml(html) {
// return {
// html,
// tags: []
// }
// }
}
]
});
console.log(out.output);
// @ts-ignore
// console.log("ok", (out as RollupOutput).output.find(x => x.fileName.endsWith(".html"))?.source);
});
async function toSingle(bundle: OutputBundle) {
const chunks = Object.values(bundle).filter((chunk) => chunk.type === 'chunk') as OutputChunk[];
const encodedMap = new Map<string, string>();
for (const chunk of chunks) {
resolveDynamicImport(chunk.fileName);
}
// console.log('bbb', bundle);
const indexHtml = Object.values(bundle).find((chunk) => chunk.type === 'asset' && chunk.fileName.endsWith('index.html')) as OutputAsset;
// console.log('~~~~', indexHtml.source);
return replaceScriptTag(indexHtml.source as string);
function normalize(specifier: string) {
const ext = path.extname(specifier);
return path.basename(specifier).replace(ext, '');
}
function resolveDynamicImport(specifier: string): string {
const normalized = normalize(specifier);
console.log("[resolveDynamicImport]", specifier);
const cached = encodedMap.get(normalized);
if (cached) {
console.log("[resolveDynamicImport] cached", normalized);
return cached;
}
console.log("[resolveDynamicImport] create", normalized);
const chunk = Object.values(bundle).find(i => i.fileName.includes(normalized)) as OutputChunk;
const result = transformImportSpecifier(chunk.code, (specifier) => {
if (specifier.startsWith('data:text')) {
return specifier;
}
const encoded = resolveDynamicImport(specifier);
return `data:text/javascript;base64,${encoded}`;
});
const newCode = result.replace(/"__VITE_PRELOAD__"/g, "void 0");
const encoded = btoa(newCode);
console.log("[resolveDynamicImport] set", normalized);
encodedMap.set(normalized, encoded);
return encoded;
}
function replaceScriptTag(html: string): string {
// const reScript = new RegExp(`<script([^>]*?) src="[./]*${scriptFilename}"([^>]*)></script>`);
return replaceAll(html, SCRIPT_TAG, (_full, before, specifier, after) => {
console.log("[replaceScriptTag]", specifier)
const encoded = resolveDynamicImport(specifier);
return `<script${before}${after}>import 'data:text/javascript;base64,${encoded}';</script>`
});
}
// we can't use String.prototype.replaceAll since it isn't supported in Node.JS 14
// const preloadMarker = /"__VITE_PRELOAD__"/g;
// const newCode = scriptCode.replace(preloadMarker, "void 0")
// a const inlined = html.replace(reScript, (_, beforeSrc, afterSrc) => `<script${beforeSrc}${afterSrc}>\n${newCode}\n</script>`)
// return removeViteModuleLoader ? _removeViteModuleLoader(inlined) : inlined
// }
}
const replaceAll = (str: string, regex: RegExp, replacer: (match: string, ...matches: string[]) => string): string => {
// @ts-ignore
return str.replaceAll(regex, replacer)
}
export function transformImportSpecifier(code: string, modify: (specifier: string) => string) {
const transpile = ts.transpileModule(code, {
compilerOptions: {
target: ts.ScriptTarget.ESNext,
module: ts.ModuleKind.ESNext,
},
transformers: {
before: [
(ctx) => (sourceFile) => {
const visit: ts.Visitor = (node: ts.Node) => {
if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
const modified = modify(node.moduleSpecifier.text);
return ctx.factory.createImportDeclaration(
node.modifiers,
node.importClause,
ctx.factory.createStringLiteral(modified)
);
}
if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword) {
const [arg] = node.arguments;
if (ts.isStringLiteral(arg)) {
// const encoded = Buffer.from(arg.text).toString('base64');
const modified = modify(arg.text);
return ctx.factory.createCallExpression(
ctx.factory.createIdentifier('import'),
undefined,
[ctx.factory.createStringLiteral(modified)]
);
}
}
return ts.visitEachChild(node, visit, ctx);
// return node;
}
return ts.visitEachChild(sourceFile, visit, ctx);
}
]
}
});
return transpile.outputText;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment