Skip to content
Closed
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
33 changes: 32 additions & 1 deletion commands/pack.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,37 @@ function printErrorDetails (err, handler) {
if (handler) handler();
}

// Empty the output directory without paying its deletion on the critical path:
// rename it aside (instant) and delete the old tree in the background while the
// build runs. Deleting a populated dist (~6.7k iLib files) costs seconds on
// Windows. Falls back to emptyDir when rename fails (e.g. dist is locked).
function clearOutput (output) {
// Sweep orphans from runs that exited before their background delete finished.
try {
const parent = path.dirname(output);
const prefix = `${path.basename(output)}.old-`;
for (const name of fs.readdirSync(parent)) {
if (name.startsWith(prefix)) {
fs.rm(path.join(parent, name), {recursive: true, force: true}, () => {});
}
}
} catch (_e) {
// best-effort cleanup
}

if (!fs.existsSync(output)) {
return fs.ensureDir(output);
}
const trash = `${output}.old-${process.pid}-${Date.now()}`;
return fs
.rename(output, trash)
.then(() => {
fs.rm(trash, {recursive: true, force: true}, () => {});
return fs.ensureDir(output);
})
.catch(() => fs.emptyDir(output));
}

function buildArgs (opts) {
const args = ['--context', app.context];
if (opts.production) args.push('--production');
Expand Down Expand Up @@ -160,7 +191,7 @@ function api (opts = {}) {
console.log('Creating an optimized production build...');
}

return fs.emptyDir(output).then(() => {
return clearOutput(output).then(() => {
const build = spawnBunScript('build.mjs', buildArgs({...opts, output}), {cwd: app.context});
if (opts.watch) {
return build;
Expand Down
13 changes: 13 additions & 0 deletions config/bun/build-options.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,19 @@ function getResolveAliases (context) {
// Package may be absent (e.g. scheduler before install); skip.
}
}
// react-is must understand the bundled React's element symbols. Apps rarely
// depend on it directly, so context resolution often fails and the bare
// import inside prop-types would fall back to its nested react-is@16, which
// cannot recognize React 19 elements — every PropTypes.node check would
// then emit "expected a ReactNode". Fall back to the CLI's React-19-aware
// copy (this was the pre-migration behavior).
if (!aliases['react-is']) {
try {
aliases['react-is'] = path.dirname(require.resolve('react-is/package.json'));
} catch (_e) {
// leave unaliased
}
}
if (fs.existsSync(path.join(context, 'node_modules', '@enact', 'i18n', 'ilib'))) {
aliases.ilib = '@enact/i18n/ilib';
} else {
Expand Down
11 changes: 5 additions & 6 deletions config/bun/build.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ async function buildReactGlobals (options, _buildOpts) {
}
}

function finalizeBuild (result, buildOpts, options) {
async function finalizeBuild (result, buildOpts, options) {
if (!result.success) {
for (const log of result.logs) console.error(log);
return null;
Expand All @@ -201,11 +201,10 @@ function finalizeBuild (result, buildOpts, options) {
}
}

nodeRequire('./post-build.js').applyPostBuild(options.context, options.outputPath, {
await nodeRequire('./post-build.js').applyPostBuild(options.context, options.outputPath, {
publicPath: options.publicPath,
ilibAdditionalResourcesPath: options.ilibAdditionalResourcesPath,
customSkin: options.customSkin,
watch: buildOpts.watch
customSkin: options.customSkin
});

writeIndexHtml(options.outputPath, {
Expand Down Expand Up @@ -319,7 +318,7 @@ async function runBuild (buildOpts) {
}

const result = await Bun.build(buildConfig);
const info = finalizeBuild(result, buildOpts, options);
const info = await finalizeBuild(result, buildOpts, options);
if (!info && !buildOpts.watch) process.exit(1);
if (info) logBuildResult(buildOpts, options, info);

Expand All @@ -341,7 +340,7 @@ async function runWatchLoop (buildOpts, options, buildConfig) {
building = true;
try {
const result = await Bun.build(buildConfig);
const rebuildInfo = finalizeBuild(result, buildOpts, options);
const rebuildInfo = await finalizeBuild(result, buildOpts, options);
if (rebuildInfo) {
console.log('Recompiled successfully.');
logBuildResult(buildOpts, options, rebuildInfo);
Expand Down
10 changes: 4 additions & 6 deletions config/bun/dev-server.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -94,10 +94,9 @@ await runBuild(buildOpts, entryFile, plugins, cacheDir);
writeDevHtml(cacheDir, {title: buildOpts.title, publicPath: buildOpts.publicPath, customSkin: buildOpts.customSkin});
// Copy iLib locale/resources (and webOS meta) into the serve output — same as pack.
// Without this, XHR to /node_modules/ilib/locale/* 404s in the browser.
nodeRequire('./post-build.js').applyPostBuild(buildOpts.context, cacheDir, {
await nodeRequire('./post-build.js').applyPostBuild(buildOpts.context, cacheDir, {
ilibAdditionalResourcesPath: buildOpts.ilibAdditionalResourcesPath,
customSkin: buildOpts.customSkin,
watch: true
customSkin: buildOpts.customSkin
});

const publicDir = path.join(buildOpts.context, 'public');
Expand All @@ -121,10 +120,9 @@ async function rebuildDevBundle () {
try {
await runBuild(buildOpts, entryFile, plugins, cacheDir);
writeDevHtml(cacheDir, {title: buildOpts.title, publicPath: buildOpts.publicPath, customSkin: buildOpts.customSkin});
nodeRequire('./post-build.js').applyPostBuild(buildOpts.context, cacheDir, {
await nodeRequire('./post-build.js').applyPostBuild(buildOpts.context, cacheDir, {
ilibAdditionalResourcesPath: buildOpts.ilibAdditionalResourcesPath,
customSkin: buildOpts.customSkin,
watch: true
customSkin: buildOpts.customSkin
});
console.log('Rebuilt.');
} catch (err) {
Expand Down
131 changes: 84 additions & 47 deletions config/bun/framework.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,45 +16,23 @@ const ROOT_PACKAGES = [
'react/jsx-dev-runtime'
];

const FRAMEWORK_IGNORE = [
'**/webpack.config.js',
'**/eslint.config.js',
'**/karma.conf.js',
'**/build/**/*.*',
'**/dist/**/*.*',
'**/@enact/dev-utils/**/*.*',
'**/@enact/docs-utils/**/*.*',
'**/@enact/storybook-utils/**/*.*',
'**/@enact/ui-test-utils/**/*.*',
'**/@enact/screenshot-test-utils/**/*.*',
'**/ilib/localedata/**/*.*',
'**/node_modules/**/*.*',
'**/samples/**/*.*',
'**/tests/**/*.*',
'**/ilib-node*.js',
'**/AsyncNodeLoader.js',
'**/NodeLoader.js',
'**/RhinoLoader.js',
'**/react-dom/cjs/react-dom-server.node.*'
];

const ILIB_IGNORE = [
'!node_modules',
'!locale',
'**/ilib-node*.js',
'**/AsyncNodeLoader.js',
'**/NodeLoader.js',
'**/RhinoLoader.js'
];

function isFrameworkModuleFile (file) {
return !/(^|[/\\])test[/\\]|\.test\.(js|jsx|es6)$|[-.]specs?\.(js|jsx|es6)$|\.bak(?:\.|\/|\\)/.test(file);
}

function getModuleId (nodeModules, file) {
const absPath = path.join(nodeModules, file);
let dir = absPath;
const parent = path.dirname(absPath);

// dir/index.js identifies dir itself — independent of package.json. The
// check must be scoped to the immediate parent only: components without a
// package.json (e.g. limestone/Chips) would otherwise ascend and claim an
// ancestor id, shadowing the package root in the framework registry.
if (/^index\.(js|jsx|es6)$/.test(path.basename(absPath)) && parent.length > nodeModules.length) {
return path.relative(nodeModules, parent).replace(/\\/g, '/');
}

let dir = parent;
while (dir.startsWith(nodeModules) && dir.length > nodeModules.length) {
const pkgPath = path.join(dir, 'package.json');
if (fs.existsSync(pkgPath)) {
Expand All @@ -69,9 +47,6 @@ function getModuleId (nodeModules, file) {
} catch (_e) {
// ignore invalid package.json
}
if (/[/\\]index\.(js|jsx|es6)$/.test(absPath)) {
return path.relative(nodeModules, dir).replace(/\\/g, '/');
}
}
dir = path.dirname(dir);
}
Expand Down Expand Up @@ -147,20 +122,82 @@ function getThemeLocalModules (context) {
});
}

// Packages excluded from the framework bundle at the package level.
const EXCLUDED_FRAMEWORK_PACKAGES = new Set([
'dev-utils',
'docs-utils',
'storybook-utils',
'ui-test-utils',
'screenshot-test-utils'
]);

// Directories never descended into when collecting framework sources. Pruning
// at descent time matters: fast-glob's ignore filters entries but still walks
// each package's real node_modules tree (~30s per enumeration).
const WALK_PRUNE_DIRS = new Set([
'node_modules',
'tests',
'__tests__',
'samples',
'dist',
'build',
'coverage',
'localedata',
'locale',
'.git'
]);

const WALK_SKIP_FILES = /(?:webpack|eslint)\.config\.js$|karma\.conf\.js$|ilib-node[^/\\]*\.js$|AsyncNodeLoader\.js$|NodeLoader\.js$|RhinoLoader\.js$|react-dom-server\.node/;

function walkPackageDir (dir, relative, results) {
let entries;
try {
entries = fs.readdirSync(dir, {withFileTypes: true});
} catch (_e) {
return;
}
for (const entry of entries) {
const rel = relative ? `${relative}/${entry.name}` : entry.name;
if (entry.isDirectory()) {
// Nested junctions (e.g. @enact/i18n/ilib) are directories via stat;
// use lstat-based check to avoid re-entering linked trees.
if (WALK_PRUNE_DIRS.has(entry.name) || entry.isSymbolicLink()) continue;
walkPackageDir(path.join(dir, entry.name), rel, results);
} else if (/\.(js|jsx|es6)$/.test(entry.name) && !WALK_SKIP_FILES.test(rel)) {
results.push(rel);
}
}
}

// Collect one package's sources by realpathing its root first. In dev
// workspaces @enact packages are junctions into the framework repo; globbing
// through the junction with followSymbolicLinks:false silently skips the whole
// package (enact.js then ships without the framework), while following links
// globally recurses into nested node_modules junctions and never finishes.
function listPackageFiles (baseDir, name) {
const pkgPath = path.join(baseDir, name);
let realDir;
try {
realDir = fs.realpathSync(pkgPath);
} catch (_e) {
return [];
}
const results = [];
walkPackageDir(realDir, '', results);
return results.filter(isFrameworkModuleFile).map(file => `${name}/${file}`);
}

function getFrameworkModuleRequests (context, options = {}) {
const nodeModules = path.join(context, 'node_modules');
const enactFiles = fastGlob.sync('@enact/**/*.@(js|jsx|es6)', {
cwd: nodeModules,
onlyFiles: true,
ignore: FRAMEWORK_IGNORE,
followSymbolicLinks: false
});
const ilibFiles = fastGlob.sync('ilib/**/*.@(js|jsx|es6)', {
cwd: nodeModules,
onlyFiles: true,
ignore: ILIB_IGNORE,
followSymbolicLinks: false
});
const enactScope = path.join(nodeModules, '@enact');
const enactFiles = [];
if (fs.existsSync(enactScope)) {
for (const name of fs.readdirSync(enactScope)) {
if (EXCLUDED_FRAMEWORK_PACKAGES.has(name)) continue;
enactFiles.push(...listPackageFiles(nodeModules, `@enact/${name}`));
}
}
const ilibFiles = listPackageFiles(nodeModules, 'ilib');

const modules = new Map();
for (const pkg of ROOT_PACKAGES) {
Expand Down
76 changes: 58 additions & 18 deletions config/bun/ilib-meta.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,25 +103,65 @@ function readManifestFiles (manifestPath) {
return data.files || [];
}

function emitManifestAssets (context, output, manifestPath, cache) {
// Copy one manifest's assets with bounded-concurrency async I/O. The iLib
// locale tree is ~6.7k small JSON files; sequential copySync with per-file
// ensureDirSync/existsSync took ~30s per build on Windows — this is the
// single largest cost of a warm `enact pack`.
const COPY_CONCURRENCY = 64;

async function statOrNull (filePath) {
try {
return await fs.promises.stat(filePath);
} catch (_e) {
return null;
}
}

async function copyManifestFile (src, dest, cache) {
const srcStat = await statOrNull(src);
if (!srcStat) return;
if (cache) {
const destStat = await statOrNull(dest);
if (destStat && srcStat.mtimeMs <= destStat.mtimeMs) {
return;
}
}
await fs.promises.copyFile(src, dest);
}

async function runWithConcurrency (tasks, limit) {
let next = 0;
const workers = Array.from({length: Math.min(limit, tasks.length)}, async () => {
while (next < tasks.length) {
const index = next++;
await tasks[index]();
}
});
await Promise.all(workers);
}

async function emitManifestAssets (context, output, manifestPath, cache) {
const dir = path.dirname(manifestPath);
const relManifest = transformPath(context, manifestPath);
const outManifest = path.join(output, relManifest);
fs.ensureDirSync(path.dirname(outManifest));

if (!cache || !fs.existsSync(outManifest) || fs.statSync(manifestPath).mtimeMs > fs.statSync(outManifest).mtimeMs) {
fs.copySync(manifestPath, outManifest, {dereference: true});
const files = readManifestFiles(manifestPath);
const entries = [{src: manifestPath, dest: outManifest}];
for (const file of files) {
const src = path.join(dir, file);
entries.push({src, dest: path.join(output, transformPath(context, src))});
}

for (const file of readManifestFiles(manifestPath)) {
const src = path.join(dir, file);
const dest = path.join(output, transformPath(context, src));
if (!fs.existsSync(src)) continue;
fs.ensureDirSync(path.dirname(dest));
if (!cache || !fs.existsSync(dest) || fs.statSync(src).mtimeMs > fs.statSync(dest).mtimeMs) {
fs.copySync(src, dest, {dereference: true});
}
// Create each output directory once, not per file.
const dirs = new Set(entries.map(entry => path.dirname(entry.dest)));
for (const dirPath of dirs) {
fs.mkdirSync(dirPath, {recursive: true});
}

await runWithConcurrency(
entries.map(entry => () => copyManifestFile(entry.src, entry.dest, cache)),
COPY_CONCURRENCY
);
}

function ensureManifest (manifestPath, create) {
Expand Down Expand Up @@ -171,7 +211,7 @@ function getIlibDefines (context, publicPath, options = {}) {
return defines;
}

function applyIlibResources (context, output, options = {}) {
async function applyIlibResources (context, output, options = {}) {
const ilibDir = options.ilib || findIlibPath(context);
if (!ilibDir) return;

Expand All @@ -198,11 +238,11 @@ function applyIlibResources (context, output, options = {}) {
}
}

for (const manifest of manifests) {
if (fs.existsSync(manifest)) {
emitManifestAssets(context, output, manifest, cache);
}
}
await Promise.all(
manifests
.filter(manifest => fs.existsSync(manifest))
.map(manifest => emitManifestAssets(context, output, manifest, cache))
);
}

module.exports = {getIlibDefines, applyIlibResources, findIlibPath, resolveIlibFsPath, bundleConst};
Loading
Loading