const {realpath} = require('fs/promises'); const {dirname, join} = require('path'); const babelmerge = require('babel-merge'); const set = require('lodash.set'); const D = require('./debug'); const explicate = require('./explicate'); const {Flecks} = require('./flecks'); const loadConfig = require('./load-config'); const Resolver = require('./resolver'); const { FLECKS_CORE_ROOT = process.cwd(), } = process.env; const debug = D('@flecks/core/build/bootstrap'); const debugSilly = debug.extend('silly'); function environmentalize(path) { return path // - `@flecks/core` -> `flecks_core` .replace(/[^a-zA-Z0-9]/g, '_') .replace(/_*(.*)_*/, '$1'); } function environmentConfiguration(config) { const keys = Object.keys(process.env); Object.keys(config) .sort((l, r) => (l < r ? 1 : -1)) .forEach((fleck) => { const prefix = `FLECKS_ENV__${environmentalize(fleck)}`; keys .filter((key) => key.startsWith(`${prefix}__`)) .map((key) => { debug('reading environment from %s...', key); return [key.slice(prefix.length + 2), process.env[key]]; }) .map(([subkey, value]) => [subkey.split('_'), value]) .forEach(([path, jsonOrString]) => { try { set(config, [fleck, ...path], JSON.parse(jsonOrString)); debug('read (%s) as JSON', jsonOrString); } catch (error) { set(config, [fleck, ...path], jsonOrString); debug('read (%s) as string', jsonOrString); } }); }); return config; } module.exports = class Server extends Flecks { aliased = {}; buildConfigs = {}; platforms = ['server']; compiled = {}; resolver = new Resolver(); roots = {}; async babel() { const merging = [ { plugins: ['@babel/plugin-syntax-dynamic-import'], presets: [ [ '@babel/preset-env', { shippedProposals: true, targets: { esmodules: true, node: 'current', }, }, ], ], }, ]; merging.push({configFile: await this.resolveBuildConfig('babel.config.js')}); merging.push(...this.invokeFlat('@flecks/core.babel')); return babelmerge.all(merging); } static async buildRuntime(originalConfig, platforms, flecks = {}) { const dealiasedConfig = Object.fromEntries( Object.entries(originalConfig) .map(([maybeAliasedPath, config]) => { const index = maybeAliasedPath.indexOf(':'); return [ -1 === index ? maybeAliasedPath : maybeAliasedPath.slice(0, index), config, ]; }), ); const resolver = new Resolver(); const explication = await explicate( Object.keys(originalConfig), { platforms, resolver, root: FLECKS_CORE_ROOT, importer: (request) => require(request), }, ); const runtime = { config: environmentConfiguration( Object.fromEntries( Object.values(explication.descriptors) .map(({path}) => [path, dealiasedConfig[path] || {}]), ), ), flecks: Object.fromEntries( Object.values(explication.descriptors) .map(({path, request}) => [path, flecks[path] || explication.roots[request] || {}]), ), }; const aliased = {}; const compiled = {}; const reverseRequest = Object.fromEntries( Object.entries(explication.descriptors) .map(([, {path, request}]) => [request, path]), ); const roots = Object.fromEntries( (await Promise.all(Object.entries(explication.roots) .map(async ([request, bootstrap]) => { const packageRequest = await realpath(await resolver.resolve(join(request, 'package.json'))); const realDirname = dirname(packageRequest); const {dependencies = {}, devDependencies = {}} = require(packageRequest); let source; let root; // One of ours? if ( [].concat( Object.keys(dependencies), Object.keys(devDependencies), ) .includes('@flecks/fleck') ) { root = realDirname.endsWith('/dist') ? realDirname.slice(0, -5) : realDirname; source = join(root, 'src'); } else { root = realDirname; source = realDirname; } return [ reverseRequest[request], { bootstrap, request, root, source, }, ]; }))) // Reverse sort for greedy root matching. .sort(([l], [r]) => (l < r ? 1 : -1)), ); await Promise.all( Object.entries(explication.descriptors) .map(async ([, {path, request}]) => { if (path !== request) { aliased[path] = request; } const [root, requestRoot] = Object.entries(roots) .find(([, {request: rootRequest}]) => request.startsWith(rootRequest)) || []; if (requestRoot && compiled[requestRoot.root]) { return; } let resolvedRequest = await resolver.resolve(request); if (!resolvedRequest) { if (!requestRoot) { return; } resolvedRequest = await resolver.resolve(join(requestRoot.root, 'package.json')); } const realResolvedRequest = await realpath(resolvedRequest); if (path !== request || resolvedRequest !== realResolvedRequest) { if (requestRoot) { if (!compiled[requestRoot.root]) { compiled[requestRoot.root] = { flecks: [], path: root, root: requestRoot.root, source: requestRoot.source, }; } compiled[requestRoot.root].flecks.push(path); } } }), ); return { aliased, compiled, resolver, roots, runtime, }; } get extensions() { return this.invokeFlat('@flecks/core.exts').flat(); } static async from( { config: configParameter, flecks: configFlecks, platforms = ['server'], } = {}, ) { // Load or use parameterized configuration. let originalConfig; let configType = 'parameter'; if (!configParameter) { // eslint-disable-next-line no-param-reassign [configType, originalConfig] = await loadConfig(); } else { originalConfig = JSON.parse(JSON.stringify(configParameter)); } debug('bootstrap configuration (%s)', configType); debugSilly(originalConfig); const { aliased, compiled, resolver, roots, runtime, } = await this.buildRuntime(originalConfig, platforms, configFlecks); const flecks = super.from(runtime); flecks.aliased = aliased; flecks.compiled = compiled; flecks.platforms = platforms; flecks.roots = roots; flecks.resolver = resolver; flecks.loadBuildConfigs(); return flecks; } loadBuildConfigs() { Object.entries(this.invoke('@flecks/core.build.config')) .forEach(([fleck, configs]) => { configs.forEach((config) => { this.buildConfigs[config] = fleck; }); }); debugSilly('build configs loaded: %O', this.buildConfigs); } get realiasedConfig() { return Object.fromEntries( Object.entries(this.config) .map(([path, config]) => { const alias = this.aliased[path]; return [alias ? `${path}:${alias}` : path, config]; }), ); } async resolveBuildConfig(config, override) { const fleck = this.buildConfigs[config]; if (!fleck) { throw new Error(`Unknown build config: '${config}'`); } const rootConfig = await this.resolver.resolve(join(FLECKS_CORE_ROOT, 'build', config)); if (rootConfig) { return rootConfig; } if (override) { const overrideConfig = await this.resolver.resolve(join(override, 'build', config)); if (overrideConfig) { return overrideConfig; } } return this.resolver.resolve(join(fleck, 'build', config)); } async runtimeCompiler(runtime, config, {allowlist = []} = {}) { // Compile? const needCompilation = Object.entries(this.compiled); if (needCompilation.length > 0) { const babelConfig = await this.babel(); const includes = []; // Alias and de-externalize. await Promise.all( needCompilation .map(async ([ root, { flecks, path, resolved, source, }, ]) => { flecks.forEach((fleck) => { allowlist.push(fleck); }); debugSilly('%s runtime de-externalized %s, alias: %s', runtime, root, resolved); // Alias. config.resolve.alias[path] = source || resolved; // Root aliases. if (root) { config.resolve.alias[ join(root, 'node_modules') ] = join(FLECKS_CORE_ROOT, 'node_modules'); config.resolve.fallback[path] = root; } includes.push(root || resolved); }), ); // Compile. config.module.rules.push( { test: /\.(m?jsx?)?$/, include: includes, use: [ { loader: require.resolve('babel-loader'), options: { cacheDirectory: true, babelrc: false, configFile: false, ...babelConfig, }, }, ], }, ); // Aliases. Object.entries(this.aliased) .forEach(([from, to]) => { if ( !Object.entries(this.compiled) .some(([, {flecks}]) => flecks.includes(from)) ) { config.resolve.alias[from] = to; } }); // Our very own lil' chunk. set(config, 'optimization.splitChunks.cacheGroups.flecks-compiled', { chunks: 'all', enforce: true, priority: 100, test: new RegExp( `(?:${ includes .map((path) => path.replace(/[\\/]/g, '[\\/]')).join('|') })`, ), }); } } get stubs() { return Object.values(this.flecks) .reduce( (r, {stubs = {}}) => ( r.concat( Object.entries(stubs) .reduce( (r, [platform, stubs]) => ( r.concat(this.platforms.includes(platform) ? stubs : []) ), [], ), ) ), [], ).flat(); } get targets() { const targets = this.invoke('@flecks/core.targets'); const duplicates = {}; const entries = Object.entries(targets); const set = new Set(); entries .forEach(([fleck, targets]) => { targets.forEach((target) => { if (set.has(target)) { if (!duplicates[target]) { duplicates[target] = []; } duplicates[target].push(fleck); } set.add(target); }); }); const errorMessage = Object.entries(duplicates).map(([target, flecks]) => ( `Multiple flecks ('${flecks.join("', '")})' tried to build target '${target}'` )).join('\n'); if (errorMessage) { throw new Error(`@flecks/core.targets:\n${errorMessage}`); } this.invoke('@flecks/core.targets.alter', set); return entries .map(([fleck, targets]) => ( targets .filter((target) => set.has(target)) .map((target) => [fleck, target]) )).flat(); } };