fix: complex aliasing

This commit is contained in:
cha0s 2022-03-24 05:30:10 -05:00
parent 9b5f6f4818
commit 4ba2b51136
4 changed files with 210 additions and 236 deletions

View File

@ -84,53 +84,8 @@ export default class ServerFlecks extends Flecks {
configType = 'parameter';
}
debug('bootstrap configuration (%s): %O', configType, config);
// Fleck discovery.
const resolvedRoot = resolve(FLECKS_CORE_ROOT, root);
const resolver = {};
const keys = Object.keys(config);
// `!platform` excludes that platform.
const without = platforms
.filter((platform) => '!'.charCodeAt(0) === platform.charCodeAt(0))
.map((platform) => platform.slice(1));
for (let i = 0; i < keys.length; ++i) {
const key = keys[i];
// 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;
}
// Resolve the path (if necessary).
let resolvedPath;
if (alias) {
resolvedPath = isAbsolute(alias) ? alias : join(resolvedRoot, alias);
}
else {
resolvedPath = path;
}
try {
R.resolve(resolvedPath);
resolver[path] = resolvedPath;
}
// eslint-disable-next-line no-empty
catch (error) {}
// Discover platform-specific variants.
if (platforms) {
platforms.forEach((platform) => {
try {
const resolvedPlatformPath = join(resolvedPath, platform);
R.resolve(resolvedPlatformPath);
resolver[join(path, platform)] = resolvedPlatformPath;
}
// eslint-disable-next-line no-empty
catch (error) {}
});
}
}
// Make resolver.
const resolver = this.makeResolver(config, platforms, root);
// Rewrite aliased config keys.
// eslint-disable-next-line no-param-reassign
config = Object.fromEntries(
@ -140,141 +95,14 @@ export default class ServerFlecks extends Flecks {
return [-1 !== index ? key.slice(0, index) : key, value];
}),
);
const paths = Object.keys(resolver);
// Load RCs.
const rcs = this.loadRcs(resolver);
debug('rcs: %O', 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);
}
const exts = this.exts(rcs);
const enhancedResolver = enhancedResolve.create.sync({
extensions: exts,
alias: aliases,
});
// Stub server-unfriendly modules.
const stubs = this.stubs(['server'], rcs);
if (stubs.length > 0) {
debug('stubbing: %O', stubs);
}
// 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;
const aliasKeys = Object.keys(aliases);
Module.prototype.require = function hackedRequire(request, options) {
for (let i = 0; i < stubs.length; ++i) {
if (request.match(stubs[i])) {
return undefined;
}
}
if (aliasKeys.find((aliasKey) => request.startsWith(aliasKey))) {
try {
const resolved = enhancedResolver(FLECKS_CORE_ROOT, request);
if (resolved) {
return Mr.call(this, resolved, options);
}
}
// eslint-disable-next-line no-empty
catch (error) {}
}
return Mr.call(this, request, options);
};
}
// Compile.
const compilations = [];
const flecks = {};
const needCompilation = paths
.filter((path) => this.fleckIsCompiled(resolver, path));
if (needCompilation.length > 0) {
// Augment the compilations with babel config from flecksrc.
const rcBabelConfig = babelmerge.all(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 = {};
needCompilation.forEach((fleck) => {
const root = this.root(resolver, fleck);
if (!compilationRootMap[root]) {
compilationRootMap[root] = [];
}
compilationRootMap[root].push(fleck);
});
// Register a compilation for each root.
Object.entries(compilationRootMap).forEach(([root, compiling]) => {
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,
[
resolved,
FLECKS_CORE_ROOT,
this.resolvePath(resolver, '@flecks/core/server'),
],
[
'babel.config.js',
],
);
const ignore = `${resolve(join(sourceroot, 'node_modules'))}/`;
const only = `${resolve(sourceroot)}/`;
const config = {
// Augment the selected config with the babel config from RCs.
configFile,
// Target the compiler to avoid unnecessary work.
ignore: [ignore],
only: [only],
};
debug('compiling %O with %j', compiling, config);
compilations.push({
ignore,
only,
compiler: new Compiler(babelmerge(config, rcBabelConfig)),
});
});
}
const findCompiler = (request) => {
for (let i = 0; i < compilations.length; ++i) {
const {compiler, ignore, only} = compilations[i];
if (request.startsWith(only) && !request.startsWith(ignore)) {
return compiler;
}
}
return undefined;
};
debug('pirating exts: %O', exts);
addHook(
(code, request) => {
const compilation = findCompiler(request).compile(code, request);
return null === compilation ? code : compilation.code;
},
{exts, matcher: (request) => !!findCompiler(request)},
this.installCompilers(rcs, resolver);
// Load the flecks.
const flecks = Object.fromEntries(
Object.keys(resolver)
.map((path) => [path, R(this.resolve(resolver, path))]),
);
// Load the rest of the flecks.
paths.forEach((path) => {
flecks[path] = R(this.resolve(resolver, path));
});
return new ServerFlecks({
config,
flecks,
@ -353,6 +181,140 @@ export default class ServerFlecks extends Flecks {
return realpath !== resolved;
}
static installCompilers(rcs, resolver) {
const paths = Object.keys(resolver);
debug('rcs: %O', rcs);
// Merge aliases;
const aliases = Object.fromEntries(
Object.entries({
// 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),
})
.map(([from, to]) => [from, to.endsWith('/index') ? to.slice(0, -6) : to]),
);
if (Object.keys(aliases).length > 0) {
debug('aliases: %O', aliases);
}
const exts = this.exts(rcs);
const enhancedResolver = enhancedResolve.create.sync({
extensions: exts,
alias: aliases,
});
// Stub server-unfriendly modules.
const stubs = this.stubs(['server'], rcs);
if (stubs.length > 0) {
debug('stubbing: %O', stubs);
}
// 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;
const aliasKeys = Object.keys(aliases);
Module.prototype.require = function hackedRequire(request, options) {
for (let i = 0; i < stubs.length; ++i) {
if (request.match(stubs[i])) {
return undefined;
}
}
if (aliasKeys.find((aliasKey) => request.startsWith(aliasKey))) {
try {
const resolved = enhancedResolver(FLECKS_CORE_ROOT, request);
if (resolved) {
return Mr.call(this, resolved, options);
}
}
// eslint-disable-next-line no-empty
catch (error) {}
}
return Mr.call(this, request, options);
};
}
// Compile.
const compilations = [];
const needCompilation = paths
.filter((path) => this.fleckIsCompiled(resolver, path));
if (needCompilation.length > 0) {
// Augment the compilations with babel config from flecksrc.
const rcBabelConfig = babelmerge.all(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 = {};
needCompilation.forEach((fleck) => {
const root = this.root(resolver, fleck);
if (!compilationRootMap[root]) {
compilationRootMap[root] = [];
}
compilationRootMap[root].push(fleck);
});
// Register a compilation for each root.
Object.entries(compilationRootMap).forEach(([root, compiling]) => {
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,
[
resolved,
FLECKS_CORE_ROOT,
this.resolvePath(resolver, '@flecks/core/server'),
],
[
'babel.config.js',
],
);
const ignore = `${resolve(join(sourceroot, 'node_modules'))}/`;
const only = `${resolve(sourceroot)}/`;
const config = {
// Augment the selected config with the babel config from RCs.
configFile,
// Target the compiler to avoid unnecessary work.
ignore: [ignore],
only: [only],
};
debug('compiling %O with %j', compiling, config);
compilations.push({
ignore,
only,
compiler: new Compiler(babelmerge(config, rcBabelConfig)),
});
});
}
const findCompiler = (request) => {
for (let i = 0; i < compilations.length; ++i) {
const {compiler, ignore, only} = compilations[i];
if (request.startsWith(only) && !request.startsWith(ignore)) {
return compiler;
}
}
return undefined;
};
debug('pirating exts: %O', exts);
addHook(
(code, request) => {
const compilation = findCompiler(request).compile(code, request);
return null === compilation ? code : compilation.code;
},
{exts, matcher: (request) => !!findCompiler(request)},
);
}
loadBuildConfigs() {
Object.entries(this.invoke('@flecks/core.build.config'))
.forEach(([fleck, configs]) => (
@ -406,6 +368,56 @@ export default class ServerFlecks extends Flecks {
return rcs;
}
static makeResolver(config, platforms = ['server'], root = FLECKS_CORE_ROOT) {
const resolvedRoot = resolve(FLECKS_CORE_ROOT, root);
const resolver = {};
const keys = Object.keys(config);
// `!platform` excludes that platform.
const without = platforms
.filter((platform) => '!'.charCodeAt(0) === platform.charCodeAt(0))
.map((platform) => platform.slice(1));
for (let i = 0; i < keys.length; ++i) {
const key = keys[i];
// 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;
}
// Resolve the path (if necessary).
let resolvedPath;
if (alias) {
resolvedPath = isAbsolute(alias) ? alias : join(resolvedRoot, alias);
}
else {
resolvedPath = path;
}
try {
R.resolve(resolvedPath);
resolver[path] = resolvedPath;
}
// eslint-disable-next-line no-empty
catch (error) {}
// Discover platform-specific variants.
if (platforms) {
platforms.forEach((platform) => {
try {
const resolvedPlatformPath = join(resolvedPath, platform);
R.resolve(resolvedPlatformPath);
resolver[join(path, platform)] = resolvedPlatformPath;
}
// eslint-disable-next-line no-empty
catch (error) {}
});
}
}
return resolver;
}
overrideConfigFromEnvironment() {
const keys = Object.keys(process.env);
const seen = [];
@ -536,13 +548,15 @@ export default class ServerFlecks extends Flecks {
debug('.flecksrc: babel: %O', rcBabel);
// Alias and de-externalize.
needCompilation
.sort(([l], [r]) => (l < r ? 1 : -1))
.forEach(([fleck, resolved]) => {
const alias = this.constructor.fleckIsAliased(resolver, fleck)
let alias = this.constructor.fleckIsAliased(resolver, fleck)
? resolved
: this.constructor.sourcepath(R.resolve(this.constructor.resolve(resolver, fleck)));
alias = alias.endsWith('/index') ? alias.slice(0, -6) : alias;
allowlist.push(fleck);
config.resolve.alias
.set(`${fleck}/`, alias);
.set(fleck, alias);
debug('%s runtime de-externalized %s, alias: %s', runtime, fleck, alias);
});
// Set up compilation at each root.

View File

@ -8,7 +8,7 @@ import {Flecks} from '@flecks/core/server';
const {version} = require('../package.json');
(async () => {
const runtime = await __non_webpack_require__('@flecks/server/runtime');
const {config, loadFlecks, platforms} = await __non_webpack_require__('@flecks/server/runtime');
// eslint-disable-next-line no-console
console.log(`flecks server v${version}`);
try {
@ -19,12 +19,21 @@ const {version} = require('../package.json');
throw error;
}
}
const debug = D(runtime.config['@flecks/core']?.id || 'flecks');
const debug = D(config['@flecks/core']?.id || 'flecks');
debug('starting server...');
const flecks = new Flecks(runtime);
global.flecks = flecks;
// Make resolver.
const resolver = Flecks.makeResolver(config);
const rcs = Flecks.loadRcs(resolver);
Flecks.installCompilers(rcs, resolver);
global.flecks = new Flecks({
config,
flecks: await loadFlecks(),
platforms,
resolver,
rcs,
});
try {
await flecks.up('@flecks/server.up');
await global.flecks.up('@flecks/server.up');
debug('up!');
}
catch (error) {

View File

@ -14,7 +14,7 @@ module.exports = async (flecks) => {
"process.env.FLECKS_CORE_BUILD_TARGET = 'server';",
'module.exports = (async () => ({',
` config: ${JSON.stringify(flecks.config)},`,
' flecks: Object.fromEntries(await Promise.all([',
' loadFlecks: async () => Object.fromEntries(await Promise.all([',
paths.map((path) => ` ['${path}', import('${path}')]`).join(',\n'),
' ].map(async ([path, M]) => [path, await M]))),',
" platforms: ['server']",

View File

@ -101,55 +101,6 @@ module.exports = async (flecks) => {
start,
],
};
// Stub out non-server-friendly modules on the server.
const exts = flecks.exts();
const stubs = flecks.stubs();
const aliases = flecks.aliases();
// Do we need to get up in `require()`'s guts?
if (
Object.keys(aliases).length > 0
|| stubs.length > 0
) {
const sanitizedStubs = stubs
.map((stub) => (
'string' === typeof stub
? JSON.stringify(stub)
: `new RegExp(${JSON.stringify(stub.toString().slice(1, -1))})`
));
const code = [
`const aliases = ${JSON.stringify(aliases)};`,
'const aliasKeys = Object.keys(aliases);',
`const stubs = [${sanitizedStubs}];`,
'const {Module} = require("module");',
'const {require: Mr} = Module.prototype;',
'const enhancedResolver = require("enhanced-resolve").create.sync({',
` extensions: ${JSON.stringify(exts)},`,
' alias: aliases,',
'});',
'Module.prototype.require = function hackedRequire(request, options) {',
' for (let i = 0; i < stubs.length; ++i) {',
' if (request.match(stubs[i])) {',
' return undefined;',
' }',
' }',
' if (aliasKeys.find((aliasKey) => request.startsWith(aliasKey))) {',
' try {',
' const resolved = enhancedResolver(process.cwd(), request);',
' if (resolved) {',
' return Mr.call(this, resolved, options);',
' }',
' }',
' // eslint-disable-next-line no-empty',
' catch (error) {}',
' }',
' return Mr.call(this, request, options);',
'};',
].join('\n');
config.use.push(banner({
banner: code,
pluginId: 'aliases-banner',
}));
}
// Build the server runtime.
config.use.push(await runtime(flecks));