refactor: bootstrap

This commit is contained in:
cha0s 2022-03-10 12:05:35 -06:00
parent cd62140a32
commit 47a3f19881

View File

@ -7,6 +7,7 @@ import {
basename,
dirname,
extname,
isAbsolute,
join,
resolve,
} from 'path';
@ -20,6 +21,7 @@ import Flecks from '../flecks';
const {
FLECKS_CORE_ROOT = process.cwd(),
FLECKS_YML = 'flecks.yml',
} = process.env;
const debug = D('@flecks/core/flecks/server');
@ -28,62 +30,10 @@ export default class ServerFlecks extends Flecks {
constructor(options = {}) {
super(options);
const {
resolver = {},
rcs = {},
} = options;
const keys = Object.keys(process.env);
// Reverse-sorting means e.g. `@flecks/core/server` comes before `@flecks/core`.
// We want to select the most specific match.
//
// `FLECKS_ENV_FLECKS_CORE_SERVER_VARIABLE` is ambiguous as it can equate to both:
// - `flecks.set('@flecks/core.server.variable');`
// - `flecks.set('@flecks/core/server.variable');`
//
// The latter will take precedence.
const seen = [];
Object.keys(this.flecks)
.sort((l, r) => (l < r ? 1 : -1))
.forEach((fleck) => {
const prefix = `FLECKS_ENV_${this.constructor.environmentalize(fleck)}`;
keys
.filter((key) => key.startsWith(`${prefix}_`) && -1 === seen.indexOf(key))
.map((key) => {
seen.push(key);
debug('reading environment from %s...', key);
return [key, process.env[key]];
})
.map(([key, value]) => [key.slice(prefix.length + 1), value])
.map(([subkey, value]) => [subkey.split('_'), value])
.forEach(([path, jsonOrString]) => {
try {
this.set([fleck, ...path], JSON.parse(jsonOrString));
debug('read (%s) as JSON', jsonOrString);
}
catch (error) {
this.set([fleck, ...path], jsonOrString);
debug('read (%s) as string', jsonOrString);
}
});
});
this.buildConfigs = Object.fromEntries(
Object.entries(this.invoke('@flecks/core.build.config'))
.map(([fleck, configs]) => (
configs.map((config) => {
const defaults = {
fleck,
root: FLECKS_CORE_ROOT,
};
if (Array.isArray(config)) {
return [config[0], {...defaults, ...config[1]}];
}
return [config, defaults];
})
))
.flat(),
);
this.resolver = resolver;
this.rcs = rcs;
this.overrideConfigFromEnvironment();
this.buildConfigs = this.loadBuildConfigs();
this.resolver = options.resolver || {};
this.rcs = options.rcs || {};
}
aliases() {
@ -115,53 +65,49 @@ export default class ServerFlecks extends Flecks {
static bootstrap(
{
config,
platforms = ['server'],
root = FLECKS_CORE_ROOT,
without = [],
} = {},
) {
const resolvedRoot = resolve(FLECKS_CORE_ROOT, root);
let initial;
// Load or use parameterized configuration.
let configType;
try {
const {load} = R('js-yaml');
const filename = join(resolvedRoot, 'build', 'flecks.yml');
const buffer = readFileSync(filename, 'utf8');
debug('parsing configuration from YML...');
initial = load(buffer, {filename}) || {};
configType = 'YML';
if (!config) {
// eslint-disable-next-line no-param-reassign
[configType, config] = this.loadConfig(root);
}
catch (error) {
if ('ENOENT' !== error.code) {
throw error;
}
initial = {'@flecks/core': {}, '@flecks/fleck': {}};
configType = 'barebones';
else {
configType = 'parameter';
}
debug('bootstrap configuration (%s): %O', configType, initial);
debug('bootstrap configuration (%s): %O', configType, config);
// Fleck discovery.
const aliased = {};
const config = {};
const resolvedRoot = resolve(FLECKS_CORE_ROOT, root);
const resolver = {};
const keys = Object.keys(initial);
const keys = Object.keys(config);
for (let i = 0; i < keys.length; ++i) {
const key = keys[i];
const index = key.lastIndexOf(':');
const [path, alias] = -1 === index ? [key, key] : [key.slice(0, index), key.slice(index + 1)];
// Parse the alias (if any).
const index = key.indexOf(':');
const [path, alias] = -1 === index
? [key, undefined]
: [key.slice(0, index), key.slice(index + 1)];
// Run it by the exception list.
if (-1 !== without.indexOf(path.split('/').pop())) {
// eslint-disable-next-line no-continue
continue;
}
const aliasPath = '.'.charCodeAt(0) === alias.charCodeAt(0)
? join(resolvedRoot, alias)
: alias;
// Resolve the path (if necessary).
let resolvedPath;
if (alias) {
resolvedPath = isAbsolute(alias) ? alias : join(resolvedRoot, alias);
}
else {
resolvedPath = path;
}
try {
config[path] = initial[key];
R.resolve(aliasPath);
if (path !== alias) {
aliased[path] = aliasPath;
}
resolver[path] = aliasPath;
R.resolve(resolvedPath);
resolver[path] = resolvedPath;
}
// eslint-disable-next-line no-empty
catch (error) {}
@ -169,14 +115,9 @@ export default class ServerFlecks extends Flecks {
if (platforms) {
platforms.forEach((platform) => {
try {
const platformAliasPath = join(aliasPath, platform);
const platformPath = join(path, platform);
R.resolve(platformAliasPath);
if (path !== alias) {
aliased[platformPath] = platformAliasPath;
}
config[platformPath] = config[platformPath] || {};
resolver[platformPath] = platformAliasPath;
const resolvedPlatformPath = join(resolvedPath, platform);
R.resolve(resolvedPlatformPath);
resolver[join(path, platform)] = resolvedPlatformPath;
}
// eslint-disable-next-line no-empty
catch (error) {}
@ -184,86 +125,65 @@ export default class ServerFlecks extends Flecks {
}
}
const paths = Object.keys(resolver);
const rcs = {};
const roots = Array.from(new Set(
paths
.map((path) => this.root(resolver, path))
.filter((e) => !!e),
));
for (let i = 0; i < roots.length; ++i) {
const root = roots[i];
try {
rcs[root] = R(join(root, 'build', '.flecksrc'));
}
catch (error) {
if ('MODULE_NOT_FOUND' !== error.code) {
throw error;
}
}
}
// Load RCs.
const rcs = this.loadRcs(resolver);
debug('rcs: %O', rcs);
// Stub platform-unfriendly modules.
const stubs = this.stubs(platforms, rcs);
// Merge aliases;
const aliases = {
// from fleck configuration above,
...(
Object.fromEntries(
Object.entries(resolver)
.filter(([from, to]) => from !== to),
)
),
// from symlinks,
...(
Object.fromEntries(
paths.filter((path) => this.fleckIsSymlinked(resolver, path))
.map((path) => [path, this.sourcepath(R.resolve(this.resolve(resolver, path)))]),
)
),
// and from RCs.
...this.aliases(rcs),
};
if (Object.keys(aliases).length > 0) {
debug('aliases: %O', aliases);
}
// Stub server-unfriendly modules.
const stubs = this.stubs(['server'], rcs);
if (stubs.length > 0) {
debug('stubbing: %O', stubs);
const regex = new RegExp(stubs.join('|'));
R('pirates').addHook(
() => '',
{
ignoreNodeModules: false,
matcher: (path) => !!path.match(regex),
},
);
}
// Do we need to get up in `require()`'s guts?
if (
Object.keys(aliases).length > 0
|| stubs.length > 0
) {
const {Module} = R('module');
const {require: Mr} = Module.prototype;
Module.prototype.require = function hackedRequire(request, options) {
if (-1 !== stubs.indexOf(request)) {
return undefined;
}
if (aliases[request]) {
return Mr.call(this, aliases[request], options);
}
return Mr.call(this, request, options);
};
}
// Flecks that are aliased or symlinked need compilation.
const flecks = {};
const needCompilation = paths
.filter((path) => (
this.fleckIsAliased(resolver, path) || this.fleckIsSymlinked(resolver, path)
this.fleckIsAliased(resolver, path)
|| this.fleckIsSymlinked(resolver, path)
));
// Lookups redirect require() requests.
const symlinked = paths
.filter((path) => this.fleckIsSymlinked(resolver, path));
if (symlinked.length > 0) {
const lookups = {
...Object.fromEntries(
symlinked
.map((path) => [
R.resolve(path),
this.sourcepath(R.resolve(this.resolve(resolver, path))),
]),
),
};
debug('symlink lookups: %O', lookups);
R('pirates').addHook(
(code, path) => `module.exports = require('${lookups[path]}')`,
{
ignoreNodeModules: false,
// eslint-disable-next-line arrow-body-style
matcher: (path) => {
return !!lookups[path];
},
},
);
}
const {Module} = R('module');
const aliases = this.aliases(rcs);
debug('aliases: %O', aliases);
// Nasty hax to give us FULL CONTROL.
const {require: Mr} = Module.prototype;
const requirers = {
...aliased,
...aliases,
};
Module.prototype.require = function hackedRequire(request, options) {
if (requirers[request]) {
return Mr.call(this, requirers[request], options);
}
return Mr.call(this, request, options);
};
if (needCompilation.length > 0) {
const rcBabel = this.babel(rcs);
debug('.flecksrc: babel: %O', rcBabel);
const register = R('@babel/register');
// Augment the compiler with babel config from flecksrc.
const rcBabelConfig = babelmerge(...this.babel(rcs).map(([, babel]) => babel));
debug('.flecksrc: babel: %O', rcBabelConfig);
// Key flecks needing compilation by their roots, so we can compile all common roots with a
// single invocation of `@babel/register`.
const compilationRootMap = {};
@ -276,9 +196,14 @@ export default class ServerFlecks extends Flecks {
});
// Register a compiler for each root and require() the flecks underneath.
Object.entries(compilationRootMap).forEach(([root, compiling]) => {
debug('compiling: %s', root);
debug('compiling at root: %s', root);
const resolved = dirname(R.resolve(join(root, 'package.json')));
const sourcepath = this.sourcepath(resolved);
const sourceroot = join(sourcepath, '..');
// Load babel config from whichever we find first:
// - The fleck being compiled's build directory
// - The root build directory
// - Finally, the built-in babel config
const configFile = this.resolveBuildConfig(
resolver,
[
@ -290,71 +215,26 @@ export default class ServerFlecks extends Flecks {
'babel.config.js',
],
);
const register = R('@babel/register');
const config = {
// Augment the selected config with the babel config from RCs.
configFile,
// Augment the compiler with babel config from flecksrc.
...babelmerge(...rcBabel.map(([, babel]) => babel)),
ignore: [resolve(join(sourcepath, '..', 'node_modules'))],
only: [resolve(join(sourcepath, '..'))],
sourceMaps: 'inline',
// Target the compiler to avoid unnecessary work.
ignore: [resolve(join(sourceroot, 'node_modules'))],
only: [resolve(sourceroot)],
};
debug("require('@babel/register')(%O)", config);
register({
...config,
...rcBabelConfig,
// Make webpack goodies exist in nodespace.
plugins: [
[
'prepend',
{
prepend: [
'require.context = (',
' directory,',
' useSubdirectories = true,',
' regExp = /^\\.\\/.*$/,',
' mode = "sync",',
') => {',
' const glob = require("glob");',
' const {resolve, sep} = require("path");',
' const keys = glob.sync(',
' useSubdirectories ? "**/*" : "*",',
' {cwd: resolve(__dirname, directory)},',
' )',
' .filter((key) => key.match(regExp))',
' .map(',
' (key) => (',
' -1 !== [".".charCodeAt(0), "/".charCodeAt(0)].indexOf(key.charCodeAt(0))',
' ? key',
' : ("." + sep + key)',
' ),',
' );',
' const R = (request) => require(keys[request]);',
' R.id = __filename',
' R.keys = () => keys;',
' return R;',
'};',
].join('\n'),
},
'require.context',
],
[
'prepend',
{
prepend: 'const __non_webpack_require__ = require;',
},
'__non_webpack_require__',
],
[
'prepend',
{
prepend: "require('source-map-support/register');",
},
'source-map-support',
],
],
plugins: this.nodespaceBabelPlugins(),
// Keep things debuggable.
sourceMaps: 'inline',
});
compiling.forEach((fleck) => {
debug('compiling %s...', fleck);
flecks[fleck] = R(this.resolve(resolver, fleck));
debug('compiled');
// Remove the required fleck from the list still needing require().
paths.splice(paths.indexOf(fleck), 1);
});
@ -419,6 +299,152 @@ export default class ServerFlecks extends Flecks {
return realpath !== resolved;
}
loadBuildConfigs() {
return Object.fromEntries(
Object.entries(this.invoke('@flecks/core.build.config'))
.map(([fleck, configs]) => (
configs.map((config) => {
const defaults = {
fleck,
root: FLECKS_CORE_ROOT,
};
if (Array.isArray(config)) {
return [config[0], {...defaults, ...config[1]}];
}
return [config, defaults];
})
))
.flat(),
);
}
static loadConfig(root) {
const resolvedRoot = resolve(FLECKS_CORE_ROOT, root);
try {
const {load} = R('js-yaml');
const filename = join(resolvedRoot, 'build', FLECKS_YML);
const buffer = readFileSync(filename, 'utf8');
debug('parsing configuration from YML...');
return ['YML', load(buffer, {filename}) || {}];
}
catch (error) {
if ('ENOENT' !== error.code) {
throw error;
}
return ['barebones', {'@flecks/core': {}, '@flecks/fleck': {}}];
}
}
static loadRcs(resolver) {
const rcs = {};
const roots = Array.from(new Set(
Object.keys(resolver)
.map((path) => this.root(resolver, path))
.filter((e) => !!e),
));
for (let i = 0; i < roots.length; ++i) {
const root = roots[i];
try {
rcs[root] = R(join(root, 'build', '.flecksrc'));
}
catch (error) {
if ('MODULE_NOT_FOUND' !== error.code) {
throw error;
}
}
}
return rcs;
}
static nodespaceBabelPlugins() {
return [
[
'prepend',
{
prepend: [
'require.context = (',
' directory,',
' useSubdirectories = true,',
' regExp = /^\\.\\/.*$/,',
' mode = "sync",',
') => {',
' const glob = require("glob");',
' const {resolve, sep} = require("path");',
' const keys = glob.sync(',
' useSubdirectories ? "**/*" : "*",',
' {cwd: resolve(__dirname, directory)},',
' )',
' .filter((key) => key.match(regExp))',
' .map(',
' (key) => (',
' -1 !== [".".charCodeAt(0), "/".charCodeAt(0)].indexOf(key.charCodeAt(0))',
' ? key',
' : ("." + sep + key)',
' ),',
' );',
' const R = (request) => require(keys[request]);',
' R.id = __filename',
' R.keys = () => keys;',
' return R;',
'};',
].join('\n'),
},
'require.context',
],
[
'prepend',
{
prepend: 'const __non_webpack_require__ = require;',
},
'__non_webpack_require__',
],
[
'prepend',
{
prepend: "require('source-map-support/register');",
},
'source-map-support',
],
];
}
overrideConfigFromEnvironment() {
const keys = Object.keys(process.env);
const seen = [];
Object.keys(this.flecks)
// Reverse-sorting means e.g. `@flecks/core/server` comes before `@flecks/core`.
// We want to select the most specific match.
//
// `FLECKS_ENV_FLECKS_CORE_SERVER_VARIABLE` is ambiguous as it can equate to both:
// - `flecks.set('@flecks/core.server.variable');`
// - `flecks.set('@flecks/core/server.variable');`
//
// The latter will take precedence.
.sort((l, r) => (l < r ? 1 : -1))
.forEach((fleck) => {
const prefix = `FLECKS_ENV_${this.constructor.environmentalize(fleck)}`;
keys
.filter((key) => key.startsWith(`${prefix}_`) && -1 === seen.indexOf(key))
.map((key) => {
seen.push(key);
debug('reading environment from %s...', key);
return [key, process.env[key]];
})
.map(([key, value]) => [key.slice(prefix.length + 1), value])
.map(([subkey, value]) => [subkey.split('_'), value])
.forEach(([path, jsonOrString]) => {
try {
this.set([fleck, ...path], JSON.parse(jsonOrString));
debug('read (%s) as JSON', jsonOrString);
}
catch (error) {
this.set([fleck, ...path], jsonOrString);
debug('read (%s) as string', jsonOrString);
}
});
});
}
rcs() {
return this.rcs;
}
@ -519,6 +545,7 @@ export default class ServerFlecks extends Flecks {
.forEach((root) => {
const resolved = dirname(R.resolve(join(root, 'package.json')));
const sourcepath = this.sourcepath(resolved);
const sourceroot = join(sourcepath, '..');
const configFile = this.buildConfig('babel.config.js');
debug('compiling: %s with %s', root, configFile);
const babel = {
@ -527,8 +554,8 @@ export default class ServerFlecks extends Flecks {
...babelmerge(...rcBabel.map(([, babel]) => babel)),
};
compileLoader({
ignore: [join(sourcepath, '..')],
include: [join(sourcepath, '..')],
ignore: [sourceroot],
include: [sourceroot],
babel,
ruleId: `@flecks/${runtime}/runtime/compile[${root}]`,
})(neutrino);