diff --git a/frontend/package.json b/frontend/package.json index 6cd3351..86806fe 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -38,7 +38,6 @@ "typescript": "^5.1.6", "uuid": "^9.0.0", "vite": "^4.4.9", - "vite-plugin-singlefile": "git://github.com/CoolElectronics/vite-plugin-singlefile.git", "vite-plugin-svelte": "^3.0.1" } } \ No newline at end of file diff --git a/frontend/src/App.svelte b/frontend/src/App.svelte index f053ff4..fbb66c8 100644 --- a/frontend/src/App.svelte +++ b/frontend/src/App.svelte @@ -238,12 +238,14 @@ - + {#if !import.meta.env.VITE_ADRIFT_SINGLEFILE} + + {/if} diff --git a/frontend/vite-singlefile.js b/frontend/vite-singlefile.js new file mode 100644 index 0000000..fb30d12 --- /dev/null +++ b/frontend/vite-singlefile.js @@ -0,0 +1,151 @@ +import micromatch from "micromatch" + +const defaultConfig = { + useRecommendedBuildConfig: true, + removeViteModuleLoader: false, + deleteInlinedFiles: true +} + +export function replaceScript( + html, + scriptFilename, + scriptCode, + removeViteModuleLoader = false +) { + const reScript = new RegExp( + `]*?) src="[./]*${scriptFilename}"([^>]*)>` + ) + // 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") + const inlined = html.replace( + reScript, + (_, beforeSrc, afterSrc) => + `\nimport "data:application/javascript;base64,${Buffer.from( + newCode + ).toString("base64")}";\n` + ) + return removeViteModuleLoader ? _removeViteModuleLoader(inlined) : inlined +} + +export function replaceCss(html, scriptFilename, scriptCode) { + const reCss = new RegExp(`]*? href="[./]*${scriptFilename}"[^>]*?>`) + const inlined = html.replace(reCss, ``) + return inlined +} + +const warnNotInlined = filename => + console.warn(`WARNING: asset not inlined: ${filename}`) + +export function viteSingleFile({ + useRecommendedBuildConfig = true, + removeViteModuleLoader = false, + inlinePattern = [], + deleteInlinedFiles = true +} = defaultConfig) { + return { + name: "vite:singlefile", + config: useRecommendedBuildConfig ? _useRecommendedBuildConfig : undefined, + enforce: "post", + generateBundle: (_, bundle) => { + const jsExtensionTest = /\.[mc]?js$/ + const htmlFiles = Object.keys(bundle).filter(i => i.endsWith(".html")) + const cssAssets = Object.keys(bundle).filter(i => i.endsWith(".css")) + const jsAssets = Object.keys(bundle).filter(i => jsExtensionTest.test(i)) + const bundlesToDelete = [] + for (const name of htmlFiles) { + const htmlChunk = bundle[name] + let replacedHtml = htmlChunk.source + for (const jsName of jsAssets) { + if ( + !inlinePattern.length || + micromatch.isMatch(jsName, inlinePattern) + ) { + const jsChunk = bundle[jsName] + if (jsChunk.code != null) { + bundlesToDelete.push(jsName) + replacedHtml = replaceScript( + replacedHtml, + jsChunk.fileName, + jsChunk.code, + removeViteModuleLoader + ) + } + } else { + warnNotInlined(jsName) + } + } + for (const cssName of cssAssets) { + if ( + !inlinePattern.length || + micromatch.isMatch(cssName, inlinePattern) + ) { + const cssChunk = bundle[cssName] + bundlesToDelete.push(cssName) + replacedHtml = replaceCss( + replacedHtml, + cssChunk.fileName, + cssChunk.source + ) + } else { + warnNotInlined(cssName) + } + } + htmlChunk.source = replacedHtml + } + if (deleteInlinedFiles) { + for (const name of bundlesToDelete) { + delete bundle[name] + } + } + for (const name of Object.keys(bundle).filter( + i => + !jsExtensionTest.test(i) && + !i.endsWith(".css") && + !i.endsWith(".html") + )) { + warnNotInlined(name) + } + } + } +} + +// Optionally remove the Vite module loader since it's no longer needed because this plugin has inlined all code. +// This assumes that the Module Loader is (1) the FIRST function declared in the module, (2) an IIFE, (4) is within +// a script with no unexpected attribute values, and (5) that the containing script is the first script tag that +// matches the above criteria. Changes to the SCRIPT tag especially could break this again in the future. It should +// work whether `minify` is enabled or not. +// Update example: +// https://github.com/richardtallent/vite-plugin-singlefile/issues/57#issuecomment-1263950209 +const _removeViteModuleLoader = html => + html.replace( + /(