diff --git a/build/next/index.ts b/build/next/index.ts index a4cb6e86119..7500c56bb7c 100644 --- a/build/next/index.ts +++ b/build/next/index.ts @@ -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 ] const command = process.argv[2]; // 'transpile' or 'bundle' @@ -340,6 +346,94 @@ async function cleanDir(dir: string): Promise { 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);