flecks/packages/core/build/server.js
2024-01-20 03:58:07 -06:00

415 lines
12 KiB
JavaScript

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