feat: enhance build process with product configuration and built-in extensions scanning

This commit is contained in:
Johannes
2026-02-04 09:42:00 +01:00
parent 2b8a35bb03
commit 7cf0cec198

View File

@@ -10,6 +10,9 @@ import { promisify } from 'util';
import glob from 'glob';
import * as watcher from '@parcel/watcher';
import { nlsPlugin, createNLSCollector, finalizeNLS, postProcessNLS } from './nls-plugin.ts';
import { getVersion } from '../lib/getVersion.ts';
import product from '../../product.json' with { type: 'json' };
import packageJson from '../../package.json' with { type: 'json' };
const globAsync = promisify(glob);
@@ -18,6 +21,9 @@ const globAsync = promisify(glob);
// ============================================================================
const REPO_ROOT = path.dirname(path.dirname(import.meta.dirname));
const commit = getVersion(REPO_ROOT);
const quality = (product as { quality?: string }).quality;
const version = (quality && quality !== 'stable') ? `${packageJson.version}-${quality}` : packageJson.version;
// CLI: transpile [--watch] | bundle [--minify] [--nls] [--out <dir>]
const command = process.argv[2]; // 'transpile' or 'bundle'
@@ -340,6 +346,94 @@ async function cleanDir(dir: string): Promise<void> {
await fs.promises.mkdir(fullPath, { recursive: true });
}
/**
* Scan for built-in extensions in the given directory.
* Returns an array of extension entries for the builtinExtensionsScannerService.
*/
function scanBuiltinExtensions(extensionsRoot: string): Array<{ extensionPath: string; packageJSON: unknown }> {
const result: Array<{ extensionPath: string; packageJSON: unknown }> = [];
const extensionsPath = path.join(REPO_ROOT, extensionsRoot);
if (!fs.existsSync(extensionsPath)) {
return result;
}
for (const entry of fs.readdirSync(extensionsPath, { withFileTypes: true })) {
if (!entry.isDirectory()) {
continue;
}
const packageJsonPath = path.join(extensionsPath, entry.name, 'package.json');
if (fs.existsSync(packageJsonPath)) {
try {
const packageJSON = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
result.push({
extensionPath: entry.name,
packageJSON
});
} catch (e) {
// Skip invalid extensions
}
}
}
return result;
}
/**
* Get the date from the out directory date file, or return current date.
*/
function readISODate(outDir: string): string {
try {
return fs.readFileSync(path.join(REPO_ROOT, outDir, 'date'), 'utf8');
} catch {
return new Date().toISOString();
}
}
/**
* Creates a file content mapper for builds.
* This transforms JS files to inject build-time configuration.
* The placeholders get bundled into all entry points that import these modules.
*/
function createFileContentMapper(outDir: string, target: BuildTarget): (filePath: string, content: string) => string {
// Cache the replacement strings (computed once)
let productConfigReplacement: string | undefined;
let builtinExtensionsReplacement: string | undefined;
return (_filePath: string, content: string): string => {
// Inject product configuration (placeholder gets bundled into many files)
if (content.includes('/*BUILD->INSERT_PRODUCT_CONFIGURATION*/')) {
if (productConfigReplacement === undefined) {
// For server-web, remove webEndpointUrlTemplate
const productForTarget = target === 'server-web'
? { ...product, webEndpointUrlTemplate: undefined }
: product;
const productConfiguration = JSON.stringify({
...productForTarget,
version,
commit,
date: readISODate(outDir)
});
// Remove the outer braces since the placeholder is inside an object literal
productConfigReplacement = productConfiguration.substring(1, productConfiguration.length - 1);
}
content = content.replace('/*BUILD->INSERT_PRODUCT_CONFIGURATION*/', () => productConfigReplacement!);
}
// Inject built-in extensions list (placeholder gets bundled into files importing the scanner)
if (content.includes('/*BUILD->INSERT_BUILTIN_EXTENSIONS*/')) {
if (builtinExtensionsReplacement === undefined) {
const builtinExtensions = JSON.stringify(scanBuiltinExtensions('.build/extensions'));
// Remove the outer brackets since the placeholder is inside an array literal
builtinExtensionsReplacement = builtinExtensions.substring(1, builtinExtensions.length - 1);
}
content = content.replace('/*BUILD->INSERT_BUILTIN_EXTENSIONS*/', () => builtinExtensionsReplacement!);
}
return content;
};
}
/**
* Only used to make encoding tests happy. The source files don't have a BOM but the
* tests expect one... so we add it here.
@@ -715,6 +809,9 @@ ${tslib}`,
indexMap = nlsResult.indexMap;
}
// Create file content mapper for web builds (injects product config, builtin extensions)
const fileContentMapper = createFileContentMapper(outDir, target);
// Post-process and write all output files
let bundled = 0;
for (const { result } of buildResults) {
@@ -725,10 +822,18 @@ ${tslib}`,
for (const file of result.outputFiles) {
await fs.promises.mkdir(path.dirname(file.path), { recursive: true });
if (doNls && file.path.endsWith('.js') && indexMap.size > 0) {
// Post-process JS files to replace NLS placeholders with indices
const processed = postProcessNLS(file.text, indexMap, preserveEnglish);
await fs.promises.writeFile(file.path, processed);
if (file.path.endsWith('.js')) {
let content = file.text;
// Apply NLS post-processing if enabled
if (doNls && indexMap.size > 0) {
content = postProcessNLS(content, indexMap, preserveEnglish);
}
// Apply file content mapping (product config, builtin extensions)
content = fileContentMapper(file.path, content);
await fs.promises.writeFile(file.path, content);
} else {
// Write other files (source maps, etc.) as-is
await fs.promises.writeFile(file.path, file.contents);