const path = require("path"); const {build} = require("vite"); const {babel, getBabelOutputPlugin} = require('@rollup/plugin-babel'); const {createFilter} = require("@rollup/pluginutils"); const { rollup } = require('rollup'); const { nodeResolve } = require('@rollup/plugin-node-resolve'); const commonjs = require('@rollup/plugin-commonjs'); const VIRTUAL_ENTRY = "hydrogen:legacy-entry"; const NODE_MODULES_NEEDING_TRANSPILATION = ["es6-promise"]; module.exports = function legacyBuild(entryModuleId, entryImportReplacements, chunkName, extraImports) { let parentRoot; let code; let legacyBundleRef; let legacyBundleFileName; return { name: "hydrogen:legacyBuild", apply: "build", configResolved: config => { parentRoot = config.root; }, async moduleParsed(info) { if (info.id === entryModuleId) { code = info.code; } }, async buildEnd() { if (!code) { throw new Error("couldnt find entry"); } for (const [importSource, newImportSource] of Object.entries(entryImportReplacements)) { code = replaceImport(this, code, importSource, newImportSource); } code = prependExtraImports(code, extraImports); const bundleCode = await buildLegacyChunk(parentRoot, chunkName, code); legacyBundleRef = this.emitFile({ type: "asset", source: bundleCode, name: `${chunkName}.js` }); }, generateBundle() { if (!legacyBundleRef) { throw new Error("no bundle"); } legacyBundleFileName = this.getFileName(legacyBundleRef); }, transformIndexHtml: { transform(html) { if (!legacyBundleFileName) { throw new Error("no bundle"); } return [{ tag: "script", attrs: {type: "text/javascript", nomodule: true, src: legacyBundleFileName}, injectTo: "head" }]; }, }, } } /** we replace the imports ourselves instead of relying on rollup-alias or similar, because * we only want to replace imports in the entry module, not anywhere in the import tree. * This allows to create sub classes for the legacy build that can still import * the non-legacy class as a base class, like LegacyPlatform does with Platform.*/ function replaceImport(pluginCtx, code, importSource, newImportSource) { const ast = pluginCtx.parse(code); for (const node of ast.body) { if (node.type === "ImportDeclaration") { const sourceNode = node.source; if (sourceNode.value === importSource) { code = code.substr(0, sourceNode.start) + JSON.stringify(newImportSource) + code.substr(sourceNode.end); return code; } } } throw new Error(`Could not find import ${JSON.stringify(importSource)} to replace`); } function prependExtraImports(code, extraImports) { return extraImports.map(i => `import ${JSON.stringify(i)};`).join("\n") + code; } async function buildLegacyChunk(root, chunkName, code) { const projectRootDir = path.resolve(path.join(root, "../../..")); const nodeModulesDir = path.join(projectRootDir, "node_modules"); const defaultFilter = createFilter([], [], {resolve: projectRootDir}); const transpiledModuleDirs = NODE_MODULES_NEEDING_TRANSPILATION.map(m => { return path.join(nodeModulesDir, m); }); const filterModule = id => { if (!defaultFilter(id)) { return false; } if (id.endsWith("?url") || id.endsWith("?raw")) { // TODO is this needed return true; } if (transpiledModuleDirs.some(d => id.startsWith(d))) { return true; } if (id.startsWith(nodeModulesDir)) { return false; } return true; }; // compile down to whatever IE 11 needs const babelPlugin = getBabelOutputPlugin({ babelrc: false, compact: false, extensions: [".js", ".ts"], // babelHelpers: 'bundled', presets: [ [ "@babel/preset-env", { modules: false, useBuiltIns: "usage", corejs: "3.4", targets: "IE 11", // we provide our own promise polyfill (es6-promise) // with support for synchronous flushing of // the queue for idb where needed exclude: ["es.promise", "es.promise.all-settled", "es.promise.finally"] } ] ] }); const bundle = await build({ root, configFile: false, logLevel: 'error', build: { write: false, minify: false, target: "esnext", assetsInlineLimit: 0, polyfillModulePreload: false, rollupOptions: { external: id => !filterModule(id), input: { [chunkName]: VIRTUAL_ENTRY }, output: { format: "esm", manualChunks: undefined }, makeAbsoluteExternalsRelative: false, }, }, plugins: [ memoryBabelInputPlugin(VIRTUAL_ENTRY, root, code), babelPlugin ] }); const assets = Array.isArray(bundle.output) ? bundle.output : [bundle.output]; const mainChunk = assets.find(a => a.name === chunkName); const babelCode = mainChunk.code; const bundle2 = await rollup({ plugins: [ memoryBabelInputPlugin(VIRTUAL_ENTRY, root, babelCode), overridesAsRollupPlugin(new Map( [["safe-buffer", "./scripts/package-overrides/safe-buffer/index.js"], ["buffer", "./scripts/package-overrides/buffer/index.js"]]), projectRootDir), commonjs(), nodeResolve(), ], input: { [chunkName]: VIRTUAL_ENTRY } }); const {output} = await bundle2.generate({ format: 'iife', name: `hydrogen` }); const bundledCode = output[0].code; return bundledCode; } function memoryBabelInputPlugin(entryName, dir, code) { return { name: "hydrogen:resolve-legacy-entry", resolveId(id, importer) { if (id === entryName) { return id; } else if (importer === entryName && id.startsWith("./")) { return this.resolve(path.join(dir, id)); } }, load(id) { if (id === entryName) { return code; } }, } } function overridesAsRollupPlugin(mapping, basedir) { return { name: "rewrite-imports", async resolveId (source, importer) { const target = mapping.get(source); if (target) { const resolvedTarget = await this.resolve(path.join(basedir, target)); console.log("resolving", source, resolvedTarget); return resolvedTarget; } } }; }