From 8c4d1e779611ce7aff5c81a562c3ab772f131ee2 Mon Sep 17 00:00:00 2001 From: Dan Ichim Date: Mon, 13 Jul 2026 13:31:00 +0300 Subject: [PATCH 1/6] bun init --- bun-plugins/framework.js | 25 ++++ bun-plugins/index.js | 6 + bun-plugins/post-build.js | 18 +++ bun-plugins/prerender.js | 7 + bun-plugins/snapshot.js | 18 +++ index.js | 5 + plugins/PrerenderPlugin/index.js | 136 ++++++++++++++++++ plugins/PrerenderPlugin/vdom-server-render.js | 86 +++++++---- 8 files changed, 275 insertions(+), 26 deletions(-) create mode 100644 bun-plugins/framework.js create mode 100644 bun-plugins/index.js create mode 100644 bun-plugins/post-build.js create mode 100644 bun-plugins/prerender.js create mode 100644 bun-plugins/snapshot.js diff --git a/bun-plugins/framework.js b/bun-plugins/framework.js new file mode 100644 index 0000000..a908f54 --- /dev/null +++ b/bun-plugins/framework.js @@ -0,0 +1,25 @@ +const path = require('path'); +const fs = require('fs-extra'); + +function resolveCliFramework () { + const roots = [ + path.join(__dirname, '..', '..', 'cli', 'config', 'bun', 'framework.js'), + path.join(__dirname, '..', '..', '..', 'cli', 'config', 'bun', 'framework.js') + ]; + for (const frameworkPath of roots) { + if (fs.existsSync(frameworkPath)) { + return require(frameworkPath); + } + } + return null; +} + +async function applyFramework (options = {}) { + const cliFramework = resolveCliFramework(); + if (cliFramework) { + return cliFramework.applyFramework(options); + } + throw new Error('Enact CLI Bun framework module not found.'); +} + +module.exports = {applyFramework}; diff --git a/bun-plugins/index.js b/bun-plugins/index.js new file mode 100644 index 0000000..1034dfb --- /dev/null +++ b/bun-plugins/index.js @@ -0,0 +1,6 @@ +module.exports = { + applyPostBuild: require('./post-build').applyPostBuild, + applyPrerender: require('./prerender').applyPrerender, + applyFramework: require('./framework').applyFramework, + applySnapshot: require('./snapshot').applySnapshot +}; diff --git a/bun-plugins/post-build.js b/bun-plugins/post-build.js new file mode 100644 index 0000000..dbc4a46 --- /dev/null +++ b/bun-plugins/post-build.js @@ -0,0 +1,18 @@ +const path = require('path'); + +function resolveCliModule (subpath) { + const roots = [ + path.join(__dirname, '..', '..', 'cli', subpath), + path.join(__dirname, '..', '..', '..', 'cli', subpath) + ]; + for (const candidate of roots) { + try { + return require(candidate); + } catch (e) { + // continue + } + } + throw new Error('Unable to resolve CLI Bun module: ' + subpath); +} + +module.exports = resolveCliModule('config/bun/post-build.js'); diff --git a/bun-plugins/prerender.js b/bun-plugins/prerender.js new file mode 100644 index 0000000..865a894 --- /dev/null +++ b/bun-plugins/prerender.js @@ -0,0 +1,7 @@ +const PrerenderPlugin = require('../plugins/PrerenderPlugin'); + +function applyPrerender (options = {}) { + return PrerenderPlugin.applyBunPostBuild(options); +} + +module.exports = {applyPrerender, parseLocales: PrerenderPlugin.parseLocales}; diff --git a/bun-plugins/snapshot.js b/bun-plugins/snapshot.js new file mode 100644 index 0000000..7dbef02 --- /dev/null +++ b/bun-plugins/snapshot.js @@ -0,0 +1,18 @@ +const path = require('path'); + +function resolveCliModule (subpath) { + const roots = [ + path.join(__dirname, '..', '..', 'cli', subpath), + path.join(__dirname, '..', '..', '..', 'cli', subpath) + ]; + for (const candidate of roots) { + try { + return require(candidate); + } catch (e) { + // continue + } + } + throw new Error('Unable to resolve CLI Bun module: ' + subpath); +} + +module.exports = resolveCliModule('config/bun/snapshot.js'); diff --git a/index.js b/index.js index acb88a2..ff061dd 100644 --- a/index.js +++ b/index.js @@ -28,3 +28,8 @@ exportOnDemand({ VerboseLogPlugin: () => require('./plugins/VerboseLogPlugin'), WebOSMetaPlugin: () => require('./plugins/WebOSMetaPlugin') }); + +// Export Bun build helpers. +exportOnDemand({ + bunPlugins: () => require('./bun-plugins') +}); diff --git a/plugins/PrerenderPlugin/index.js b/plugins/PrerenderPlugin/index.js index 85c6376..73c2016 100644 --- a/plugins/PrerenderPlugin/index.js +++ b/plugins/PrerenderPlugin/index.js @@ -622,4 +622,140 @@ function emitAsset(compilation, name, data) { // Usage: PrerenderPlugin.getHooks(compilation).HOOK_NAME.tapAsync('YourPluginName', () => { ... }); PrerenderPlugin.getHooks = getPrerenderPluginHooks; +function stageBunChunk(code, opts) { + if (/__webpack_require__\.e\s*=/.test(code)) { + code = code.replace('__webpack_require__.e =', '__webpack_require__.e = function() {}; var origE ='); + } + if (/function webpackAsyncContext\(req\)/.test(code)) { + code = code.replace( + 'function webpackAsyncContext(req) {', + 'function webpackAsyncContext(req) {\n\treturn new Promise(function() {});' + ); + } + + // Prevent browser-only startup paths from running under Node during prerender. + code = code.replace(/typeof\s+window\s*!==?\s*["']undefined["']/g, 'false'); + code = code.replace(/typeof\s+window\s*===?\s*["']undefined["']/g, 'true'); + code = code.replace(/typeof\s+document\s*!==?\s*["']undefined["']/g, 'false'); + code = code.replace(/typeof\s+document\s*===?\s*["']undefined["']/g, 'true'); + code = code.replace(/typeof\s+window\s*<\s*["']u["']/g, 'false'); + code = code.replace(/typeof\s+document\s*<\s*["']u["']/g, 'false'); + + code = code.replace(/(?]*class="((?!enact-locale-)[^"])*)(\senact-locale-[^"]*)"/i, + (match, before, s, classAttr) => { + status.attr[i].classes = classAttr; + return before + '"'; + } + ); + const index = status.prerender.indexOf(appHtml); + if (index === -1) { + status.prerender[i] = appHtml; + } else { + status.alias[i] = locales[index]; + } + } catch (e) { + status.err = {locale: locales[i], result: e}; + break; + } + } + vdomServer.unstage(); + if (status.err) { + throw status.err.result; + } + if (status.alias.some(Boolean)) { + simplifyAliases(locales, status); + } + + const indexPath = path.join(opts.output, 'index.html'); + let html = fs.readFileSync(indexPath, {encoding: 'utf8'}); + const jsAssets = opts.startupAssets || [ + path.posix.join(opts.publicPath || '/', opts.chunk).replace(/\/{2,}/g, '/') + ]; + const startupScript = templates.startup(opts.screenTypes, jsAssets); + html = html.replace(/type="module"/g, 'type="text/javascript"'); + html = html.replace(/<\/head>/i, match => `\t\n\t${match}`); + + const applyToRoot = rootInjection(html); + for (let i = 0; i < locales.length; i++) { + if (!status.prerender[i] || status.alias[i]) continue; + const linked = Object.keys(status.alias).filter(key => status.alias[key] === locales[i]); + let mapping; + if (linked.length === 0) { + status.prerender[i] = status.prerender[i].replace( + /(]*class="[^"]*)"/i, + '$1' + status.attr[i].classes + '"' + ); + } else { + mapping = linked.reduce( + (m, c) => Object.assign(m, {[locales[c].toLowerCase()]: status.attr[c]}), + {} + ); + } + const appHtml = parsePrerender(status.prerender[i]); + const updater = templates.update(mapping, opts.deep, appHtml.prerender); + let localeHtml = applyToRoot(appHtml.prerender); + if (updater) { + localeHtml = localeHtml.replace(/<\/body>/i, match => `\t\n\t${match}`); + } + if (locales.length === 1) { + html = localeHtml; + } else { + fs.writeFileSync(path.join(opts.output, 'index.' + locales[i] + '.html'), localeHtml, {encoding: 'utf8'}); + } + } + fs.writeFileSync(indexPath, html, {encoding: 'utf8'}); + + if (opts.mapfile && locales.length > 1) { + const mapper = (m, c, i) => + status.alias.includes(c) ? m : Object.assign(m, {[c]: `index.${status.alias[i] || c}.html`}); + const mapping = {fallback: 'index.html', locales: locales.reduce(mapper, {})}; + fs.writeFileSync(path.join(opts.output, opts.mapfile), JSON.stringify(mapping, null, '\t'), {encoding: 'utf8'}); + } +} + +PrerenderPlugin.applyBunPostBuild = applyBunPostBuild; +PrerenderPlugin.parseLocales = parseLocales; + module.exports = PrerenderPlugin; diff --git a/plugins/PrerenderPlugin/vdom-server-render.js b/plugins/PrerenderPlugin/vdom-server-render.js index d79d0f0..98bef7e 100644 --- a/plugins/PrerenderPlugin/vdom-server-render.js +++ b/plugins/PrerenderPlugin/vdom-server-render.js @@ -16,17 +16,31 @@ require('console.mute'); let chunkTarget; let prerenderCache; -import('find-cache-directory').then(({default: findCacheDirectory}) => { - prerenderCache = path.join( - findCacheDirectory({ - name: 'enact-dev', - create: true - }), - 'prerender' - ); - - if (!fs.existsSync(prerenderCache)) fs.mkdirSync(prerenderCache); -}); +function clearReactModules () { + Object.keys(require.cache) + .filter(c => /[\\/]node_modules[\\/]react(-dom)?[\\/]/.test(c)) + .forEach(c => delete require.cache[c]); +} + +function getPrerenderCache () { + if (prerenderCache) return prerenderCache; + + try { + const findCacheDirectory = require('find-cache-directory'); + prerenderCache = path.join( + findCacheDirectory({ + name: 'enact-dev', + create: true + }), + 'prerender' + ); + } catch (e) { + prerenderCache = path.join(process.cwd(), 'node_modules', '.cache', 'enact-dev', 'prerender'); + } + + fs.mkdirSync(prerenderCache, {recursive: true}); + return prerenderCache; +} // Skip using the polyfills embedded within the bundle and instead use a local core-js, // since the bundle's target may differ in compatibility from the active Node process @@ -57,7 +71,7 @@ module.exports = { 'require("' + path.resolve(path.join(opts.externals, 'enact.js')) + '")' ); } - chunkTarget = path.join(prerenderCache, opts.chunk); + chunkTarget = path.join(getPrerenderCache(), opts.chunk); fs.writeFileSync(chunkTarget, code, {encoding: 'utf8'}); }, @@ -102,26 +116,46 @@ module.exports = { global.process.env.LANG = opts.locale; + clearReactModules(); + if (opts.externals) { - // Ensure locale switching support is loaded globally with external framework usage. - const framework = requireUncached(path.resolve(path.join(opts.externals, 'enact.js'))); + const frameworkPath = path.resolve(path.join(opts.externals, 'enact.js')); + let framework = requireUncached(frameworkPath); + if (framework && typeof framework.default === 'function') { + framework = framework.default; + } + if (typeof framework !== 'function') { + const previousFramework = global.enact_framework; + delete global.enact_framework; + requireUncached(frameworkPath); + framework = global.enact_framework; + if (previousFramework !== undefined) { + global.enact_framework = previousFramework; + } else { + delete global.enact_framework; + } + } + if (typeof framework !== 'function') { + throw new Error('External Enact framework must export enact_framework(id).'); + } + global.enact_framework = framework; global.React = framework('react'); - } else { - delete global.React; + } else if (!global.React) { + global.React = require('react'); } - const chunk = requireUncached(path.resolve(chunkTarget)); - - // Clear any server-related children modules from cache - Object.keys(require.cache) - .filter(c => c.startsWith(path.dirname(opts.server))) - .forEach(c => delete require.cache[c]); + reroute('react', global.React); - // Use the specified server, optionally with exposed React, and generate HTML string - if (global.React) reroute('react', global.React); - const server = requireUncached(opts.server); + const chunk = requireUncached(path.resolve(chunkTarget)); + let server; + if (opts.externals && global.enact_framework) { + const domServer = global.enact_framework('react-dom/server'); + server = domServer.default || domServer; + } else { + server = requireUncached(opts.server); + } rendered = server.renderToString(chunk['default'] || chunk); - if (global.React) reroute.stop('react'); + reroute.stop('react'); if (style) { rendered = '\n' + style + '\n' + rendered; From ac1b0445156077494ddf5ebfd638a6b35cba3a37 Mon Sep 17 00:00:00 2001 From: Dan Ichim Date: Tue, 14 Jul 2026 15:23:19 +0300 Subject: [PATCH 2/6] fix prerender --- plugins/PrerenderPlugin/FileXHR.js | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/plugins/PrerenderPlugin/FileXHR.js b/plugins/PrerenderPlugin/FileXHR.js index b9f0cc4..524b1e5 100644 --- a/plugins/PrerenderPlugin/FileXHR.js +++ b/plugins/PrerenderPlugin/FileXHR.js @@ -1,5 +1,9 @@ const fs = require('fs'); +function escapeRegExp (string) { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + function FileXHR() {} FileXHR.prototype.open = function (method, uri, async) { @@ -15,15 +19,15 @@ FileXHR.prototype.addEventListener = function (evt, fn) { FileXHR.prototype.send = function () { if (this.method.toUpperCase() === 'GET' && this.uri && this.sync) { if (process.env.ILIB_BASE_PATH) { - // Backward compatability for old iLib location - if (/i18n[/\\]ilib[/\\]*$/.test(process.env.ILIB_BASE_PATH)) { - this.uri = this.uri.replace( - new RegExp('^' + process.env.ILIB_BASE_PATH), - 'node_modules/@enact/i18n/ilib' - ); - } else { - this.uri = this.uri.replace(new RegExp('^' + process.env.ILIB_BASE_PATH), 'node_modules/ilib'); - } + const replacement = process.env.ILIB_FS_PATH || ( + /i18n[/\\]ilib[/\\]*$/.test(process.env.ILIB_BASE_PATH) + ? 'node_modules/@enact/i18n/ilib' + : 'node_modules/ilib' + ); + this.uri = this.uri.replace( + new RegExp('^' + escapeRegExp(process.env.ILIB_BASE_PATH)), + replacement + ); } const parsedURI = this.uri.replace(/\\/g, '/').replace(/^(_\/)+/g, match => match.replace(/_/g, '..')); try { From 7025a3bedd4d4edcde8a22e2f253495e3c23278b Mon Sep 17 00:00:00 2001 From: Dan Ichim Date: Tue, 14 Jul 2026 16:30:01 +0300 Subject: [PATCH 3/6] issue prerender --- plugins/PrerenderPlugin/FileXHR.js | 71 ++++++++++++++++++++++++------ 1 file changed, 57 insertions(+), 14 deletions(-) diff --git a/plugins/PrerenderPlugin/FileXHR.js b/plugins/PrerenderPlugin/FileXHR.js index 524b1e5..60f4be7 100644 --- a/plugins/PrerenderPlugin/FileXHR.js +++ b/plugins/PrerenderPlugin/FileXHR.js @@ -1,10 +1,64 @@ const fs = require('fs'); +const path = require('path'); function escapeRegExp (string) { return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } -function FileXHR() {} +function untransformPath (uri) { + return uri + .replace(/\\/g, '/') + .replace(/(^|\/)(_)($|\/)/g, (match, before, segment, after) => before + '..' + (after || '')); +} + +function resolveIlibReplacement () { + if (process.env.ILIB_FS_PATH) { + return process.env.ILIB_FS_PATH.replace(/\\/g, '/'); + } + + const basePath = (process.env.ILIB_BASE_PATH || '').replace(/\\/g, '/'); + if (/i18n[/\\]ilib[/\\]*$/.test(basePath)) { + return 'node_modules/@enact/i18n/ilib'; + } + + return 'node_modules/ilib'; +} + +function resolveFilePath (uri) { + const cwd = process.env.ILIB_CONTEXT || process.cwd(); + let filePath = uri.replace(/\\/g, '/'); + + if (process.env.ILIB_BASE_PATH) { + const replacement = resolveIlibReplacement(); + filePath = filePath.replace( + new RegExp('^' + escapeRegExp(process.env.ILIB_BASE_PATH.replace(/\\/g, '/'))), + replacement + ); + } + + filePath = untransformPath(filePath); + + if (path.isAbsolute(filePath)) { + return filePath; + } + + const resolved = path.resolve(cwd, filePath); + if (fs.existsSync(resolved)) { + return resolved; + } + + // When linked @enact/i18n/ilib is missing, fall back to standalone ilib. + if (filePath.includes('@enact/i18n/ilib')) { + const fallback = path.resolve(cwd, filePath.replace('@enact/i18n/ilib', 'ilib')); + if (fs.existsSync(fallback)) { + return fallback; + } + } + + return resolved; +} + +function FileXHR () {} FileXHR.prototype.open = function (method, uri, async) { this.method = method; @@ -18,20 +72,9 @@ FileXHR.prototype.addEventListener = function (evt, fn) { FileXHR.prototype.send = function () { if (this.method.toUpperCase() === 'GET' && this.uri && this.sync) { - if (process.env.ILIB_BASE_PATH) { - const replacement = process.env.ILIB_FS_PATH || ( - /i18n[/\\]ilib[/\\]*$/.test(process.env.ILIB_BASE_PATH) - ? 'node_modules/@enact/i18n/ilib' - : 'node_modules/ilib' - ); - this.uri = this.uri.replace( - new RegExp('^' + escapeRegExp(process.env.ILIB_BASE_PATH)), - replacement - ); - } - const parsedURI = this.uri.replace(/\\/g, '/').replace(/^(_\/)+/g, match => match.replace(/_/g, '..')); + const parsedURI = resolveFilePath(this.uri); try { - if (!fs.existsSync(parsedURI)) throw new Error('File not found: ' + this.uri); + if (!fs.existsSync(parsedURI)) throw new Error('File not found: ' + parsedURI); this.response = this.responseText = fs.readFileSync(parsedURI, {encoding: 'utf8'}); this.status = 200; From 6abeaf43461d5593273490b19126798926863c60 Mon Sep 17 00:00:00 2001 From: Dan Ichim Date: Tue, 14 Jul 2026 22:00:14 +0300 Subject: [PATCH 4/6] tv local iso --- plugins/PrerenderPlugin/FileXHR.js | 90 ++++++++++++++++++++++++++++-- 1 file changed, 84 insertions(+), 6 deletions(-) diff --git a/plugins/PrerenderPlugin/FileXHR.js b/plugins/PrerenderPlugin/FileXHR.js index 60f4be7..53babd9 100644 --- a/plugins/PrerenderPlugin/FileXHR.js +++ b/plugins/PrerenderPlugin/FileXHR.js @@ -11,12 +11,25 @@ function untransformPath (uri) { .replace(/(^|\/)(_)($|\/)/g, (match, before, segment, after) => before + '..' + (after || '')); } +const ILIB_URL_PREFIXES = [ + '/node_modules/ilib', + '/node_modules/@enact/i18n/ilib', + '/node_modules/_enact/i18n/ilib', + 'node_modules/ilib', + 'node_modules/@enact/i18n/ilib', + 'node_modules/_enact/i18n/ilib' +]; + +function normalizePathSlashes (filePath) { + return filePath.replace(/\\/g, '/'); +} + function resolveIlibReplacement () { if (process.env.ILIB_FS_PATH) { - return process.env.ILIB_FS_PATH.replace(/\\/g, '/'); + return normalizePathSlashes(process.env.ILIB_FS_PATH); } - const basePath = (process.env.ILIB_BASE_PATH || '').replace(/\\/g, '/'); + const basePath = normalizePathSlashes(process.env.ILIB_BASE_PATH || ''); if (/i18n[/\\]ilib[/\\]*$/.test(basePath)) { return 'node_modules/@enact/i18n/ilib'; } @@ -24,21 +37,87 @@ function resolveIlibReplacement () { return 'node_modules/ilib'; } +function getIlibUrlPrefixes () { + const prefixes = [ + process.env.ILIB_BASE_PATH, + ...ILIB_URL_PREFIXES + ] + .filter(Boolean) + .map(normalizePathSlashes); + + return [...new Set(prefixes)]; +} + +function tryResolvePath (candidate) { + return fs.existsSync(candidate) ? candidate : null; +} + +function resolveFromIlibPrefix (filePath, prefix, cwd) { + if (!prefix || !filePath.startsWith(prefix)) { + return null; + } + + const suffix = filePath.slice(prefix.length).replace(/^\//, ''); + const fsPath = process.env.ILIB_FS_PATH; + + if (fsPath) { + const fromFs = tryResolvePath(path.join(fsPath, suffix)); + if (fromFs) { + return fromFs; + } + } + + const replacement = resolveIlibReplacement(); + const relative = normalizePathSlashes(path.join(replacement, suffix)); + + let resolved = tryResolvePath(path.resolve(cwd, relative)); + if (resolved) { + return resolved; + } + + if (relative.includes('@enact/i18n/ilib')) { + resolved = tryResolvePath(path.resolve(cwd, relative.replace('@enact/i18n/ilib', 'ilib'))); + if (resolved) { + return resolved; + } + } + + return null; +} + +function isWebRootNodeModulesPath (filePath) { + return /^\/node_modules\//.test(filePath); +} + function resolveFilePath (uri) { const cwd = process.env.ILIB_CONTEXT || process.cwd(); - let filePath = uri.replace(/\\/g, '/'); + let filePath = normalizePathSlashes(uri); + + for (const prefix of getIlibUrlPrefixes()) { + const resolved = resolveFromIlibPrefix(filePath, prefix, cwd); + if (resolved) { + return resolved; + } + } if (process.env.ILIB_BASE_PATH) { const replacement = resolveIlibReplacement(); filePath = filePath.replace( - new RegExp('^' + escapeRegExp(process.env.ILIB_BASE_PATH.replace(/\\/g, '/'))), + new RegExp('^' + escapeRegExp(normalizePathSlashes(process.env.ILIB_BASE_PATH))), replacement ); } filePath = untransformPath(filePath); - if (path.isAbsolute(filePath)) { + if (isWebRootNodeModulesPath(filePath)) { + const resolved = tryResolvePath(path.resolve(cwd, filePath.replace(/^\//, ''))); + if (resolved) { + return resolved; + } + } + + if (path.isAbsolute(filePath) && !isWebRootNodeModulesPath(filePath)) { return filePath; } @@ -47,7 +126,6 @@ function resolveFilePath (uri) { return resolved; } - // When linked @enact/i18n/ilib is missing, fall back to standalone ilib. if (filePath.includes('@enact/i18n/ilib')) { const fallback = path.resolve(cwd, filePath.replace('@enact/i18n/ilib', 'ilib')); if (fs.existsSync(fallback)) { From bacf58141fb79396417b996edafc51965e894dc5 Mon Sep 17 00:00:00 2001 From: Dan Ichim Date: Wed, 15 Jul 2026 12:55:20 +0300 Subject: [PATCH 5/6] ilib iso --- plugins/PrerenderPlugin/index.js | 3 +- plugins/PrerenderPlugin/vdom-server-render.js | 100 ++++++++++++++++-- 2 files changed, 96 insertions(+), 7 deletions(-) diff --git a/plugins/PrerenderPlugin/index.js b/plugins/PrerenderPlugin/index.js index 73c2016..adaddc8 100644 --- a/plugins/PrerenderPlugin/index.js +++ b/plugins/PrerenderPlugin/index.js @@ -678,7 +678,8 @@ function applyBunPostBuild(options = {}) { server, locale: locales[i], externals: opts.externals, - fontGenerator: opts.fontGenerator + fontGenerator: opts.fontGenerator, + context: opts.context }; let appHtml = vdomServer.render(renderOpts); status.attr[i] = {classes: ''}; diff --git a/plugins/PrerenderPlugin/vdom-server-render.js b/plugins/PrerenderPlugin/vdom-server-render.js index 98bef7e..463d89e 100644 --- a/plugins/PrerenderPlugin/vdom-server-render.js +++ b/plugins/PrerenderPlugin/vdom-server-render.js @@ -16,10 +16,80 @@ require('console.mute'); let chunkTarget; let prerenderCache; -function clearReactModules () { +const ILIB_LOCALEMATCH_GLOBAL = '__ENACT_PRERENDER_LOCALEMATCH__'; +const ILIB_DATA_INIT_PATTERN = /cache: \{\} \}, typeof module2 != "undefined" && \(module2\.exports = ilib, module2\.exports\.ilib = ilib\)/g; +const ILIB_DATA_INIT_SEED = 'cache: {} }, global.' + ILIB_LOCALEMATCH_GLOBAL + '&&(ilib.data.localematch=global.' + ILIB_LOCALEMATCH_GLOBAL + '), typeof module2 != "undefined" && (module2.exports = ilib, module2.exports.ilib = ilib)'; + +function readLocaleMatchData () { + const fsPath = process.env.ILIB_FS_PATH || process.env.ILIB_BASE_PATH; + if (!fsPath) return null; + + const localeMatchPath = path.join(fsPath, 'locale', 'localematch.json'); + if (!fs.existsSync(localeMatchPath)) return null; + + try { + const localematch = JSON.parse(fs.readFileSync(localeMatchPath, {encoding: 'utf8'})); + return localematch && localematch.likelyLocales ? localematch : null; + } catch (_e) { + return null; + } +} + +function resetIlibGlobalState () { + // ilib.js uses `var ilib = ilib || {}`, so repeated prerender passes share one global + // singleton. After many locale renders, cached localematch data can become incomplete. + if (global.ilib) { + if (global.ilib.data && typeof global.ilib.clearCache === 'function') { + global.ilib.clearCache(); + } + delete global.ilib; + } +} + +function seedIlibLocaleMatch () { + const localematch = readLocaleMatchData(); + if (!localematch) return; + + global.ilib = global.ilib || {}; + global.ilib.data = global.ilib.data || {}; + global.ilib.data.localematch = localematch; + if (global.ilib._load) { + global.ilib._load._cacheValidated = false; + } +} + +function injectBundledIlibLocaleMatchSeed (code) { + return code.replace(ILIB_DATA_INIT_PATTERN, ILIB_DATA_INIT_SEED); +} + +function resolveFromContext (moduleName, context) { + if (!context) { + return require.resolve(moduleName); + } + return require.resolve(moduleName, {paths: [path.join(context, 'node_modules')]}); +} + +function clearPrerenderModules (serverPath) { + const chunkPath = chunkTarget ? path.resolve(chunkTarget) : null; + const serverDir = serverPath ? path.dirname(path.resolve(serverPath)) : null; + Object.keys(require.cache) - .filter(c => /[\\/]node_modules[\\/]react(-dom)?[\\/]/.test(c)) + .filter(c => { + if (chunkPath && path.resolve(c) === chunkPath) return true; + if (serverDir && c.startsWith(serverDir)) return true; + return /[\\/]node_modules[\\/](react(-dom)?|ilib|@enact)([\\/]|$)/.test(c); + }) .forEach(c => delete require.cache[c]); + + try { + reroute.stop('react'); + } catch (_e) { + // ignore if react was not rerouted yet + } + + delete global.React; + delete global.enact_framework; + delete global.enactHooks; } function getPrerenderCache () { @@ -71,6 +141,7 @@ module.exports = { 'require("' + path.resolve(path.join(opts.externals, 'enact.js')) + '")' ); } + code = injectBundledIlibLocaleMatchSeed(code); chunkTarget = path.join(getPrerenderCache(), opts.chunk); fs.writeFileSync(chunkTarget, code, {encoding: 'utf8'}); }, @@ -100,7 +171,7 @@ module.exports = { console.mute(); try { - const generator = require(path.resolve(opts.fontGenerator)); + const generator = requireUncached(path.resolve(opts.fontGenerator)); const locale = opts.locale || 'en-US'; style = generator(locale); if (generator.fontOverrideGenerator) style += generator.fontOverrideGenerator(locale); @@ -116,7 +187,12 @@ module.exports = { global.process.env.LANG = opts.locale; - clearReactModules(); + clearPrerenderModules(opts.server); + + if (opts.locale) { + resetIlibGlobalState(); + seedIlibLocaleMatch(); + } if (opts.externals) { const frameworkPath = path.resolve(path.join(opts.externals, 'enact.js')); @@ -140,13 +216,25 @@ module.exports = { } global.enact_framework = framework; global.React = framework('react'); - } else if (!global.React) { - global.React = require('react'); + } else { + global.React = requireUncached(resolveFromContext('react', opts.context)); } reroute('react', global.React); + let localeMatchSeed; + if (opts.locale) { + localeMatchSeed = readLocaleMatchData(); + if (localeMatchSeed) { + global[ILIB_LOCALEMATCH_GLOBAL] = localeMatchSeed; + } + } + const chunk = requireUncached(path.resolve(chunkTarget)); + + if (localeMatchSeed) { + delete global[ILIB_LOCALEMATCH_GLOBAL]; + } let server; if (opts.externals && global.enact_framework) { const domServer = global.enact_framework('react-dom/server'); From 83d19f92dbe38d3bfa7d8d9cbc18429dbbe91234 Mon Sep 17 00:00:00 2001 From: Dan Ichim Date: Thu, 23 Jul 2026 17:57:02 +0300 Subject: [PATCH 6/6] review changes --- bun-plugins/framework.js | 6 +- bun-plugins/post-build.js | 2 +- bun-plugins/prerender.js | 2 +- bun-plugins/snapshot.js | 2 +- plugins/PrerenderPlugin/FileXHR.js | 39 +++---- plugins/PrerenderPlugin/index.js | 29 ++--- plugins/PrerenderPlugin/vdom-server-render.js | 110 ++++++++++++++++-- 7 files changed, 135 insertions(+), 55 deletions(-) diff --git a/bun-plugins/framework.js b/bun-plugins/framework.js index a908f54..9ce248f 100644 --- a/bun-plugins/framework.js +++ b/bun-plugins/framework.js @@ -1,7 +1,7 @@ const path = require('path'); -const fs = require('fs-extra'); +const fs = require('fs'); -function resolveCliFramework () { +function resolveCliFramework() { const roots = [ path.join(__dirname, '..', '..', 'cli', 'config', 'bun', 'framework.js'), path.join(__dirname, '..', '..', '..', 'cli', 'config', 'bun', 'framework.js') @@ -14,7 +14,7 @@ function resolveCliFramework () { return null; } -async function applyFramework (options = {}) { +async function applyFramework(options = {}) { const cliFramework = resolveCliFramework(); if (cliFramework) { return cliFramework.applyFramework(options); diff --git a/bun-plugins/post-build.js b/bun-plugins/post-build.js index dbc4a46..492c6b1 100644 --- a/bun-plugins/post-build.js +++ b/bun-plugins/post-build.js @@ -1,6 +1,6 @@ const path = require('path'); -function resolveCliModule (subpath) { +function resolveCliModule(subpath) { const roots = [ path.join(__dirname, '..', '..', 'cli', subpath), path.join(__dirname, '..', '..', '..', 'cli', subpath) diff --git a/bun-plugins/prerender.js b/bun-plugins/prerender.js index 865a894..4f073b6 100644 --- a/bun-plugins/prerender.js +++ b/bun-plugins/prerender.js @@ -1,6 +1,6 @@ const PrerenderPlugin = require('../plugins/PrerenderPlugin'); -function applyPrerender (options = {}) { +function applyPrerender(options = {}) { return PrerenderPlugin.applyBunPostBuild(options); } diff --git a/bun-plugins/snapshot.js b/bun-plugins/snapshot.js index 7dbef02..951983b 100644 --- a/bun-plugins/snapshot.js +++ b/bun-plugins/snapshot.js @@ -1,6 +1,6 @@ const path = require('path'); -function resolveCliModule (subpath) { +function resolveCliModule(subpath) { const roots = [ path.join(__dirname, '..', '..', 'cli', subpath), path.join(__dirname, '..', '..', '..', 'cli', subpath) diff --git a/plugins/PrerenderPlugin/FileXHR.js b/plugins/PrerenderPlugin/FileXHR.js index 53babd9..8fb5c1a 100644 --- a/plugins/PrerenderPlugin/FileXHR.js +++ b/plugins/PrerenderPlugin/FileXHR.js @@ -1,11 +1,11 @@ const fs = require('fs'); const path = require('path'); -function escapeRegExp (string) { +function escapeRegExp(string) { return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } -function untransformPath (uri) { +function untransformPath(uri) { return uri .replace(/\\/g, '/') .replace(/(^|\/)(_)($|\/)/g, (match, before, segment, after) => before + '..' + (after || '')); @@ -20,11 +20,11 @@ const ILIB_URL_PREFIXES = [ 'node_modules/_enact/i18n/ilib' ]; -function normalizePathSlashes (filePath) { +function normalizePathSlashes(filePath) { return filePath.replace(/\\/g, '/'); } -function resolveIlibReplacement () { +function resolveIlibReplacement() { if (process.env.ILIB_FS_PATH) { return normalizePathSlashes(process.env.ILIB_FS_PATH); } @@ -37,22 +37,17 @@ function resolveIlibReplacement () { return 'node_modules/ilib'; } -function getIlibUrlPrefixes () { - const prefixes = [ - process.env.ILIB_BASE_PATH, - ...ILIB_URL_PREFIXES - ] - .filter(Boolean) - .map(normalizePathSlashes); +function getIlibUrlPrefixes() { + const prefixes = [process.env.ILIB_BASE_PATH, ...ILIB_URL_PREFIXES].filter(Boolean).map(normalizePathSlashes); return [...new Set(prefixes)]; } -function tryResolvePath (candidate) { +function tryResolvePath(candidate) { return fs.existsSync(candidate) ? candidate : null; } -function resolveFromIlibPrefix (filePath, prefix, cwd) { +function resolveFromIlibPrefix(filePath, prefix, cwd) { if (!prefix || !filePath.startsWith(prefix)) { return null; } @@ -85,18 +80,18 @@ function resolveFromIlibPrefix (filePath, prefix, cwd) { return null; } -function isWebRootNodeModulesPath (filePath) { +function isWebRootNodeModulesPath(filePath) { return /^\/node_modules\//.test(filePath); } -function resolveFilePath (uri) { +function resolveFilePath(uri) { const cwd = process.env.ILIB_CONTEXT || process.cwd(); let filePath = normalizePathSlashes(uri); for (const prefix of getIlibUrlPrefixes()) { - const resolved = resolveFromIlibPrefix(filePath, prefix, cwd); - if (resolved) { - return resolved; + const fromPrefix = resolveFromIlibPrefix(filePath, prefix, cwd); + if (fromPrefix) { + return fromPrefix; } } @@ -111,9 +106,9 @@ function resolveFilePath (uri) { filePath = untransformPath(filePath); if (isWebRootNodeModulesPath(filePath)) { - const resolved = tryResolvePath(path.resolve(cwd, filePath.replace(/^\//, ''))); - if (resolved) { - return resolved; + const fromWebRoot = tryResolvePath(path.resolve(cwd, filePath.replace(/^\//, ''))); + if (fromWebRoot) { + return fromWebRoot; } } @@ -136,7 +131,7 @@ function resolveFilePath (uri) { return resolved; } -function FileXHR () {} +function FileXHR() {} FileXHR.prototype.open = function (method, uri, async) { this.method = method; diff --git a/plugins/PrerenderPlugin/index.js b/plugins/PrerenderPlugin/index.js index adaddc8..34ea72e 100644 --- a/plugins/PrerenderPlugin/index.js +++ b/plugins/PrerenderPlugin/index.js @@ -633,17 +633,9 @@ function stageBunChunk(code, opts) { ); } - // Prevent browser-only startup paths from running under Node during prerender. - code = code.replace(/typeof\s+window\s*!==?\s*["']undefined["']/g, 'false'); - code = code.replace(/typeof\s+window\s*===?\s*["']undefined["']/g, 'true'); - code = code.replace(/typeof\s+document\s*!==?\s*["']undefined["']/g, 'false'); - code = code.replace(/typeof\s+document\s*===?\s*["']undefined["']/g, 'true'); - code = code.replace(/typeof\s+window\s*<\s*["']u["']/g, 'false'); - code = code.replace(/typeof\s+document\s*<\s*["']u["']/g, 'false'); - - code = code.replace(/(?/i, match => `\t\n\t${match}`); + html = html.replace( + /<\/head>/i, + match => `\t\n\t${match}` + ); const applyToRoot = rootInjection(html); for (let i = 0; i < locales.length; i++) { @@ -729,16 +724,16 @@ function applyBunPostBuild(options = {}) { '$1' + status.attr[i].classes + '"' ); } else { - mapping = linked.reduce( - (m, c) => Object.assign(m, {[locales[c].toLowerCase()]: status.attr[c]}), - {} - ); + mapping = linked.reduce((m, c) => Object.assign(m, {[locales[c].toLowerCase()]: status.attr[c]}), {}); } const appHtml = parsePrerender(status.prerender[i]); const updater = templates.update(mapping, opts.deep, appHtml.prerender); let localeHtml = applyToRoot(appHtml.prerender); if (updater) { - localeHtml = localeHtml.replace(/<\/body>/i, match => `\t\n\t${match}`); + localeHtml = localeHtml.replace( + /<\/body>/i, + match => `\t\n\t${match}` + ); } if (locales.length === 1) { html = localeHtml; diff --git a/plugins/PrerenderPlugin/vdom-server-render.js b/plugins/PrerenderPlugin/vdom-server-render.js index 463d89e..c43aefa 100644 --- a/plugins/PrerenderPlugin/vdom-server-render.js +++ b/plugins/PrerenderPlugin/vdom-server-render.js @@ -7,6 +7,8 @@ const fs = require('fs'); const path = require('path'); +const vm = require('vm'); +const Module = require('module'); const requireUncached = require('import-fresh'); const reroute = require('mock-require'); const FileXHR = require('./FileXHR'); @@ -17,10 +19,98 @@ let chunkTarget; let prerenderCache; const ILIB_LOCALEMATCH_GLOBAL = '__ENACT_PRERENDER_LOCALEMATCH__'; -const ILIB_DATA_INIT_PATTERN = /cache: \{\} \}, typeof module2 != "undefined" && \(module2\.exports = ilib, module2\.exports\.ilib = ilib\)/g; -const ILIB_DATA_INIT_SEED = 'cache: {} }, global.' + ILIB_LOCALEMATCH_GLOBAL + '&&(ilib.data.localematch=global.' + ILIB_LOCALEMATCH_GLOBAL + '), typeof module2 != "undefined" && (module2.exports = ilib, module2.exports.ilib = ilib)'; +const ILIB_DATA_INIT_PATTERN = + /cache: \{\} \}, typeof module2 != "undefined" && \(module2\.exports = ilib, module2\.exports\.ilib = ilib\)/g; +const ILIB_DATA_INIT_SEED = + 'cache: {} }, global.' + + ILIB_LOCALEMATCH_GLOBAL + + '&&(ilib.data.localematch=global.' + + ILIB_LOCALEMATCH_GLOBAL + + '), typeof module2 != "undefined" && (module2.exports = ilib, module2.exports.ilib = ilib)'; + +/** + * Load a staged chunk in a vm context that shares Node require / prerender + * globals but intentionally omits window/document/self so browser-only paths + * see typeof window === 'undefined' without regex-rewriting the bundle text. + */ +function loadStagedChunk(chunkPath) { + const code = fs.readFileSync(chunkPath, {encoding: 'utf8'}); + const moduleObject = {exports: {}}; + const localRequire = Module.createRequire(chunkPath); + const context = { + module: moduleObject, + exports: moduleObject.exports, + require: localRequire, + __filename: chunkPath, + __dirname: path.dirname(chunkPath), + console, + process, + Buffer, + setTimeout, + clearTimeout, + setInterval, + clearInterval, + setImmediate, + clearImmediate, + queueMicrotask, + URL, + URLSearchParams, + Promise, + Object, + Array, + String, + Number, + Boolean, + Symbol, + Math, + Date, + RegExp, + Error, + TypeError, + RangeError, + SyntaxError, + JSON, + Map, + Set, + WeakMap, + WeakSet, + Proxy, + Reflect, + parseInt, + parseFloat, + isNaN, + isFinite, + encodeURIComponent, + decodeURIComponent, + encodeURI, + decodeURI, + // Prerender-shared state (also mirrored onto context.global below) + React: global.React, + enact_framework: global.enact_framework, + enactHooks: global.enactHooks, + ilib: global.ilib, + XMLHttpRequest: global.XMLHttpRequest, + skipPolyfills: global.skipPolyfills + }; + if (global[ILIB_LOCALEMATCH_GLOBAL] !== undefined) { + context[ILIB_LOCALEMATCH_GLOBAL] = global[ILIB_LOCALEMATCH_GLOBAL]; + } + // No window / document / self — keeps DOM startup paths inert. + context.global = context; + context.globalThis = context; + + vm.createContext(context); + vm.runInContext(code, context, { + filename: chunkPath, + displayErrors: true + }); + + return moduleObject.exports && moduleObject.exports.default !== undefined + ? moduleObject.exports + : context.module.exports; +} -function readLocaleMatchData () { +function readLocaleMatchData() { const fsPath = process.env.ILIB_FS_PATH || process.env.ILIB_BASE_PATH; if (!fsPath) return null; @@ -35,7 +125,7 @@ function readLocaleMatchData () { } } -function resetIlibGlobalState () { +function resetIlibGlobalState() { // ilib.js uses `var ilib = ilib || {}`, so repeated prerender passes share one global // singleton. After many locale renders, cached localematch data can become incomplete. if (global.ilib) { @@ -46,7 +136,7 @@ function resetIlibGlobalState () { } } -function seedIlibLocaleMatch () { +function seedIlibLocaleMatch() { const localematch = readLocaleMatchData(); if (!localematch) return; @@ -58,18 +148,18 @@ function seedIlibLocaleMatch () { } } -function injectBundledIlibLocaleMatchSeed (code) { +function injectBundledIlibLocaleMatchSeed(code) { return code.replace(ILIB_DATA_INIT_PATTERN, ILIB_DATA_INIT_SEED); } -function resolveFromContext (moduleName, context) { +function resolveFromContext(moduleName, context) { if (!context) { return require.resolve(moduleName); } return require.resolve(moduleName, {paths: [path.join(context, 'node_modules')]}); } -function clearPrerenderModules (serverPath) { +function clearPrerenderModules(serverPath) { const chunkPath = chunkTarget ? path.resolve(chunkTarget) : null; const serverDir = serverPath ? path.dirname(path.resolve(serverPath)) : null; @@ -92,7 +182,7 @@ function clearPrerenderModules (serverPath) { delete global.enactHooks; } -function getPrerenderCache () { +function getPrerenderCache() { if (prerenderCache) return prerenderCache; try { @@ -230,7 +320,7 @@ module.exports = { } } - const chunk = requireUncached(path.resolve(chunkTarget)); + const chunk = loadStagedChunk(path.resolve(chunkTarget)); if (localeMatchSeed) { delete global[ILIB_LOCALEMATCH_GLOBAL];