diff --git a/.vite/lib/content-scripts.js b/.vite/lib/content-scripts.js index a4c67d1..2b7f51a 100644 --- a/.vite/lib/content-scripts.js +++ b/.vite/lib/content-scripts.js @@ -4,6 +4,7 @@ import path from "path"; import fs from "fs"; import { SwapDefinedVariablesPlugin } from "../plugins/swap-defined-variables.js"; import { ScssViteReadEnvVariableFunctionPlugin } from "../plugins/scss-read-env-variable-function.js"; +import { ConvertCjsToAmd } from "../plugins/convert-cjs-to-amd.js"; /** * Create the result base file name for the file. @@ -208,6 +209,7 @@ export async function buildScriptsAndStyles(buildOptions) { alias: aliasesSettings, }, plugins: [ + ConvertCjsToAmd(), wrapScriptIntoIIFE(), collectDependenciesForManifestBuilding((fileName, dependencies) => { pathsReplacementByOutputPath diff --git a/.vite/plugins/convert-cjs-to-amd.js b/.vite/plugins/convert-cjs-to-amd.js new file mode 100644 index 0000000..c313504 --- /dev/null +++ b/.vite/plugins/convert-cjs-to-amd.js @@ -0,0 +1,170 @@ +/** + * # CJS → AMD module format converter. + * + * Incredibly scary and potentially unstable way to bring back AMD modules on Vite 8+. Converts require() calls into + * list of dependencies and forms define() function with module name, dependencies and exports. + * + * @return {import('vite').Plugin} + */ +export function ConvertCjsToAmd() { + const requireAssignmentsRegExp = /(?:const\s*|,\s*)(?[a-z0-9_$]+)\s*=\s*require\((?['"][^'"]+['"])\)/iy; + const inlineRequireRegExp = /require\((?['"][^'"]+['"])\)\.(?[a-z0-9_$]+)/gi + const exportsDefinitionRegExp = /Object.defineProperty\(exports,['"](?[^'"]+)['"],{enumerable:!\d,get:function\(\){return\s*(?[^};]+)}}\)(?[,;])/gi; + const exportVariablePrefix = "$"; + const exportVariableCollisionsRegExp = /\$[a-z_$]+/g; + const amdExportsModule = '"exports"'; + + /** + * Clears the name of the module to be fit with what we had before. + * @param {string} path Path to normalize. + * @returns {string} Normalized path. + */ + function normalizeDependencyName(path) { + return path.replace(/\.js(['"])$/, "$1"); + } + + /** + * Deduce the potentially unused variable from the current code. Uses special rarely encountered prefix. + * @param {string} code Code to parse the used symbols from. Ideally, code should be passed here before any + * replacements. + * @param {Set} usedSymbols Set of symbols which were already used or found in code. + * @returns {string} Not-yet used variable which is likely safe to use. + */ + function findUnusedSpecialVariable(code, usedSymbols) { + // Run complete scan of the source code first, to find any potential collisions. + if (!usedSymbols.size) { + let match; + + exportVariableCollisionsRegExp.lastIndex = 0; + + while (match = exportVariableCollisionsRegExp.exec(code)) { + usedSymbols.add(match[0]); + } + } + + if (!usedSymbols.size || !usedSymbols.has(exportVariablePrefix)) { + usedSymbols.add(exportVariablePrefix); + return exportVariablePrefix; + } + + let count = 0; + let resultVariable; + + do { + resultVariable = exportVariablePrefix + count.toString(36); + } while (count++ < 1000 && usedSymbols.has(resultVariable)); + + if (usedSymbols.has(resultVariable)) { + throw new Error('Failed to convert CJS to AMD: Failed to find non-colliding variable after 100 tries!'); + } + + usedSymbols.add(resultVariable); + return resultVariable; + } + + /** + * Main function doing the conversion of one chunk. + * @param {import('rolldown').OutputChunk} chunk + */ + function convertChunkToAmd(chunk) { + /** @type {string} */ + let chunkCode = chunk.code; + let lastRequireIndex = 0; + /** + * Saved per-chunk set of used symbols. This set is reused for multiple calls to not parse code multiple times. + * @type {Set} + */ + const chunkUsedSymbols = new Set(); + + /** @type {Record} */ + let dependenciesVarsToPaths = {}; + + // region Extracting `const x = require()` calls from the start of the source code... + + do { + requireAssignmentsRegExp.lastIndex = lastRequireIndex; + const result = requireAssignmentsRegExp.exec(chunkCode); + + if (!result) { + break; + } + + dependenciesVarsToPaths[result.groups.variable] = normalizeDependencyName(result.groups.path); + lastRequireIndex += result["0"].length; + } while (true); + + if (lastRequireIndex > 0) { + const characterAfterRequires = chunkCode[lastRequireIndex]; + + if (characterAfterRequires !== ';') { + throw new Error("Failed to convert CJS to AMD: reached last require() assignment, but list didn't stop."); + } + } + + // endregion + + // region Finding and replacing inlined `require().x` calls... + + chunkCode = chunkCode.replaceAll(inlineRequireRegExp, (match, dependencyPath, variable) => { + const dependencyVariable = findUnusedSpecialVariable(chunkCode, chunkUsedSymbols); + dependenciesVarsToPaths[dependencyVariable] = dependencyPath; + return `${dependencyVariable}.${variable}`; + }); + + // endregion + + // region Finding and replacing all exports with AMD exports... + + /** @type {string|undefined} */ + let chunkExportsVariable; + + exportsDefinitionRegExp.lastIndex = 0; + + chunkCode = chunkCode.replaceAll(exportsDefinitionRegExp, (substring, exportVariable, assignedVariable, separator) => { + chunkExportsVariable ??= findUnusedSpecialVariable(chunkCode, chunkUsedSymbols); + + if (!(chunkExportsVariable in dependenciesVarsToPaths)) { + dependenciesVarsToPaths = { + [chunkExportsVariable]: amdExportsModule, + ...dependenciesVarsToPaths, + }; + } + + return `${chunkExportsVariable}.${exportVariable}=${assignedVariable}${separator}`; + }); + + // endregion + + const normalizedModuleName = normalizeDependencyName(`"${chunk.fileName}"`); + const dependenciesVariables = Object.keys(dependenciesVarsToPaths).join(','); + const dependenciesPaths = Object.values(dependenciesVarsToPaths).join(','); + + if (lastRequireIndex > 0) { + chunkCode = chunkCode.substring(lastRequireIndex + 1); + } + + chunk.code = `define(${normalizedModuleName},[${dependenciesPaths}],function(${dependenciesVariables}){${chunkCode}})`; + } + + return { + name: "koloml:covert-cjs-to-amd", + apply: "build", + enforce: "pre", + + /** + * @param _ + * @param {Record} bundle + */ + generateBundle(_, bundle) { + for (const chunkId in bundle) { + const chunkOrAsset = bundle[chunkId]; + + if (chunkOrAsset.type === 'asset') { + continue; + } + + convertChunkToAmd(chunkOrAsset); + } + } + } +}