Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions bun-plugins/framework.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
const path = require('path');
const fs = require('fs');

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};
6 changes: 6 additions & 0 deletions bun-plugins/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module.exports = {
applyPostBuild: require('./post-build').applyPostBuild,
applyPrerender: require('./prerender').applyPrerender,
applyFramework: require('./framework').applyFramework,
applySnapshot: require('./snapshot').applySnapshot
};
18 changes: 18 additions & 0 deletions bun-plugins/post-build.js
Original file line number Diff line number Diff line change
@@ -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');
7 changes: 7 additions & 0 deletions bun-plugins/prerender.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const PrerenderPlugin = require('../plugins/PrerenderPlugin');

function applyPrerender(options = {}) {
return PrerenderPlugin.applyBunPostBuild(options);
}

module.exports = {applyPrerender, parseLocales: PrerenderPlugin.parseLocales};
18 changes: 18 additions & 0 deletions bun-plugins/snapshot.js
Original file line number Diff line number Diff line change
@@ -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');
5 changes: 5 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,8 @@ exportOnDemand({
VerboseLogPlugin: () => require('./plugins/VerboseLogPlugin'),
WebOSMetaPlugin: () => require('./plugins/WebOSMetaPlugin')
});

// Export Bun build helpers.
exportOnDemand({
bunPlugins: () => require('./bun-plugins')
});
146 changes: 133 additions & 13 deletions plugins/PrerenderPlugin/FileXHR.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,135 @@
const fs = require('fs');
const path = require('path');

function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

function untransformPath(uri) {
return uri
.replace(/\\/g, '/')
.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 normalizePathSlashes(process.env.ILIB_FS_PATH);
}

const basePath = normalizePathSlashes(process.env.ILIB_BASE_PATH || '');
if (/i18n[/\\]ilib[/\\]*$/.test(basePath)) {
return 'node_modules/@enact/i18n/ilib';
}

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 = normalizePathSlashes(uri);

for (const prefix of getIlibUrlPrefixes()) {
const fromPrefix = resolveFromIlibPrefix(filePath, prefix, cwd);
if (fromPrefix) {
return fromPrefix;
}
}

if (process.env.ILIB_BASE_PATH) {
const replacement = resolveIlibReplacement();
filePath = filePath.replace(
new RegExp('^' + escapeRegExp(normalizePathSlashes(process.env.ILIB_BASE_PATH))),
replacement
);
}

filePath = untransformPath(filePath);

if (isWebRootNodeModulesPath(filePath)) {
const fromWebRoot = tryResolvePath(path.resolve(cwd, filePath.replace(/^\//, '')));
if (fromWebRoot) {
return fromWebRoot;
}
}

if (path.isAbsolute(filePath) && !isWebRootNodeModulesPath(filePath)) {
return filePath;
}

const resolved = path.resolve(cwd, filePath);
if (fs.existsSync(resolved)) {
return resolved;
}

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() {}

Expand All @@ -14,20 +145,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) {
// 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 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;
Expand Down
132 changes: 132 additions & 0 deletions plugins/PrerenderPlugin/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -622,4 +622,136 @@ 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() {});'
);
}

// Do not regex-replace bare window/document — that corrupts string literals
// (e.g. "window" → "undefined"). vdom-server-render loads the chunk in a vm
// context that omits those globals instead.
vdomServer.stage(code, opts);
}

function applyBunPostBuild(options = {}) {
const opts = Object.assign(
{
chunk: 'main.js',
locales: 'en-US',
mapfile: 'locale-map.json',
publicPath: '/',
screenTypes: [],
deep: null,
externals: null,
fontGenerator: null,
externalStartup: false
},
options
);
const chunkPath = path.join(opts.output, opts.chunk);
if (!fs.existsSync(chunkPath)) {
throw new Error('PrerenderPlugin: Unable to find build chunk at ' + chunkPath);
}
const code = fs.readFileSync(chunkPath, {encoding: 'utf8'});
const locales = parseLocales(opts.context, opts.locales);
const server = opts.server || path.join(opts.context, 'node_modules', 'react-dom', 'server.js');
const status = {prerender: [], attr: [], alias: []};

stageBunChunk(code, {chunk: opts.chunk, externals: opts.externals});
for (let i = 0; i < locales.length; i++) {
try {
const renderOpts = {
server,
locale: locales[i],
externals: opts.externals,
fontGenerator: opts.fontGenerator,
context: opts.context
};
let appHtml = vdomServer.render(renderOpts);
status.attr[i] = {classes: ''};
appHtml = appHtml.replace(
/(<div[^>]*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<script type="text/javascript">${startupScript.trim()}</script>\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(
/(<div[^>]*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<script type="text/javascript">${updater.trim()}</script>\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;
Loading
Loading