From f657c5ea8b62d144aff2cfd07ff1eb8a090c217c Mon Sep 17 00:00:00 2001 From: cha0s Date: Thu, 25 Jan 2024 06:42:31 -0600 Subject: [PATCH] refactor: explication --- .vscode/settings.json | 2 +- packages/build/build/babel.config.js | 6 + packages/build/build/build.js | 233 ++++------------- packages/build/build/build.webpack.config.js | 2 +- packages/build/build/explicate.js | 234 ++++++++---------- packages/build/build/flecks.bootstrap.js | 2 +- packages/build/build/resolve.js | 27 ++ packages/build/build/resolver.js | 35 ++- packages/build/build/webpack.js | 6 + packages/build/test/server/explicate.js | 103 +++++--- .../explicate/aliased-platforms/package.json | 1 + .../explicate/aliased-platforms/src/index.js | 0 .../explicate/aliased-platforms/src/server.js | 0 .../server/explicate/modules-root/index.js | 0 .../explicate/modules-root/package.json | 1 + .../server/explicate/src-root/package.json | 1 + .../server/explicate/src-root/src/index.js | 0 .../explicate/src-root/src/server/index.js | 0 packages/core/build/core.webpack.config.js | 2 +- packages/core/build/stub.js | 15 -- .../create-app/template/.vscode/settings.json | 2 +- .../template/package.json.noconflict | 1 + packages/dox/build/parser.js | 12 +- packages/fleck/build/commands.js | 2 +- packages/fleck/build/fleck.webpack.config.js | 1 + packages/server/build/runtime.js | 23 +- .../server/build/server.webpack.config.js | 2 +- packages/server/src/entry.js | 4 +- packages/web/build/flecks.bootstrap.js | 6 - packages/web/build/runtime.js | 37 +-- .../web/build/web-vendor.webpack.config.js | 1 + 31 files changed, 334 insertions(+), 427 deletions(-) create mode 100644 packages/build/build/resolve.js create mode 100644 packages/build/test/server/explicate/aliased-platforms/package.json create mode 100644 packages/build/test/server/explicate/aliased-platforms/src/index.js create mode 100644 packages/build/test/server/explicate/aliased-platforms/src/server.js create mode 100644 packages/build/test/server/explicate/modules-root/index.js create mode 100644 packages/build/test/server/explicate/modules-root/package.json create mode 100644 packages/build/test/server/explicate/src-root/package.json create mode 100644 packages/build/test/server/explicate/src-root/src/index.js create mode 100644 packages/build/test/server/explicate/src-root/src/server/index.js delete mode 100644 packages/core/build/stub.js diff --git a/.vscode/settings.json b/.vscode/settings.json index 8ccede1..950b3ee 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -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"}, } \ No newline at end of file diff --git a/packages/build/build/babel.config.js b/packages/build/build/babel.config.js index 3dd60c8..4b97edc 100644 --- a/packages/build/build/babel.config.js +++ b/packages/build/build/babel.config.js @@ -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', + }, }, ], ], diff --git a/packages/build/build/build.js b/packages/build/build/build.js index 6816966..a4fc73a 100644 --- a/packages/build/build/build.js +++ b/packages/build/build/build.js @@ -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() { diff --git a/packages/build/build/build.webpack.config.js b/packages/build/build/build.webpack.config.js index b1a195f..dff3508 100644 --- a/packages/build/build/build.webpack.config.js +++ b/packages/build/build/build.webpack.config.js @@ -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()); diff --git a/packages/build/build/explicate.js b/packages/build/build/explicate.js index 5126828..96d823e 100644 --- a/packages/build/build/explicate.js +++ b/packages/build/build/explicate.js @@ -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}; }; diff --git a/packages/build/build/flecks.bootstrap.js b/packages/build/build/flecks.bootstrap.js index bd34bf4..b548d1e 100644 --- a/packages/build/build/flecks.bootstrap.js +++ b/packages/build/build/flecks.bootstrap.js @@ -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)); diff --git a/packages/build/build/resolve.js b/packages/build/build/resolve.js new file mode 100644 index 0000000..01d398d --- /dev/null +++ b/packages/build/build/resolve.js @@ -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); + } + }; +}; diff --git a/packages/build/build/resolver.js b/packages/build/build/resolver.js index 45a08c5..52e789e 100644 --- a/packages/build/build/resolver.js +++ b/packages/build/build/resolver.js @@ -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; + } + } + }; diff --git a/packages/build/build/webpack.js b/packages/build/build/webpack.js index ece8153..d692381 100644 --- a/packages/build/build/webpack.js +++ b/packages/build/build/webpack.js @@ -41,6 +41,12 @@ exports.defaultConfig = (flecks, specializedConfig) => { }, plugins: [], resolve: { + alias: {}, + extensions, + fallback: {}, + modules: [], + }, + resolveLoader: { alias: {}, extensions, fallback: {}, diff --git a/packages/build/test/server/explicate.js b/packages/build/test/server/explicate.js index e787386..363f0ca 100644 --- a/packages/build/test/server/explicate.js +++ b/packages/build/test/server/explicate.js @@ -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'], + }); }); }); diff --git a/packages/build/test/server/explicate/aliased-platforms/package.json b/packages/build/test/server/explicate/aliased-platforms/package.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/packages/build/test/server/explicate/aliased-platforms/package.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/packages/build/test/server/explicate/aliased-platforms/src/index.js b/packages/build/test/server/explicate/aliased-platforms/src/index.js new file mode 100644 index 0000000..e69de29 diff --git a/packages/build/test/server/explicate/aliased-platforms/src/server.js b/packages/build/test/server/explicate/aliased-platforms/src/server.js new file mode 100644 index 0000000..e69de29 diff --git a/packages/build/test/server/explicate/modules-root/index.js b/packages/build/test/server/explicate/modules-root/index.js new file mode 100644 index 0000000..e69de29 diff --git a/packages/build/test/server/explicate/modules-root/package.json b/packages/build/test/server/explicate/modules-root/package.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/packages/build/test/server/explicate/modules-root/package.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/packages/build/test/server/explicate/src-root/package.json b/packages/build/test/server/explicate/src-root/package.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/packages/build/test/server/explicate/src-root/package.json @@ -0,0 +1 @@ +{} diff --git a/packages/build/test/server/explicate/src-root/src/index.js b/packages/build/test/server/explicate/src-root/src/index.js new file mode 100644 index 0000000..e69de29 diff --git a/packages/build/test/server/explicate/src-root/src/server/index.js b/packages/build/test/server/explicate/src-root/src/server/index.js new file mode 100644 index 0000000..e69de29 diff --git a/packages/core/build/core.webpack.config.js b/packages/core/build/core.webpack.config.js index 026ac54..d8df2e0 100644 --- a/packages/core/build/core.webpack.config.js +++ b/packages/core/build/core.webpack.config.js @@ -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; diff --git a/packages/core/build/stub.js b/packages/core/build/stub.js deleted file mode 100644 index ba458c1..0000000 --- a/packages/core/build/stub.js +++ /dev/null @@ -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); - }; -}; diff --git a/packages/create-app/template/.vscode/settings.json b/packages/create-app/template/.vscode/settings.json index 8ccede1..75c4427 100644 --- a/packages/create-app/template/.vscode/settings.json +++ b/packages/create-app/template/.vscode/settings.json @@ -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"} } \ No newline at end of file diff --git a/packages/create-app/template/package.json.noconflict b/packages/create-app/template/package.json.noconflict index ca82d82..31b442e 100644 --- a/packages/create-app/template/package.json.noconflict +++ b/packages/create-app/template/package.json.noconflict @@ -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" }, diff --git a/packages/dox/build/parser.js b/packages/dox/build/parser.js index a7fb5eb..7cc8009 100644 --- a/packages/dox/build/parser.js +++ b/packages/dox/build/parser.js @@ -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)]), ) ); diff --git a/packages/fleck/build/commands.js b/packages/fleck/build/commands.js index bcafe09..614ed67 100644 --- a/packages/fleck/build/commands.js +++ b/packages/fleck/build/commands.js @@ -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) => { diff --git a/packages/fleck/build/fleck.webpack.config.js b/packages/fleck/build/fleck.webpack.config.js index 81857ad..a828148 100644 --- a/packages/fleck/build/fleck.webpack.config.js +++ b/packages/fleck/build/fleck.webpack.config.js @@ -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; }; diff --git a/packages/server/build/runtime.js b/packages/server/build/runtime.js index 7c3b045..831ed9a 100644 --- a/packages/server/build/runtime.js +++ b/packages/server/build/runtime.js @@ -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}); }; diff --git a/packages/server/build/server.webpack.config.js b/packages/server/build/server.webpack.config.js index 287cee1..8160ab6 100644 --- a/packages/server/build/server.webpack.config.js +++ b/packages/server/build/server.webpack.config.js @@ -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( diff --git a/packages/server/src/entry.js b/packages/server/src/entry.js index 00e2013..262c7df 100644 --- a/packages/server/src/entry.js +++ b/packages/server/src/entry.js @@ -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 { diff --git a/packages/web/build/flecks.bootstrap.js b/packages/web/build/flecks.bootstrap.js index 783d18e..ba1ad20 100644 --- a/packages/web/build/flecks.bootstrap.js +++ b/packages/web/build/flecks.bootstrap.js @@ -315,9 +315,3 @@ exports.hooks = { return JSON.stringify(config); }, }; - -exports.stubs = { - server: [ - /\.(c|s[ac])ss$/, - ], -}; diff --git a/packages/web/build/runtime.js b/packages/web/build/runtime.js index fd8a2e7..9018a14 100644 --- a/packages/web/build/runtime.js +++ b/packages/web/build/runtime.js @@ -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); diff --git a/packages/web/build/web-vendor.webpack.config.js b/packages/web/build/web-vendor.webpack.config.js index f776f40..08d9632 100644 --- a/packages/web/build/web-vendor.webpack.config.js +++ b/packages/web/build/web-vendor.webpack.config.js @@ -35,6 +35,7 @@ module.exports = async (env, argv, flecks) => { stream: false, util: require.resolve('util'), }, + modules: flecks.resolver.modules, }, stats: { warningsFilter: [