Created
October 19, 2023 08:11
-
-
Save mizchi/8643b1fb7b6ae9dd6938620d33bf7786 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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