refactor: explication

This commit is contained in:
cha0s 2024-01-25 06:42:31 -06:00
parent 85958c2d87
commit f657c5ea8b
31 changed files with 334 additions and 427 deletions

View File

@ -1,4 +1,4 @@
{ {
"eslint.workingDirectories": [{"pattern": "./packages/*"}], "eslint.workingDirectories": [{"pattern": "./packages/*"}],
"eslint.options": {"overrideConfigFile": "./node_modules/@flecks/build/build/eslint.config.js"}, "eslint.options": {"overrideConfigFile": "../build/dist/build/eslint.config.js"},
} }

View File

@ -10,6 +10,7 @@ module.exports = (api) => {
setSpreadProperties: true, setSpreadProperties: true,
}, },
plugins: [ plugins: [
'@babel/plugin-syntax-dynamic-import',
'@babel/plugin-syntax-class-properties', '@babel/plugin-syntax-class-properties',
'@babel/plugin-syntax-logical-assignment-operators', '@babel/plugin-syntax-logical-assignment-operators',
'@babel/plugin-syntax-nullish-coalescing-operator', '@babel/plugin-syntax-nullish-coalescing-operator',
@ -24,6 +25,11 @@ module.exports = (api) => {
'@babel/plugin-transform-async-to-generator', '@babel/plugin-transform-async-to-generator',
'@babel/plugin-transform-object-super', '@babel/plugin-transform-object-super',
], ],
shippedProposals: true,
targets: {
esmodules: true,
node: 'current',
},
}, },
], ],
], ],

View File

@ -1,5 +1,4 @@
const {realpath} = require('fs/promises'); const {join} = require('path');
const {dirname, join, relative} = require('path');
const D = require('@flecks/core/build/debug'); const D = require('@flecks/core/build/debug');
const {Flecks} = require('@flecks/core/build/flecks'); const {Flecks} = require('@flecks/core/build/flecks');
@ -65,31 +64,18 @@ module.exports = class Build extends Flecks {
roots = {}; roots = {};
async babel() { async babel() {
const merging = [ return babelmerge.all([
{ {configFile: await this.resolveBuildConfig('babel.config.js')},
plugins: ['@babel/plugin-syntax-dynamic-import'], ...this.invokeFlat('@flecks/core.babel'),
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 = {}) { static async buildRuntime(originalConfig, platforms, flecks = {}) {
const dealiasedConfig = Object.fromEntries( const cleanConfig = JSON.parse(JSON.stringify(originalConfig));
Object.entries(originalConfig) // Dealias the config keys.
const dealiasedConfig = environmentConfiguration(
Object.fromEntries(
Object.entries(cleanConfig)
.map(([maybeAliasedPath, config]) => { .map(([maybeAliasedPath, config]) => {
const index = maybeAliasedPath.indexOf(':'); const index = maybeAliasedPath.indexOf(':');
return [ return [
@ -97,110 +83,25 @@ module.exports = class Build extends Flecks {
config, config,
]; ];
}), }),
),
); );
const resolver = new Resolver(); const resolver = new Resolver({root: FLECKS_CORE_ROOT});
const explication = await explicate( const {paths, roots} = await explicate({
Object.keys(originalConfig), paths: Object.keys(originalConfig),
{
platforms, platforms,
resolver, resolver,
root: FLECKS_CORE_ROOT,
importer: (request) => require(request), importer: (request) => require(request),
}, });
);
const runtime = { const runtime = {
config: environmentConfiguration( config: Object.fromEntries(paths.map((path) => [path, dealiasedConfig[path] || {}])),
Object.fromEntries( flecks: Object.fromEntries(paths.map((path) => [
Object.values(explication.descriptors) path,
.map(({path}) => [path, dealiasedConfig[path] || {}]), flecks[path] || roots[path]?.bootstrap || {},
), ])),
),
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 { return {
aliased,
compiled,
resolver, resolver,
roots, roots: Object.entries(roots).map(([path, {request}]) => [path, request]),
runtime, runtime,
}; };
} }
@ -225,15 +126,11 @@ module.exports = class Build extends Flecks {
debug('bootstrap configuration (%s)', configType); debug('bootstrap configuration (%s)', configType);
debugSilly(originalConfig); debugSilly(originalConfig);
const { const {
aliased,
compiled,
resolver, resolver,
roots, roots,
runtime, runtime,
} = await this.buildRuntime(originalConfig, platforms, configFlecks); } = await this.buildRuntime(originalConfig, platforms, configFlecks);
const flecks = super.from(runtime); const flecks = super.from(runtime);
flecks.aliased = aliased;
flecks.compiled = compiled;
flecks.platforms = platforms; flecks.platforms = platforms;
flecks.roots = roots; flecks.roots = roots;
flecks.resolver = resolver; flecks.resolver = resolver;
@ -255,7 +152,7 @@ module.exports = class Build extends Flecks {
return Object.fromEntries( return Object.fromEntries(
Object.entries(this.config) Object.entries(this.config)
.map(([path, config]) => { .map(([path, config]) => {
const alias = this.aliased[path]; const alias = this.resolver.fallbacks[path];
return [alias ? `${path}:${alias}` : path, config]; return [alias ? `${path}:${alias}` : path, config];
}), }),
); );
@ -279,38 +176,15 @@ module.exports = class Build extends Flecks {
return this.resolver.resolve(join(fleck, 'build', config)); return this.resolver.resolve(join(fleck, 'build', config));
} }
async runtimeCompiler(runtime, config, {additionalModuleDirs = [], allowlist = []} = {}) { async runtimeCompiler(runtime, config) {
// Compile? // Compile?
const needCompilation = Object.entries(this.compiled); const compiled = this.roots.filter(([path, request]) => path !== request);
if (needCompilation.length > 0) { if (compiled.length > 0) {
const babelConfig = await this.babel(); const include = Object.values(this.resolver.aliases);
const includes = [];
// Alias and de-externalize.
await Promise.all(
needCompilation
.map(async ([
root,
{
path,
source,
},
]) => {
allowlist.push(new RegExp(`^${path}`));
debugSilly('%s runtime de-externalized %s, alias: %s', runtime, root, source || path);
// Alias.
config.resolve.alias[path] = source || path;
// Root aliases.
config.resolve.fallback[path] = root;
config.resolve.modules.push(relative(FLECKS_CORE_ROOT, join(root, 'node_modules')));
additionalModuleDirs.push(relative(FLECKS_CORE_ROOT, join(root, 'node_modules')));
includes.push(root);
}),
);
// Compile.
config.module.rules.push( config.module.rules.push(
{ {
test: /\.(m?jsx?)?$/, test: /\.(m?jsx?)?$/,
include: includes, include,
use: [ use: [
{ {
loader: require.resolve('babel-loader'), loader: require.resolve('babel-loader'),
@ -318,35 +192,30 @@ module.exports = class Build extends Flecks {
cacheDirectory: true, cacheDirectory: true,
babelrc: false, babelrc: false,
configFile: false, configFile: false,
...babelConfig, ...await this.babel(),
}, },
}, },
], ],
}, },
); );
// 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. // Our very own lil' chunk.
Flecks.set(config, 'optimization.splitChunks.cacheGroups.flecks-compiled', { Flecks.set(config, 'optimization.splitChunks.cacheGroups.flecks-compiled', {
chunks: 'all', chunks: 'all',
enforce: true, enforce: true,
priority: 100, priority: 100,
test: new RegExp( test: new RegExp(`(?:${
`(?:${ include.map((path) => path.replace(/[\\/]/g, '[\\/]')).join('|')
includes })`),
.map((path) => path.replace(/[\\/]/g, '[\\/]')).join('|')
})`,
),
}); });
} }
// Resolution.
const {resolve, resolveLoader} = config;
resolve.alias = {...resolve.alias, ...this.resolver.aliases};
resolve.fallback = {...resolve.fallback, ...this.resolver.fallbacks};
resolve.modules = [...resolve.modules, ...this.resolver.modules];
resolveLoader.alias = {...resolveLoader.alias, ...this.resolver.aliases};
resolveLoader.fallback = {...resolveLoader.fallback, ...this.resolver.fallbacks};
resolveLoader.modules = [...resolveLoader.modules, ...this.resolver.modules];
} }
get stubs() { get stubs() {

View File

@ -9,7 +9,7 @@ module.exports = async (env, argv) => {
config.plugins.push(new ProcessAssets('fleck', flecks)); config.plugins.push(new ProcessAssets('fleck', flecks));
// Small hack because internals. // Small hack because internals.
flecks.hooks['@flecks/build.processAssets'] = [{ flecks.hooks['@flecks/build.processAssets'] = [{
hook: '@flecks/build', fleck: '@flecks/build',
fn: (target, assets, compilation) => processFleckAssets(assets, compilation), fn: (target, assets, compilation) => processFleckAssets(assets, compilation),
}]; }];
config.plugins.push(executable()); config.plugins.push(executable());

View File

@ -1,159 +1,127 @@
const {join, relative, resolve} = require('path'); const {access, realpath} = require('fs/promises');
const {Module} = require('module');
const { const {
FLECKS_CORE_ROOT = process.cwd(), delimiter,
} = process.env; join,
resolve,
} = require('path');
module.exports = async function explicate( module.exports = async function explicate(
maybeAliasedPaths,
{ {
importer, importer,
paths: maybeAliasedPaths,
platforms = ['server'], platforms = ['server'],
resolver, resolver,
root,
}, },
) { ) {
const descriptors = {}; const dependentPaths = [];
const seen = {};
const roots = {}; const roots = {};
function createDescriptor(maybeAliasedPath) { async function addRoot(path, request) {
const index = maybeAliasedPath.indexOf(':'); // Already added it?
return -1 === index if (Object.keys(roots).some((rootPath) => path.startsWith(rootPath))) {
? {
path: maybeAliasedPath,
request: maybeAliasedPath,
}
: {
path: maybeAliasedPath.slice(0, index),
request: resolve(root, maybeAliasedPath.slice(index + 1)),
};
}
async function doExplication(descriptor) {
const {path, request} = descriptor;
if (
platforms
.filter((platform) => platform.startsWith('!'))
.map((platform) => platform.slice(1))
.includes(path.split('/').pop())
) {
return; return;
} }
descriptors[request] = descriptor;
}
async function getRootDescriptor(descriptor) {
const {path, request} = descriptor;
// Walk up and find the root, if any. // Walk up and find the root, if any.
const pathParts = path.split('/'); const pathParts = path.split('/');
const requestParts = request.split('/'); const requestParts = path === request
let rootDescriptor; ? pathParts.slice()
: resolve(resolver.root, request).split('/');
/* eslint-disable no-await-in-loop */
while (pathParts.length > 0 && requestParts.length > 0) { while (pathParts.length > 0 && requestParts.length > 0) {
const candidate = requestParts.join('/'); const candidate = requestParts.join('/');
// eslint-disable-next-line no-await-in-loop
if (await resolver.resolve(join(candidate, 'package.json'))) { if (await resolver.resolve(join(candidate, 'package.json'))) {
rootDescriptor = { const rootPath = pathParts.join('/');
path: pathParts.join('/'), // Don't add the root if this path doesn't actually exist.
request: requestParts.join('/'), if (path !== rootPath && !await resolver.resolve(path)) {
break;
}
// Resolve symlinks.
let realCandidate;
try {
realCandidate = await realpath(candidate);
}
catch (error) {
realCandidate = candidate;
}
// Aliased or symlinked? Include submodules.
if (path !== request || realCandidate !== candidate) {
const submodules = join(realCandidate, 'node_modules');
resolver.addModules(submodules);
// Runtime NODE_PATH hacking.
const {env} = process;
env.NODE_PATH = (env.NODE_PATH || '') + delimiter + submodules;
// eslint-disable-next-line no-underscore-dangle
Module._initPaths();
}
// Load `bootstrap.js`.
const bootstrapPath = await resolver.resolve(join(candidate, 'build', 'flecks.bootstrap'));
const bootstrap = bootstrapPath ? importer(bootstrapPath) : {};
// First add dependencies.
const {dependencies = []} = bootstrap;
if (dependencies.length > 0) {
await Promise.all(dependencies.map((dependency) => addRoot(dependency, dependency)));
dependentPaths.push(...dependencies);
}
// Add root as a dependency.
dependentPaths.push(rootPath);
// Add root.
roots[rootPath] = {
bootstrap,
request: realCandidate !== candidate ? realCandidate : candidate,
}; };
break; break;
} }
pathParts.pop(); pathParts.pop();
requestParts.pop(); requestParts.pop();
} }
return rootDescriptor; /* eslint-enable no-await-in-loop */
}
function descriptorsAreTheSame(l, r) {
return (l && !r) || (!l && r) ? false : l.request === r.request;
}
async function explicateDescriptor(descriptor) {
if (descriptors[descriptor.request] || seen[descriptor.request]) {
return;
}
seen[descriptor.request] = true;
const areDescriptorsTheSame = descriptorsAreTheSame(
descriptor,
await getRootDescriptor(descriptor),
);
const resolved = await resolver.resolve(descriptor.request);
if (resolved || areDescriptorsTheSame) {
// eslint-disable-next-line no-use-before-define
await explicateRoot(descriptor);
}
if (!resolved && areDescriptorsTheSame) {
descriptors[descriptor.request] = descriptor;
}
if (resolved) {
await doExplication(descriptor);
}
let descriptorRequest = descriptor.request;
if (areDescriptorsTheSame) {
descriptorRequest = join(descriptorRequest, 'src');
}
if (descriptor.path !== descriptor.request) {
resolver.addAlias(descriptor.path, descriptorRequest);
if (descriptorRequest !== descriptor.request) {
resolver.addFallback(descriptor.path, descriptor.request);
}
}
await Promise.all(
platforms
.filter((platform) => !platform.startsWith('!'))
.map(async (platform) => {
if (await resolver.resolve(join(descriptor.request, platform))) {
const [path, request] = [
join(descriptor.path, platform),
join(descriptor.request, platform),
];
await doExplication({path, request});
if (path !== request) {
resolver.addAlias(path, request);
}
}
else if (await resolver.resolve(join(descriptorRequest, 'src', platform))) {
const [path, request] = [
join(descriptor.path, platform),
join(descriptorRequest, 'src', platform),
];
await doExplication({path, request});
if (path !== request) {
resolver.addAlias(path, request);
}
} }
// Normalize maybe aliased paths into path and request.
const normalized = await Promise.all(
maybeAliasedPaths.map(async (maybeAliasedPath) => {
const index = maybeAliasedPath.indexOf(':');
return -1 === index
? [maybeAliasedPath, maybeAliasedPath]
: [maybeAliasedPath.slice(0, index), maybeAliasedPath.slice(index + 1)];
}), }),
); );
} // Add roots.
async function explicateRoot(descriptor) { await Promise.all(normalized.map(([path, request]) => addRoot(path, request)));
// Walk up and find the root, if any. // Add aliases and fallbacks.
const rootDescriptor = await getRootDescriptor(descriptor);
if (!rootDescriptor || roots[rootDescriptor.request]) {
return;
}
const {path, request} = rootDescriptor;
if (path !== request) {
resolver.addModules(relative(FLECKS_CORE_ROOT, join(request, 'node_modules')));
}
roots[request] = true;
// Import bootstrap script.
const bootstrapPath = await resolver.resolve(join(request, 'build', 'flecks.bootstrap'));
const bootstrap = bootstrapPath ? importer(bootstrapPath) : {};
roots[request] = bootstrap;
// Explicate dependcies.
const {dependencies = []} = bootstrap;
if (dependencies.length > 0) {
await Promise.all( await Promise.all(
dependencies Object.entries(roots)
.map(createDescriptor) .filter(([path, {request}]) => path !== request)
.map(explicateDescriptor), .map(async ([path, {request}]) => {
try {
await access(join(request, 'src'));
resolver.addAlias(path, join(request, 'src'));
}
// eslint-disable-next-line no-empty
catch (error) {}
resolver.addFallback(path, request);
}),
); );
} const paths = (
await explicateDescriptor(rootDescriptor); // Resolve dependent, normalized, and platform paths.
}
await Promise.all( await Promise.all(
maybeAliasedPaths dependentPaths.map((path) => [path, path])
.map(createDescriptor) .concat(normalized)
.map(explicateDescriptor), .map(([path]) => path)
); .reduce((platformed, path) => (
return { platformed.concat([path], platforms.map((platform) => join(path, platform)))
descriptors, ), [])
roots, .map(async (path) => [path, await resolver.resolve(path)]),
}; )
)
// Filter unresolved except roots.
.filter(([path, resolved]) => resolved || roots[path])
.map(([path]) => path)
// Filter excluded platforms.
.filter((path) => (
!platforms
.filter((platform) => platform.startsWith('!'))
.map((platform) => platform.slice(1))
.some((excluded) => path.endsWith(`/${excluded}`))
));
return {paths: [...new Set(paths)], roots};
}; };

View File

@ -20,7 +20,7 @@ exports.hooks = {
}), }),
); );
} }
if (Object.entries(flecks.compiled).length > 0) { if (flecks.roots.some(([path, request]) => path !== request)) {
config.resolve.symlinks = false; config.resolve.symlinks = false;
} }
config.plugins.push(new ProcessAssets(target, flecks)); config.plugins.push(new ProcessAssets(target, flecks));

View File

@ -0,0 +1,27 @@
const Resolver = require('./resolver');
module.exports = function resolve({aliases, fallbacks}, stubs) {
const {Module} = require('module');
const {require: Mr} = Module.prototype;
const resolver = new Resolver({aliases, fallbacks, useSyncFileSystemCalls: true});
Module.prototype.require = function hackedRequire(request, options) {
for (let i = 0; i < stubs.length; ++i) {
if (request.startsWith(stubs[i])) {
return undefined;
}
}
try {
return Mr.call(this, request, options);
}
catch (error) {
if (!error.message.startsWith('Cannot find module')) {
throw error;
}
const resolved = resolver.resolveSync(request);
if (!resolved) {
throw error;
}
return Mr.call(this, resolved, options);
}
};
};

View File

@ -22,18 +22,29 @@ const nodeFileSystem = new CachedInputFileSystem(fs, 4000);
module.exports = class Resolver { module.exports = class Resolver {
constructor(options) { constructor(options = {}) {
const {
modules = [join(FLECKS_CORE_ROOT, 'node_modules'), 'node_modules'],
root = FLECKS_CORE_ROOT,
...rest
} = options;
this.resolver = ResolverFactory.createResolver({ this.resolver = ResolverFactory.createResolver({
conditionNames: ['node'], conditionNames: ['node'],
extensions: ['.js', '.json', '.node'], extensions: ['.js', '.json', '.node'],
fileSystem: nodeFileSystem, fileSystem: nodeFileSystem,
modules,
symlinks: false, symlinks: false,
...options, ...rest,
}); });
this.aliases = {};
this.fallbacks = {};
this.modules = modules;
this.root = root;
} }
addAlias(name, alias) { addAlias(name, alias) {
debugSilly("adding alias: '%s' -> '%s'", name, alias); debugSilly("adding alias: '%s' -> '%s'", name, alias);
this.aliases[name] = alias;
new AliasPlugin( new AliasPlugin(
'raw-resolve', 'raw-resolve',
{name, onlyModule: false, alias}, {name, onlyModule: false, alias},
@ -50,6 +61,7 @@ module.exports = class Resolver {
addFallback(name, alias) { addFallback(name, alias) {
debugSilly("adding fallback: '%s' -> '%s'", name, alias); debugSilly("adding fallback: '%s' -> '%s'", name, alias);
this.fallbacks[name] = alias;
new AliasPlugin( new AliasPlugin(
'described-resolve', 'described-resolve',
{name, onlyModule: false, alias}, {name, onlyModule: false, alias},
@ -59,10 +71,11 @@ module.exports = class Resolver {
addModules(path) { addModules(path) {
debugSilly("adding modules: '%s'", path); debugSilly("adding modules: '%s'", path);
this.modules.push(path);
new ModulesInHierarchicalDirectoriesPlugin( new ModulesInHierarchicalDirectoriesPlugin(
"raw-module", 'raw-module',
path, path,
"module" 'module',
).apply(this.resolver); ).apply(this.resolver);
} }
@ -73,7 +86,7 @@ module.exports = class Resolver {
async resolve(request) { async resolve(request) {
try { try {
return await new Promise((resolve, reject) => { return await new Promise((resolve, reject) => {
this.resolver.resolve(nodeContext, FLECKS_CORE_ROOT, request, {}, (error, path) => { this.resolver.resolve(nodeContext, this.root, request, {}, (error, path) => {
if (error) { if (error) {
reject(error); reject(error);
} }
@ -91,4 +104,16 @@ module.exports = class Resolver {
} }
} }
resolveSync(request) {
try {
return this.resolver.resolveSync(nodeContext, this.root, request);
}
catch (error) {
if (!this.constructor.isResolutionError(error)) {
throw error;
}
return undefined;
}
}
}; };

View File

@ -41,6 +41,12 @@ exports.defaultConfig = (flecks, specializedConfig) => {
}, },
plugins: [], plugins: [],
resolve: { resolve: {
alias: {},
extensions,
fallback: {},
modules: [],
},
resolveLoader: {
alias: {}, alias: {},
extensions, extensions,
fallback: {}, fallback: {},

View File

@ -12,69 +12,94 @@ const {
const root = join(FLECKS_CORE_ROOT, 'test', 'server', 'explicate'); const root = join(FLECKS_CORE_ROOT, 'test', 'server', 'explicate');
function createExplication(paths, platforms) { function createExplication(paths, platforms) {
const resolver = new Resolver({modules: [join(root, 'fake_node_modules')]}); const resolver = new Resolver({
return explicate( modules: [join(root, 'fake_node_modules')],
root,
});
return explicate({
paths, paths,
{
platforms, platforms,
resolver, resolver,
root,
importer: (request) => __non_webpack_require__(request), importer: (request) => __non_webpack_require__(request),
}, });
);
} }
describe('explication', () => { describe('explication', () => {
it('derives platforms', async () => { it('derives platforms', async () => {
expect(Object.keys((await createExplication(['platformed'])).descriptors)) expect(await createExplication(['platformed']))
.to.deep.equal([ .to.deep.include({
'platformed', 'platformed/server', paths: ['platformed', 'platformed/server'],
]); });
expect(Object.keys((await createExplication(['server-only'])).descriptors)) expect(await createExplication(['server-only']))
.to.deep.equal([ .to.deep.include({
'server-only/server', paths: ['server-only/server'],
]); });
}); });
it('derives through bootstrap', async () => { it('derives through bootstrap', async () => {
expect(Object.keys((await createExplication(['real-root'])).descriptors)) expect(await createExplication(['real-root']))
.to.deep.equal([ .to.deep.include({
paths: [
'dependency', 'dependency/server', 'dependency', 'dependency/server',
'real-root', 'real-root/server', 'real-root', 'real-root/server',
]); ],
});
}); });
it('excludes platforms', async () => { it('excludes platforms', async () => {
expect(Object.keys( expect(
(await createExplication( await createExplication(
['platformed/client', 'dependency'], ['platformed/client', 'dependency'],
['server', '!client'], ['server', '!client'],
)).descriptors, ),
)) )
.to.deep.equal([ .to.deep.include({
'dependency', 'dependency/server', paths: ['dependency', 'dependency/server'],
]); });
}); });
it('explicates parents first', async () => { it('explicates parents first', async () => {
expect(Object.keys((await createExplication(['real-root/server'])).descriptors)) expect(await createExplication(['real-root/server']))
.to.deep.equal([ .to.deep.include({
paths: [
'dependency', 'dependency/server', 'dependency', 'dependency/server',
'real-root', 'real-root/server', 'real-root', 'real-root/server',
]); ],
});
}); });
it('explicates only bootstrapped', async () => { it('explicates only bootstrapped', async () => {
expect(Object.keys((await createExplication(['only-bootstrapped'])).descriptors)) expect(await createExplication(['only-bootstrapped']))
.to.deep.equal([ .to.deep.include({
'only-bootstrapped', paths: ['only-bootstrapped'],
]); });
});
it('explicates root with src', async () => {
expect(await createExplication(['src-root:./src-root']))
.to.deep.include({
paths: ['src-root', 'src-root/server'],
});
}); });
it('skips nonexistent', async () => { it('skips nonexistent', async () => {
expect(await createExplication(['real-root/nonexistent'])) expect(await createExplication(['real-root/nonexistent']))
.to.deep.equal({descriptors: {}, roots: {}}); .to.deep.equal({paths: [], roots: {}});
});
it('includes modules', async () => {
expect(await createExplication(['modules-root:./modules-root', 'foo']))
.to.deep.include({
paths: ['modules-root', 'foo'],
});
});
it('explicates aliased platforms', async () => {
expect(await createExplication(['aliased-platforms:./aliased-platforms']))
.to.deep.include({
paths: ['aliased-platforms', 'aliased-platforms/server'],
});
}); });
}); });

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1 @@
{}

View File

@ -8,7 +8,7 @@ module.exports = async (env, argv) => {
config.plugins.push(new ProcessAssets('fleck', flecks)); config.plugins.push(new ProcessAssets('fleck', flecks));
// Small hack because internals. // Small hack because internals.
flecks.hooks['@flecks/build.processAssets'] = [{ flecks.hooks['@flecks/build.processAssets'] = [{
hook: '@flecks/build', fleck: '@flecks/build',
fn: (target, assets, compilation) => processFleckAssets(assets, compilation), fn: (target, assets, compilation) => processFleckAssets(assets, compilation),
}]; }];
return config; return config;

View File

@ -1,15 +0,0 @@
module.exports = function stub(stubs) {
if (0 === stubs.length) {
return;
}
const {Module} = require('module');
const {require: Mr} = Module.prototype;
Module.prototype.require = function hackedRequire(request, options) {
for (let i = 0; i < stubs.length; ++i) {
if (request.match(stubs[i])) {
return undefined;
}
}
return Mr.call(this, request, options);
};
};

View File

@ -1,4 +1,4 @@
{ {
"eslint.workingDirectories": [{"pattern": "./packages/*"}], "eslint.workingDirectories": [{"pattern": "./packages/*"}],
"eslint.options": {"overrideConfigFile": "./node_modules/@flecks/build/build/eslint.config.js"}, "eslint.options": {"overrideConfigFile": "./node_modules/@flecks/build/build/eslint.config.js"}
} }

View File

@ -6,6 +6,7 @@
"build:only": "flecks build", "build:only": "flecks build",
"debug": "DEBUG=@flecks/* npm run dev", "debug": "DEBUG=@flecks/* npm run dev",
"dev": "npm run -- build:only -dh", "dev": "npm run -- build:only -dh",
"postinstall": "patch-package",
"repl": "npx flecks repl --rlwrap", "repl": "npx flecks repl --rlwrap",
"start": "DEBUG=@flecks/*,-*:silly npm run dev" "start": "DEBUG=@flecks/*,-*:silly npm run dev"
}, },

View File

@ -108,13 +108,13 @@ exports.parseSource = async (path, source) => {
return exports.parseNormalSource(path, source); return exports.parseNormalSource(path, source);
}; };
exports.parseFleckRoot = async (path, root) => ( exports.parseFleckRoot = async (request) => (
Promise.all( Promise.all(
(await Promise.all([ (await Promise.all([
...await glob(join(root, 'src', '**', '*.js')), ...await glob(join(request, 'src', '**', '*.js')),
...await glob(join(root, 'build', '**', '*.js')), ...await glob(join(request, 'build', '**', '*.js')),
])) ]))
.map((filename) => [relative(root, filename), filename]) .map((filename) => [relative(request, filename), filename])
.map(async ([path, filename]) => { .map(async ([path, filename]) => {
const buffer = await readFile(filename); const buffer = await readFile(filename);
return [path, await exports.parseSource(path, buffer.toString('utf8'))]; return [path, await exports.parseSource(path, buffer.toString('utf8'))];
@ -124,7 +124,7 @@ exports.parseFleckRoot = async (path, root) => (
exports.parseFlecks = async (flecks) => ( exports.parseFlecks = async (flecks) => (
Promise.all( Promise.all(
Object.entries(flecks.roots) flecks.roots
.map(async ([path, {root}]) => [path, await exports.parseFleckRoot(path, root)]), .map(async ([path, request]) => [path, await exports.parseFleckRoot(request)]),
) )
); );

View File

@ -77,7 +77,7 @@ module.exports = (program, flecks) => {
}); });
}); });
}; };
require('@flecks/core/build/stub')(flecks.stubs); require('@flecks/build/build/resolve')(flecks.resolver, flecks.stubs);
if (!watch) { if (!watch) {
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
child.on('exit', (code) => { child.on('exit', (code) => {

View File

@ -2,6 +2,7 @@ const flecksConfigFn = require('@flecks/build/build/fleck.webpack.config');
module.exports = async (env, argv, flecks) => { module.exports = async (env, argv, flecks) => {
const config = await flecksConfigFn(env, argv, flecks); const config = await flecksConfigFn(env, argv, flecks);
config.resolve.modules.push('node_modules');
config.stats = flecks.get('@flecks/fleck.stats'); config.stats = flecks.get('@flecks/fleck.stats');
return config; return config;
}; };

View File

@ -1,5 +1,9 @@
const {externals} = require('@flecks/build/server'); const {externals} = require('@flecks/build/server');
const D = require('@flecks/core/build/debug');
const debug = D('@flecks/server/build/runtime');
module.exports = async (config, env, argv, flecks) => { module.exports = async (config, env, argv, flecks) => {
const runtimePath = await flecks.resolver.resolve('@flecks/server/runtime'); const runtimePath = await flecks.resolver.resolve('@flecks/server/runtime');
// Inject flecks configuration. // Inject flecks configuration.
@ -10,6 +14,10 @@ module.exports = async (config, env, argv, flecks) => {
.filter(([, resolved]) => resolved) .filter(([, resolved]) => resolved)
.map(([path]) => path); .map(([path]) => path);
const runtime = { const runtime = {
resolver: JSON.stringify({
aliases: flecks.resolver.aliases,
fallbacks: flecks.resolver.fallbacks,
}),
config: JSON.stringify(flecks.config), config: JSON.stringify(flecks.config),
loadFlecks: [ loadFlecks: [
'async () => (', 'async () => (',
@ -98,14 +106,19 @@ module.exports = async (config, env, argv, flecks) => {
/^@babel\/runtime\/helpers\/esm/, /^@babel\/runtime\/helpers\/esm/,
]; ];
config.resolve.alias['@flecks/server/runtime$'] = runtimePath; config.resolve.alias['@flecks/server/runtime$'] = runtimePath;
const nodeExternalsConfig = { Object.entries(flecks.resolver.aliases).forEach(([path, request]) => {
allowlist, debug('server runtime de-externalized %s, alias: %s', path, request);
}; allowlist.push(new RegExp(`^${path}`));
await flecks.runtimeCompiler('server', config, nodeExternalsConfig); });
// Stubs.
flecks.stubs.forEach((stub) => {
config.resolve.alias[stub] = false;
});
await flecks.runtimeCompiler('server', config);
// Rewrite to signals for HMR. // Rewrite to signals for HMR.
if ('production' !== argv.mode) { if ('production' !== argv.mode) {
allowlist.push(/^webpack\/hot\/signal/); allowlist.push(/^webpack\/hot\/signal/);
} }
// Externalize the rest. // Externalize the rest.
config.externals = externals(nodeExternalsConfig); config.externals = externals({allowlist});
}; };

View File

@ -50,7 +50,7 @@ module.exports = async (env, argv, flecks) => {
config.entry.index.push('@flecks/server/entry'); config.entry.index.push('@flecks/server/entry');
// Augment the application-starting configuration. // Augment the application-starting configuration.
if (isStarting) { if (isStarting) {
if (Object.entries(flecks.compiled).length > 0) { if (flecks.roots.some(([path, request]) => path !== request)) {
nodeEnv.NODE_PRESERVE_SYMLINKS = 1; nodeEnv.NODE_PRESERVE_SYMLINKS = 1;
} }
config.plugins.push( config.plugins.push(

View File

@ -8,7 +8,7 @@ const {version} = require('../package.json');
(async () => { (async () => {
const runtime = await __non_webpack_require__('@flecks/server/runtime'); const runtime = await __non_webpack_require__('@flecks/server/runtime');
const {loadFlecks, stubs} = runtime; const {loadFlecks, resolver, stubs} = runtime;
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log(`flecks server v${version}`); console.log(`flecks server v${version}`);
try { try {
@ -24,7 +24,7 @@ const {version} = require('../package.json');
const unserializedStubs = stubs.map((stub) => (Array.isArray(stub) ? new RegExp(...stub) : stub)); const unserializedStubs = stubs.map((stub) => (Array.isArray(stub) ? new RegExp(...stub) : stub));
if (unserializedStubs.length > 0) { if (unserializedStubs.length > 0) {
debug('stubbing with %O', unserializedStubs); debug('stubbing with %O', unserializedStubs);
__non_webpack_require__('@flecks/core/build/stub')(unserializedStubs); __non_webpack_require__('@flecks/build/build/resolve')(resolver, unserializedStubs);
} }
global.flecks = await Flecks.from({...runtime, flecks: await loadFlecks()}); global.flecks = await Flecks.from({...runtime, flecks: await loadFlecks()});
try { try {

View File

@ -315,9 +315,3 @@ exports.hooks = {
return JSON.stringify(config); return JSON.stringify(config);
}, },
}; };
exports.stubs = {
server: [
/\.(c|s[ac])ss$/,
],
};

View File

@ -22,16 +22,12 @@ module.exports = async (config, env, argv, flecks) => {
Object.keys(flecks.flecks) Object.keys(flecks.flecks)
.map(async (fleck) => { .map(async (fleck) => {
// No root? How to infer? // No root? How to infer?
const [root] = Object.entries(flecks.roots) const [root, request] = flecks.roots.find(([root]) => fleck.startsWith(root)) || [];
.find(([root]) => fleck.startsWith(root)) || [];
if (!root) { if (!root) {
return undefined; return undefined;
} }
// Compiled? It will be included with the compilation. // Compiled? It will be included with the compilation.
if ( if (root !== request) {
Object.entries(flecks.compiled)
.some(([, {flecks}]) => flecks.includes(fleck))
) {
return undefined; return undefined;
} }
try { try {
@ -96,12 +92,7 @@ module.exports = async (config, env, argv, flecks) => {
config.resolve.alias['@flecks/web/runtime$'] = runtime; config.resolve.alias['@flecks/web/runtime$'] = runtime;
// Stubs. // Stubs.
buildFlecks.stubs.forEach((stub) => { buildFlecks.stubs.forEach((stub) => {
config.module.rules.push( config.resolve.alias[stub] = false;
{
test: stub,
use: 'null-loader',
},
);
}); });
await buildFlecks.runtimeCompiler('web', config); await buildFlecks.runtimeCompiler('web', config);
// Styles. // Styles.
@ -109,26 +100,18 @@ module.exports = async (config, env, argv, flecks) => {
// Tests. // Tests.
if (!isProduction) { if (!isProduction) {
const testEntries = (await Promise.all( const testEntries = (await Promise.all(
Object.entries(buildFlecks.roots) buildFlecks.roots
.map(async ([parent, {request}]) => { .map(async ([root, request]) => {
const tests = []; const tests = [];
const resolved = dirname(await resolver.resolve(join(request, 'package.json'))); const rootTests = await glob(join(request, 'test', '*.js'));
const rootTests = await glob(join(resolved, 'test', '*.js')); tests.push(...rootTests.map((test) => test.replace(request, root)));
tests.push(
...rootTests
.map((test) => test.replace(resolved, parent)),
);
const platformTests = await Promise.all( const platformTests = await Promise.all(
buildFlecks.platforms.map((platform) => ( buildFlecks.platforms.map((platform) => (
glob(join(resolved, 'test', 'platforms', platform, '*.js')) glob(join(request, 'test', platform, '*.js'))
)), )),
); );
tests.push( tests.push(...platformTests.flat().map((test) => test.replace(request, root)));
...platformTests return [root, tests];
.flat()
.map((test) => test.replace(resolved, parent)),
);
return [parent, tests];
}), }),
)) ))
.filter(([, tests]) => tests.length > 0); .filter(([, tests]) => tests.length > 0);

View File

@ -35,6 +35,7 @@ module.exports = async (env, argv, flecks) => {
stream: false, stream: false,
util: require.resolve('util'), util: require.resolve('util'),
}, },
modules: flecks.resolver.modules,
}, },
stats: { stats: {
warningsFilter: [ warningsFilter: [