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

View File

@ -1,5 +1,4 @@
const {realpath} = require('fs/promises');
const {dirname, join, relative} = require('path');
const {join} = require('path');
const D = require('@flecks/core/build/debug');
const {Flecks} = require('@flecks/core/build/flecks');
@ -65,142 +64,44 @@ module.exports = class Build extends Flecks {
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);
return babelmerge.all([
{configFile: await this.resolveBuildConfig('babel.config.js')},
...this.invokeFlat('@flecks/core.babel'),
]);
}
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] || {}]),
),
const cleanConfig = JSON.parse(JSON.stringify(originalConfig));
// Dealias the config keys.
const dealiasedConfig = environmentConfiguration(
Object.fromEntries(
Object.entries(cleanConfig)
.map(([maybeAliasedPath, config]) => {
const index = maybeAliasedPath.indexOf(':');
return [
-1 === index ? maybeAliasedPath : maybeAliasedPath.slice(0, index),
config,
];
}),
),
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,
const resolver = new Resolver({root: FLECKS_CORE_ROOT});
const {paths, roots} = await explicate({
paths: Object.keys(originalConfig),
platforms,
resolver,
roots,
importer: (request) => require(request),
});
const runtime = {
config: Object.fromEntries(paths.map((path) => [path, dealiasedConfig[path] || {}])),
flecks: Object.fromEntries(paths.map((path) => [
path,
flecks[path] || roots[path]?.bootstrap || {},
])),
};
return {
resolver,
roots: Object.entries(roots).map(([path, {request}]) => [path, request]),
runtime,
};
}
@ -225,15 +126,11 @@ module.exports = class Build extends Flecks {
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;
@ -255,7 +152,7 @@ module.exports = class Build extends Flecks {
return Object.fromEntries(
Object.entries(this.config)
.map(([path, config]) => {
const alias = this.aliased[path];
const alias = this.resolver.fallbacks[path];
return [alias ? `${path}:${alias}` : path, config];
}),
);
@ -279,38 +176,15 @@ module.exports = class Build extends Flecks {
return this.resolver.resolve(join(fleck, 'build', config));
}
async runtimeCompiler(runtime, config, {additionalModuleDirs = [], allowlist = []} = {}) {
async runtimeCompiler(runtime, config) {
// 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,
{
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.
const compiled = this.roots.filter(([path, request]) => path !== request);
if (compiled.length > 0) {
const include = Object.values(this.resolver.aliases);
config.module.rules.push(
{
test: /\.(m?jsx?)?$/,
include: includes,
include,
use: [
{
loader: require.resolve('babel-loader'),
@ -318,35 +192,30 @@ module.exports = class Build extends Flecks {
cacheDirectory: true,
babelrc: 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.
Flecks.set(config, 'optimization.splitChunks.cacheGroups.flecks-compiled', {
chunks: 'all',
enforce: true,
priority: 100,
test: new RegExp(
`(?:${
includes
.map((path) => path.replace(/[\\/]/g, '[\\/]')).join('|')
})`,
),
test: new RegExp(`(?:${
include.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() {

View File

@ -9,7 +9,7 @@ module.exports = async (env, argv) => {
config.plugins.push(new ProcessAssets('fleck', flecks));
// Small hack because internals.
flecks.hooks['@flecks/build.processAssets'] = [{
hook: '@flecks/build',
fleck: '@flecks/build',
fn: (target, assets, compilation) => processFleckAssets(assets, compilation),
}];
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 {
FLECKS_CORE_ROOT = process.cwd(),
} = process.env;
delimiter,
join,
resolve,
} = require('path');
module.exports = async function explicate(
maybeAliasedPaths,
{
importer,
paths: maybeAliasedPaths,
platforms = ['server'],
resolver,
root,
},
) {
const descriptors = {};
const seen = {};
const dependentPaths = [];
const roots = {};
function createDescriptor(maybeAliasedPath) {
const index = maybeAliasedPath.indexOf(':');
return -1 === index
? {
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())
) {
async function addRoot(path, request) {
// Already added it?
if (Object.keys(roots).some((rootPath) => path.startsWith(rootPath))) {
return;
}
descriptors[request] = descriptor;
}
async function getRootDescriptor(descriptor) {
const {path, request} = descriptor;
// Walk up and find the root, if any.
const pathParts = path.split('/');
const requestParts = request.split('/');
let rootDescriptor;
const requestParts = path === request
? pathParts.slice()
: resolve(resolver.root, request).split('/');
/* eslint-disable no-await-in-loop */
while (pathParts.length > 0 && requestParts.length > 0) {
const candidate = requestParts.join('/');
// eslint-disable-next-line no-await-in-loop
if (await resolver.resolve(join(candidate, 'package.json'))) {
rootDescriptor = {
path: pathParts.join('/'),
request: requestParts.join('/'),
const rootPath = pathParts.join('/');
// Don't add the root if this path doesn't actually exist.
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;
}
pathParts.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);
}
}
}),
);
}
async function explicateRoot(descriptor) {
// Walk up and find the root, if any.
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(
dependencies
.map(createDescriptor)
.map(explicateDescriptor),
);
}
await explicateDescriptor(rootDescriptor);
}
await Promise.all(
maybeAliasedPaths
.map(createDescriptor)
.map(explicateDescriptor),
// 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)];
}),
);
return {
descriptors,
roots,
};
// Add roots.
await Promise.all(normalized.map(([path, request]) => addRoot(path, request)));
// Add aliases and fallbacks.
await Promise.all(
Object.entries(roots)
.filter(([path, {request}]) => path !== request)
.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 = (
// Resolve dependent, normalized, and platform paths.
await Promise.all(
dependentPaths.map((path) => [path, path])
.concat(normalized)
.map(([path]) => path)
.reduce((platformed, path) => (
platformed.concat([path], platforms.map((platform) => join(path, platform)))
), [])
.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.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 {
constructor(options) {
constructor(options = {}) {
const {
modules = [join(FLECKS_CORE_ROOT, 'node_modules'), 'node_modules'],
root = FLECKS_CORE_ROOT,
...rest
} = options;
this.resolver = ResolverFactory.createResolver({
conditionNames: ['node'],
extensions: ['.js', '.json', '.node'],
fileSystem: nodeFileSystem,
modules,
symlinks: false,
...options,
...rest,
});
this.aliases = {};
this.fallbacks = {};
this.modules = modules;
this.root = root;
}
addAlias(name, alias) {
debugSilly("adding alias: '%s' -> '%s'", name, alias);
this.aliases[name] = alias;
new AliasPlugin(
'raw-resolve',
{name, onlyModule: false, alias},
@ -50,6 +61,7 @@ module.exports = class Resolver {
addFallback(name, alias) {
debugSilly("adding fallback: '%s' -> '%s'", name, alias);
this.fallbacks[name] = alias;
new AliasPlugin(
'described-resolve',
{name, onlyModule: false, alias},
@ -59,10 +71,11 @@ module.exports = class Resolver {
addModules(path) {
debugSilly("adding modules: '%s'", path);
this.modules.push(path);
new ModulesInHierarchicalDirectoriesPlugin(
"raw-module",
'raw-module',
path,
"module"
'module',
).apply(this.resolver);
}
@ -73,7 +86,7 @@ module.exports = class Resolver {
async resolve(request) {
try {
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) {
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: [],
resolve: {
alias: {},
extensions,
fallback: {},
modules: [],
},
resolveLoader: {
alias: {},
extensions,
fallback: {},

View File

@ -12,69 +12,94 @@ const {
const root = join(FLECKS_CORE_ROOT, 'test', 'server', 'explicate');
function createExplication(paths, platforms) {
const resolver = new Resolver({modules: [join(root, 'fake_node_modules')]});
return explicate(
const resolver = new Resolver({
modules: [join(root, 'fake_node_modules')],
root,
});
return explicate({
paths,
{
platforms,
resolver,
root,
importer: (request) => __non_webpack_require__(request),
},
);
platforms,
resolver,
importer: (request) => __non_webpack_require__(request),
});
}
describe('explication', () => {
it('derives platforms', async () => {
expect(Object.keys((await createExplication(['platformed'])).descriptors))
.to.deep.equal([
'platformed', 'platformed/server',
]);
expect(Object.keys((await createExplication(['server-only'])).descriptors))
.to.deep.equal([
'server-only/server',
]);
expect(await createExplication(['platformed']))
.to.deep.include({
paths: ['platformed', 'platformed/server'],
});
expect(await createExplication(['server-only']))
.to.deep.include({
paths: ['server-only/server'],
});
});
it('derives through bootstrap', async () => {
expect(Object.keys((await createExplication(['real-root'])).descriptors))
.to.deep.equal([
'dependency', 'dependency/server',
'real-root', 'real-root/server',
]);
expect(await createExplication(['real-root']))
.to.deep.include({
paths: [
'dependency', 'dependency/server',
'real-root', 'real-root/server',
],
});
});
it('excludes platforms', async () => {
expect(Object.keys(
(await createExplication(
expect(
await createExplication(
['platformed/client', 'dependency'],
['server', '!client'],
)).descriptors,
))
.to.deep.equal([
'dependency', 'dependency/server',
]);
),
)
.to.deep.include({
paths: ['dependency', 'dependency/server'],
});
});
it('explicates parents first', async () => {
expect(Object.keys((await createExplication(['real-root/server'])).descriptors))
.to.deep.equal([
'dependency', 'dependency/server',
'real-root', 'real-root/server',
]);
expect(await createExplication(['real-root/server']))
.to.deep.include({
paths: [
'dependency', 'dependency/server',
'real-root', 'real-root/server',
],
});
});
it('explicates only bootstrapped', async () => {
expect(Object.keys((await createExplication(['only-bootstrapped'])).descriptors))
.to.deep.equal([
'only-bootstrapped',
]);
expect(await createExplication(['only-bootstrapped']))
.to.deep.include({
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 () => {
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));
// Small hack because internals.
flecks.hooks['@flecks/build.processAssets'] = [{
hook: '@flecks/build',
fleck: '@flecks/build',
fn: (target, assets, compilation) => processFleckAssets(assets, compilation),
}];
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.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",
"debug": "DEBUG=@flecks/* npm run dev",
"dev": "npm run -- build:only -dh",
"postinstall": "patch-package",
"repl": "npx flecks repl --rlwrap",
"start": "DEBUG=@flecks/*,-*:silly npm run dev"
},

View File

@ -108,13 +108,13 @@ exports.parseSource = async (path, source) => {
return exports.parseNormalSource(path, source);
};
exports.parseFleckRoot = async (path, root) => (
exports.parseFleckRoot = async (request) => (
Promise.all(
(await Promise.all([
...await glob(join(root, 'src', '**', '*.js')),
...await glob(join(root, 'build', '**', '*.js')),
...await glob(join(request, 'src', '**', '*.js')),
...await glob(join(request, 'build', '**', '*.js')),
]))
.map((filename) => [relative(root, filename), filename])
.map((filename) => [relative(request, filename), filename])
.map(async ([path, filename]) => {
const buffer = await readFile(filename);
return [path, await exports.parseSource(path, buffer.toString('utf8'))];
@ -124,7 +124,7 @@ exports.parseFleckRoot = async (path, root) => (
exports.parseFlecks = async (flecks) => (
Promise.all(
Object.entries(flecks.roots)
.map(async ([path, {root}]) => [path, await exports.parseFleckRoot(path, root)]),
flecks.roots
.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) {
await new Promise((resolve, reject) => {
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) => {
const config = await flecksConfigFn(env, argv, flecks);
config.resolve.modules.push('node_modules');
config.stats = flecks.get('@flecks/fleck.stats');
return config;
};

View File

@ -1,5 +1,9 @@
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) => {
const runtimePath = await flecks.resolver.resolve('@flecks/server/runtime');
// Inject flecks configuration.
@ -10,6 +14,10 @@ module.exports = async (config, env, argv, flecks) => {
.filter(([, resolved]) => resolved)
.map(([path]) => path);
const runtime = {
resolver: JSON.stringify({
aliases: flecks.resolver.aliases,
fallbacks: flecks.resolver.fallbacks,
}),
config: JSON.stringify(flecks.config),
loadFlecks: [
'async () => (',
@ -98,14 +106,19 @@ module.exports = async (config, env, argv, flecks) => {
/^@babel\/runtime\/helpers\/esm/,
];
config.resolve.alias['@flecks/server/runtime$'] = runtimePath;
const nodeExternalsConfig = {
allowlist,
};
await flecks.runtimeCompiler('server', config, nodeExternalsConfig);
Object.entries(flecks.resolver.aliases).forEach(([path, request]) => {
debug('server runtime de-externalized %s, alias: %s', path, request);
allowlist.push(new RegExp(`^${path}`));
});
// Stubs.
flecks.stubs.forEach((stub) => {
config.resolve.alias[stub] = false;
});
await flecks.runtimeCompiler('server', config);
// Rewrite to signals for HMR.
if ('production' !== argv.mode) {
allowlist.push(/^webpack\/hot\/signal/);
}
// 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');
// Augment the application-starting configuration.
if (isStarting) {
if (Object.entries(flecks.compiled).length > 0) {
if (flecks.roots.some(([path, request]) => path !== request)) {
nodeEnv.NODE_PRESERVE_SYMLINKS = 1;
}
config.plugins.push(

View File

@ -8,7 +8,7 @@ const {version} = require('../package.json');
(async () => {
const runtime = await __non_webpack_require__('@flecks/server/runtime');
const {loadFlecks, stubs} = runtime;
const {loadFlecks, resolver, stubs} = runtime;
// eslint-disable-next-line no-console
console.log(`flecks server v${version}`);
try {
@ -24,7 +24,7 @@ const {version} = require('../package.json');
const unserializedStubs = stubs.map((stub) => (Array.isArray(stub) ? new RegExp(...stub) : stub));
if (unserializedStubs.length > 0) {
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()});
try {

View File

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

View File

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