diff --git a/package.json b/package.json index d85f013..3d4c4ae 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,10 @@ "@flecks/electron": "*", "@flecks/fleck": "*", "@flecks/governor": "*", + "@flecks/passport": "*", + "@flecks/passport-local": "*", + "@flecks/passport-local-react": "*", + "@flecks/passport-react": "*", "@flecks/react": "*", "@flecks/redis": "*", "@flecks/redux": "*", @@ -27,10 +31,6 @@ "@flecks/server": "*", "@flecks/session": "*", "@flecks/socket": "*", - "@flecks/passport": "*", - "@flecks/passport-local": "*", - "@flecks/passport-local-react": "*", - "@flecks/passport-react": "*", "@flecks/web": "*" }, "dependencies": { diff --git a/packages/core/build/add-fleck-to-yml.js b/packages/core/build/add-fleck-to-yml.js new file mode 100644 index 0000000..48cd130 --- /dev/null +++ b/packages/core/build/add-fleck-to-yml.js @@ -0,0 +1,19 @@ +const {readFile, writeFile} = require('fs/promises'); +const { + join, + sep, +} = require('path'); + +const {dump: dumpYml, load: loadYml} = require('js-yaml'); + +const { + FLECKS_CORE_ROOT = process.cwd(), +} = process.env; + +module.exports = async (fleck, path) => { + const key = [fleck].concat(path ? `.${sep}${join('packages', path, 'src')}` : []).join(':'); + const ymlPath = join(FLECKS_CORE_ROOT, 'build', 'flecks.yml'); + let yml = loadYml(await readFile(ymlPath)); + yml = Object.fromEntries(Object.entries(yml).concat([[key, {}]])); + await writeFile(ymlPath, dumpYml(yml, {sortKeys: true})); +}; diff --git a/packages/core/src/server/build/babel.config.js b/packages/core/build/babel.config.js similarity index 100% rename from packages/core/src/server/build/babel.config.js rename to packages/core/build/babel.config.js diff --git a/packages/core/build/class.js b/packages/core/build/class.js new file mode 100644 index 0000000..5d94a34 --- /dev/null +++ b/packages/core/build/class.js @@ -0,0 +1 @@ +module.exports = class {}; diff --git a/packages/core/build/cli.js b/packages/core/build/cli.js new file mode 100755 index 0000000..9fd037a --- /dev/null +++ b/packages/core/build/cli.js @@ -0,0 +1,67 @@ +#!/usr/bin/env node + +const {Command} = require('commander'); + +const {processCode} = require('./commands'); +const D = require('./debug'); +const Server = require('./server'); + +const debug = D('@flecks/core/cli'); +const debugSilly = debug.extend('silly'); + +// Asynchronous command process code forwarding. +const forwardProcessCode = (fn) => async (...args) => { + const child = await fn(...args); + if ('object' !== typeof child) { + const code = 'undefined' !== typeof child ? child : 0; + debugSilly('action returned code %d', code); + process.exitCode = code; + return; + } + try { + const code = await processCode(child); + debugSilly('action exited with code %d', code); + process.exitCode = code; + } + catch (error) { + // eslint-disable-next-line no-console + console.error(error); + process.exitCode = child.exitCode || 1; + } +}; +// Initialize Commander. +const program = new Command(); +program + .enablePositionalOptions() + .name('flecks') + .usage('[command] [...]'); +// Bootstrap. +(async () => { + debugSilly('bootstrapping flecks...'); + const flecks = await Server.from(); + debugSilly('bootstrapped'); + // Register commands. + const commands = flecks.invokeMerge('@flecks/core.commands', program); + const keys = Object.keys(commands).sort(); + for (let i = 0; i < keys.length; ++i) { + const { + action, + args = [], + description, + name = keys[i], + options = [], + } = commands[keys[i]]; + debugSilly('adding command %s...', name); + const cmd = program.command(name); + cmd.description(description); + for (let i = 0; i < args.length; ++i) { + cmd.addArgument(args[i]); + } + for (let i = 0; i < options.length; ++i) { + cmd.option(...options[i]); + } + cmd.action(forwardProcessCode(action)); + } + // Parse commandline. + program.parse(process.argv); +})(); diff --git a/packages/core/src/server/commands.js b/packages/core/build/commands.js similarity index 67% rename from packages/core/src/server/commands.js rename to packages/core/build/commands.js index 167a525..5363e8c 100644 --- a/packages/core/src/server/commands.js +++ b/packages/core/build/commands.js @@ -1,13 +1,12 @@ -import {spawn} from 'child_process'; -import {join, normalize} from 'path'; +const {spawn} = require('child_process'); +const {join, normalize} = require('path'); -import {Argument} from 'commander'; -import {glob} from 'glob'; -import flatten from 'lodash.flatten'; -import rimraf from 'rimraf'; +const {Argument, Option, program} = require('commander'); +const {glob} = require('glob'); +const rimraf = require('rimraf'); -import D from '../debug'; -import Flecks from './flecks'; +const D = require('./debug'); +const addFleckToYml = require('./add-fleck-to-yml'); const { FLECKS_CORE_ROOT = process.cwd(), @@ -17,32 +16,8 @@ const debug = D('@flecks/core/commands'); const debugSilly = debug.extend('silly'); const flecksRoot = normalize(FLECKS_CORE_ROOT); -export {Argument, Option, program} from 'commander'; - -export const processCode = (child) => new Promise((resolve, reject) => { - child.on('error', reject); - child.on('exit', (code) => { - child.off('error', reject); - resolve(code); - }); -}); - -export const spawnWith = (cmd, opts = {}) => { - debug("spawning: '%s'", cmd.join(' ')); - debugSilly('with options: %O', opts); - const child = spawn(cmd[0], cmd.slice(1), { - stdio: 'inherit', - ...opts, - env: { - ...process.env, - ...opts.env, - }, - }); - return child; -}; - -export default (program, flecks) => { - const {packageManager} = flecks.get('@flecks/core/server'); +exports.commands = (program, flecks) => { + const {packageManager} = flecks.get('@flecks/core'); const commands = { add: { args: [ @@ -58,8 +33,8 @@ export default (program, flecks) => { args.push(packageManager, ['install', fleck]); } args.push({stdio: 'inherit'}); - await processCode(spawn(...args)); - await Flecks.addFleckToYml(fleck); + await module.exports.processCode(spawn(...args)); + await addFleckToYml(fleck); }, }, clean: { @@ -83,11 +58,11 @@ export default (program, flecks) => { }, }, }; - const targets = flatten(flecks.invokeFlat('@flecks/core.targets')); + const {targets} = flecks; if (targets.length > 0) { commands.build = { args: [ - new Argument('[target]', 'build target').choices(targets), + new Argument('[target]', 'build target').choices(targets.map(([, target]) => target)), ], options: [ ['-d, --no-production', 'dev build'], @@ -95,21 +70,21 @@ export default (program, flecks) => { ['-w, --watch', 'watch for changes'], ], description: 'build a target in your application', - action: (target, opts) => { + action: async (target, opts) => { const { hot, production, watch, } = opts; debug('Building...', opts); - const webpackConfig = flecks.buildConfig('fleckspack.config.js'); + const webpackConfig = await flecks.resolveBuildConfig('fleckspack.config.js'); const cmd = [ 'npx', 'webpack', '--config', webpackConfig, '--mode', (production && !hot) ? 'production' : 'development', ...((watch || hot) ? ['--watch'] : []), ]; - return spawnWith( + return module.exports.spawnWith( cmd, { env: { @@ -131,28 +106,30 @@ export default (program, flecks) => { if (0 === packages.length) { packages.push('.'); } - packages - .map((pkg) => join(process.cwd(), pkg)) - .forEach((cwd) => { - const cmd = [ - 'npx', 'eslint', - '--config', flecks.buildConfig('eslint.config.js'), - '.', - ]; - promises.push(new Promise((resolve, reject) => { - const child = spawnWith( - cmd, - { - cwd, - }, - ); - child.on('error', reject); - child.on('exit', (code) => { - child.off('error', reject); - resolve(code); - }); - })); - }); + await Promise.all( + packages + .map((pkg) => join(process.cwd(), pkg)) + .map(async (cwd) => { + const cmd = [ + 'npx', 'eslint', + '--config', await flecks.resolveBuildConfig('eslint.config.js'), + '.', + ]; + promises.push(new Promise((resolve, reject) => { + const child = module.exports.spawnWith( + cmd, + { + cwd, + }, + ); + child.on('error', reject); + child.on('exit', (code) => { + child.off('error', reject); + resolve(code); + }); + })); + }), + ); const promise = Promise.all(promises) .then( (codes) => ( @@ -176,3 +153,29 @@ export default (program, flecks) => { }; return commands; }; + +exports.processCode = (child) => new Promise((resolve, reject) => { + child.on('error', reject); + child.on('exit', (code) => { + child.off('error', reject); + resolve(code); + }); +}); + +exports.spawnWith = (cmd, opts = {}) => { + debug("spawning: '%s'", cmd.join(' ')); + debugSilly('with options: %O', opts); + const child = spawn(cmd[0], cmd.slice(1), { + stdio: 'inherit', + ...opts, + env: { + ...process.env, + ...opts.env, + }, + }); + return child; +}; + +exports.Argument = Argument; +exports.Option = Option; +exports.program = program; diff --git a/packages/core/src/compose.js b/packages/core/build/compose.js similarity index 77% rename from packages/core/src/compose.js rename to packages/core/build/compose.js index 2603fe3..c3d69e0 100644 --- a/packages/core/src/compose.js +++ b/packages/core/build/compose.js @@ -1,4 +1,4 @@ -export default function compose(...funcs) { +module.exports = function compose(...funcs) { if (funcs.length === 0) { return (arg) => arg; } @@ -6,4 +6,4 @@ export default function compose(...funcs) { return funcs[0]; } return funcs.reduce((a, b) => (...args) => a(b(...args))); -} +}; diff --git a/packages/core/build/core.eslint.config.js b/packages/core/build/core.eslint.config.js new file mode 100644 index 0000000..33adf03 --- /dev/null +++ b/packages/core/build/core.eslint.config.js @@ -0,0 +1,4 @@ +const defaultConfigFn = require('./default.eslint.config'); +const Server = require('./server'); + +module.exports = defaultConfigFn(Server.from()); diff --git a/packages/core/build/core.webpack.config.js b/packages/core/build/core.webpack.config.js new file mode 100644 index 0000000..6c66be2 --- /dev/null +++ b/packages/core/build/core.webpack.config.js @@ -0,0 +1,10 @@ +const Server = require('./server'); +const configFn = require('./fleck.webpack.config'); +const {executable} = require('./webpack'); + +module.exports = async (env, argv) => { + const flecks = await Server.from(); + const config = await configFn(env, argv, flecks); + config.plugins.push(...executable()); + return config; +}; diff --git a/packages/core/src/debug.js b/packages/core/build/debug.js similarity index 100% rename from packages/core/src/debug.js rename to packages/core/build/debug.js diff --git a/packages/core/build/default.eslint.config.js b/packages/core/build/default.eslint.config.js new file mode 100644 index 0000000..9864c15 --- /dev/null +++ b/packages/core/build/default.eslint.config.js @@ -0,0 +1,74 @@ +const globals = require('globals'); + +module.exports = async (flecks) => ({ + extends: [ + require.resolve('eslint-config-airbnb'), + require.resolve('eslint-config-airbnb/hooks'), + ], + globals: { + ...globals.browser, + ...globals.es2021, + ...globals.mocha, + ...globals.node, + __non_webpack_require__: true, + }, + ignorePatterns: [ + 'dist/**', + // Not even gonna try. + 'build/dox/hooks.js', + ], + overrides: [ + { + files: [ + 'build/**/*.js', + ], + rules: { + 'import/no-dynamic-require': 'off', + 'global-require': 'off', + }, + }, + { + files: [ + 'test/**/*.js', + ], + rules: { + 'brace-style': 'off', + 'class-methods-use-this': 'off', + 'import/no-extraneous-dependencies': 'off', + 'import/no-unresolved': 'off', + 'max-classes-per-file': 'off', + 'no-new': 'off', + 'no-unused-expressions': 'off', + 'padded-blocks': 'off', + }, + }, + ], + parser: require.resolve('@babel/eslint-parser'), + parserOptions: { + requireConfigFile: false, + babelOptions: await flecks.babel(), + }, + plugins: ['@babel'], + rules: { + 'brace-style': ['error', 'stroustrup'], + // Bug: https://github.com/import-js/eslint-plugin-import/issues/2181 + 'import/no-import-module-exports': 'off', + 'import/prefer-default-export': 'off', + 'jsx-a11y/control-has-associated-label': ['error', {assert: 'either'}], + 'jsx-a11y/label-has-associated-control': ['error', {assert: 'either'}], + 'no-param-reassign': ['error', {props: false}], + 'no-plusplus': 'off', + 'no-shadow': 'off', + 'object-curly-spacing': 'off', + 'padded-blocks': ['error', {classes: 'always'}], + yoda: 'off', + }, + settings: { + 'import/resolver': { + node: {}, + }, + react: { + version: '18.2.0', + }, + }, +}); diff --git a/packages/core/src/digraph.js b/packages/core/build/digraph.js similarity index 98% rename from packages/core/src/digraph.js rename to packages/core/build/digraph.js index 3355fd9..0677560 100644 --- a/packages/core/src/digraph.js +++ b/packages/core/build/digraph.js @@ -87,4 +87,4 @@ class Digraph { } -export default Digraph; +module.exports = Digraph; diff --git a/packages/core/build/dox/concepts/build.md b/packages/core/build/dox/concepts/build.md index 26ae3bf..1ed51e3 100644 --- a/packages/core/build/dox/concepts/build.md +++ b/packages/core/build/dox/concepts/build.md @@ -68,7 +68,7 @@ Have fun! ## Resolution order 🤔 -The flecks server provides an interface (`flecks.buildConfig()`) for gathering configuration files +The flecks server provides an interface (`flecks.resolveBuildConfig()`) for gathering configuration files from the `build` directory. The resolution order is determined by a few variables: - `filename` specifies the name of the configuration file, e.g. `server.webpack.config.js`. diff --git a/packages/core/build/eslint.config.js b/packages/core/build/eslint.config.js index 7d44f73..7e0836d 100644 --- a/packages/core/build/eslint.config.js +++ b/packages/core/build/eslint.config.js @@ -1,3 +1,71 @@ -const defaultConfigFn = require('../src/server/build/default.eslint.config'); +const {spawnSync} = require('child_process'); +const { + mkdirSync, + readFileSync, + statSync, + writeFileSync, +} = require('fs'); +const {join} = require('path'); -module.exports = defaultConfigFn(); +const D = require('./debug'); +const Server = require('./server'); + +const debug = D('@flecks/core/build/eslint.config.js'); + +const { + FLECKS_CORE_ROOT = process.cwd(), + FLECKS_CORE_SYNC_FOR_ESLINT = false, +} = process.env; + +// This is kinda nuts, but ESLint doesn't support its configuration files returning a promise! +if (FLECKS_CORE_SYNC_FOR_ESLINT) { + (async () => { + debug('bootstrapping flecks...'); + const flecks = await Server.from(); + debug('bootstrapped'); + // Load and finalize ESLint configuration. + const eslintConfig = await require( + await flecks.resolveBuildConfig('default.eslint.config.js'), + )(flecks); + const {resolve} = await require( + await flecks.resolveBuildConfig('fleck.webpack.config.js'), + )({}, {mode: 'development'}, flecks); + eslintConfig.settings['import/resolver'].webpack = {config: {resolve}}; + // Write it out to stdout. + process.stdout.write(JSON.stringify(eslintConfig, null, 2)); + })(); +} +else { + // Check cache first. + const cacheDirectory = join(FLECKS_CORE_ROOT, 'node_modules', '.cache', '@flecks', 'core'); + try { + statSync(join(cacheDirectory, 'eslint.config.json')); + module.exports = JSON.parse(readFileSync(join(cacheDirectory, 'eslint.config.json')).toString()); + } + catch (error) { + // Just silly. By synchronously spawning... ourselves, the child can use async. + const {stderr, stdout} = spawnSync('node', [__filename], { + env: { + FLECKS_CORE_SYNC_FOR_ESLINT: true, + NODE_PATH: join(FLECKS_CORE_ROOT, 'node_modules'), + ...process.env, + }, + }); + // eslint-disable-next-line no-console + console.error(stderr.toString()); + // Read the JSON written out to stdout. + const json = stdout.toString(); + try { + const parsed = JSON.parse(json); + statSync(join(FLECKS_CORE_ROOT, 'node_modules')); + mkdirSync(cacheDirectory, {recursive: true}); + // Cache. + writeFileSync(join(cacheDirectory, 'eslint.config.json'), json); + module.exports = parsed; + } + catch (error) { + // eslint-disable-next-line no-console + console.error(error); + } + } +} diff --git a/packages/core/src/event-emitter.js b/packages/core/build/event-emitter.js similarity index 97% rename from packages/core/src/event-emitter.js rename to packages/core/build/event-emitter.js index 874f41a..d7957d3 100644 --- a/packages/core/src/event-emitter.js +++ b/packages/core/build/event-emitter.js @@ -6,7 +6,7 @@ const createListener = (fn, that, type, once) => ({ bound: that ? fn.bind(that) : fn, }); -export default function EventEmitterDecorator(Superclass) { +module.exports = function EventEmitterDecorator(Superclass) { return class EventEmitter extends Superclass { @@ -123,4 +123,4 @@ export default function EventEmitterDecorator(Superclass) { }; -} +}; diff --git a/packages/core/build/explicate.js b/packages/core/build/explicate.js new file mode 100644 index 0000000..4568bd3 --- /dev/null +++ b/packages/core/build/explicate.js @@ -0,0 +1,135 @@ +const { + join, + resolve, +} = require('path'); + +module.exports = async function explicate( + maybeAliasedPaths, + { + importer, + platforms = ['server'], + resolver, + root, + }, +) { + const descriptors = {}; + const seen = {}; + 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()) + ) { + return; + } + if (path !== request) { + resolver.addAlias(path, request); + } + 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; + 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('/'), + }; + break; + } + pathParts.pop(); + requestParts.pop(); + } + return rootDescriptor; + } + 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); + } + await Promise.all( + platforms + .filter((platform) => !platform.startsWith('!')) + .map(async (platform) => { + if (await resolver.resolve(join(descriptor.request, platform))) { + return doExplication({ + path: join(descriptor.path, platform), + request: join(descriptor.request, platform), + }); + } + return undefined; + }), + ); + } + async function explicateRoot(descriptor) { + // Walk up and find the root, if any. + const rootDescriptor = await getRootDescriptor(descriptor); + if (!rootDescriptor || roots[rootDescriptor.request]) { + return; + } + const {request} = rootDescriptor; + 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), + ); + return { + descriptors, + roots, + }; +}; diff --git a/packages/core/src/server/build/webpack.config.js b/packages/core/build/fleck.webpack.config.js similarity index 65% rename from packages/core/src/server/build/webpack.config.js rename to packages/core/build/fleck.webpack.config.js index abd0550..f49321e 100644 --- a/packages/core/src/server/build/webpack.config.js +++ b/packages/core/build/fleck.webpack.config.js @@ -5,34 +5,28 @@ const { join, } = require('path'); -const babelmerge = require('babel-merge'); const CopyPlugin = require('copy-webpack-plugin'); const glob = require('glob'); -const D = require('../../debug'); -const R = require('../../require'); -const {defaultConfig, externals, regexFromExtensions} = require('../webpack'); +const {defaultConfig, externals, regexFromExtensions} = require('./webpack'); const { FLECKS_CORE_ROOT = process.cwd(), } = process.env; -const debug = D('@flecks/core/server/build/fleck.webpack.config.js'); -const debugSilly = debug.extend('silly'); - const source = join(FLECKS_CORE_ROOT, 'src'); const tests = join(FLECKS_CORE_ROOT, 'test'); const resolveValidModulePath = (source) => (path) => { // Does the file resolve as source? try { - R.resolve(`${source}/${path}`); + require.resolve(`${source}/${path}`); } catch (error) { const ext = extname(path); // Try the implicit [path]/index[.ext] variation. try { - R.resolve(`${source}/${dirname(path)}/${basename(path, ext)}/index${ext}`); + require.resolve(`${source}/${dirname(path)}/${basename(path, ext)}/index${ext}`); } catch (error) { return false; @@ -41,8 +35,8 @@ const resolveValidModulePath = (source) => (path) => { return true; }; -module.exports = (env, argv, flecks) => { - const {name, files = []} = R(join(FLECKS_CORE_ROOT, 'package.json')); +module.exports = async (env, argv, flecks) => { + const {name, files = []} = require(join(FLECKS_CORE_ROOT, 'package.json')); const config = defaultConfig(flecks, { externals: externals({importType: 'umd'}), node: { @@ -86,6 +80,9 @@ module.exports = (env, argv, flecks) => { alias: { [name]: source, }, + fallback: { + [name]: FLECKS_CORE_ROOT, + }, }, stats: { colors: true, @@ -93,30 +90,7 @@ module.exports = (env, argv, flecks) => { }, target: 'node', }); - const merging = [ - { - plugins: ['@babel/plugin-syntax-dynamic-import'], - presets: [ - [ - '@babel/preset-env', - { - shippedProposals: true, - targets: { - esmodules: true, - node: 'current', - }, - }, - ], - ], - }, - ]; - if (flecks) { - merging.push({configFile: flecks.buildConfig('babel.config.js')}); - const flecksBabelConfig = flecks.babel(); - debugSilly('flecks.config.js: babel: %j', flecksBabelConfig); - merging.push(...flecksBabelConfig.map(([, babel]) => babel)); - } - const babelConfig = babelmerge.all(merging); + const babelConfig = await flecks.babel(); const extensionsRegex = regexFromExtensions(config.resolve.extensions); config.module.rules.push( { @@ -144,14 +118,12 @@ module.exports = (env, argv, flecks) => { }); // Test entry. const testPaths = glob.sync(join(tests, '*.js')); - const platforms = flecks - ? flecks.platforms - : ['server']; + const {platforms} = flecks; for (let i = 0; i < platforms.length; ++i) { - testPaths.push(...glob.sync(join(tests, `platforms/${platforms[i]}/*.js`))); + testPaths.push(...glob.sync(join(tests, platforms[i], '*.js'))); } if (testPaths.length > 0) { - config.entry.test = testPaths; + config.entry.test = ['source-map-support/register', ...testPaths]; } return config; }; diff --git a/packages/core/build/flecks.bootstrap.js b/packages/core/build/flecks.bootstrap.js new file mode 100644 index 0000000..105ed20 --- /dev/null +++ b/packages/core/build/flecks.bootstrap.js @@ -0,0 +1,60 @@ +const {join} = require('path'); + +const webpack = require('webpack'); + +const {commands} = require('./commands'); + +const { + FLECKS_CORE_ROOT = process.cwd(), +} = process.env; + +exports.hooks = { + '@flecks/core.exts': () => ['.mjs', '.js', '.json', '.wasm'], + '@flecks/core.build': async (target, config, env, argv, flecks) => { + if (flecks.get('@flecks/core.profile').includes(target)) { + config.plugins.push( + new webpack.debug.ProfilingPlugin({ + outputPath: join(FLECKS_CORE_ROOT, `profile.build-${target}.json`), + }), + ); + } + }, + '@flecks/core.build.config': () => [ + /** + * Babel configuration. See: https://babeljs.io/docs/en/config-files + */ + 'babel.config.js', + /** + * ESLint defaults. The generated `eslint.config.js` just reads from this file so that the + * build can dynamically configure parts of ESLint. + */ + 'default.eslint.config.js', + /** + * ESLint configuration managed by flecks to allow async. + */ + 'eslint.config.js', + /** + * Flecks webpack configuration. See: https://webpack.js.org/configuration/ + */ + 'fleckspack.config.js', + /** + * Fleck build configuration. See: https://webpack.js.org/configuration/ + */ + 'fleck.webpack.config.js', + ], + '@flecks/core.commands': commands, + '@flecks/core.config': () => ({ + /** + * The ID of your application. + */ + id: 'flecks', + /** + * The package manager used for tasks. + */ + packageManager: 'npm', + /** + * Build targets to profile with `webpack.debug.ProfilingPlugin`. + */ + profile: [], + }), +}; diff --git a/packages/core/build/flecks.config.js b/packages/core/build/flecks.config.js deleted file mode 100644 index da22f41..0000000 --- a/packages/core/build/flecks.config.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = { - exts: ['.mjs', '.js'], -}; diff --git a/packages/core/src/flecks.js b/packages/core/build/flecks.js similarity index 94% rename from packages/core/src/flecks.js rename to packages/core/build/flecks.js index ee11e25..316fbb2 100644 --- a/packages/core/src/flecks.js +++ b/packages/core/build/flecks.js @@ -1,18 +1,18 @@ // eslint-disable-next-line max-classes-per-file -import { +const { basename, dirname, extname, join, -} from 'path'; +} = require('path'); -import get from 'lodash.get'; -import set from 'lodash.set'; +const get = require('lodash.get'); +const set = require('lodash.set'); -import compose from './compose'; -import D from './debug'; -import Digraph from './digraph'; -import Middleware from './middleware'; +const compose = require('./compose'); +const D = require('./debug'); +const Digraph = require('./digraph'); +const Middleware = require('./middleware'); const debug = D('@flecks/core/flecks'); const debugSilly = debug.extend('silly'); @@ -21,8 +21,8 @@ const debugSilly = debug.extend('silly'); const HookPriority = Symbol.for('@flecks/core.hookPriority'); // Symbols for Gathered classes. -export const ById = Symbol.for('@flecks/core.byId'); -export const ByType = Symbol.for('@flecks/core.byType'); +exports.ById = Symbol.for('@flecks/core.byId'); +exports.ByType = Symbol.for('@flecks/core.byType'); /** * Capitalize a string. @@ -59,7 +59,7 @@ const wrapGathered = (Class, id, idProperty, type, typeProperty) => { return Subclass; }; -export default class Flecks { +exports.Flecks = class Flecks { config = {}; @@ -69,23 +69,19 @@ export default class Flecks { hooks = {}; - platforms = {}; - /** - * @param {object} init - * @param {object} init.config The Flecks configuration (e.g. loaded from `flecks.yml`). - * @param {string[]} init.platforms Platforms this instance is running on. + * @param {object} runtime + * @param {object} runtime.config configuration (e.g. loaded from `flecks.yml`). + * @param {object} runtime.flecks fleck modules. */ constructor({ config = {}, flecks = {}, - platforms = [], } = {}) { const emptyConfigForAllFlecks = Object.fromEntries( Object.keys(flecks).map((path) => [path, {}]), ); this.config = {...emptyConfigForAllFlecks, ...config}; - this.platforms = platforms; const entries = Object.entries(flecks); debugSilly('paths: %O', entries.map(([fleck]) => fleck)); for (let i = 0; i < entries.length; i++) { @@ -191,7 +187,6 @@ export default class Flecks { this.config = {}; this.hooks = {}; this.flecks = {}; - this.platforms = []; } /** @@ -206,17 +201,9 @@ export default class Flecks { } const flecks = this.lookupFlecks(hook); let expanded = []; - // Expand configured flecks. for (let i = 0; i < flecks.length; ++i) { const fleck = flecks[i]; expanded.push(fleck); - for (let j = 0; j < this.platforms.length; ++j) { - const platform = this.platforms[j]; - const variant = join(fleck, platform); - if (this.fleck(variant)) { - expanded.push(variant); - } - } } // Handle elision. const index = expanded.findIndex((fleck) => '...' === fleck); @@ -238,13 +225,6 @@ export default class Flecks { if (!expanded.includes(fleck)) { elided.push(fleck); } - for (let j = 0; j < this.platforms.length; ++j) { - const platform = this.platforms[j]; - const variant = join(fleck, platform); - if (this.fleck(variant) && !expanded.includes(variant)) { - elided.push(variant); - } - } } // Map the fleck implementations to vertices in a dependency graph. const graph = this.flecksHookGraph([...before, ...elided, ...after], hook); @@ -380,13 +360,13 @@ export default class Flecks { * @param {Object} config Configuration. * @returns {Flecks} A flecks instance. */ - static from(config) { - const {flecks} = config; + static from(runtime) { + const {flecks} = runtime; const mixins = Object.entries(flecks) .map(([, M]) => M.hooks?.['@flecks/core.mixin']) .filter((e) => e); const Flecks = compose(...mixins)(this); - return new Flecks(config); + return new Flecks(runtime); } /** @@ -431,8 +411,8 @@ export default class Flecks { const gathered = { ...ids, ...types, - [ById]: ids, - [ByType]: types, + [exports.ById]: ids, + [exports.ByType]: types, }; // Register for HMR? hotGathered.set( @@ -444,7 +424,7 @@ export default class Flecks { gathered, }, ); - debug("gathered '%s': %O", hook, Object.keys(gathered[ByType])); + debug("gathered '%s': %O", hook, Object.keys(gathered[exports.ByType])); return gathered; } @@ -846,7 +826,10 @@ export default class Flecks { const {[type]: {[idProperty]: id}} = gathered; const Subclass = wrapGathered(Class, id, idProperty, type, typeProperty); // eslint-disable-next-line no-multi-assign - gathered[type] = gathered[id] = gathered[ById][id] = gathered[ByType][type] = Subclass; + gathered[type] = Subclass; + gathered[id] = Subclass; + gathered[exports.ById][id] = Subclass; + gathered[exports.ByType][type] = Subclass; this.invoke('@flecks/core.hmr.gathered.class', Subclass, hook); }); this.invoke('@flecks/core.hmr.gathered', gathered, hook); @@ -905,7 +888,7 @@ export default class Flecks { } } -} +}; -Flecks.get = get; -Flecks.set = set; +exports.Flecks.get = get; +exports.Flecks.set = set; diff --git a/packages/core/build/flecks.yml b/packages/core/build/flecks.yml deleted file mode 100644 index e8e3a6a..0000000 --- a/packages/core/build/flecks.yml +++ /dev/null @@ -1,2 +0,0 @@ -# This isn't a "real" `flecks.yml`. It only exists for testing purposes. -'@flecks/core:./src': {} diff --git a/packages/core/build/fleckspack.config.js b/packages/core/build/fleckspack.config.js new file mode 100644 index 0000000..58e9393 --- /dev/null +++ b/packages/core/build/fleckspack.config.js @@ -0,0 +1,65 @@ +require('source-map-support/register'); + +const D = require('./debug'); +const Server = require('./server'); + +const debug = D('@flecks/core/build/fleckspack.config.js'); + +const { + FLECKS_CORE_BUILD_LIST = '', +} = process.env; + +const buildList = FLECKS_CORE_BUILD_LIST + .split(',') + .map((name) => name.trim()) + .filter((e) => e); + +module.exports = async (env, argv) => { + debug('bootstrapping flecks...'); + const flecks = await Server.from(); + debug('bootstrapped'); + debug('gathering configs'); + const {targets} = flecks; + const building = targets + .filter(([, target]) => 0 === buildList.length || buildList.includes(target)); + debug('building: %O', building.map(([target]) => target)); + if (0 === building.length) { + debug('no build configuration found! aborting...'); + await new Promise(() => {}); + } + const entries = await Promise.all(building.map( + async ([fleck, target]) => { + const configFn = require(await flecks.resolveBuildConfig(`${target}.webpack.config.js`, fleck)); + if ('function' !== typeof configFn) { + debug(`'${ + target + }' build configuration expected function got ${ + typeof configFn + }! aborting...`); + return undefined; + } + return [target, await configFn(env, argv, flecks)]; + }, + )); + await Promise.all( + entries.map(async ([target, config]) => ( + Promise.all(flecks.invokeFlat('@flecks/core.build', target, config, env, argv)) + )), + ); + const webpackConfigs = Object.fromEntries(entries); + await Promise.all(flecks.invokeFlat('@flecks/core.build.alter', webpackConfigs, env, argv)); + const enterableWebpackConfigs = Object.values(webpackConfigs) + .filter((webpackConfig) => { + if (!webpackConfig.entry) { + debug('webpack configurations %O had no entry... discarding', webpackConfig); + return false; + } + return true; + }); + if (0 === enterableWebpackConfigs.length) { + debug('no webpack configuration found! aborting...'); + await new Promise(() => {}); + } + debug('webpack configurations %O', enterableWebpackConfigs); + return enterableWebpackConfigs; +}; diff --git a/packages/core/build/load-config.js b/packages/core/build/load-config.js new file mode 100644 index 0000000..8ab92e8 --- /dev/null +++ b/packages/core/build/load-config.js @@ -0,0 +1,32 @@ +const {readFile} = require('fs/promises'); +const {join} = require('path'); + +const D = require('./debug'); + +const debug = D('@flecks/core:load-config'); + +const { + FLECKS_CORE_ROOT = process.cwd(), +} = process.env; + +module.exports = async function loadConfig() { + try { + const {load} = require('js-yaml'); + const filename = join(FLECKS_CORE_ROOT, 'build', 'flecks.yml'); + const buffer = await readFile(filename, 'utf8'); + debug('parsing configuration from YML...'); + return ['YML', load(buffer, {filename})]; + } + catch (error) { + if ('ENOENT' !== error.code) { + throw error; + } + const {name} = require(join(FLECKS_CORE_ROOT, 'package.json')); + const barebones = {'@flecks/core': {}, '@flecks/fleck': {}}; + if (barebones[name]) { + delete barebones[name]; + } + barebones[`${name}:${FLECKS_CORE_ROOT}`] = {}; + return ['barebones', barebones]; + } +}; diff --git a/packages/core/src/middleware.js b/packages/core/build/middleware.js similarity index 97% rename from packages/core/src/middleware.js rename to packages/core/build/middleware.js index 60314df..4354987 100644 --- a/packages/core/src/middleware.js +++ b/packages/core/build/middleware.js @@ -1,4 +1,4 @@ -export default class Middleware { +module.exports = class Middleware { constructor(middleware = []) { this.middleware = []; @@ -61,4 +61,4 @@ export default class Middleware { this.middleware.push(this.constructor.check(fn)); } -} +}; diff --git a/packages/core/build/resolver.js b/packages/core/build/resolver.js new file mode 100644 index 0000000..5c6612d --- /dev/null +++ b/packages/core/build/resolver.js @@ -0,0 +1,88 @@ +const {join} = require('path'); + +const {CachedInputFileSystem, ResolverFactory} = require('enhanced-resolve'); +const AppendPlugin = require('enhanced-resolve/lib/AppendPlugin'); +const AliasPlugin = require('enhanced-resolve/lib/AliasPlugin'); +const fs = require('graceful-fs'); + +const D = require('./debug'); + +const debug = D('@flecks/core/build/resolver'); +const debugSilly = debug.extend('silly'); + +const { + FLECKS_CORE_ROOT = process.cwd(), +} = process.env; + +const nodeContext = { + environments: ['node+es3+es5+process+native'], +}; + +const nodeFileSystem = new CachedInputFileSystem(fs, 4000); + +module.exports = class Resolver { + + constructor(options) { + this.resolver = ResolverFactory.createResolver({ + conditionNames: ['node'], + extensions: ['.js', '.json', '.node'], + fileSystem: nodeFileSystem, + symlinks: false, + ...{ + modules: [join(FLECKS_CORE_ROOT, 'node_modules')], + ...options, + }, + }); + } + + addAlias(name, alias) { + debugSilly("adding alias: '%s' -> '%s'", name, alias); + new AliasPlugin( + 'raw-resolve', + {name, onlyModule: false, alias}, + 'internal-resolve', + ).apply(this.resolver); + } + + addExtensions(extensions) { + debugSilly("adding extensions: '%O'", extensions); + extensions.forEach((extension) => { + new AppendPlugin('raw-file', extension, 'file').apply(this); + }); + } + + addFallback(name, alias) { + debugSilly("adding fallback: '%s' -> '%s'", name, alias); + new AliasPlugin( + 'described-resolve', + {name, onlyModule: false, alias}, + 'internal-resolve', + ).apply(this.resolver); + } + + static isResolutionError(error) { + return error.message.startsWith("Can't resolve"); + } + + async resolve(request) { + try { + return await new Promise((resolve, reject) => { + this.resolver.resolve(nodeContext, FLECKS_CORE_ROOT, request, {}, (error, path) => { + if (error) { + reject(error); + } + else { + resolve(path); + } + }); + }); + } + catch (error) { + if (!this.constructor.isResolutionError(error)) { + throw error; + } + return undefined; + } + } + +}; diff --git a/packages/core/build/server.js b/packages/core/build/server.js new file mode 100644 index 0000000..f1557a1 --- /dev/null +++ b/packages/core/build/server.js @@ -0,0 +1,318 @@ +const {realpath} = require('fs/promises'); +const {join} = require('path'); + +const babelmerge = require('babel-merge'); +const set = require('lodash.set'); + +const D = require('./debug'); +const explicate = require('./explicate'); +const {Flecks} = require('./flecks'); +const loadConfig = require('./load-config'); +const Resolver = require('./resolver'); + +const { + FLECKS_CORE_ROOT = process.cwd(), +} = process.env; + +const debug = D('@flecks/core/build/bootstrap'); +const debugSilly = debug.extend('silly'); + +function environmentalize(path) { + return path + // - `@flecks/core` -> `flecks_core` + .replace(/[^a-zA-Z0-9]/g, '_') + .replace(/_*(.*)_*/, '$1'); +} + +function environmentConfiguration(config) { + const keys = Object.keys(process.env); + Object.keys(config) + .sort((l, r) => (l < r ? 1 : -1)) + .forEach((fleck) => { + const prefix = `FLECKS_ENV__${environmentalize(fleck)}`; + keys + .filter((key) => key.startsWith(`${prefix}__`)) + .map((key) => { + debug('reading environment from %s...', key); + return [key.slice(prefix.length + 2), process.env[key]]; + }) + .map(([subkey, value]) => [subkey.split('_'), value]) + .forEach(([path, jsonOrString]) => { + try { + set(config, [fleck, ...path], JSON.parse(jsonOrString)); + debug('read (%s) as JSON', jsonOrString); + } + catch (error) { + set(config, [fleck, ...path], jsonOrString); + debug('read (%s) as string', jsonOrString); + } + }); + }); + return config; +} + +module.exports = class Server extends Flecks { + + buildConfigs = {}; + + platforms = ['server']; + + resolved = {}; + + resolver = new Resolver(); + + 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); + } + + 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] || {}]), + ), + ), + flecks: Object.fromEntries( + Object.values(explication.descriptors) + .map(({path, request}) => [path, flecks[path] || explication.roots[request] || {}]), + ), + }; + const resolved = {}; + await Promise.all( + Object.entries(explication.descriptors) + .map(async ([, {path, request}]) => { + try { + if (path !== request || request !== await realpath(request)) { + resolved[path] = request; + } + } + // eslint-disable-next-line no-empty + catch (error) {} + }), + ); + const reverseRequest = Object.fromEntries( + Object.entries(explication.descriptors) + .map(([, {path, request}]) => [request, path]), + ); + return { + resolved, + resolver, + roots: Object.fromEntries( + Object.entries(explication.roots) + .map(([request, bootstrap]) => [reverseRequest[request], {bootstrap, request}]), + ), + runtime, + }; + } + + get extensions() { + return this.invokeFlat('@flecks/core.exts').flat(); + } + + static async from( + { + config: configParameter, + flecks: configFlecks, + platforms = ['server'], + } = {}, + ) { + // Load or use parameterized configuration. + let originalConfig; + let configType = 'parameter'; + if (!configParameter) { + // eslint-disable-next-line no-param-reassign + [configType, originalConfig] = await loadConfig(); + } + else { + originalConfig = JSON.parse(JSON.stringify(configParameter)); + } + debug('bootstrap configuration (%s)', configType); + debugSilly(originalConfig); + const { + resolved, + resolver, + roots, + runtime, + } = await this.buildRuntime(originalConfig, platforms, configFlecks); + const flecks = super.from(runtime); + flecks.roots = roots; + flecks.platforms = platforms; + flecks.resolved = resolved; + flecks.resolver = resolver; + flecks.loadBuildConfigs(); + return flecks; + } + + loadBuildConfigs() { + Object.entries(this.invoke('@flecks/core.build.config')) + .forEach(([fleck, configs]) => { + configs.forEach((config) => { + this.buildConfigs[config] = fleck; + }); + }); + debugSilly('build configs loaded: %O', this.buildConfigs); + } + + async resolveBuildConfig(config, override) { + const fleck = this.buildConfigs[config]; + if (!fleck) { + throw new Error(`Unknown build config: '${config}'`); + } + const rootConfig = await this.resolver.resolve(join(FLECKS_CORE_ROOT, 'build', config)); + if (rootConfig) { + return rootConfig; + } + if (override) { + const overrideConfig = await this.resolver.resolve(join(override, 'build', config)); + if (overrideConfig) { + return overrideConfig; + } + } + return this.resolver.resolve(join(fleck, 'build', config)); + } + + async runtimeCompiler(runtime, config, {allowlist = []} = {}) { + // Compile. + const needCompilation = Object.entries(this.resolved); + if (needCompilation.length > 0) { + const babelConfig = await this.babel(); + // const flecksBabelConfig = this.babel(); + // Alias and de-externalize. + await Promise.all( + needCompilation + .map(async ([fleck, resolved]) => { + allowlist.push(fleck); + // Create alias. + config.resolve.alias[fleck] = resolved; + debugSilly('%s runtime de-externalized %s, alias: %s', runtime, fleck, resolved); + // Alias this compiled fleck's `node_modules` to the root `node_modules`. + config.resolve.alias[ + join(resolved, 'node_modules') + ] = join(FLECKS_CORE_ROOT, 'node_modules'); + config.module.rules.push( + { + test: /\.(m?jsx?)?$/, + include: [resolved], + use: [ + { + loader: require.resolve('babel-loader'), + options: { + cacheDirectory: true, + babelrc: false, + configFile: false, + ...babelConfig, + }, + }, + ], + }, + ); + }), + ); + // Our very own lil' chunk. + set(config, 'optimization.splitChunks.cacheGroups.flecks-compiled', { + chunks: 'all', + enforce: true, + priority: 100, + test: new RegExp( + `(?:${ + Object.keys(this.resolved) + .map((path) => path.replace(/[\\/]/g, '[\\/]')).join('|') + })`, + ), + }); + } + } + + get stubs() { + return Object.values(this.flecks) + .reduce( + (r, {stubs = {}}) => ( + r.concat( + Object.entries(stubs) + .reduce( + (r, [platform, stubs]) => ( + r.concat(this.platforms.includes(platform) ? stubs : []) + ), + [], + ), + ) + ), + [], + ).flat(); + } + + get targets() { + const targets = this.invoke('@flecks/core.targets'); + const duplicates = {}; + const entries = Object.entries(targets); + const set = new Set(); + entries + .forEach(([fleck, targets]) => { + targets.forEach((target) => { + if (set.has(target)) { + if (!duplicates[target]) { + duplicates[target] = []; + } + duplicates[target].push(fleck); + } + set.add(target); + }); + }); + const errorMessage = Object.entries(duplicates).map(([target, flecks]) => ( + `Multiple flecks ('${flecks.join("', '")})' tried to build target '${target}'` + )).join('\n'); + if (errorMessage) { + throw new Error(`@flecks/core.targets:\n${errorMessage}`); + } + this.invoke('@flecks/core.targets.alter', set); + return entries + .map(([fleck, targets]) => ( + targets + .filter((target) => set.has(target)) + .map((target) => [fleck, target]) + )).flat(); + } + +}; diff --git a/packages/core/src/server/stream.js b/packages/core/build/stream.js similarity index 81% rename from packages/core/src/server/stream.js rename to packages/core/build/stream.js index 3475ae5..b6ffa16 100644 --- a/packages/core/src/server/stream.js +++ b/packages/core/build/stream.js @@ -1,8 +1,8 @@ // eslint-disable-next-line max-classes-per-file -import JsonParse from 'jsonparse'; -import {Transform} from 'stream'; +const JsonParse = require('jsonparse'); +const {Transform} = require('stream'); -export class JsonStream extends Transform { +exports.JsonStream = class JsonStream extends Transform { constructor() { super(); @@ -23,9 +23,9 @@ export class JsonStream extends Transform { this.parser.write(chunk); } -} +}; -JsonStream.PrettyPrint = class extends Transform { +exports.JsonStream.PrettyPrint = class extends Transform { constructor(indent = 2) { super(); @@ -40,7 +40,7 @@ JsonStream.PrettyPrint = class extends Transform { }; -export const transform = (fn, opts = {}) => { +exports.transform = (fn, opts = {}) => { class EasyTransform extends Transform { constructor() { diff --git a/packages/core/build/stub.js b/packages/core/build/stub.js new file mode 100644 index 0000000..ba458c1 --- /dev/null +++ b/packages/core/build/stub.js @@ -0,0 +1,15 @@ +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/core/build/webpack.config.js b/packages/core/build/webpack.config.js deleted file mode 100644 index a11eeaa..0000000 --- a/packages/core/build/webpack.config.js +++ /dev/null @@ -1,15 +0,0 @@ -const configFn = require('../src/server/build/webpack.config'); -const {executable} = require('../src/server/webpack'); -const eslintConfigFn = require('../src/server/build/default.eslint.config'); - -module.exports = async (env, argv) => { - const config = await configFn(env, argv); - config.plugins.push(...executable()); - const eslint = await eslintConfigFn(); - eslint.settings['import/resolver'].webpack = { - config: { - resolve: config.resolve, - }, - }; - return config; -}; diff --git a/packages/core/src/server/webpack.js b/packages/core/build/webpack.js similarity index 85% rename from packages/core/src/server/webpack.js rename to packages/core/build/webpack.js index 793532d..927fac5 100644 --- a/packages/core/src/server/webpack.js +++ b/packages/core/build/webpack.js @@ -20,10 +20,7 @@ exports.banner = (options) => ( exports.copy = (options) => (new CopyPlugin(options)); exports.defaultConfig = (flecks, specializedConfig) => { - const extensions = ['.mjs', '.js', '.json', '.wasm']; - if (flecks) { - extensions.push(...flecks.exts()); - } + const {extensions} = flecks; const extensionsRegex = exports.regexFromExtensions(extensions); const defaults = { context: FLECKS_CORE_ROOT, @@ -47,10 +44,10 @@ exports.defaultConfig = (flecks, specializedConfig) => { alias: {}, extensions, fallback: {}, - modules: [ - 'node_modules', - join(FLECKS_CORE_ROOT, 'node_modules'), - ], + // modules: [ + // 'node_modules', + // join(FLECKS_CORE_ROOT, 'node_modules'), + // ], }, stats: { colors: true, @@ -90,10 +87,6 @@ exports.defaultConfig = (flecks, specializedConfig) => { // Include a shebang and set the executable bit.. exports.executable = () => ([ - exports.banner({ - banner: '#!/usr/bin/env node', - include: /^cli\.js$/, - }), new class Executable { // eslint-disable-next-line class-methods-use-this @@ -101,7 +94,7 @@ exports.executable = () => ([ compiler.hooks.afterEmit.tapAsync( 'Executable', (compilation, callback) => { - chmod(join(FLECKS_CORE_ROOT, 'dist', 'cli.js'), 0o755, callback); + chmod(join(FLECKS_CORE_ROOT, 'dist', 'build', 'cli.js'), 0o755, callback); }, ); } diff --git a/packages/core/package.json b/packages/core/package.json index 2405acf..f4f164e 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -8,38 +8,26 @@ "publishConfig": { "access": "public" }, - "version": "2.0.3", + "version": "3.0.0", "main": "index.js", "author": "cha0s", "license": "MIT", "bin": { - "flecks": "./cli.js" + "flecks": "./build/cli.js" }, "scripts": { - "build": "NODE_PATH=./node_modules webpack --config ./build/webpack.config.js --mode production", + "build": "NODE_PATH=./node_modules webpack --config ./build/core.webpack.config.js --mode production", "clean": "rm -rf dist bun.lockb && bun install", "lint": "NODE_PATH=./node_modules eslint --config ./build/eslint.config.js .", "postversion": "cp package.json dist", - "test": "npm run build && mocha -t 10000 --colors ./dist/test.js" + "test": "npm run build -d && mocha -t 10000 --colors ./dist/test.js" }, "files": [ "build", - "cli.js", - "cli.js.map", "index.js", "index.js.map", "server.js", "server.js.map", - "server/build/babel.config.js", - "server/build/babel.config.js.map", - "server/build/default.eslint.config.js", - "server/build/default.eslint.config.js.map", - "server/build/eslint.config.js", - "server/build/eslint.config.js.map", - "server/build/webpack.config.js", - "server/build/webpack.config.js.map", - "server/build/fleckspack.config.js", - "server/build/fleckspack.config.js.map", "src", "start.js", "start.js.map", @@ -51,13 +39,10 @@ "@babel/core": "^7.12.10", "@babel/eslint-parser": "^7.23.3", "@babel/eslint-plugin": "^7.22.10", - "@babel/plugin-proposal-optional-chaining": "^7.12.16", "@babel/plugin-transform-regenerator": "^7.16.7", "@babel/preset-env": "^7.12.11", - "@babel/register": "7.13.0", "babel-loader": "^9.1.3", "babel-merge": "^3.0.0", - "babel-plugin-prepend": "^1.0.2", "chai": "4.2.0", "chai-as-promised": "7.1.1", "commander": "11.1.0", @@ -72,17 +57,16 @@ "eslint-plugin-jsx-a11y": "^6.8.0", "eslint-plugin-react": "^7.33.2", "eslint-plugin-react-hooks": "^4.6.0", - "eslint-webpack-plugin": "^4.0.1", + "eslint-webpack-plugin": "^3.2.0", "glob": "^10.3.10", "globals": "^13.23.0", + "graceful-fs": "^4.2.11", "js-yaml": "4.1.0", "jsonparse": "^1.3.1", - "lodash.flatten": "^4.4.0", "lodash.get": "^4.4.2", - "lodash.intersectionby": "4.7.0", "lodash.set": "^4.3.2", "mocha": "^8.3.2", - "pirates": "^4.0.5", + "null-loader": "^4.0.1", "rimraf": "^3.0.2", "source-map-loader": "4.0.1", "source-map-support": "0.5.19", diff --git a/packages/core/src/class.js b/packages/core/src/class.js deleted file mode 100644 index a6e68e9..0000000 --- a/packages/core/src/class.js +++ /dev/null @@ -1 +0,0 @@ -export default class {} diff --git a/packages/core/src/cli.js b/packages/core/src/cli.js deleted file mode 100755 index b4ab06f..0000000 --- a/packages/core/src/cli.js +++ /dev/null @@ -1,116 +0,0 @@ -import {fork} from 'child_process'; -import {join, resolve, sep} from 'path'; - -import {Command} from 'commander'; - -import D from './debug'; -import {processCode} from './server/commands'; -import Flecks from './server/flecks'; - -const { - FLECKS_CORE_ROOT = process.cwd(), -} = process.env; - -const debug = D('@flecks/core/cli'); -const debugSilly = debug.extend('silly'); - -// Guarantee local node_modules path. -const defaultNodeModules = resolve(join(FLECKS_CORE_ROOT, 'node_modules')); -const nodePathSeparator = '/' === sep ? ':' : ';'; -let updatedNodePath; -if (!process.env.NODE_PATH) { - updatedNodePath = defaultNodeModules; -} -else { - const parts = process.env.NODE_PATH.split(nodePathSeparator); - if (!parts.some((part) => resolve(part) === defaultNodeModules)) { - parts.push(defaultNodeModules); - updatedNodePath = parts.join(nodePathSeparator); - } -} -// Guarantee symlink preservation for linked flecks. -const updateSymlinkPreservation = !process.env.NODE_PRESERVE_SYMLINKS; - -const environmentUpdates = ( - updatedNodePath - || updateSymlinkPreservation -) - ? { - NODE_PATH: updatedNodePath, - NODE_PRESERVE_SYMLINKS: 1, - } - : undefined; -if (environmentUpdates) { - debugSilly('updating environment, forking with %O...', environmentUpdates); - const forkOptions = { - env: { - ...process.env, - ...environmentUpdates, - DEBUG_COLORS: 'true', - }, - stdio: 'inherit', - }; - fork(__filename, process.argv.slice(2), forkOptions) - .on('exit', (code, signal) => { - process.exitCode = code; - process.exit(signal); - }); -} -else { - // Asynchronous command process code forwarding. - const forwardProcessCode = (fn) => async (...args) => { - const child = await fn(...args); - if ('object' !== typeof child) { - const code = 'undefined' !== typeof child ? child : 0; - debugSilly('action returned code %d', code); - process.exitCode = code; - return; - } - try { - const code = await processCode(child); - debugSilly('action exited with code %d', code); - process.exitCode = code; - } - catch (error) { - // eslint-disable-next-line no-console - console.error(error); - process.exitCode = child.exitCode || 1; - } - }; - // Initialize Commander. - const program = new Command(); - program - .enablePositionalOptions() - .name('flecks') - .usage('[command] [...]'); - // Bootstrap. - (async () => { - debugSilly('bootstrapping flecks...'); - const flecks = Flecks.bootstrap(); - debugSilly('bootstrapped'); - // Register commands. - const commands = flecks.invokeMerge('@flecks/core.commands', program); - const keys = Object.keys(commands).sort(); - for (let i = 0; i < keys.length; ++i) { - const { - action, - args = [], - description, - name = keys[i], - options = [], - } = commands[keys[i]]; - debugSilly('adding command %s...', name); - const cmd = program.command(name); - cmd.description(description); - for (let i = 0; i < args.length; ++i) { - cmd.addArgument(args[i]); - } - for (let i = 0; i < options.length; ++i) { - cmd.option(...options[i]); - } - cmd.action(forwardProcessCode(action)); - } - // Parse commandline. - program.parse(process.argv); - })(); -} diff --git a/packages/core/src/index.js b/packages/core/src/index.js index e1d8f51..059f489 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -1,18 +1,9 @@ -export {default as Class} from './class'; -export {default as compose} from './compose'; -export {default as D} from './debug'; -export {default as EventEmitter} from './event-emitter'; +export {default as Class} from '../build/class'; +export {default as compose} from '../build/compose'; +export {default as D} from '../build/debug'; +export {default as EventEmitter} from '../build/event-emitter'; export { - default as Flecks, ById, ByType, -} from './flecks'; - -export const hooks = { - '@flecks/core.config': () => ({ - /** - * The ID of your application. - */ - id: 'flecks', - }), -}; + Flecks, +} from '../build/flecks'; diff --git a/packages/core/src/require.js b/packages/core/src/require.js deleted file mode 100644 index 76e08c0..0000000 --- a/packages/core/src/require.js +++ /dev/null @@ -1,4 +0,0 @@ -// Get a runtime require function by hook or by crook. :) - -// eslint-disable-next-line no-eval -module.exports = eval('"undefined" !== typeof require ? require : undefined'); diff --git a/packages/core/src/server/build/default.eslint.config.js b/packages/core/src/server/build/default.eslint.config.js deleted file mode 100644 index cba4b97..0000000 --- a/packages/core/src/server/build/default.eslint.config.js +++ /dev/null @@ -1,93 +0,0 @@ -const babelmerge = require('babel-merge'); -const globals = require('globals'); - -const R = require('../../require'); - -module.exports = (flecks) => { - const merging = [ - { - plugins: [R.resolve('@babel/plugin-syntax-dynamic-import')], - presets: [ - [ - R.resolve('@babel/preset-env'), - { - shippedProposals: true, - targets: { - esmodules: true, - node: 'current', - }, - }, - ], - ], - }, - ]; - if (flecks) { - merging.push({configFile: flecks.buildConfig('babel.config.js')}); - const flecksBabelConfig = flecks.babel(); - merging.push(...flecksBabelConfig.map(([, babel]) => babel)); - } - const babelConfig = babelmerge.all(merging); - return { - extends: [ - R.resolve('eslint-config-airbnb'), - R.resolve('eslint-config-airbnb/hooks'), - ], - globals: { - ...globals.browser, - ...globals.es2021, - ...globals.mocha, - ...globals.node, - __non_webpack_require__: true, - }, - ignorePatterns: [ - 'dist/**', - // Not even gonna try. - 'build/dox/hooks.js', - ], - overrides: [ - { - files: [ - 'test/**/*.js', - ], - rules: { - 'brace-style': 'off', - 'class-methods-use-this': 'off', - 'import/no-extraneous-dependencies': 'off', - 'import/no-unresolved': 'off', - 'max-classes-per-file': 'off', - 'no-new': 'off', - 'no-unused-expressions': 'off', - 'padded-blocks': 'off', - }, - }, - ], - parser: R.resolve('@babel/eslint-parser'), - parserOptions: { - requireConfigFile: false, - babelOptions: babelConfig, - }, - plugins: ['@babel'], - rules: { - 'brace-style': ['error', 'stroustrup'], - // Bug: https://github.com/import-js/eslint-plugin-import/issues/2181 - 'import/no-import-module-exports': 'off', - 'import/prefer-default-export': 'off', - 'jsx-a11y/control-has-associated-label': ['error', {assert: 'either'}], - 'jsx-a11y/label-has-associated-control': ['error', {assert: 'either'}], - 'no-param-reassign': ['error', {props: false}], - 'no-plusplus': 'off', - 'no-shadow': 'off', - 'object-curly-spacing': 'off', - 'padded-blocks': ['error', {classes: 'always'}], - yoda: 'off', - }, - settings: { - 'import/resolver': { - node: {}, - }, - react: { - version: '18.2.0', - }, - }, - }; -}; diff --git a/packages/core/src/server/build/eslint.config.js b/packages/core/src/server/build/eslint.config.js deleted file mode 100644 index 9ae62c0..0000000 --- a/packages/core/src/server/build/eslint.config.js +++ /dev/null @@ -1,65 +0,0 @@ -const {spawnSync} = require('child_process'); -const { - mkdirSync, - readFileSync, - statSync, - writeFileSync, -} = require('fs'); -const {join} = require('path'); - -const D = require('../../debug'); -const {default: Flecks} = require('../flecks'); - -const debug = D('@flecks/core/server/build/eslint.config.js'); - -const { - FLECKS_CORE_ROOT = process.cwd(), - FLECKS_CORE_SYNC_FOR_ESLINT = false, -} = process.env; - -// This is kinda nuts, but ESLint doesn't support its configuration files returning a promise! -if (FLECKS_CORE_SYNC_FOR_ESLINT) { - (async () => { - debug('bootstrapping flecks...'); - const flecks = Flecks.bootstrap(); - debug('bootstrapped'); - const eslintConfigFn = __non_webpack_require__(flecks.buildConfig('default.eslint.config.js')); - const eslintConfig = await eslintConfigFn(flecks); - const webpackConfigFn = __non_webpack_require__(flecks.buildConfig('webpack.config.js', 'fleck')); - const webpackConfig = await webpackConfigFn({}, {mode: 'development'}, flecks); - eslintConfig.settings['import/resolver'].webpack = { - config: { - resolve: webpackConfig.resolve, - }, - }; - process.stdout.write(JSON.stringify(eslintConfig, null, 2)); - })(); -} -else { - const cacheDirectory = join(FLECKS_CORE_ROOT, 'node_modules', '.cache', '@flecks', 'core'); - try { - statSync(join(cacheDirectory, 'eslint.config.json')); - module.exports = JSON.parse(readFileSync(join(cacheDirectory, 'eslint.config.json')).toString()); - } - catch (error) { - // Just silly. By synchronously spawning... ourselves, the spawned copy can use async. - const {stderr, stdout} = spawnSync('node', [__filename], { - env: { - FLECKS_CORE_SYNC_FOR_ESLINT: true, - NODE_PATH: join(FLECKS_CORE_ROOT, 'node_modules'), - ...process.env, - }, - }); - // eslint-disable-next-line no-console - console.error(stderr.toString()); - const json = stdout.toString(); - try { - statSync(join(FLECKS_CORE_ROOT, 'node_modules')); - mkdirSync(cacheDirectory, {recursive: true}); - writeFileSync(join(cacheDirectory, 'eslint.config.json'), json); - } - // eslint-disable-next-line no-empty - catch (error) {} - module.exports = JSON.parse(json); - } -} diff --git a/packages/core/src/server/build/fleckspack.config.js b/packages/core/src/server/build/fleckspack.config.js deleted file mode 100644 index 3b7e0df..0000000 --- a/packages/core/src/server/build/fleckspack.config.js +++ /dev/null @@ -1,94 +0,0 @@ -/* eslint-disable import/first */ -import 'source-map-support/register'; - -if ('production' !== process.env.NODE_ENV) { - try { - // eslint-disable-next-line global-require, import/no-unresolved - __non_webpack_require__('dotenv/config'); - } - // eslint-disable-next-line no-empty - catch (error) {} -} - -import intersectionBy from 'lodash.intersectionby'; - -import D from '../../debug'; -import Flecks from '../flecks'; - -const debug = D('@flecks/core/server/build/fleckspack.config.js'); - -const { - FLECKS_CORE_BUILD_LIST = '', - FLECKS_CORE_ROOT = process.cwd(), -} = process.env; - -const buildList = FLECKS_CORE_BUILD_LIST - .split(',') - .map((name) => name.trim()) - .filter((e) => e); - -export default async (env, argv) => { - debug('bootstrapping flecks...'); - const flecks = Flecks.bootstrap(); - debug('bootstrapped'); - - debug('gathering configs'); - const targets = []; - Object.entries(flecks.invoke('@flecks/core.targets')) - .forEach(([fleck, fleckTargets]) => { - intersectionBy(fleckTargets, buildList.length ? buildList : fleckTargets) - .forEach((target) => { - targets.push([target, fleck]); - }); - }); - debug('building: %O', targets.map(([target]) => target)); - if (0 === targets.length) { - debug('no build configuration found! aborting...'); - await new Promise(() => {}); - } - const entries = await Promise.all(targets.map( - async ([target, fleck]) => { - const buildConfig = flecks.resolveBuildConfig( - [ - FLECKS_CORE_ROOT, - flecks.resolvePath(fleck), - ], - [ - `${target}.webpack.config.js`, - 'webpack.config.js', - ], - ); - const configFn = __non_webpack_require__(buildConfig); - if ('function' !== typeof configFn) { - debug(`'${ - target - }' build configuration expected function got ${ - typeof configFn - }! aborting...`); - return undefined; - } - return [target, await configFn(env, argv, flecks)]; - }, - )); - await Promise.all( - entries.map(async ([target, config]) => ( - flecks.invokeFlat('@flecks/core.build', target, config, env, argv) - )), - ); - const webpackConfigs = Object.fromEntries(entries); - await Promise.all(flecks.invokeFlat('@flecks/core.build.alter', webpackConfigs, env, argv)); - const enterableWebpackConfigs = Object.values(webpackConfigs) - .filter((webpackConfig) => { - if (!webpackConfig.entry) { - debug('webpack configurations %O had no entry... discarding', webpackConfig); - return false; - } - return true; - }); - if (0 === enterableWebpackConfigs.length) { - debug('no webpack configuration found! aborting...'); - await new Promise(() => {}); - } - debug('webpack configurations %O', enterableWebpackConfigs); - return enterableWebpackConfigs; -}; diff --git a/packages/core/src/server/compiler.js b/packages/core/src/server/compiler.js deleted file mode 100644 index 6dc4663..0000000 --- a/packages/core/src/server/compiler.js +++ /dev/null @@ -1,163 +0,0 @@ -import {statSync} from 'fs'; -import {dirname, sep} from 'path'; - -import { - getEnv, - OptionManager, - transformSync, - version, -} from '@babel/core'; -import sourceMapSupport from 'source-map-support'; - -sourceMapSupport.install(); - -const cache = require('@babel/register/lib/cache'); - -const identity = (i) => i; - -// This is basically what @babel/register does, but better in several ways. -class Compiler { - - constructor(options) { - // Make some goodies exist in nodespace. - this.options = { - ...options, - plugins: [ - ...(options.plugins || []), - ...this.constructor.nodespaceBabelPlugins(), - ], - }; - this.constructor.warmUpCache(); - } - - compile(input, request) { - const options = new OptionManager() - .init({ - sourceRoot: `${dirname(request)}${sep}`, - ...this.options, - filename: request, - }); - if (null === options) { - return null; - } - const {cached, store} = this.lookup(options, request); - if (cached) { - return cached; - } - const {code, map} = transformSync(input, { - ...options, - sourceMaps: 'both', - ast: false, - }); - this.constructor.maps[request] = map; - return store({code, map}); - } - - static installSourceMapSupport() { - this.maps = Object.create(null); - sourceMapSupport.install({ - handleUncaughtExceptions: false, - environment: 'node', - retrieveSourceMap: (request) => { - const map = this.maps[request]; - if (map) { - return {url: null, map}; - } - return null; - }, - }); - } - - lookup(options, request) { - if (!this.constructor.cache) { - return { - cached: null, - store: identity, - }; - } - let cacheKey = `${JSON.stringify(options)}:${version}`; - const env = getEnv(); - if (env) { - cacheKey += `:${env}`; - } - const cached = this.constructor.cache[cacheKey]; - const mtime = +statSync(request).mtime; - if (cached && cached.mtime === mtime) { - return { - cached: cached.value, - store: identity, - }; - } - return { - cached: null, - store: (value) => { - this.constructor.cache[cacheKey] = {mtime, value}; - cache.setDirty(); - return value; - }, - }; - } - - static nodespaceBabelPlugins() { - return [ - [ - 'prepend', - { - prepend: [ - 'require.context = (', - ' directory,', - ' useSubdirectories = true,', - ' regExp = /^\\.\\/.*$/,', - ' mode = "sync",', - ') => {', - ' const glob = require("glob");', - ' const {join} = require("path");', - ' const {resolve, sep} = require("path");', - ' const keys = glob.sync(', - ' useSubdirectories ? "**/*" : "*",', - ' {cwd: resolve(__dirname, directory)},', - ' )', - ' .filter((key) => key.match(regExp))', - ' .map(', - ' (key) => (', - ' -1 !== [".".charCodeAt(0), "/".charCodeAt(0)].indexOf(key.charCodeAt(0))', - ' ? key', - ' : ("." + sep + key)', - ' ),', - ' );', - ' const R = (request) => {', - ' if (-1 === keys.indexOf(request)) {', - // eslint-disable-next-line no-template-curly-in-string - ' throw new Error(`Cannot find module \'${request}\'`);', - ' }', - ' return require(join(__dirname, directory, request));', - ' };', - ' R.id = __filename', - ' R.keys = () => keys;', - ' return R;', - '};', - ].join('\n'), - }, - 'require.context', - ], - [ - 'prepend', - { - prepend: '"undefined" === typeof global.__non_webpack_require__ && (global.__non_webpack_require__ = require);', - }, - '__non_webpack_require__', - ], - ]; - } - - static warmUpCache() { - if ('undefined' === typeof this.cache) { - cache.load(); - this.cache = cache.get(); - this.installSourceMapSupport(); - } - } - -} - -export default Compiler; diff --git a/packages/core/src/server/flecks.js b/packages/core/src/server/flecks.js deleted file mode 100644 index 011bbb0..0000000 --- a/packages/core/src/server/flecks.js +++ /dev/null @@ -1,733 +0,0 @@ -import { - readFileSync, - realpathSync, - statSync, -} from 'fs'; -import {readFile, writeFile} from 'fs/promises'; -import { - basename, - dirname, - extname, - isAbsolute, - join, - resolve, - sep, -} from 'path'; - -import babelmerge from 'babel-merge'; -import enhancedResolve from 'enhanced-resolve'; -import {dump as dumpYml, load as loadYml} from 'js-yaml'; -import {addHook} from 'pirates'; - -import D from '../debug'; -import Flecks from '../flecks'; -import R from '../require'; -import Compiler from './compiler'; - -const { - FLECKS_CORE_ROOT = process.cwd(), - FLECKS_YML = 'flecks.yml', -} = process.env; - -const debug = D('@flecks/core/flecks/server'); -const debugSilly = debug.extend('silly'); - -export default class ServerFlecks extends Flecks { - - constructor(options = {}) { - super(options); - this.overrideConfigFromEnvironment(); - this.buildConfigs = {}; - this.loadBuildConfigs(); - this.flecksConfig = options.flecksConfig || {}; - this.resolver = options.resolver || {}; - } - - static async addFleckToYml(fleck, path) { - const key = [fleck].concat(path ? `.${sep}${join('packages', path, 'src')}` : []).join(':'); - const ymlPath = join(FLECKS_CORE_ROOT, 'build', 'flecks.yml'); - let yml = loadYml(await readFile(ymlPath)); - yml = Object.fromEntries(Object.entries(yml).concat([[key, {}]])); - await writeFile(ymlPath, dumpYml(yml, {sortKeys: true})); - } - - get aliasedConfig() { - const aliases = this.aliases(); - return Object.fromEntries( - Object.entries( - this.config, - ) - .map(([path, config]) => [ - this.fleckIsAliased(path) ? `${path}:${aliases[path]}` : path, - config, - ]), - ); - } - - aliases() { - return this.constructor.aliases(this.flecksConfig); - } - - static aliases(flecksConfig) { - const keys = Object.keys(flecksConfig); - let aliases = {}; - for (let i = 0; i < keys.length; ++i) { - const key = keys[i]; - const config = flecksConfig[key]; - if (config.aliases && Object.keys(config.aliases).length > 0) { - aliases = {...aliases, ...config.aliases}; - } - } - return aliases; - } - - babel() { - return this.constructor.babel(this.flecksConfig); - } - - static babel(flecksConfig) { - return Object.entries(flecksConfig) - .filter(([, {babel}]) => babel) - .map(([key, {babel}]) => [key, babel]); - } - - static bootstrap( - { - config, - platforms = ['server'], - root = FLECKS_CORE_ROOT, - } = {}, - ) { - // Load or use parameterized configuration. - let configType; - if (!config) { - // eslint-disable-next-line no-param-reassign - [configType, config] = this.loadConfig(root); - } - else { - configType = 'parameter'; - } - debug('bootstrap configuration (%s)', configType); - debugSilly(config); - // Make resolver and load flecksConfig. - const {flecksConfig, resolver} = this.makeResolverAndLoadRcs( - Object.keys(config), - platforms, - root, - ); - // Rewrite aliased config keys. - // eslint-disable-next-line no-param-reassign - config = Object.fromEntries( - Object.entries(config) - .map(([key, value]) => { - const index = key.indexOf(':'); - return [-1 !== index ? key.slice(0, index) : key, value]; - }), - ); - this.installCompilers(flecksConfig, resolver); - // Instantiate with mixins. - return ServerFlecks.from({ - config, - flecks: Object.fromEntries( - Object.keys(resolver) - .map((path) => [path, R(this.resolve(resolver, path))]), - ), - platforms, - flecksConfig, - resolver, - }); - } - - buildConfig(path, specific) { - const config = this.buildConfigs[path]; - if (!config) { - throw new Error(`Unknown build config '${path}'`); - } - const paths = []; - if (specific) { - if ('specifier' in config) { - if (false === config.specifier) { - paths.shift(); - } - else { - paths.push(config.specifier(specific)); - } - } - else { - paths.push(`${specific}.${path}`); - } - } - paths.push(path); - const roots = [config.root]; - if (config.root !== FLECKS_CORE_ROOT) { - roots.push(FLECKS_CORE_ROOT); - } - roots.push(this.resolvePath(this.resolve(config.fleck))); - return this.constructor.resolveBuildConfig(this.resolver, roots, paths); - } - - static environmentalize(path) { - return path - // - `@flecks/core` -> `flecks_core` - .replace(/[^a-zA-Z0-9]/g, '_') - .replace(/_*(.*)_*/, '$1'); - } - - exts() { - return this.constructor.exts(this.flecksConfig); - } - - static exts(flecksConfig) { - const keys = Object.keys(flecksConfig); - const exts = []; - for (let i = 0; i < keys.length; ++i) { - const key = keys[i]; - const config = flecksConfig[key]; - if (config.exts) { - exts.push(...config.exts); - } - } - return exts; - } - - fleckIsAliased(fleck) { - return this.constructor.fleckIsAliased(this.resolver, fleck); - } - - static fleckIsAliased(resolver, fleck) { - return fleck !== this.resolve(resolver, fleck); - } - - fleckIsCompiled(fleck) { - return this.constructor.fleckIsCompiled(this.resolver, fleck); - } - - static fleckIsCompiled(resolver, fleck) { - return this.fleckIsAliased(resolver, fleck) || this.fleckIsSymlinked(resolver, fleck); - } - - fleckIsSymlinked(fleck) { - return this.constructor.fleckIsSymlinked(this.resolver, fleck); - } - - static fleckIsSymlinked(resolver, fleck) { - const resolved = R.resolve(this.resolve(resolver, fleck)); - const realpath = realpathSync(resolved); - return realpath !== resolved; - } - - static installCompilers(flecksConfig, resolver) { - const paths = Object.keys(resolver); - debugSilly('flecksConfig: %O', flecksConfig); - // Merge aliases; - const aliases = Object.fromEntries( - Object.entries({ - // from fleck configuration above, - ...Object.fromEntries(Object.entries(resolver).filter(([from, to]) => from !== to)), - // from symlinks, - ...( - Object.fromEntries( - paths.filter((path) => this.fleckIsSymlinked(resolver, path)) - .map((path) => [path, this.sourcepath(R.resolve(this.resolve(resolver, path)))]), - ) - ), - // and from RCs. - ...this.aliases(flecksConfig), - }) - .map(([from, to]) => [from, to.endsWith('/index') ? to.slice(0, -6) : to]), - ); - if (Object.keys(aliases).length > 0) { - debugSilly('aliases: %O', aliases); - } - const exts = this.exts(flecksConfig); - const enhancedResolver = enhancedResolve.create.sync({ - extensions: exts, - alias: aliases, - }); - // Stub server-unfriendly modules. - const stubs = this.stubs(['server'], flecksConfig); - if (stubs.length > 0) { - debugSilly('stubbing: %O', stubs); - } - // Do we need to get up in `require()`'s guts? - if ( - Object.keys(aliases).length > 0 - || stubs.length > 0 - ) { - const {Module} = R('module'); - const {require: Mr} = Module.prototype; - const aliasKeys = Object.keys(aliases); - Module.prototype.require = function hackedRequire(request, options) { - for (let i = 0; i < stubs.length; ++i) { - if (request.match(stubs[i])) { - return undefined; - } - } - if (aliasKeys.find((aliasKey) => request.startsWith(aliasKey))) { - try { - const resolved = enhancedResolver(FLECKS_CORE_ROOT, request); - if (resolved) { - return Mr.call(this, resolved, options); - } - } - // eslint-disable-next-line no-empty - catch (error) {} - } - return Mr.call(this, request, options); - }; - } - // Compile. - const compilations = []; - const needCompilation = paths - .filter((path) => this.fleckIsCompiled(resolver, path)); - if (needCompilation.length > 0) { - // Augment the compilations with babel config from flecksrc. - const flecksBabelConfig = babelmerge.all(this.babel(flecksConfig).map(([, babel]) => babel)); - debugSilly('.flecksrc: babel: %O', flecksBabelConfig); - // Key flecks needing compilation by their roots, so we can compile all common roots with a - // single invocation of `@babel/register`. - const compilationRootMap = {}; - needCompilation.forEach((fleck) => { - const root = this.root(resolver, fleck); - if (!compilationRootMap[root]) { - compilationRootMap[root] = []; - } - compilationRootMap[root].push(fleck); - }); - // Register a compilation for each root. - Object.entries(compilationRootMap).forEach(([root, compiling]) => { - const resolved = dirname(R.resolve(join(root, 'package.json'))); - const sourcepath = this.sourcepath(resolved); - const sourceroot = join(sourcepath, '..'); - // Load babel config from whichever we find first: - // - The fleck being compiled's build directory - // - The root build directory - // - Finally, the built-in babel config - let builtInPath; - try { - builtInPath = this.resolvePath(resolver, '@flecks/core/server'); - } - catch (error) { - // This file won't be resolved during testing. - builtInPath = join(__dirname, '..', 'src', 'server'); - } - const configFile = this.resolveBuildConfig( - resolver, - [ - resolved, - FLECKS_CORE_ROOT, - builtInPath, - ], - [ - 'babel.config.js', - ], - ); - const ignore = `${resolve(join(sourceroot, 'node_modules'))}/`; - const only = `${resolve(sourceroot)}/`; - const config = { - // Augment the selected config with the babel config from RCs. - configFile, - // Target the compiler to avoid unnecessary work. - ignore: [ignore], - only: [only], - }; - debugSilly('compiling %O with %j at %s', compiling, config, only); - compilations.push({ - ignore, - only, - compiler: new Compiler(babelmerge(config, flecksBabelConfig)), - }); - }); - } - const findCompiler = (request) => { - for (let i = 0; i < compilations.length; ++i) { - const {compiler, ignore, only} = compilations[i]; - if (request.startsWith(only) && !request.startsWith(ignore)) { - return compiler; - } - } - return undefined; - }; - debugSilly('pirating exts: %O', exts); - addHook( - (code, request) => { - const compilation = findCompiler(request).compile(code, request); - return null === compilation ? code : compilation.code; - }, - {exts, matcher: (request) => !!findCompiler(request)}, - ); - } - - loadBuildConfigs() { - Object.entries(this.invoke('@flecks/core.build.config')) - .forEach(([fleck, configs]) => { - configs.forEach((config) => { - const defaults = { - fleck, - }; - if (Array.isArray(config)) { - this.registerBuildConfig(config[0], {...defaults, ...config[1]}); - } - this.registerBuildConfig(config, defaults); - }); - }); - } - - static loadConfig(root) { - const resolvedRoot = resolve(FLECKS_CORE_ROOT, root); - try { - const {load} = R('js-yaml'); - const filename = join(resolvedRoot, 'build', FLECKS_YML); - const buffer = readFileSync(filename, 'utf8'); - debugSilly('parsing configuration from YML...'); - return ['YML', load(buffer, {filename}) || {}]; - } - catch (error) { - if ('ENOENT' !== error.code) { - throw error; - } - return ['barebones', {'@flecks/core': {}, '@flecks/fleck': {}}]; - } - } - - static makeResolver(maybeAliasedPath, platforms, root) { - const resolvedRoot = resolve(FLECKS_CORE_ROOT, root); - const resolver = {}; - // `!platform` excludes that platform. - const without = platforms - .filter((platform) => '!'.charCodeAt(0) === platform.charCodeAt(0)) - .map((platform) => platform.slice(1)); - // Parse the alias (if any). - const index = maybeAliasedPath.indexOf(':'); - const [path, alias] = -1 === index - ? [maybeAliasedPath, undefined] - : [maybeAliasedPath.slice(0, index), maybeAliasedPath.slice(index + 1)]; - // Run it by the exception list. - if (-1 !== without.indexOf(path.split('/').pop())) { - // eslint-disable-next-line no-continue - return {}; - } - // Resolve the path (if necessary). - let resolvedPath; - if (alias) { - resolvedPath = isAbsolute(alias) ? alias : join(resolvedRoot, alias); - } - else { - if (path.startsWith('.')) { - throw new Error(`non-aliased relative path '${path}' in configuration`); - } - resolvedPath = path; - } - try { - R.resolve(resolvedPath); - resolver[path] = resolvedPath; - } - // eslint-disable-next-line no-empty - catch (error) {} - // Discover platform-specific variants. - if (platforms) { - platforms.forEach((platform) => { - try { - const resolvedPlatformPath = join(resolvedPath, platform); - R.resolve(resolvedPlatformPath); - resolver[join(path, platform)] = resolvedPlatformPath; - } - // eslint-disable-next-line no-empty - catch (error) {} - }); - } - return resolver; - } - - static makeResolverAndLoadRcs( - maybeAliasedPaths, - platforms = ['server'], - root = FLECKS_CORE_ROOT, - ) { - const resolver = {}; - const rootsFrom = (paths) => ( - Array.from(new Set( - paths - .map((path) => this.root(resolver, path)) - .filter((e) => !!e), - )) - ); - for (let i = 0; i < maybeAliasedPaths.length; ++i) { - const maybeAliasedPath = maybeAliasedPaths[i]; - Object.entries(this.makeResolver(maybeAliasedPath, platforms, root)) - .forEach(([path, alias]) => { - resolver[path] = alias; - }); - } - const flecksConfig = {}; - const roots = Array.from(new Set( - rootsFrom(Object.keys(resolver)) - .concat(FLECKS_CORE_ROOT), - )); - let rootsToScan = roots; - while (rootsToScan.length > 0) { - const dependencies = []; - for (let i = 0; i < rootsToScan.length; ++i) { - const root = rootsToScan[i]; - try { - flecksConfig[root] = R(join(root, 'build', 'flecks.config')); - const {dependencies: flecksConfigDependencies = []} = flecksConfig[root]; - dependencies.push(...flecksConfigDependencies); - flecksConfigDependencies.forEach((dependency) => { - Object.entries(this.makeResolver(dependency, platforms, root)) - .forEach(([path, alias]) => { - resolver[path] = alias; - }); - }); - } - catch (error) { - if ('MODULE_NOT_FOUND' !== error.code) { - throw error; - } - } - } - rootsToScan = rootsFrom(dependencies) - .filter((root) => !flecksConfig[root]); - } - return {flecksConfig, resolver}; - } - - overrideConfigFromEnvironment() { - const keys = Object.keys(process.env); - const seen = []; - Object.keys(this.flecks) - .sort((l, r) => (l < r ? 1 : -1)) - .forEach((fleck) => { - const prefix = `FLECKS_ENV__${this.constructor.environmentalize(fleck)}`; - keys - .filter((key) => key.startsWith(`${prefix}__`) && -1 === seen.indexOf(key)) - .map((key) => { - seen.push(key); - debug('reading environment from %s...', key); - return [key, process.env[key]]; - }) - .map(([key, value]) => [key.slice(prefix.length + 2), value]) - .map(([subkey, value]) => [subkey.split('_'), value]) - .forEach(([path, jsonOrString]) => { - try { - this.set([fleck, ...path], JSON.parse(jsonOrString)); - debug('read (%s) as JSON', jsonOrString); - } - catch (error) { - this.set([fleck, ...path], jsonOrString); - debug('read (%s) as string', jsonOrString); - } - }); - }); - } - - flecksConfig() { - return this.flecksConfig; - } - - registerBuildConfig(filename, config) { - const defaults = { - root: FLECKS_CORE_ROOT, - }; - this.buildConfigs[filename] = {...defaults, ...config}; - } - - registerResolver(from, to = from) { - this.resolver[from] = to; - } - - resolve(path) { - return this.constructor.resolve(this.resolver, path); - } - - static resolve(resolver, fleck) { - return resolver[fleck] || fleck; - } - - resolveBuildConfig(roots, paths) { - return this.constructor.resolveBuildConfig(this.resolver, roots, paths); - } - - static resolveBuildConfig(resolver, roots, paths) { - const tried = []; - for (let i = 0; i < roots.length; ++i) { - const root = roots[i]; - for (let j = 0; j < paths.length; ++j) { - const path = paths[j]; - const resolved = join(root, 'build', path); - try { - tried.push(resolved); - statSync(resolved); - return resolved; - } - // eslint-disable-next-line no-empty - catch (error) {} - } - } - throw new Error(`Couldn't resolve build file '${paths.pop()}', tried: ${tried.join(', ')}`); - } - - resolvePath(path) { - return this.constructor.resolvePath(this.resolver, path); - } - - static resolvePath(resolver, path) { - const resolved = R.resolve(this.resolve(resolver, path)); - const ext = extname(resolved); - const base = basename(resolved, ext); - return join(dirname(resolved), 'index' === base ? '' : base); - } - - root(fleck) { - return this.constructor.root(this.resolver, fleck); - } - - static root(resolver, fleck) { - const parts = this.resolve(resolver, fleck).split('/'); - try { - R.resolve(parts.join('/')); - } - catch (error) { - try { - R.resolve(join(parts.join('/'), 'build', 'flecks.config')); - } - catch (error) { - return undefined; - } - } - while (parts.length > 0) { - try { - R.resolve(join(parts.join('/'), 'package.json')); - return parts.join('/'); - } - catch (error) { - parts.pop(); - } - } - return undefined; - } - - runtimeCompiler(resolver, runtime, config, {allowlist = []} = {}) { - // Compile. - const needCompilation = Object.entries(resolver) - .filter(([fleck]) => this.constructor.fleckIsCompiled(resolver, fleck)); - if (needCompilation.length > 0) { - const flecksBabelConfig = this.babel(); - debugSilly('flecks.config.js: babel: %O', flecksBabelConfig); - // Alias and de-externalize. - needCompilation - .sort(([l], [r]) => (l < r ? 1 : -1)) - .forEach(([fleck, resolved]) => { - let alias = this.constructor.fleckIsAliased(resolver, fleck) - ? resolved - : this.constructor.sourcepath(R.resolve(this.constructor.resolve(resolver, fleck))); - alias = alias.endsWith('/index') ? alias.slice(0, -6) : alias; - allowlist.push(fleck); - config.resolve.alias[fleck] = alias; - debugSilly('%s runtime de-externalized %s, alias: %s', runtime, fleck, alias); - }); - // Set up compilation at each root. - const compiledPaths = []; - Array.from(new Set( - needCompilation - .map(([fleck]) => fleck) - .map((fleck) => this.constructor.root(resolver, fleck)), - )) - .forEach((root) => { - const resolved = dirname(R.resolve(join(root, 'package.json'))); - const sourcepath = this.constructor.sourcepath(resolved); - const sourceroot = join(sourcepath, '..'); - compiledPaths.push(sourceroot); - // @todo Ideally the fleck's 3rd party modules would be externalized. - // Alias this compiled fleck's `node_modules` to the root `node_modules`. - config.resolve.alias[join(sourceroot, 'node_modules')] = join(FLECKS_CORE_ROOT, 'node_modules'); - const configFile = this.buildConfig('babel.config.js'); - debugSilly('compiling: %s with %s', root, configFile); - const babel = { - configFile, - // Augment the compiler with babel config from `flecks.config.js`. - ...babelmerge.all(flecksBabelConfig.map(([, babel]) => babel)), - }; - config.module.rules.push( - { - test: /\.(m?jsx?)?$/, - include: [sourceroot], - use: [ - { - loader: R.resolve('babel-loader'), - options: { - cacheDirectory: true, - babelrc: false, - configFile: false, - ...babel, - }, - }, - ], - }, - ); - }); - const compiledPathsRegex = new RegExp( - `(?:${compiledPaths.map((path) => path.replace(/[\\/]/g, '[\\/]')).join('|')})`, - ); - if (!config.optimization) { - config.optimization = {}; - } - if (!config.optimization.splitChunks) { - config.optimization.splitChunks = {}; - } - if (!config.optimization.splitChunks.cacheGroups) { - config.optimization.splitChunks.cacheGroups = {}; - } - config.optimization.splitChunks.cacheGroups.flecksCompiled = { - chunks: 'all', - enforce: true, - priority: 100, - test: compiledPathsRegex, - }; - } - } - - sourcepath(fleck) { - return this.constructor.sourcepath(fleck); - } - - static sourcepath(path) { - let sourcepath = realpathSync(path); - const parts = sourcepath.split('/'); - const indexOf = parts.lastIndexOf('dist'); - if (-1 !== indexOf) { - parts.splice(indexOf, 1, 'src'); - sourcepath = parts.join('/'); - sourcepath = join(dirname(sourcepath), basename(sourcepath, extname(sourcepath))); - } - else { - sourcepath = join(sourcepath, 'src'); - } - return sourcepath; - } - - stubs() { - return this.constructor.stubs(this.platforms, this.flecksConfig); - } - - static stubs(platforms, flecksConfig) { - const keys = Object.keys(flecksConfig); - const stubs = []; - for (let i = 0; i < keys.length; ++i) { - const key = keys[i]; - const config = flecksConfig[key]; - if (config.stubs) { - Object.entries(config.stubs) - .forEach(([platform, patterns]) => { - if (-1 !== platforms.indexOf(platform)) { - patterns.forEach((pattern) => { - stubs.push(pattern); - }); - } - }); - } - } - return stubs; - } - -} diff --git a/packages/core/src/server/index.js b/packages/core/src/server/index.js index 732800f..45840ff 100644 --- a/packages/core/src/server/index.js +++ b/packages/core/src/server/index.js @@ -1,14 +1,7 @@ -import {join} from 'path'; import {inspect} from 'util'; import webpack from 'webpack'; -import commands from './commands'; - -const { - FLECKS_CORE_ROOT = process.cwd(), -} = process.env; - const {defaultOptions} = inspect; defaultOptions.breakLength = 160; defaultOptions.compact = 6; @@ -17,66 +10,18 @@ defaultOptions.sorted = true; export {glob} from 'glob'; export {dump as dumpYml, load as loadYml} from 'js-yaml'; -export { - Argument, - default as commands, - Option, - processCode, - program, - spawnWith, -} from './commands'; -export {default as Flecks} from './flecks'; -export {default as require} from '../require'; -export {JsonStream, transform} from './stream'; -export * from './webpack'; +export {commands, processCode, spawnWith} from '../../build/commands'; +export {JsonStream, transform} from '../../build/stream'; +export * from '../../build/webpack'; export {webpack}; export const hooks = { - '@flecks/core.build': async (target, config, env, argv, flecks) => { - const { - profile, - } = flecks.get('@flecks/core/server'); - if (profile.includes(target)) { - config.plugins.push( - new webpack.debug.ProfilingPlugin({ - outputPath: join(FLECKS_CORE_ROOT, `profile.build-${target}.json`), - }), - ); - } - }, - '@flecks/core.build.config': () => [ - /** - * Babel configuration. See: https://babeljs.io/docs/en/config-files - */ - 'babel.config.js', - /** - * ESLint defaults. The generated `eslint.config.js` just reads from this file so that the - * build can dynamically configure parts of ESLint. - */ - ['default.eslint.config.js', {specifier: false}], - /** - * ESLint configuration. See: https://eslint.org/docs/user-guide/configuring/ - */ - ['eslint.config.js', {specifier: false}], - /** - * Flecks webpack configuration. See: https://webpack.js.org/configuration/ - */ - ['fleckspack.config.js', {specifier: false}], - /** - * Webpack configuration. See: https://webpack.js.org/configuration/ - */ - 'webpack.config.js', - ], - '@flecks/core.commands': commands, - '@flecks/core.config': () => ({ - /** - * The package manager used for tasks. - */ - packageManager: 'npm', - /** - * Build targets to profile with `webpack.debug.ProfilingPlugin`. - */ - profile: [], + '@flecks/web.config': async (req, flecks) => ({ + '@flecks/core': { + id: flecks.get('@flecks/core.id'), + packageManager: undefined, + profile: undefined, + }, }), }; diff --git a/packages/core/test/build/flecks.yml b/packages/core/test/build/flecks.yml deleted file mode 100644 index d2e41a6..0000000 --- a/packages/core/test/build/flecks.yml +++ /dev/null @@ -1,4 +0,0 @@ -'@flecks/core:../src': {} -'@flecks/core/one:./one': {} -'@flecks/core/one/server:./one/server': {} -'@flecks/core/two:./two': {} diff --git a/packages/core/test/gather.js b/packages/core/test/gather.js index 878872b..3c478cd 100644 --- a/packages/core/test/gather.js +++ b/packages/core/test/gather.js @@ -2,8 +2,8 @@ import {expect} from 'chai'; import {Flecks, ById, ByType} from '@flecks/core'; -const testOne = require('./one'); -const testTwo = require('./two'); +const testOne = require('./packages/one'); +const testTwo = require('./packages/two'); it('can gather', () => { const flecks = Flecks.from({ diff --git a/packages/core/test/instance.js b/packages/core/test/instance.js index f477349..5ebf332 100644 --- a/packages/core/test/instance.js +++ b/packages/core/test/instance.js @@ -2,7 +2,7 @@ import {expect} from 'chai'; import {Flecks} from '@flecks/core'; -const testOne = require('./one'); +const testOne = require('./packages/one'); it('can create an empty instance', () => { const flecks = new Flecks(); diff --git a/packages/core/test/invoke.js b/packages/core/test/invoke.js index 6297b6d..6db7323 100644 --- a/packages/core/test/invoke.js +++ b/packages/core/test/invoke.js @@ -7,8 +7,8 @@ chai.use(chaiAsPromised); const {expect} = chai; -const testOne = require('./one'); -const testTwo = require('./two'); +const testOne = require('./packages/one'); +const testTwo = require('./packages/two'); let flecks; diff --git a/packages/core/test/middleware.js b/packages/core/test/middleware.js index 0779f72..183a5ad 100644 --- a/packages/core/test/middleware.js +++ b/packages/core/test/middleware.js @@ -2,9 +2,9 @@ import {expect} from 'chai'; import {Flecks} from '@flecks/core'; -const testOne = require('./one'); -const testTwo = require('./two'); -const testThree = require('./three'); +const testOne = require('./packages/one'); +const testTwo = require('./packages/two'); +const testThree = require('./packages/three'); it('can make middleware', (done) => { const flecks = Flecks.from({ diff --git a/packages/core/test/one/client/index.js b/packages/core/test/packages/one/client/index.js similarity index 100% rename from packages/core/test/one/client/index.js rename to packages/core/test/packages/one/client/index.js diff --git a/packages/core/test/one/index.js b/packages/core/test/packages/one/index.js similarity index 100% rename from packages/core/test/one/index.js rename to packages/core/test/packages/one/index.js diff --git a/packages/core/test/one/server/index.js b/packages/core/test/packages/one/server/index.js similarity index 100% rename from packages/core/test/one/server/index.js rename to packages/core/test/packages/one/server/index.js diff --git a/packages/core/test/one/things/decorators/three.js b/packages/core/test/packages/one/things/decorators/three.js similarity index 100% rename from packages/core/test/one/things/decorators/three.js rename to packages/core/test/packages/one/things/decorators/three.js diff --git a/packages/core/test/one/things/one.js b/packages/core/test/packages/one/things/one.js similarity index 100% rename from packages/core/test/one/things/one.js rename to packages/core/test/packages/one/things/one.js diff --git a/packages/core/test/one/things/two.js b/packages/core/test/packages/one/things/two.js similarity index 100% rename from packages/core/test/one/things/two.js rename to packages/core/test/packages/one/things/two.js diff --git a/packages/core/test/three/index.js b/packages/core/test/packages/three/index.js similarity index 100% rename from packages/core/test/three/index.js rename to packages/core/test/packages/three/index.js diff --git a/packages/core/test/two/index.js b/packages/core/test/packages/two/index.js similarity index 100% rename from packages/core/test/two/index.js rename to packages/core/test/packages/two/index.js diff --git a/packages/core/test/two/things/three.js b/packages/core/test/packages/two/things/three.js similarity index 100% rename from packages/core/test/two/things/three.js rename to packages/core/test/packages/two/things/three.js diff --git a/packages/core/test/platforms/server/bootstrap.js b/packages/core/test/platforms/server/bootstrap.js deleted file mode 100644 index 441afac..0000000 --- a/packages/core/test/platforms/server/bootstrap.js +++ /dev/null @@ -1,51 +0,0 @@ -import {expect} from 'chai'; - -// eslint-disable-next-line import/no-unresolved, import/no-extraneous-dependencies -import {Flecks} from '../../../src/server'; - -it('bootstraps FLECKS_CORE_ROOT by default', () => { - const flecks = Flecks.bootstrap(); - expect(flecks.fleck('@flecks/core')).to.not.equal(undefined); -}); - -it('bootstraps server platform by default', () => { - const flecks = Flecks.bootstrap(); - expect(flecks.fleck('@flecks/core/server')).to.not.equal(undefined); -}); - -it('can bootstrap from a foreign root', () => { - const flecks = Flecks.bootstrap({ - root: './test', - }); - expect(flecks.fleck('@flecks/core/one')).to.not.equal(undefined); - expect(flecks.fleck('@flecks/core/two')).to.not.equal(undefined); -}); - -it('can bootstrap other platforms', () => { - const flecks = Flecks.bootstrap({ - platforms: ['client'], - root: './test', - }); - expect(flecks.fleck('@flecks/core/one')).to.not.equal(undefined); - expect(flecks.fleck('@flecks/core/one/client')).to.not.equal(undefined); - expect(flecks.fleck('@flecks/core/one/server')).to.not.equal(undefined); -}); - -it('can exclude platforms', () => { - const flecks = Flecks.bootstrap({ - platforms: ['client', '!server'], - root: './test', - }); - expect(flecks.fleck('@flecks/core/one')).to.not.equal(undefined); - expect(flecks.fleck('@flecks/core/one/client')).to.not.equal(undefined); - expect(flecks.fleck('@flecks/core/one/server')).to.equal(undefined); -}); - -it('provides webpack goodies in nodespace', () => { - const flecks = Flecks.bootstrap({ - root: './test', - }); - flecks.fleck('@flecks/core/one').testNodespace().forEach((result) => { - expect(result).to.not.equal('undefined'); - }); -}); diff --git a/packages/core/test/server/explicate.js b/packages/core/test/server/explicate.js new file mode 100644 index 0000000..5a92656 --- /dev/null +++ b/packages/core/test/server/explicate.js @@ -0,0 +1,80 @@ +import {join} from 'path'; + +import {expect} from 'chai'; + +import explicate from '@flecks/core/build/explicate'; +import Resolver from '@flecks/core/build/resolver'; + +const { + FLECKS_CORE_ROOT = process.cwd(), +} = process.env; + +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( + paths, + { + platforms, + resolver, + root, + 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', + ]); + }); + + it('derives through bootstrap', async () => { + expect(Object.keys((await createExplication(['real-root'])).descriptors)) + .to.deep.equal([ + 'dependency', 'dependency/server', + 'real-root', 'real-root/server', + ]); + }); + + it('excludes platforms', async () => { + expect(Object.keys( + (await createExplication( + ['platformed/client', 'dependency'], + ['server', '!client'], + )).descriptors, + )) + .to.deep.equal([ + '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', + ]); + }); + + it('explicates only bootstrapped', async () => { + expect(Object.keys((await createExplication(['only-bootstrapped'])).descriptors)) + .to.deep.equal([ + 'only-bootstrapped', + ]); + }); + + it('skips nonexistent', async () => { + expect(await createExplication(['real-root/nonexistent'])) + .to.deep.equal({descriptors: {}, roots: {}}); + }); + +}); diff --git a/packages/core/test/server/explicate/fake_node_modules/dependency/index.js b/packages/core/test/server/explicate/fake_node_modules/dependency/index.js new file mode 100644 index 0000000..e69de29 diff --git a/packages/core/test/server/explicate/fake_node_modules/dependency/server/index.js b/packages/core/test/server/explicate/fake_node_modules/dependency/server/index.js new file mode 100644 index 0000000..e69de29 diff --git a/packages/core/test/server/explicate/fake_node_modules/only-bootstrapped/build/flecks.bootstrap.js b/packages/core/test/server/explicate/fake_node_modules/only-bootstrapped/build/flecks.bootstrap.js new file mode 100644 index 0000000..e69de29 diff --git a/packages/core/test/server/explicate/fake_node_modules/only-bootstrapped/package.json b/packages/core/test/server/explicate/fake_node_modules/only-bootstrapped/package.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/packages/core/test/server/explicate/fake_node_modules/only-bootstrapped/package.json @@ -0,0 +1 @@ +{} diff --git a/packages/core/test/server/explicate/fake_node_modules/platformed/client/index.js b/packages/core/test/server/explicate/fake_node_modules/platformed/client/index.js new file mode 100644 index 0000000..e69de29 diff --git a/packages/core/test/server/explicate/fake_node_modules/platformed/index.js b/packages/core/test/server/explicate/fake_node_modules/platformed/index.js new file mode 100644 index 0000000..e69de29 diff --git a/packages/core/test/server/explicate/fake_node_modules/platformed/server/index.js b/packages/core/test/server/explicate/fake_node_modules/platformed/server/index.js new file mode 100644 index 0000000..e69de29 diff --git a/packages/core/test/server/explicate/fake_node_modules/real-root/build/flecks.bootstrap.js b/packages/core/test/server/explicate/fake_node_modules/real-root/build/flecks.bootstrap.js new file mode 100644 index 0000000..a2c50b8 --- /dev/null +++ b/packages/core/test/server/explicate/fake_node_modules/real-root/build/flecks.bootstrap.js @@ -0,0 +1 @@ +exports.dependencies = ['dependency']; diff --git a/packages/core/test/server/explicate/fake_node_modules/real-root/index.js b/packages/core/test/server/explicate/fake_node_modules/real-root/index.js new file mode 100644 index 0000000..e69de29 diff --git a/packages/core/test/server/explicate/fake_node_modules/real-root/package.json b/packages/core/test/server/explicate/fake_node_modules/real-root/package.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/packages/core/test/server/explicate/fake_node_modules/real-root/package.json @@ -0,0 +1 @@ +{} diff --git a/packages/core/test/server/explicate/fake_node_modules/real-root/server/index.js b/packages/core/test/server/explicate/fake_node_modules/real-root/server/index.js new file mode 100644 index 0000000..e69de29 diff --git a/packages/core/test/server/explicate/fake_node_modules/server-only/server.js b/packages/core/test/server/explicate/fake_node_modules/server-only/server.js new file mode 100644 index 0000000..e69de29 diff --git a/packages/core/test/server/resolve.js b/packages/core/test/server/resolve.js new file mode 100644 index 0000000..40702d4 --- /dev/null +++ b/packages/core/test/server/resolve.js @@ -0,0 +1,24 @@ +import {join} from 'path'; + +import {expect} from 'chai'; + +import Resolver from '@flecks/core/build/resolver'; + +const { + FLECKS_CORE_ROOT = process.cwd(), +} = process.env; + +it('can resolve', async () => { + const resolver = new Resolver(); + expect(await resolver.resolve('./test/server/resolve')) + .to.equal(join(FLECKS_CORE_ROOT, 'test', 'server', 'resolve.js')); +}); + +it('can create aliases at runtime', async () => { + const resolver = new Resolver(); + expect(await resolver.resolve('./test/server/foobar')) + .to.be.undefined; + resolver.addAlias('./test/server/foobar', './test/server/resolve'); + expect(await resolver.resolve('./test/server/foobar')) + .to.not.be.undefined; +}); diff --git a/packages/create-app/src/build.js b/packages/create-app/build/build.js similarity index 100% rename from packages/create-app/src/build.js rename to packages/create-app/build/build.js diff --git a/packages/create-app/src/cli.js b/packages/create-app/build/cli.js similarity index 94% rename from packages/create-app/src/cli.js rename to packages/create-app/build/cli.js index 08d1d0b..f026294 100644 --- a/packages/create-app/src/cli.js +++ b/packages/create-app/build/cli.js @@ -1,3 +1,5 @@ +#!/usr/bin/env node + import {join} from 'path'; import { @@ -19,7 +21,7 @@ const { (async () => { program.argument('', 'name of the app to create'); program.addOption( - new Option('--package-manager ', 'package manager binary') + new Option('-pm,--package-manager ', 'package manager binary') .choices(['npm', 'bun', 'yarn']) .default('npm'), ); diff --git a/packages/create-app/build/fleck.webpack.config.js b/packages/create-app/build/fleck.webpack.config.js index c0aaa47..c7c9c7e 100644 --- a/packages/create-app/build/fleck.webpack.config.js +++ b/packages/create-app/build/fleck.webpack.config.js @@ -1,6 +1,6 @@ const {copy, executable} = require('@flecks/core/server'); // eslint-disable-next-line import/no-extraneous-dependencies -const configFn = require('@flecks/fleck/server/build/fleck.webpack.config'); +const configFn = require('@flecks/fleck/build/fleck.webpack.config'); module.exports = async (env, argv, flecks) => { const config = await configFn(env, argv, flecks); diff --git a/packages/create-app/src/move.js b/packages/create-app/build/move.js similarity index 90% rename from packages/create-app/src/move.js rename to packages/create-app/build/move.js index 1ebb56c..4455dc7 100644 --- a/packages/create-app/src/move.js +++ b/packages/create-app/build/move.js @@ -1,12 +1,7 @@ -import { - stat, -} from 'fs/promises'; +import {stat} from 'fs/promises'; import {basename, dirname, join} from 'path'; -import { - JsonStream, - transform, -} from '@flecks/core/server'; +import {JsonStream, transform} from '@flecks/core/server'; import FileTree from './tree'; diff --git a/packages/create-app/src/tree.js b/packages/create-app/build/tree.js similarity index 100% rename from packages/create-app/src/tree.js rename to packages/create-app/build/tree.js diff --git a/packages/create-app/package.json b/packages/create-app/package.json index cfeeadb..b2349e2 100644 --- a/packages/create-app/package.json +++ b/packages/create-app/package.json @@ -8,7 +8,7 @@ "publishConfig": { "access": "public" }, - "version": "2.0.3", + "version": "3.0.0", "bin": { "create-app": "./cli.js" }, @@ -22,15 +22,14 @@ "files": [ "cli.js", "server.js", - "src", "template" ], "dependencies": { - "@flecks/core": "^2.0.3", + "@flecks/core": "^3.0.0", "minimatch": "^5.0.1", "validate-npm-package-name": "^3.0.0" }, "devDependencies": { - "@flecks/fleck": "^2.0.3" + "@flecks/fleck": "^3.0.0" } } diff --git a/packages/create-app/src/server.js b/packages/create-app/src/server.js index 19ec933..e66c84b 100644 --- a/packages/create-app/src/server.js +++ b/packages/create-app/src/server.js @@ -1,5 +1 @@ export {default as validate} from 'validate-npm-package-name'; - -export {default as build} from './build'; -export {default as move, testDestination} from './move'; -export {default as FileTree} from './tree'; diff --git a/packages/create-app/template/package.json.noconflict b/packages/create-app/template/package.json.noconflict index c201cf6..b2b2020 100644 --- a/packages/create-app/template/package.json.noconflict +++ b/packages/create-app/template/package.json.noconflict @@ -9,11 +9,11 @@ "start": "DEBUG=@flecks/*,-*:silly npm run dev" }, "dependencies": { - "@flecks/core": "^2.0.0", - "@flecks/server": "^2.0.0" + "@flecks/core": "3.0.0", + "@flecks/server": "3.0.0" }, "devDependencies": { - "@flecks/create-fleck": "^2.0.0", + "@flecks/create-fleck": "3.0.0", "lerna": "^3.22.1" } } diff --git a/packages/create-fleck/src/cli.js b/packages/create-fleck/build/cli.js similarity index 77% rename from packages/create-fleck/src/cli.js rename to packages/create-fleck/build/cli.js index 24a8585..4a81b88 100644 --- a/packages/create-fleck/src/cli.js +++ b/packages/create-fleck/build/cli.js @@ -1,13 +1,14 @@ -import {stat} from 'fs/promises'; -import {join} from 'path'; +#!/usr/bin/env node -import { - build, - move, - testDestination, - validate, -} from '@flecks/create-app/server'; -import {Flecks, program} from '@flecks/core/server'; +const {stat} = require('fs/promises'); +const {join} = require('path'); + +const addFleckToYml = require('@flecks/core/build/add-fleck-to-yml'); +const {Server, program} = require('@flecks/core/server'); +const build = require('@flecks/create-app/build/build'); +const move = require('@flecks/create-app/build/move'); +const testDestination = require('@flecks/create-app/build/testDestination'); +const {validate} = require('@flecks/create-app/server'); const { FLECKS_CORE_ROOT = process.cwd(), @@ -65,8 +66,8 @@ const target = async (fleck) => { program.action(async (fleck, o) => { const {alias, add} = o; try { - const flecks = await Flecks.bootstrap(); - const {packageManager} = flecks.get('@flecks/core/server'); + const flecks = await Server.from(); + const {packageManager} = flecks.get('@flecks/core'); const isMonorepo = await checkIsMonorepo(); const [scope, pkg] = await target(fleck); const name = [scope, pkg].filter((e) => !!e).join('/'); @@ -86,7 +87,7 @@ const target = async (fleck) => { await fileTree.writeTo(destination); await build(packageManager, destination); if (isMonorepo && add) { - await Flecks.addFleckToYml(...[name].concat(alias ? pkg : [])); + await addFleckToYml(...[name].concat(alias ? pkg : [])); } } catch (error) { diff --git a/packages/create-fleck/build/fleck.webpack.config.js b/packages/create-fleck/build/fleck.webpack.config.js index c0aaa47..c7c9c7e 100644 --- a/packages/create-fleck/build/fleck.webpack.config.js +++ b/packages/create-fleck/build/fleck.webpack.config.js @@ -1,6 +1,6 @@ const {copy, executable} = require('@flecks/core/server'); // eslint-disable-next-line import/no-extraneous-dependencies -const configFn = require('@flecks/fleck/server/build/fleck.webpack.config'); +const configFn = require('@flecks/fleck/build/fleck.webpack.config'); module.exports = async (env, argv, flecks) => { const config = await configFn(env, argv, flecks); diff --git a/packages/create-fleck/package.json b/packages/create-fleck/package.json index 1297e06..d027da9 100644 --- a/packages/create-fleck/package.json +++ b/packages/create-fleck/package.json @@ -8,7 +8,7 @@ "publishConfig": { "access": "public" }, - "version": "2.0.3", + "version": "3.0.0", "bin": { "create-fleck": "./cli.js" }, @@ -20,15 +20,13 @@ "test": "flecks test" }, "files": [ - "cli.js", - "src", "template" ], "dependencies": { - "@flecks/core": "^2.0.3", - "@flecks/create-app": "^2.0.3" + "@flecks/core": "^3.0.0", + "@flecks/create-app": "^3.0.0" }, "devDependencies": { - "@flecks/fleck": "^2.0.3" + "@flecks/fleck": "^3.0.0" } } diff --git a/packages/create-fleck/template/package.json.noconflict b/packages/create-fleck/template/package.json.noconflict index 30b0576..805736c 100644 --- a/packages/create-fleck/template/package.json.noconflict +++ b/packages/create-fleck/template/package.json.noconflict @@ -11,9 +11,9 @@ "index.js" ], "dependencies": { - "@flecks/core": "^2.0.0" + "@flecks/core": "3.0.0" }, "devDependencies": { - "@flecks/fleck": "^2.0.0" + "@flecks/fleck": "3.0.0" } } diff --git a/packages/db/package.json b/packages/db/package.json index fe65ad0..700608d 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -8,7 +8,7 @@ "publishConfig": { "access": "public" }, - "version": "2.0.3", + "version": "3.0.0", "scripts": { "build": "flecks build", "clean": "flecks clean", @@ -20,11 +20,11 @@ "server.js" ], "dependencies": { - "@flecks/core": "^2.0.3", + "@flecks/core": "^3.0.0", "sequelize": "^6.3.5", "sqlite3": "^5.0.2" }, "devDependencies": { - "@flecks/fleck": "^2.0.3" + "@flecks/fleck": "^3.0.0" } } diff --git a/packages/docker/package.json b/packages/docker/package.json index f907d78..2a92b91 100644 --- a/packages/docker/package.json +++ b/packages/docker/package.json @@ -8,7 +8,7 @@ "publishConfig": { "access": "public" }, - "version": "2.0.3", + "version": "3.0.0", "main": "index.js", "scripts": { "build": "flecks build", @@ -21,10 +21,10 @@ "server.js" ], "dependencies": { - "@flecks/core": "^2.0.3", + "@flecks/core": "^3.0.0", "debug": "^4.3.3" }, "devDependencies": { - "@flecks/fleck": "^2.0.3" + "@flecks/fleck": "^3.0.0" } } diff --git a/packages/dox/src/server/commands.js b/packages/dox/build/commands.js similarity index 91% rename from packages/dox/src/server/commands.js rename to packages/dox/build/commands.js index c15c9f8..b584561 100644 --- a/packages/dox/src/server/commands.js +++ b/packages/dox/build/commands.js @@ -1,24 +1,25 @@ -import { +const { access, cp, mkdir, rename, rmdir, -} from 'fs/promises'; -import {dirname, join} from 'path'; +} = require('fs/promises'); +const {dirname, join} = require('path'); -import { +const {Argument} = require('@flecks/core/build/commands'); + +const { generate, resolveSiteDir, spawn, -} from './docusaurus'; +} = require('./docusaurus'); const { FLECKS_CORE_ROOT = process.cwd(), } = process.env; -export default (program, flecks) => { - const {Argument} = flecks.fleck('@flecks/core/server'); +module.exports = (program, flecks) => { const commands = {}; const siteDirArgument = new Argument('[siteDir]', 'Docusaurus directory'); siteDirArgument.defaultValue = 'website'; diff --git a/packages/dox/src/server/docusaurus.js b/packages/dox/build/docusaurus.js similarity index 83% rename from packages/dox/src/server/docusaurus.js rename to packages/dox/build/docusaurus.js index cb492fb..4710d33 100644 --- a/packages/dox/src/server/docusaurus.js +++ b/packages/dox/build/docusaurus.js @@ -1,23 +1,23 @@ -import {mkdir, writeFile} from 'fs/promises'; -import {isAbsolute, join, resolve} from 'path'; +const {mkdir, writeFile} = require('fs/promises'); +const {isAbsolute, join, resolve} = require('path'); -import {spawnWith} from '@flecks/core/server'; -import {themes as prismThemes} from 'prism-react-renderer'; -import {rimraf} from 'rimraf'; +const {spawnWith} = require('@flecks/core/server'); +const {themes: prismThemes} = require('prism-react-renderer'); +const {rimraf} = require('rimraf'); -import { +const { generateBuildConfigsPage, generateConfigPage, generateHookPage, generateTodoPage, -} from './generate'; -import {parseFlecks} from './parser'; +} = require('./generate'); +const {parseFlecks} = require('./parser'); const { FLECKS_CORE_ROOT = process.cwd(), } = process.env; -export function configDefaults() { +exports.configDefaults = function configDefaults() { /** @type {import('@docusaurus/types').Config} */ const config = { tagline: 'built with flecks', @@ -49,15 +49,15 @@ export function configDefaults() { }, }; return config; -} +}; -export function resolveSiteDir(siteDir) { +exports.resolveSiteDir = function resolveSiteDir(siteDir) { return isAbsolute(siteDir) ? siteDir : resolve(FLECKS_CORE_ROOT, siteDir); -} +}; -export async function generate(flecks, siteDir) { +exports.generate = async function generate(flecks, siteDir) { // Generate "docs". const docsDirectory = join(siteDir, 'docs', 'flecks'); await rimraf(docsDirectory); @@ -72,9 +72,9 @@ export async function generate(flecks, siteDir) { await writeFile(join(generatedDirectory, 'TODO.md'), todoPage); await writeFile(join(generatedDirectory, 'build-configs.md'), buildConfigsPage); await writeFile(join(generatedDirectory, 'config.mdx'), configPage); -} +}; -export function spawn(subcommand, siteDir) { +exports.spawn = function spawn(subcommand, siteDir) { const args = []; switch (subcommand) { case 'start': @@ -112,4 +112,4 @@ export function spawn(subcommand, siteDir) { child.kill(); }); return child; -} +}; diff --git a/packages/dox/src/server/index.js b/packages/dox/build/flecks.bootstrap.js similarity index 69% rename from packages/dox/src/server/index.js rename to packages/dox/build/flecks.bootstrap.js index 510e900..8a4b859 100644 --- a/packages/dox/src/server/index.js +++ b/packages/dox/build/flecks.bootstrap.js @@ -1,8 +1,6 @@ -import commands from './commands'; +const commands = require('./commands'); -export {configDefaults} from './docusaurus'; - -export const hooks = { +exports.hooks = { '@flecks/core.commands': commands, '@flecks/core.config': () => ({ /** diff --git a/packages/dox/build/flecks.yml b/packages/dox/build/flecks.yml deleted file mode 100644 index bd06d44..0000000 --- a/packages/dox/build/flecks.yml +++ /dev/null @@ -1,2 +0,0 @@ -'@flecks/core': {} -'@flecks/fleck': {} diff --git a/packages/dox/src/server/generate.js b/packages/dox/build/generate.js similarity index 92% rename from packages/dox/src/server/generate.js rename to packages/dox/build/generate.js index 7168eac..0f8e6e0 100644 --- a/packages/dox/src/server/generate.js +++ b/packages/dox/build/generate.js @@ -6,7 +6,7 @@ const makeFilenameRewriter = (filenameRewriters) => (filename, line, column) => ) ); -export const generateBuildConfigsPage = (buildConfigs) => { +exports.generateBuildConfigsPage = (buildConfigs) => { const source = []; source.push('# Build configuration'); source.push(''); @@ -25,9 +25,9 @@ export const generateBuildConfigsPage = (buildConfigs) => { return source.join('\n'); }; -export const generateConfigPage = (configs) => { +exports.generateConfigPage = (configs) => { const source = []; - source.push("import CodeBlock from '@theme/CodeBlock';"); + source.push("const CodeBlock = require('@theme/CodeBlock');"); source.push(''); source.push('# Fleck configuration'); source.push(''); @@ -61,8 +61,8 @@ export const generateConfigPage = (configs) => { return source.join('\n'); }; -export const generateHookPage = (hooks, flecks) => { - const {filenameRewriters} = flecks.get('@flecks/dox/server'); +exports.generateHookPage = (hooks, flecks) => { + const {filenameRewriters} = flecks.get('@flecks/dox'); const rewriteFilename = makeFilenameRewriter(filenameRewriters); const source = []; source.push('# Hooks'); @@ -126,8 +126,8 @@ export const generateHookPage = (hooks, flecks) => { return source.join('\n'); }; -export const generateTodoPage = (todos, flecks) => { - const {filenameRewriters} = flecks.get('@flecks/dox/server'); +exports.generateTodoPage = (todos, flecks) => { + const {filenameRewriters} = flecks.get('@flecks/dox'); const rewriteFilename = makeFilenameRewriter(filenameRewriters); const source = []; source.push('# TODO list'); diff --git a/packages/dox/src/server/parser.js b/packages/dox/build/parser.js similarity index 92% rename from packages/dox/src/server/parser.js rename to packages/dox/build/parser.js index 423bcde..00098c6 100644 --- a/packages/dox/src/server/parser.js +++ b/packages/dox/build/parser.js @@ -1,14 +1,14 @@ -import {readFile} from 'fs/promises'; -import { +const {readFile} = require('fs/promises'); +const { basename, dirname, extname, join, -} from 'path'; +} = require('path'); -import {transformAsync} from '@babel/core'; -import traverse from '@babel/traverse'; -import { +const {transformAsync} = require('@babel/core'); +const traverse = require('@babel/traverse'); +const { isArrayExpression, isArrowFunctionExpression, isIdentifier, @@ -18,9 +18,9 @@ import { isStringLiteral, isThisExpression, isVariableDeclaration, -} from '@babel/types'; -import {glob, require as R} from '@flecks/core/server'; -import {parse as parseComment} from 'comment-parser'; +} = require('@babel/types'); +const {glob} = require('@flecks/core/server'); +const {parse: parseComment} = require('comment-parser'); class ParserState { @@ -272,7 +272,7 @@ const FlecksTodos = (state, filename) => ({ }, }); -export const parseCode = async (code) => { +exports.parseCode = async (code) => { const config = { ast: true, code: false, @@ -281,10 +281,10 @@ export const parseCode = async (code) => { return ast; }; -export const parseFile = async (filename, resolved, state) => { +exports.parseFile = async (filename, resolved, state) => { const buffer = await readFile(filename); const source = buffer.toString('utf8'); - const ast = await parseCode(source); + const ast = await exports.parseCode(source); traverse(ast, FlecksBuildConfigs(state, resolved)); traverse(ast, FlecksConfigs(state, resolved, source)); traverse(ast, FlecksInvocations(state, resolved)); @@ -294,19 +294,19 @@ export const parseFile = async (filename, resolved, state) => { const fleckSources = async (path) => glob(join(path, 'src', '**', '*.js')); -export const parseFleckRoot = async (root, state) => { - const resolved = dirname(R.resolve(join(root, 'package.json'))); +exports.parseFleckRoot = async (root, state) => { + const resolved = dirname(require.resolve(join(root, 'package.json'))); const sources = await fleckSources(resolved); await Promise.all( sources.map(async (source) => { // @todo Aliased fleck paths are gonna be bad. - await parseFile(source, join(root, source.slice(resolved.length)), state); + await exports.parseFile(source, join(root, source.slice(resolved.length)), state); }), ); try { const buffer = await readFile(join(resolved, 'build', 'dox', 'hooks.js')); const source = buffer.toString('utf8'); - const ast = await parseCode(source); + const ast = await exports.parseCode(source); traverse(ast, FlecksSpecifications(state, source)); } catch (error) { @@ -316,7 +316,7 @@ export const parseFleckRoot = async (root, state) => { } }; -export const parseFlecks = async (flecks) => { +exports.parseFlecks = async (flecks) => { const state = new ParserState(); const paths = Object.keys(flecks.resolver); const roots = Array.from(new Set( @@ -327,7 +327,7 @@ export const parseFlecks = async (flecks) => { await Promise.all( roots .map(async (root) => { - await parseFleckRoot(root, state); + await exports.parseFleckRoot(root, state); }), ); return state; diff --git a/packages/dox/package.json b/packages/dox/package.json index 95dc36e..b6c3e9d 100644 --- a/packages/dox/package.json +++ b/packages/dox/package.json @@ -8,7 +8,7 @@ "publishConfig": { "access": "public" }, - "version": "2.0.3", + "version": "3.0.0", "main": "index.js", "scripts": { "build": "flecks build", @@ -18,7 +18,6 @@ "test": "flecks test" }, "files": [ - "server.js", "website" ], "dependencies": { @@ -29,7 +28,7 @@ "@docusaurus/module-type-aliases": "3.0.1", "@docusaurus/preset-classic": "3.0.1", "@docusaurus/types": "3.0.1", - "@flecks/core": "^2.0.3", + "@flecks/core": "^3.0.0", "@mdx-js/react": "^3.0.0", "clsx": "^2.0.0", "comment-parser": "^1.3.0", @@ -40,6 +39,6 @@ "rimraf": "^5.0.5" }, "devDependencies": { - "@flecks/fleck": "^2.0.3" + "@flecks/fleck": "^3.0.0" } } diff --git a/packages/dox/website/docusaurus.config.js b/packages/dox/website/docusaurus.config.js index d4b174e..8647808 100644 --- a/packages/dox/website/docusaurus.config.js +++ b/packages/dox/website/docusaurus.config.js @@ -4,10 +4,9 @@ // There are various equivalent ways to declare your Docusaurus config. // See: https://docusaurus.io/docs/api/docusaurus-config -// For some reason we get a webpack warning if we use import here... -const {configDefaults} = require('@flecks/dox/server'); // eslint-disable-line import/no-extraneous-dependencies +const {configDefaults} = require('@flecks/dox/build/docusaurus'); -export default async function flecksDocusaurus() { +module.exports = async function flecksDocusaurus() { const defaults = configDefaults(); /** @type {import('@docusaurus/types').Config} */ const config = { @@ -18,4 +17,4 @@ export default async function flecksDocusaurus() { baseUrl: '/', }; return config; -} +}; diff --git a/packages/dox/website/pages/index.jsx b/packages/dox/website/pages/index.jsx index 4c693d7..c37fe4c 100644 --- a/packages/dox/website/pages/index.jsx +++ b/packages/dox/website/pages/index.jsx @@ -1,9 +1,9 @@ -import clsx from 'clsx'; -import Link from '@docusaurus/Link'; -import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; -import Layout from '@theme/Layout'; +const clsx = require('clsx'); +const Link = require('@docusaurus/Link'); +const useDocusaurusContext = require('@docusaurus/useDocusaurusContext'); +const Layout = require('@theme/Layout'); -import Heading from '@theme/Heading'; +const Heading = require('@theme/Heading'); import styles from './index.module.css'; function HomepageHeader() { diff --git a/packages/electron/build/flecks.bootstrap.js b/packages/electron/build/flecks.bootstrap.js new file mode 100644 index 0000000..60090cb --- /dev/null +++ b/packages/electron/build/flecks.bootstrap.js @@ -0,0 +1,59 @@ +const {join} = require('path'); + +const {banner} = require('@flecks/core/server'); + +exports.hooks = { + '@flecks/core.build': (target, config) => { + if ('server' === target) { + config.plugins.push( + banner({ + // Bootstrap our `require()` magic. + banner: "require('module').Module._initPaths();", + include: 'index.js', + }), + ); + } + }, + '@flecks/core.config': () => ({ + /** + * Browser window options. + * + * See: https://www.electronjs.org/docs/latest/api/browser-window + */ + browserWindowOptions: {}, + /** + * Install devtools extensions (by default). + * + * If `true`, will install some devtools extensions based on which flecks are enabled. + * + * You can pass an array of Chrome store IDs to install a list of custom extensions. + * + * Extensions will not be installed if `'production' === process.env.NODE_ENV` + */ + installExtensions: true, + /** + * Quit the app when all windows are closed. + */ + quitOnClosed: true, + /** + * The URL to load in electron by default. + * + * Defaults to `http://${flecks.get('@flecks/web.public')}`. + */ + url: undefined, + }), + '@flecks/core.build.alter': (configs) => { + const {server: config} = configs; + if (config) { + const plugin = config.plugins.find(({pluginName}) => pluginName === 'StartServerPlugin'); + // Extremely hackish, c'est la vie. + if (plugin) { + const {exec} = plugin.options; + plugin.options.exec = (compilation) => { + plugin.options.args = [join(config.output.path, compilation.getPath(exec))]; + return join('..', '..', 'node_modules', '.bin', 'electron'); + }; + } + } + }, +}; diff --git a/packages/electron/package.json b/packages/electron/package.json index 3ba2f70..9868c61 100644 --- a/packages/electron/package.json +++ b/packages/electron/package.json @@ -8,7 +8,7 @@ "publishConfig": { "access": "public" }, - "version": "2.0.3", + "version": "3.0.0", "scripts": { "build": "flecks build", "clean": "flecks clean", @@ -20,11 +20,11 @@ "server.js" ], "dependencies": { - "@flecks/core": "^2.0.3", + "@flecks/core": "^3.0.0", "electron": "^18.0.1", "electron-devtools-installer": "^3.2.0" }, "devDependencies": { - "@flecks/fleck": "^2.0.3" + "@flecks/fleck": "^3.0.0" } } diff --git a/packages/electron/src/server/index.js b/packages/electron/src/server/index.js index c5dfa51..d77b55a 100644 --- a/packages/electron/src/server/index.js +++ b/packages/electron/src/server/index.js @@ -1,7 +1,4 @@ -import {join} from 'path'; - import {Flecks} from '@flecks/core'; -import {banner} from '@flecks/core/server'; const electron = __non_webpack_require__('electron'); @@ -13,66 +10,13 @@ let win; async function createWindow(flecks) { const {BrowserWindow} = flecks.electron; - const {browserWindowOptions} = flecks.get('@flecks/electron/server'); - flecks.invoke('@flecks/electron/server.browserWindowOptions.alter', browserWindowOptions) + const {browserWindowOptions} = flecks.get('@flecks/electron'); + flecks.invoke('@flecks/electron/server.browserWindowOptions.alter', browserWindowOptions); win = new BrowserWindow(browserWindowOptions); await flecks.invokeSequentialAsync('@flecks/electron/server.window', win); } export const hooks = { - '@flecks/core.build': (target, config) => { - if ('server' === target) { - config.plugins.push( - banner({ - // Bootstrap our `require()` magic. - banner: "require('module').Module._initPaths();", - include: 'index.js', - }), - ); - } - }, - '@flecks/core.config': () => ({ - /** - * Browser window options. - * - * See: https://www.electronjs.org/docs/latest/api/browser-window - */ - browserWindowOptions: {}, - /** - * Install devtools extensions (by default). - * - * If `true`, will install some devtools extensions based on which flecks are enabled. - * - * You can pass an array of Chrome store IDs to install a list of custom extensions. - * - * Extensions will not be installed if `'production' === process.env.NODE_ENV` - */ - installExtensions: true, - /** - * Quit the app when all windows are closed. - */ - quitOnClosed: true, - /** - * The URL to load in electron by default. - * - * Defaults to `http://${flecks.get('@flecks/web/server.public')}`. - */ - url: undefined, - }), - '@flecks/core.build.alter': (configs) => { - const {server: config} = configs; - if (config) { - const plugin = config.plugins.find(({pluginName}) => pluginName === 'StartServerPlugin'); - // Extremely hackish, c'est la vie. - if (plugin) { - const {exec} = plugin.options; - plugin.options.exec = (compilation) => { - plugin.options.args = [join(config.output.path, compilation.getPath(exec))]; - return join('..', '..', 'node_modules', '.bin', 'electron'); - }; - } - } - }, '@flecks/core.mixin': (Flecks) => ( class FlecksWithElectron extends Flecks { @@ -82,7 +26,7 @@ export const hooks = { ), '@flecks/electron/server.initialize': async (electron, flecks) => { electron.app.on('window-all-closed', () => { - const {quitOnClosed} = flecks.get('@flecks/electron/server'); + const {quitOnClosed} = flecks.get('@flecks/electron'); if (!quitOnClosed) { return; } @@ -101,11 +45,11 @@ export const hooks = { await createWindow(flecks); }, '@flecks/electron/server.window': async (win, flecks) => { - const {public: $$public} = flecks.get('@flecks/web/server'); + const {public: $$public} = flecks.get('@flecks/web'); const { installExtensions, url = `http://${$$public}`, - } = flecks.get('@flecks/electron/server'); + } = flecks.get('@flecks/electron'); if (installExtensions && 'production' !== NODE_ENV) { const { default: installExtension, diff --git a/packages/fleck/src/server/commands.js b/packages/fleck/build/commands.js similarity index 83% rename from packages/fleck/src/server/commands.js rename to packages/fleck/build/commands.js index 8410492..7aae60d 100644 --- a/packages/fleck/src/server/commands.js +++ b/packages/fleck/build/commands.js @@ -1,11 +1,11 @@ -import {stat, unlink} from 'fs/promises'; -import {join} from 'path'; +const {stat, unlink} = require('fs/promises'); +const {join} = require('path'); -import {D} from '@flecks/core'; -import {commands as coreCommands, glob} from '@flecks/core/server'; -import chokidar from 'chokidar'; -import clearModule from 'clear-module'; -import Mocha from 'mocha'; +const {D} = require('@flecks/core'); +const {commands: coreCommands, glob} = require('@flecks/core/server'); +const chokidar = require('chokidar'); +const clearModule = require('clear-module'); +const Mocha = require('mocha'); const debug = D('@flecks/core.commands'); @@ -13,7 +13,7 @@ const { FLECKS_CORE_ROOT = process.cwd(), } = process.env; -export default (program, flecks) => { +module.exports = (program, flecks) => { const commands = {}; commands.test = { options: [ @@ -27,7 +27,7 @@ export default (program, flecks) => { watch, } = opts; const {build} = coreCommands(program, flecks); - const child = build.action(undefined, opts); + const child = await build.action(undefined, opts); const testPaths = await glob(join(FLECKS_CORE_ROOT, 'test/*.js')); if (0 === testPaths.length) { // eslint-disable-next-line no-console @@ -71,6 +71,7 @@ export default (program, flecks) => { }); }); }; + require('@flecks/core/build/stub')(flecks.stubs); if (!watch) { await new Promise((resolve, reject) => { child.on('exit', (code) => { diff --git a/packages/fleck/src/server/build/fleck.webpack.config.js b/packages/fleck/build/fleck.webpack.config.js similarity index 62% rename from packages/fleck/src/server/build/fleck.webpack.config.js rename to packages/fleck/build/fleck.webpack.config.js index 94c46ec..b47abf5 100644 --- a/packages/fleck/src/server/build/fleck.webpack.config.js +++ b/packages/fleck/build/fleck.webpack.config.js @@ -1,10 +1,10 @@ -const flecksConfigFn = require('@flecks/core/server/build/webpack.config'); +const flecksConfigFn = require('@flecks/core/build/fleck.webpack.config'); const ProcessAssets = require('./process-assets'); module.exports = async (env, argv, flecks) => { const config = await flecksConfigFn(env, argv, flecks); config.plugins.push(new ProcessAssets(flecks)); - config.stats = flecks.get('@flecks/fleck/server.stats'); + config.stats = flecks.get('@flecks/fleck.stats'); return config; }; diff --git a/packages/fleck/src/server/index.js b/packages/fleck/build/flecks.bootstrap.js similarity index 68% rename from packages/fleck/src/server/index.js rename to packages/fleck/build/flecks.bootstrap.js index 8e46d1f..aa1903a 100644 --- a/packages/fleck/src/server/index.js +++ b/packages/fleck/build/flecks.bootstrap.js @@ -1,14 +1,14 @@ -import {join} from 'path'; +const {join} = require('path'); -import {glob} from '@flecks/core/server'; +const {glob} = require('@flecks/core/server'); -import commands from './commands'; +const commands = require('./commands'); const { FLECKS_CORE_ROOT = process.cwd(), } = process.env; -export const hooks = { +exports.hooks = { '@flecks/core.commands': commands, '@flecks/core.config': () => ({ /** @@ -20,20 +20,24 @@ export const hooks = { }, }), '@flecks/core.targets': () => ['fleck'], - '@flecks/fleck/server.processAssets': async (assets, compilation, flecks) => { + '@flecks/fleck.processAssets': async (assets, compilation, flecks) => { const {RawSource} = compilation.compiler.webpack.sources; const packageJson = assets['package.json']; const json = JSON.parse(packageJson.source().toString()); const {files} = json; // Add defaults. - files.push('build', 'src'); + files.push('build'); + // Add source if it exists. + if ((await glob(join(FLECKS_CORE_ROOT, 'src/**/*.js'))).length > 0) { + files.push('src'); + } // Add tests if they exist. - const testFiles = await glob(join(FLECKS_CORE_ROOT, 'test/*.js')); + const testFiles = await glob(join(FLECKS_CORE_ROOT, 'test/**/*.js')); if (testFiles.length > 0) { files.push('test', 'test.js'); } // Let others have a say. - await flecks.invokeSequentialAsync('@flecks/fleck/server.packageJson', json, compilation); + await flecks.invokeSequentialAsync('@flecks/fleck.packageJson', json, compilation); // Add any sourcemaps. json.files = json.files .map((filename) => { diff --git a/packages/fleck/build/flecks.yml b/packages/fleck/build/flecks.yml index d96334c..fdfa02f 100644 --- a/packages/fleck/build/flecks.yml +++ b/packages/fleck/build/flecks.yml @@ -1,2 +1,2 @@ '@flecks/core': {} -'@flecks/fleck:./src': {} +'@flecks/fleck:.': {} diff --git a/packages/fleck/src/server/build/process-assets.js b/packages/fleck/build/process-assets.js similarity index 69% rename from packages/fleck/src/server/build/process-assets.js rename to packages/fleck/build/process-assets.js index 1bf6913..7e3e284 100644 --- a/packages/fleck/src/server/build/process-assets.js +++ b/packages/fleck/build/process-assets.js @@ -5,15 +5,15 @@ class ProcessAssets { } apply(compiler) { - compiler.hooks.thisCompilation.tap('@flecks/fleck/server/build/process-assets', (compilation) => { + compiler.hooks.thisCompilation.tap('@flecks/fleck/build/process-assets', (compilation) => { compilation.hooks.processAssets.tapAsync( { - name: '@flecks/fleck/server/build/process-assets', + name: '@flecks/fleck/build/process-assets', stage: compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_REPORT, }, async (assets, callback) => { await this.flecks.invokeSequentialAsync( - '@flecks/fleck/server.processAssets', + '@flecks/fleck.processAssets', assets, compilation, ); diff --git a/packages/fleck/package.json b/packages/fleck/package.json index c9298b8..b826759 100644 --- a/packages/fleck/package.json +++ b/packages/fleck/package.json @@ -8,7 +8,7 @@ "publishConfig": { "access": "public" }, - "version": "2.0.3", + "version": "3.0.0", "author": "cha0s", "license": "MIT", "scripts": { @@ -17,12 +17,9 @@ "postversion": "cp package.json dist", "test": "flecks test" }, - "files": [ - "server/build/fleck.webpack.config.js", - "server.js" - ], + "files": [], "dependencies": { - "@flecks/core": "^2.0.3", + "@flecks/core": "^3.0.0", "babel-merge": "^3.0.0", "chokidar": "^3.5.3", "clear-module": "^4.1.2", diff --git a/packages/governor/package.json b/packages/governor/package.json index ddb5032..d2e9987 100644 --- a/packages/governor/package.json +++ b/packages/governor/package.json @@ -8,7 +8,7 @@ "publishConfig": { "access": "public" }, - "version": "2.0.3", + "version": "3.0.0", "main": "index.js", "scripts": { "build": "flecks build", @@ -22,12 +22,12 @@ "server.js" ], "dependencies": { - "@flecks/core": "^2.0.3", - "@flecks/db": "^2.0.3", + "@flecks/core": "^3.0.0", + "@flecks/db": "^3.0.0", "rate-limiter-flexible": "^2.1.13", "redis": "^3.1.2" }, "devDependencies": { - "@flecks/fleck": "^2.0.3" + "@flecks/fleck": "^3.0.0" } } diff --git a/packages/passport-local-react/build/flecks.bootstrap.js b/packages/passport-local-react/build/flecks.bootstrap.js new file mode 100644 index 0000000..4b9e854 --- /dev/null +++ b/packages/passport-local-react/build/flecks.bootstrap.js @@ -0,0 +1 @@ +exports.dependencies = ['@flecks/passport-local', '@flecks/passport-react']; diff --git a/packages/passport-local-react/build/flecks.config.js b/packages/passport-local-react/build/flecks.config.js deleted file mode 100644 index cc887d1..0000000 --- a/packages/passport-local-react/build/flecks.config.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = { - dependencies: ['@flecks/passport-local', '@flecks/passport-react'], -}; diff --git a/packages/passport-local-react/package.json b/packages/passport-local-react/package.json index 8da0f9c..787d0fe 100644 --- a/packages/passport-local-react/package.json +++ b/packages/passport-local-react/package.json @@ -8,7 +8,7 @@ "publishConfig": { "access": "public" }, - "version": "2.0.3", + "version": "3.0.0", "scripts": { "build": "flecks build", "clean": "flecks clean", @@ -20,11 +20,11 @@ "index.js" ], "dependencies": { - "@flecks/core": "^2.0.3", - "@flecks/passport-local": "^2.0.3", - "@flecks/react": "^2.0.3" + "@flecks/core": "^3.0.0", + "@flecks/passport-local": "^3.0.0", + "@flecks/react": "^3.0.0" }, "devDependencies": { - "@flecks/fleck": "^2.0.3" + "@flecks/fleck": "^3.0.0" } } diff --git a/packages/passport-local/build/flecks.bootstrap.js b/packages/passport-local/build/flecks.bootstrap.js new file mode 100644 index 0000000..6edc0a4 --- /dev/null +++ b/packages/passport-local/build/flecks.bootstrap.js @@ -0,0 +1 @@ +exports.dependencies = ['@flecks/passport']; diff --git a/packages/passport-local/build/flecks.config.js b/packages/passport-local/build/flecks.config.js deleted file mode 100644 index 8472d0c..0000000 --- a/packages/passport-local/build/flecks.config.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = { - dependencies: ['@flecks/passport'], -}; diff --git a/packages/passport-local/package.json b/packages/passport-local/package.json index 7c9dad5..c7eb0c2 100644 --- a/packages/passport-local/package.json +++ b/packages/passport-local/package.json @@ -8,7 +8,7 @@ "publishConfig": { "access": "public" }, - "version": "2.0.3", + "version": "3.0.0", "scripts": { "build": "flecks build", "clean": "flecks clean", @@ -20,12 +20,12 @@ "server.js" ], "dependencies": { - "@flecks/core": "^2.0.3", - "@flecks/passport": "^2.0.3", + "@flecks/core": "^3.0.0", + "@flecks/passport": "^3.0.0", "bcrypt": "^5.1.1", "passport-local": "^1.0.0" }, "devDependencies": { - "@flecks/fleck": "^2.0.3" + "@flecks/fleck": "^3.0.0" } } diff --git a/packages/passport-react/build/flecks.bootstrap.js b/packages/passport-react/build/flecks.bootstrap.js new file mode 100644 index 0000000..926fcb7 --- /dev/null +++ b/packages/passport-react/build/flecks.bootstrap.js @@ -0,0 +1 @@ +exports.dependencies = ['@flecks/passport', '@flecks/react']; diff --git a/packages/passport-react/build/flecks.config.js b/packages/passport-react/build/flecks.config.js deleted file mode 100644 index 9f235fb..0000000 --- a/packages/passport-react/build/flecks.config.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = { - dependencies: ['@flecks/passport', '@flecks/react'], -}; diff --git a/packages/passport-react/build/flecks.yml b/packages/passport-react/build/flecks.yml deleted file mode 100644 index 6c8bf17..0000000 --- a/packages/passport-react/build/flecks.yml +++ /dev/null @@ -1,3 +0,0 @@ -'@flecks/core': {} -'@flecks/fleck': {} -'@flecks/react': {} diff --git a/packages/passport-react/package.json b/packages/passport-react/package.json index f4ee84e..c823e4f 100644 --- a/packages/passport-react/package.json +++ b/packages/passport-react/package.json @@ -8,7 +8,7 @@ "publishConfig": { "access": "public" }, - "version": "2.0.3", + "version": "3.0.0", "scripts": { "build": "flecks build", "clean": "flecks clean", @@ -20,12 +20,13 @@ "index.js" ], "dependencies": { - "@flecks/core": "^2.0.3", - "@flecks/passport": "^2.0.3", - "@flecks/react": "^2.0.3", - "@flecks/redux": "^2.0.3" + "@flecks/core": "^3.0.0", + "@flecks/passport": "^3.0.0", + "@flecks/react": "^3.0.0", + "@flecks/redux": "^3.0.0", + "@flecks/web": "^3.0.0" }, "devDependencies": { - "@flecks/fleck": "^2.0.3" + "@flecks/fleck": "^3.0.0" } } diff --git a/packages/passport/build/flecks.bootstrap.js b/packages/passport/build/flecks.bootstrap.js new file mode 100644 index 0000000..d7d351e --- /dev/null +++ b/packages/passport/build/flecks.bootstrap.js @@ -0,0 +1 @@ +exports.dependencies = ['@flecks/db', '@flecks/session']; diff --git a/packages/passport/build/flecks.config.js b/packages/passport/build/flecks.config.js deleted file mode 100644 index 00c1159..0000000 --- a/packages/passport/build/flecks.config.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = { - dependencies: ['@flecks/db', '@flecks/session'], -}; diff --git a/packages/passport/package.json b/packages/passport/package.json index ca88df8..5885d81 100644 --- a/packages/passport/package.json +++ b/packages/passport/package.json @@ -8,7 +8,7 @@ "publishConfig": { "access": "public" }, - "version": "2.0.3", + "version": "3.0.0", "scripts": { "build": "flecks build", "clean": "flecks clean", @@ -21,13 +21,13 @@ "server.js" ], "dependencies": { - "@flecks/core": "^2.0.3", - "@flecks/db": "^2.0.3", - "@flecks/redux": "^2.0.3", - "@flecks/session": "^2.0.3", + "@flecks/core": "^3.0.0", + "@flecks/db": "^3.0.0", + "@flecks/redux": "^3.0.0", + "@flecks/session": "^3.0.0", "passport": "^0.7.0" }, "devDependencies": { - "@flecks/fleck": "^2.0.3" + "@flecks/fleck": "^3.0.0" } } diff --git a/packages/passport/src/server/index.js b/packages/passport/src/server/index.js index 31e2d4e..2a41d3c 100644 --- a/packages/passport/src/server/index.js +++ b/packages/passport/src/server/index.js @@ -1,6 +1,5 @@ import {D, Flecks} from '@flecks/core'; import passport from 'passport'; -import LogOps from 'passport/lib/http/request'; const debug = D('@flecks/passport'); const debugSilly = debug.extend('silly'); @@ -88,7 +87,7 @@ export const hooks = { }), '@flecks/socket/server.request.socket': Flecks.priority( (flecks) => (socket, next) => { - const {req} = socket; + const {handshake: req} = socket; flecks.passport.initialize(req, undefined, () => { flecks.passport.session(req, undefined, async () => { if (!req.user) { @@ -99,12 +98,6 @@ export const hooks = { else { debugSilly('socket user ID: %s', req.user.id); } - req.login = LogOps.logIn; - req.logIn = LogOps.logIn; - req.logout = LogOps.logOut; - req.logOut = LogOps.logOut; - req.isAuthenticated = LogOps.isAuthenticated; - req.isUnauthenticated = LogOps.isUnauthenticated; await socket.join(`/u/${req.user.id}`); next(); }); diff --git a/packages/react/build/flecks.bootstrap.js b/packages/react/build/flecks.bootstrap.js new file mode 100644 index 0000000..9aca97a --- /dev/null +++ b/packages/react/build/flecks.bootstrap.js @@ -0,0 +1,29 @@ +const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin'); + +const plugins = []; + +const { + FLECKS_CORE_IS_PRODUCTION, +} = process.env; + +if ('true' !== FLECKS_CORE_IS_PRODUCTION) { + plugins.push('react-refresh/babel'); +} + +exports.dependencies = ['@flecks/web']; + +exports.hooks = { + '@flecks/core.babel': () => ({ + plugins, + presets: [ + '@babel/preset-react', + ], + }), + '@flecks/core.build': (target, config, env, argv) => { + const isProduction = 'production' === argv.mode; + if (!isProduction) { + config.plugins.push(new ReactRefreshWebpackPlugin()); + } + }, + '@flecks/core.exts': () => ['.jsx'], +}; diff --git a/packages/react/build/flecks.config.js b/packages/react/build/flecks.config.js deleted file mode 100644 index 4312dc9..0000000 --- a/packages/react/build/flecks.config.js +++ /dev/null @@ -1,20 +0,0 @@ -const plugins = []; - -const { - FLECKS_CORE_IS_PRODUCTION, -} = process.env; - -if ('true' !== FLECKS_CORE_IS_PRODUCTION) { - plugins.push('react-refresh/babel'); -} - -module.exports = { - babel: { - plugins, - presets: [ - '@babel/preset-react', - ], - }, - dependencies: ['@flecks/web'], - exts: ['.jsx'], -}; diff --git a/packages/react/package.json b/packages/react/package.json index ee459ca..bd1663f 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -8,7 +8,7 @@ "publishConfig": { "access": "public" }, - "version": "2.0.3", + "version": "3.0.0", "main": "index.js", "scripts": { "build": "flecks build", @@ -32,8 +32,8 @@ ], "dependencies": { "@babel/preset-react": "^7.23.3", - "@flecks/core": "^2.0.3", - "@flecks/web": "^2.0.3", + "@flecks/core": "^3.0.0", + "@flecks/web": "^3.0.0", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.11", "babel-merge": "^3.0.0", "classnames": "^2.3.1", @@ -43,11 +43,12 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-refresh": "^0.14.0", - "react-router-dom": "6.20.0", + "react-router": "6.2.1", + "react-router-dom": "6.2.1", "react-tabs": "^6.0.2", "redux-first-history": "5.1.1" }, "devDependencies": { - "@flecks/fleck": "^2.0.3" + "@flecks/fleck": "^3.0.0" } } diff --git a/packages/react/src/client.js b/packages/react/src/client.js index eef1176..e8e399c 100644 --- a/packages/react/src/client.js +++ b/packages/react/src/client.js @@ -13,7 +13,7 @@ export {FlecksContext}; export const hooks = { '@flecks/web/client.up': async (flecks) => { const {ssr} = flecks.get('@flecks/react'); - const {appMountId} = flecks.get('@flecks/web/client'); + const {appMountId} = flecks.get('@flecks/web'); const container = window.document.getElementById(appMountId); debug('%sing...', ssr ? 'hydrat' : 'render'); const RootComponent = React.createElement( diff --git a/packages/react/src/index.js b/packages/react/src/index.js index 47caa83..221bb11 100644 --- a/packages/react/src/index.js +++ b/packages/react/src/index.js @@ -13,6 +13,10 @@ export {default as usePrevious} from './hooks/use-previous'; export const hooks = { '@flecks/core.config': () => ({ + /** + * React providers. + */ + providers: ['...'], /** * Whether to enable server-side rendering. */ diff --git a/packages/react/src/router/client.js b/packages/react/src/router/client.js index 54f7a84..cf4e7c7 100644 --- a/packages/react/src/router/client.js +++ b/packages/react/src/router/client.js @@ -1,7 +1,7 @@ // eslint-disable-next-line import/no-extraneous-dependencies import {createReduxHistory, history} from '@flecks/react/router/context'; import {unstable_HistoryRouter as HistoryRouter} from 'react-router-dom'; -import {HistoryRouter as ReduxHistoryRouter} from 'redux-first-history/rr6'; +import {HistoryRouter as ReduxHistoryRouter} from './history-router'; export const hooks = { '@flecks/react.providers': (req, flecks) => ( diff --git a/packages/react/src/router/history-router.js b/packages/react/src/router/history-router.js new file mode 100644 index 0000000..24cace6 --- /dev/null +++ b/packages/react/src/router/history-router.js @@ -0,0 +1,24 @@ +/* eslint-disable react/prop-types */ + +// redux-first-router/rr6 doesn't honor hoisting. + +const {createElement, useLayoutEffect, useState} = require('react'); +const {Router} = require('react-router'); + +exports.HistoryRouter = function HistoryRouter({basename, children, history}) { + const [state, setState] = useState({ + action: history.action, + location: history.location, + }); + useLayoutEffect(() => { + history.listen(setState); + }, [history]); + // eslint-disable-next-line react/no-children-prop + return createElement(Router, { + basename, + children, + location: state.location, + navigationType: state.action, + navigator: history, + }); +} diff --git a/packages/react/src/server.js b/packages/react/src/server.js index 39ccfdc..a91b2f0 100644 --- a/packages/react/src/server.js +++ b/packages/react/src/server.js @@ -1,15 +1,8 @@ import {Flecks} from '@flecks/core'; -import ReactRefreshWebpackPlugin from '@pmmmwh/react-refresh-webpack-plugin'; import ssr from './ssr'; export const hooks = { - '@flecks/core.build': (target, config, env, argv) => { - const isProduction = 'production' === argv.mode; - if (!isProduction) { - config.plugins.push(new ReactRefreshWebpackPlugin()); - } - }, '@flecks/web/server.stream.html': Flecks.priority( (stream, req, flecks) => ( flecks.get('@flecks/react.ssr') ? ssr(stream, req, flecks) : stream diff --git a/packages/react/src/ssr.js b/packages/react/src/ssr.js index e10a97d..0592eae 100644 --- a/packages/react/src/ssr.js +++ b/packages/react/src/ssr.js @@ -1,3 +1,5 @@ +import {Readable} from 'stream'; + import {WritableStream} from 'htmlparser2/lib/WritableStream'; import React from 'react'; import {renderToPipeableStream} from 'react-dom/server'; @@ -13,7 +15,7 @@ export default async (stream, req, flecks) => { icon, meta, title, - } = flecks.get('@flecks/web/server'); + } = flecks.get('@flecks/web'); // Extract assets. const css = []; let hasVendor = false; @@ -56,6 +58,10 @@ export default async (stream, req, flecks) => { } }, }); + const chunks = []; + stream.on('data', (chunk) => { + chunks.push(chunk); + }); await new Promise((resolve, reject) => { const piped = stream.pipe(parserStream); piped.on('error', reject); @@ -85,11 +91,15 @@ export default async (stream, req, flecks) => { { bootstrapScripts: js, bootstrapScriptContent: inline, - onError() { - resolve(stream); + onError(error) { + // eslint-disable-next-line no-console + console.error('SSR error:', error); + resolve(Readable.from(Buffer.concat(chunks))); }, - onShellError() { - resolve(stream); + onShellError(error) { + // eslint-disable-next-line no-console + console.error('SSR shell error:', error); + resolve(Readable.from(Buffer.concat(chunks))); }, onShellReady() { resolve(rendered); diff --git a/packages/redis/package.json b/packages/redis/package.json index d61c145..29e79ac 100644 --- a/packages/redis/package.json +++ b/packages/redis/package.json @@ -8,7 +8,7 @@ "publishConfig": { "access": "public" }, - "version": "2.0.3", + "version": "3.0.0", "scripts": { "build": "flecks build", "clean": "flecks clean", @@ -21,13 +21,13 @@ "server.js" ], "dependencies": { - "@flecks/core": "^2.0.3", + "@flecks/core": "^3.0.0", "@socket.io/redis-adapter": "7.1.0", "connect-redis": "^5.0.0", "express-session": "^1.17.1", "redis": "4.0.3" }, "devDependencies": { - "@flecks/fleck": "^2.0.3" + "@flecks/fleck": "^3.0.0" } } diff --git a/packages/redux/package.json b/packages/redux/package.json index c2c02f4..2d7f7b8 100644 --- a/packages/redux/package.json +++ b/packages/redux/package.json @@ -8,7 +8,7 @@ "publishConfig": { "access": "public" }, - "version": "2.0.3", + "version": "3.0.0", "main": "index.js", "scripts": { "build": "flecks build", @@ -23,7 +23,7 @@ "server.js" ], "dependencies": { - "@flecks/core": "^2.0.3", + "@flecks/core": "^3.0.0", "@reduxjs/toolkit": "^1.5.0", "debug": "^4.3.3", "lodash.throttle": "^4.1.1", @@ -31,6 +31,6 @@ "reduce-reducers": "^1.0.4" }, "devDependencies": { - "@flecks/fleck": "^2.0.3" + "@flecks/fleck": "^3.0.0" } } diff --git a/packages/repl/src/commands.js b/packages/repl/build/commands.js similarity index 84% rename from packages/repl/src/commands.js rename to packages/repl/build/commands.js index f60983f..528375f 100644 --- a/packages/repl/src/commands.js +++ b/packages/repl/build/commands.js @@ -1,14 +1,14 @@ -import {spawn} from 'child_process'; -import {readdir} from 'fs/promises'; -import {tmpdir} from 'os'; -import {join} from 'path'; +const {spawn} = require('child_process'); +const {readdir} = require('fs/promises'); +const {tmpdir} = require('os'); +const {join} = require('path'); -import {D} from '@flecks/core'; -import commandExists from 'command-exists'; +const {D} = require('@flecks/core'); +const commandExists = require('command-exists'); const debug = D('@flecks/repl/commands'); -export default (program, flecks) => { +module.exports = (program, flecks) => { const commands = {}; commands.repl = { options: [ diff --git a/packages/repl/build/flecks.bootstrap.js b/packages/repl/build/flecks.bootstrap.js new file mode 100644 index 0000000..e5e20d0 --- /dev/null +++ b/packages/repl/build/flecks.bootstrap.js @@ -0,0 +1,5 @@ +const commands = require('./commands'); + +exports.hooks = { + '@flecks/core.commands': commands, +}; diff --git a/packages/repl/package.json b/packages/repl/package.json index f79bb5d..c3f5351 100644 --- a/packages/repl/package.json +++ b/packages/repl/package.json @@ -8,7 +8,7 @@ "publishConfig": { "access": "public" }, - "version": "2.0.3", + "version": "3.0.0", "scripts": { "build": "flecks build", "clean": "flecks clean", @@ -20,11 +20,11 @@ "server.js" ], "dependencies": { - "@flecks/core": "^2.0.3", + "@flecks/core": "^3.0.0", "command-exists": "^1.2.9", "debug": "4.3.1" }, "devDependencies": { - "@flecks/fleck": "^2.0.3" + "@flecks/fleck": "^3.0.0" } } diff --git a/packages/repl/src/server.js b/packages/repl/src/server.js index b0a4a20..ac89b05 100644 --- a/packages/repl/src/server.js +++ b/packages/repl/src/server.js @@ -1,8 +1,6 @@ -import commands from './commands'; import {createReplServer} from './repl'; export const hooks = { - '@flecks/core.commands': commands, '@flecks/core.mixin': (Flecks) => ( class FlecksWithRepl extends Flecks { diff --git a/packages/server/src/index.js b/packages/server/build/flecks.bootstrap.js similarity index 51% rename from packages/server/src/index.js rename to packages/server/build/flecks.bootstrap.js index 17ec408..f940cce 100644 --- a/packages/server/src/index.js +++ b/packages/server/build/flecks.bootstrap.js @@ -1,4 +1,10 @@ -export const hooks = { +exports.hooks = { + '@flecks/core.build.config': () => [ + /** + * Server build configuration. See: https://webpack.js.org/configuration/ + */ + 'server.webpack.config.js', + ], '@flecks/core.config': () => ({ /** * Whether HMR is enabled. @@ -20,4 +26,11 @@ export const hooks = { errorDetails: true, }, }), + '@flecks/core.targets': () => ['server'], + '@flecks/core.targets.alter': (targets) => { + // Don't build if there's a fleck target. + if (targets.has('fleck')) { + targets.delete('server'); + } + }, }; diff --git a/packages/server/src/server/build/runtime.js b/packages/server/build/runtime.js similarity index 76% rename from packages/server/src/server/build/runtime.js rename to packages/server/build/runtime.js index 2946922..10cb701 100644 --- a/packages/server/src/server/build/runtime.js +++ b/packages/server/build/runtime.js @@ -1,21 +1,26 @@ -const {realpath} = require('fs/promises'); -const {join} = require('path'); - -const {externals, require: R} = require('@flecks/core/server'); +const {externals} = require('@flecks/core/server'); module.exports = async (config, env, argv, flecks) => { - const runtime = await realpath(R.resolve(join(flecks.resolve('@flecks/server'), 'runtime'))); - const {resolver} = flecks; + const runtime = await flecks.resolver.resolve('@flecks/server/runtime'); // Inject flecks configuration. - const paths = Object.keys(resolver); + const paths = Object.keys(flecks.flecks); + const resolvedPaths = (await Promise.all( + paths.map(async (path) => [path, await flecks.resolver.resolve(path)]), + )) + .filter(([, resolved]) => resolved) + .map(([path]) => path); const source = [ "process.env.FLECKS_CORE_BUILD_TARGET = 'server';", 'module.exports = (async () => ({', ` config: ${JSON.stringify(flecks.config)},`, ' loadFlecks: async () => Object.fromEntries(await Promise.all([', - paths.map((path) => ` ['${path}', import('${path}')]`).join(',\n'), + ...resolvedPaths.map((path) => ( + ` ['${path}', import('${path}')],` + )), ' ].map(async ([path, M]) => [path, await M]))),', - " platforms: ['server']", + ` stubs: ${JSON.stringify(flecks.stubs.map((stub) => ( + stub instanceof RegExp ? [stub.source, stub.flags] : stub + )))}`, '}))();', ]; // HMR. @@ -40,7 +45,7 @@ module.exports = async (config, env, argv, flecks) => { source.push(' }'); source.push(' });'); // Hooks for each fleck. - paths.forEach((path) => { + resolvedPaths.forEach((path) => { source.push(` module.hot.accept('${path}', async () => {`); source.push(` global.flecks.refresh('${path}', require('${path}'));`); source.push(` global.flecks.invoke('@flecks/core.hmr', '${path}');`); @@ -70,7 +75,7 @@ module.exports = async (config, env, argv, flecks) => { const nodeExternalsConfig = { allowlist, }; - flecks.runtimeCompiler(flecks.resolver, 'server', config, nodeExternalsConfig); + flecks.runtimeCompiler('server', config, nodeExternalsConfig); // Rewrite to signals for HMR. if ('production' !== argv.mode) { allowlist.push(/^webpack/); diff --git a/packages/server/src/server/build/server.webpack.config.js b/packages/server/build/server.webpack.config.js similarity index 100% rename from packages/server/src/server/build/server.webpack.config.js rename to packages/server/build/server.webpack.config.js diff --git a/packages/server/src/server/build/start.js b/packages/server/build/start.js similarity index 100% rename from packages/server/src/server/build/start.js rename to packages/server/build/start.js diff --git a/packages/server/package.json b/packages/server/package.json index f6485c8..9723d4f 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -8,7 +8,7 @@ "publishConfig": { "access": "public" }, - "version": "2.0.3", + "version": "3.0.0", "main": "index.js", "license": "MIT", "scripts": { @@ -20,16 +20,12 @@ }, "files": [ "entry.js", - "index.js", - "runtime.js", - "server/build/server.webpack.config.js", - "server.js" + "runtime.js" ], "dependencies": { - "@flecks/core": "^2.0.3", - "debug": "^4.3.3" + "@flecks/core": "^3.0.0" }, "devDependencies": { - "@flecks/fleck": "^2.0.3" + "@flecks/fleck": "^3.0.0" } } diff --git a/packages/server/src/entry.js b/packages/server/src/entry.js index 5dc54aa..845e525 100644 --- a/packages/server/src/entry.js +++ b/packages/server/src/entry.js @@ -3,12 +3,12 @@ import {tmpdir} from 'os'; import {join} from 'path'; import {D} from '@flecks/core'; -import {Flecks} from '@flecks/core/server'; +import Server from '@flecks/core/build/server'; const {version} = require('../package.json'); (async () => { - const {config, loadFlecks, platforms} = await __non_webpack_require__('@flecks/server/runtime'); + const {config, loadFlecks, stubs} = await __non_webpack_require__('@flecks/server/runtime'); // eslint-disable-next-line no-console console.log(`flecks server v${version}`); try { @@ -21,17 +21,12 @@ const {version} = require('../package.json'); } const debug = D('@flecks/server/entry'); debug('starting server...'); - // Make resolver. - // Flecks mixins. - const {flecksConfig, resolver} = Flecks.makeResolverAndLoadRcs(Object.keys(config)); - Flecks.installCompilers(flecksConfig, resolver); - global.flecks = Flecks.from({ - config, - flecks: await loadFlecks(), - flecksConfig, - platforms, - resolver, - }); + 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); + } + global.flecks = await Server.from({config, flecks: await loadFlecks()}); try { await Promise.all(global.flecks.invokeFlat('@flecks/core.starting')); await global.flecks.invokeSequentialAsync('@flecks/server.up'); diff --git a/packages/server/src/server/index.js b/packages/server/src/server/index.js deleted file mode 100644 index ee501fb..0000000 --- a/packages/server/src/server/index.js +++ /dev/null @@ -1,3 +0,0 @@ -export const hooks = { - '@flecks/core.targets': () => ['server'], -}; diff --git a/packages/session/package.json b/packages/session/package.json index 501e7dc..eee6a99 100644 --- a/packages/session/package.json +++ b/packages/session/package.json @@ -8,7 +8,7 @@ "publishConfig": { "access": "public" }, - "version": "2.0.3", + "version": "3.0.0", "scripts": { "build": "flecks build", "clean": "flecks clean", @@ -20,11 +20,11 @@ "server.js" ], "dependencies": { - "@flecks/core": "^2.0.3", + "@flecks/core": "^3.0.0", "express": "^4.18.2", "express-session": "^1.17.3" }, "devDependencies": { - "@flecks/fleck": "^2.0.3" + "@flecks/fleck": "^3.0.0" } } diff --git a/packages/socket/package.json b/packages/socket/package.json index 18dc406..da17bd2 100644 --- a/packages/socket/package.json +++ b/packages/socket/package.json @@ -8,7 +8,7 @@ "publishConfig": { "access": "public" }, - "version": "2.0.3", + "version": "3.0.0", "main": "index.js", "scripts": { "build": "flecks build", @@ -23,8 +23,8 @@ "server.js" ], "dependencies": { - "@flecks/core": "^2.0.3", - "@flecks/react": "^2.0.3", + "@flecks/core": "^3.0.0", + "@flecks/react": "^3.0.0", "msgpack-lite": "^0.1.26", "proxy-addr": "^2.0.6", "schemapack": "^1.4.2", @@ -32,6 +32,6 @@ "socket.io-client": "^4.1.2" }, "devDependencies": { - "@flecks/fleck": "^2.0.3" + "@flecks/fleck": "^3.0.0" } } diff --git a/packages/web/build/fleck.webpack.config.js b/packages/web/build/fleck.webpack.config.js index f26db54..79b80bb 100644 --- a/packages/web/build/fleck.webpack.config.js +++ b/packages/web/build/fleck.webpack.config.js @@ -1,6 +1,6 @@ const {copy, externals} = require('@flecks/core/server'); // eslint-disable-next-line import/no-extraneous-dependencies -const configFn = require('@flecks/fleck/server/build/fleck.webpack.config'); +const configFn = require('@flecks/fleck/build/fleck.webpack.config'); module.exports = async (env, argv, flecks) => { // eslint-disable-next-line import/no-extraneous-dependencies, global-require @@ -18,10 +18,6 @@ module.exports = async (env, argv, flecks) => { { from: 'src/server/build/entry.js', to: 'server/build/entry.js', - }, - { - from: 'src/server/build/template.ejs', - to: 'server/build/template.ejs', info: { minimized: true }, }, { diff --git a/packages/web/build/flecks.bootstrap.js b/packages/web/build/flecks.bootstrap.js new file mode 100644 index 0000000..32deea4 --- /dev/null +++ b/packages/web/build/flecks.bootstrap.js @@ -0,0 +1,309 @@ +const {stat, unlink} = require('fs/promises'); +const {join} = require('path'); + +const {regexFromExtensions, spawnWith} = require('@flecks/core/server'); +const MiniCssExtractPlugin = require('mini-css-extract-plugin'); + +const { + FLECKS_CORE_ROOT = process.cwd(), +} = process.env; + +exports.hooks = { + '@flecks/core.build': async (target, config, env, argv, flecks) => { + const isProduction = 'production' === argv.mode; + let finalLoader; + switch (target) { + case 'fleck': { + finalLoader = {loader: MiniCssExtractPlugin.loader}; + config.plugins.push(new MiniCssExtractPlugin({filename: 'assets/[name].css'})); + break; + } + case 'server': { + finalLoader = {loader: MiniCssExtractPlugin.loader, options: {emit: false}}; + config.plugins.push(new MiniCssExtractPlugin({filename: 'assets/[name].css'})); + break; + } + case 'web': { + finalLoader = {loader: MiniCssExtractPlugin.loader}; + config.plugins.push(new MiniCssExtractPlugin({filename: 'assets/[name].css'})); + break; + } + default: break; + } + const buildOneOf = (test, loaders, cssOptions = {}) => ({ + test, + use: [ + finalLoader, + { + loader: 'css-loader', + options: { + ...cssOptions, + importLoaders: loaders.length, + }, + }, + ...loaders, + 'source-map-loader', + ], + }); + const stylesWithModulesRule = (extensions, loaders) => ({ + oneOf: [ + // `.module.*` must match first. + buildOneOf( + regexFromExtensions(extensions.map((ext) => `module${ext}`)), + loaders, + { + modules: { + localIdentName: isProduction + ? '[hash:base64:5]' + : '[path][name]__[local]', + }, + }, + ), + buildOneOf( + regexFromExtensions(extensions), + loaders, + ), + ], + }); + const postcss = { + loader: 'postcss-loader', + options: { + postcssOptions: { + config: await flecks.resolveBuildConfig('postcss.config.js'), + }, + }, + }; + // Originally separated because Sass can't handle incoming source maps, but probably more + // performant with 3rd-party CSS anyway. + config.module.rules.push(stylesWithModulesRule(['.css'], [postcss])); + config.module.rules.push(stylesWithModulesRule(['.sass', '.scss'], [postcss, 'sass-loader'])); + // Fonts. + if (isProduction) { + config.module.rules.push({ + generator: { + filename: 'assets/[hash][ext][query]', + }, + test: /\.(eot|ttf|woff|woff2)(\?v=\d+\.\d+\.\d+)?$/, + type: 'asset', + }); + } + else { + config.module.rules.push({ + test: /\.(eot|ttf|woff|woff2)(\?v=\d+\.\d+\.\d+)?$/, + type: 'asset/inline', + }); + } + // Images. + config.module.rules.push({ + generator: { + filename: 'assets/[hash][ext][query]', + }, + test: /\.(ico|png|jpg|jpeg|gif|svg|webp)(\?v=\d+\.\d+\.\d+)?$/, + type: 'asset', + }); + }, + '@flecks/core.build.alter': async (configs, env, argv, flecks) => { + const isProduction = 'production' === argv.mode; + // Only build vendor in dev. + if (configs['web-vendor']) { + if (isProduction) { + delete configs['web-vendor']; + } + // Only build if something actually changed. + const dll = flecks.get('@flecks/web.dll'); + if (dll.length > 0) { + const manifest = join( + FLECKS_CORE_ROOT, + 'node_modules', + '.cache', + '@flecks', + 'web', + 'vendor', + 'manifest.json', + ); + let timestamp = 0; + try { + const stats = await stat(manifest); + timestamp = stats.mtime; + } + // eslint-disable-next-line no-empty + catch (error) {} + let latest = 0; + for (let i = 0; i < dll.length; ++i) { + const path = dll[i]; + try { + // eslint-disable-next-line no-await-in-loop + const stats = await stat(join(FLECKS_CORE_ROOT, 'node_modules', path)); + if (stats.mtime > latest) { + latest = stats.mtime; + } + } + // eslint-disable-next-line no-empty + catch (error) {} + } + if (timestamp > latest) { + delete configs['web-vendor']; + } + else if (timestamp > 0) { + await unlink(manifest); + } + } + } + // Bail if there's no web build. + if (!configs.web) { + return; + } + // Bail if the build isn't watching. + if (!process.argv.find((arg) => '--watch' === arg)) { + return; + } + // Otherwise, spawn `webpack-dev-server` (WDS). + const cmd = [ + // `npx` doesn't propagate signals! + // 'npx', 'webpack', + join(FLECKS_CORE_ROOT, 'node_modules', '.bin', 'webpack'), + 'serve', + '--mode', 'development', + '--hot', + '--config', await flecks.resolveBuildConfig('fleckspack.config.js'), + ]; + const child = spawnWith( + cmd, + { + env: { + FLECKS_CORE_BUILD_LIST: 'web', + }, + }, + ); + // Clean up on exit. + process.on('exit', () => { + child.kill(); + }); + // Remove the build config since we're handing off to WDS. + delete configs.web; + }, + '@flecks/core.build.config': () => [ + /** + * Template file used to generate the client HTML. + * + * See: https://github.com/jantimon/html-webpack-plugin/blob/main/docs/template-option.md + */ + 'template.ejs', + /** + * PostCSS config file. + * + * See: https://github.com/postcss/postcss#usage + */ + 'postcss.config.js', + /** + * Web client build configuration. See: https://webpack.js.org/configuration/ + */ + 'web.webpack.config.js', + /** + * Web vendor DLL build configuration. See: https://webpack.js.org/configuration/ + */ + 'web-vendor.webpack.config.js', + ], + '@flecks/core.config': () => ({ + /** + * The ID of the root element on the page. + */ + appMountId: 'root', + /** + * Base tag path. + */ + base: '/', + /** + * (webpack-dev-server) Disable the host check. + * + * See: https://github.com/webpack/webpack-dev-server/issues/887 + */ + devDisableHostCheck: false, + /** + * (webpack-dev-server) Host to bind. + */ + devHost: 'localhost', + /** + * (webpack-dev-server) Port to bind. + */ + devPort: undefined, + /** + * (webpack-dev-server) Public path to serve. + * + * Defaults to `flecks.get('@flecks/web.public')`. + */ + devPublic: undefined, + /** + * (webpack-dev-server) Webpack stats output. + */ + devStats: { + colors: true, + errorDetails: true, + }, + /** + * Modules to externalize using `webpack.DllPlugin`. + */ + dll: [], + /** + * Host to bind. + */ + host: '0.0.0.0', + /** + * Path to icon. + */ + icon: '', + /** + * Port to bind. + */ + port: 32340, + /** + * Meta tags. + */ + meta: { + charset: 'utf-8', + viewport: 'width=device-width, user-scalable=no', + }, + /** + * Public path to server. + */ + public: 'localhost:32340', + /** + * Webpack stats configuration. + */ + stats: { + colors: true, + errorDetails: true, + }, + /** + * HTML title. + */ + title: '[@flecks/core.id]', + /** + * Proxies to trust. + * + * See: https://www.npmjs.com/package/proxy-addr + */ + trust: false, + }), + '@flecks/core.targets': (flecks) => [ + 'web', + ...(flecks.get('@flecks/web.dll').length > 0 ? ['web-vendor'] : []), + ], + '@flecks/core.targets.alter': (targets) => { + // Don't build if there's a fleck target. + if (targets.has('fleck')) { + targets.delete('web'); + } + }, + '@flecks/fleck.packageJson': (json, compilation) => { + if (Object.keys(compilation.assets).some((filename) => filename.match(/^assets\//))) { + json.files.push('assets'); + } + }, +}; + +exports.stubs = { + server: [ + /\.(c|s[ac])ss$/, + ], +}; diff --git a/packages/web/build/flecks.config.js b/packages/web/build/flecks.config.js deleted file mode 100644 index ac40207..0000000 --- a/packages/web/build/flecks.config.js +++ /dev/null @@ -1,9 +0,0 @@ -const {join} = require('path'); - -module.exports = { - stubs: { - server: [ - /\.(c|s[ac])ss$/, - ], - }, -}; diff --git a/packages/web/src/server/build/postcss.config.js b/packages/web/build/postcss.config.js similarity index 100% rename from packages/web/src/server/build/postcss.config.js rename to packages/web/build/postcss.config.js diff --git a/packages/web/src/server/build/runtime.js b/packages/web/build/runtime.js similarity index 54% rename from packages/web/src/server/build/runtime.js rename to packages/web/build/runtime.js index 1c47924..41de0d7 100644 --- a/packages/web/src/server/build/runtime.js +++ b/packages/web/build/runtime.js @@ -1,4 +1,4 @@ -const {access, readFile, realpath} = require('fs/promises'); +const {access, readFile} = require('fs/promises'); const { basename, dirname, @@ -6,35 +6,30 @@ const { join, } = require('path'); -const {D} = require('@flecks/core'); -const {Flecks, glob, require: R} = require('@flecks/core/server'); - -const debug = D('@flecks/web/runtime'); +const Server = require('@flecks/core/build/server'); +const {glob} = require('@flecks/core/server'); module.exports = async (config, env, argv, flecks) => { - debug('bootstrapping flecks...'); - const webFlecks = Flecks.bootstrap({ + const buildFlecks = await Server.from({ + config: flecks.config, platforms: ['client', '!server'], }); - debug('bootstrapped'); - const rootMap = {}; - Object.keys(webFlecks.resolver) - .forEach((fleck) => { - rootMap[fleck] = webFlecks.root(fleck); - }); + const {resolver, flecks: webFlecks} = buildFlecks; + const paths = Object.keys(webFlecks) + .filter((fleck) => !['@flecks/server'].includes(fleck)); const styles = ( await Promise.all( - Object.entries(rootMap) - .map(async ([fleck, root]) => { + Object.entries(flecks.roots) + .map(async ([fleck, {request}]) => { // Compiled? It will be included with the compilation. - if (webFlecks.fleckIsCompiled(fleck)) { + if (flecks.resolved[fleck]) { return undefined; } - const fleckResolved = R.resolve(fleck); - const rootResolved = dirname(R.resolve(join(root, 'package.json'))); - const sub = fleckResolved.slice(rootResolved.length + 1); - const style = join(rootResolved, 'assets', `${basename(sub, extname(sub))}.css`); try { + const fleckResolved = await resolver.resolve(fleck); + const parentResolved = dirname(await resolver.resolve(join(request, 'package.json'))); + const sub = fleckResolved.slice(parentResolved.length + 1); + const style = join(parentResolved, 'assets', `${basename(sub, extname(sub))}.css`); await access(style); return style; } @@ -45,30 +40,32 @@ module.exports = async (config, env, argv, flecks) => { ) ) .filter((filename) => !!filename); - const runtime = await realpath(R.resolve(join(webFlecks.resolve('@flecks/web'), 'runtime'))); - const {resolver} = webFlecks; + const runtime = await flecks.resolver.resolve(join('@flecks/web/runtime')); const isProduction = 'production' === argv.mode; - const paths = Object.entries(resolver); + const resolvedPaths = (await Promise.all( + paths.map(async (path) => [path, await flecks.resolver.resolve(path)]), + )) + .filter(([, resolved]) => resolved) + .map(([path]) => path); const source = [ 'module.exports = (update) => (async () => ({', " config: window[Symbol.for('@flecks/web.config')],", ' flecks: Object.fromEntries(await Promise.all([', - paths - .map(([path]) => [ + ...resolvedPaths + .map((path) => [ ' [', ` '${path}',`, ` import('${path}').then((M) => (update(${paths.length}, '${path}'), M)),`, - ' ]', - ].join('\n')) - .join(',\n'), + ' ],', + ]).flat(), ' ].map(async ([path, M]) => [path, await M]))),', " platforms: ['client'],", '}))();', '', ]; - // HMR. + // HMrequire. source.push('if (module.hot) {'); - paths.forEach(([path]) => { + resolvedPaths.forEach((path) => { source.push(` module.hot.accept('${path}', async () => {`); source.push(` const updatedFleck = require('${path}');`); source.push(` window.flecks.refresh('${path}', updatedFleck);`); @@ -90,39 +87,43 @@ module.exports = async (config, env, argv, flecks) => { ], }); config.resolve.alias['@flecks/web/runtime$'] = runtime; - flecks.runtimeCompiler(webFlecks.resolver, 'web', config); + // Stubs. + buildFlecks.stubs.forEach((stub) => { + config.module.rules.push( + { + test: stub, + use: 'null-loader', + }, + ); + }); + buildFlecks.runtimeCompiler('web', config); // Aliases. - const aliases = webFlecks.aliases(); - if (Object.keys(aliases).length > 0) { - Object.entries(aliases) - .forEach(([from, to]) => { - config.resolve.alias[from] = to; - }); - } + Object.entries(buildFlecks.resolved) + .forEach(([from, to]) => { + config.resolve.alias[from] = to; + }); // Styles. config.entry.index.push(...styles); // Tests. if (!isProduction) { - // const testPaths = []; - const roots = Array.from(new Set(Object.entries(rootMap).map(([root]) => root))); const testEntries = await Promise.all( - roots.map(async (root) => { - const paths = []; - const resolved = dirname(__non_webpack_require__.resolve(root)); - const rootTests = await glob(join(resolved, 'test', '*.js')); - paths.push(...rootTests); - const platformTests = await Promise.all( - webFlecks.platforms.map((platform) => ( - glob(join(resolved, 'test', 'platforms', platform, '*.js')) - )), - ); - paths.push(...platformTests.flat()); - return [root, paths]; - }), + Object.entries(buildFlecks.roots) + .map(async ([parent, {request}]) => { + const paths = []; + const rootTests = await glob(join(request, 'test', '*.js')); + paths.push(...rootTests); + const platformTests = await Promise.all( + buildFlecks.platforms.map((platform) => ( + glob(join(request, 'test', 'platforms', platform, '*.js')) + )), + ); + paths.push(...platformTests.flat()); + return [parent, paths]; + }), + ); + const tests = await resolver.resolve( + join('@flecks/web', 'server', 'build', 'tests'), ); - const tests = await realpath(R.resolve( - join(webFlecks.resolve('@flecks/web'), 'server', 'build', 'tests'), - )); const testsSource = (await readFile(tests)).toString(); config.module.rules.push({ test: tests, diff --git a/packages/web/src/server/build/template.ejs b/packages/web/build/template.ejs similarity index 100% rename from packages/web/src/server/build/template.ejs rename to packages/web/build/template.ejs diff --git a/packages/web/src/server/build/wait-for-manifest.js b/packages/web/build/wait-for-manifest.js similarity index 100% rename from packages/web/src/server/build/wait-for-manifest.js rename to packages/web/build/wait-for-manifest.js diff --git a/packages/web/src/server/build/web-vendor.webpack.config.js b/packages/web/build/web-vendor.webpack.config.js similarity index 74% rename from packages/web/src/server/build/web-vendor.webpack.config.js rename to packages/web/build/web-vendor.webpack.config.js index 4694df8..2b6271b 100644 --- a/packages/web/src/server/build/web-vendor.webpack.config.js +++ b/packages/web/build/web-vendor.webpack.config.js @@ -1,7 +1,7 @@ const {join} = require('path'); -const {defaultConfig, require: R, webpack} = require('@flecks/core/server'); -const { CleanWebpackPlugin } = require('clean-webpack-plugin'); +const {defaultConfig, webpack} = require('@flecks/core/server'); +const {CleanWebpackPlugin} = require('clean-webpack-plugin'); const { FLECKS_CORE_ROOT = process.cwd(), @@ -27,23 +27,23 @@ module.exports = async (env, argv, flecks) => { ], resolve: { fallback: { - assert: R.resolve('assert'), + assert: require.resolve('assert'), child_process: false, fs: false, - path: R.resolve('path-browserify'), - process: R.resolve('process/browser'), + path: require.resolve('path-browserify'), + process: require.resolve('process/browser'), stream: false, - util: R.resolve('util'), + util: require.resolve('util'), }, }, stats: { warningsFilter: [ /Failed to parse source map/, ], - ...flecks.get('@flecks/web/server.stats'), + ...flecks.get('@flecks/web.stats'), }, }); - const dll = flecks.get('@flecks/web/server.dll'); + const dll = flecks.get('@flecks/web.dll'); if (dll.length > 0) { // Build the library and manifest. config.entry.index = dll; diff --git a/packages/web/src/server/build/web.webpack.config.js b/packages/web/build/web.webpack.config.js similarity index 50% rename from packages/web/src/server/build/web.webpack.config.js rename to packages/web/build/web.webpack.config.js index 140ac1c..0e72a5a 100644 --- a/packages/web/src/server/build/web.webpack.config.js +++ b/packages/web/build/web.webpack.config.js @@ -3,7 +3,6 @@ const {join} = require('path'); const { defaultConfig, regexFromExtensions, - require: R, webpack, } = require('@flecks/core/server'); const AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin'); @@ -29,7 +28,7 @@ module.exports = async (env, argv, flecks) => { icon, port, title, - } = flecks.get('@flecks/web/server'); + } = flecks.get('@flecks/web'); const isProduction = 'production' === argv.mode; const plugins = [ // Environment. @@ -40,11 +39,15 @@ module.exports = async (env, argv, flecks) => { Buffer: ['buffer', 'Buffer'], process: 'process/browser', }), - // Inline the main entrypoint (nice for FCP). - new InlineChunkHtmlPlugin(HtmlWebpackPlugin, [/^assets\/index(\.[^.]*)?\.js$/]), ]; + if (isProduction) { + plugins.push( + // Inline the main entrypoint (nice for FCP). + new InlineChunkHtmlPlugin(HtmlWebpackPlugin, [/^assets\/index(\.[^.]*)?\.js$/]), + ); + } // DLL - const dll = flecks.get('@flecks/web/server.dll'); + const dll = flecks.get('@flecks/web.dll'); if (!isProduction && dll.length > 0) { const manifest = join(FLECKS_CORE_ROOT, 'node_modules', '.cache', '@flecks', 'web', 'vendor'); plugins.push(new WaitForManifestPlugin(join(manifest, 'manifest.json'))); @@ -62,91 +65,96 @@ module.exports = async (env, argv, flecks) => { ); } const entry = {}; - [ + const entries = [ ['index', { entry: '@flecks/web/server/build/entry', }], - ['tests', { + ]; + if (!isProduction) { + entries.push(['tests', { entry: '@flecks/web/server/build/tests', title: 'Testbed', - }], - ] - .forEach(([name, mainsConfig]) => { - const {entry: entryPoint, ...htmlTemplateConfig} = mainsConfig; - // @todo source maps working? - entry[name] = [entryPoint]; - plugins.push(new HtmlWebpackPlugin({ - appMountId: flecks.interpolate(appMountId), - base: flecks.interpolate(base), - chunks: [name], - filename: `${name}.html`, - inject: false, - lang: 'en', - meta, - template: flecks.buildConfig('template.ejs', name), - templateParameters: (compilation, assets, assetTags, options) => { - function createTag(tagName, attributes, content) { - const tag = HtmlWebpackPlugin.createHtmlTagObject( - tagName, - attributes, - content, - {plugin: '@flecks/web/server'}, - ); - tag.toString = () => htmlTagObjectToString(tag, false); - return tag; - } - if (icon) { - assetTags.headTags.push('link', {rel: 'icon', href: icon}); - } - if ('index' === name) { - const styleChunks = Array.from(compilation.chunks) - .filter((chunk) => chunk.idNameHints.has('flecksCompiled')); - for (let i = 0; i < styleChunks.length; ++i) { - const styleChunk = styleChunks[i]; - const styleChunkFiles = Array.from(styleChunk.files) - .filter((file) => file.match(/\.css$/)); - const styleAssets = styleChunkFiles.map((filename) => compilation.assets[filename]); - for (let j = 0; j < styleAssets.length; ++j) { - const asset = styleAssets[j]; - if (asset) { - assetTags.headTags = assetTags.headTags - .filter(({attributes}) => attributes?.href !== styleChunkFiles[j]); - assetTags.headTags.unshift( - createTag( - ...isProduction - ? [ - 'style', - {'data-href': `/${styleChunkFiles[j]}`}, - asset.source(), - ] - : [ - 'link', - { - href: `/${styleChunkFiles[j]}`, - rel: 'stylesheet', - type: 'text/css', - }, - ], - ), - ); + }]); + } + await Promise.all( + entries + .map(async ([name, mainsConfig]) => { + const {entry: entryPoint, ...htmlTemplateConfig} = mainsConfig; + // @todo source maps working? + entry[name] = [entryPoint]; + plugins.push(new HtmlWebpackPlugin({ + appMountId: flecks.interpolate(appMountId), + base: flecks.interpolate(base), + chunks: [name], + filename: `${name}.html`, + inject: false, + lang: 'en', + meta, + template: await flecks.resolveBuildConfig('template.ejs', name), + templateParameters: (compilation, assets, assetTags, options) => { + function createTag(tagName, attributes, content) { + const tag = HtmlWebpackPlugin.createHtmlTagObject( + tagName, + attributes, + content, + {plugin: '@flecks/web/server'}, + ); + tag.toString = () => htmlTagObjectToString(tag, false); + return tag; + } + if (icon) { + assetTags.headTags.push('link', {rel: 'icon', href: icon}); + } + if ('index' === name) { + const styleChunks = Array.from(compilation.chunks) + .filter((chunk) => chunk.idNameHints.has('flecks-compiled')); + for (let i = 0; i < styleChunks.length; ++i) { + const styleChunk = styleChunks[i]; + const styleChunkFiles = Array.from(styleChunk.files) + .filter((file) => file.match(/\.css$/)); + const styleAssets = styleChunkFiles.map((filename) => compilation.assets[filename]); + for (let j = 0; j < styleAssets.length; ++j) { + const asset = styleAssets[j]; + if (asset) { + assetTags.headTags = assetTags.headTags + .filter(({attributes}) => attributes?.href !== styleChunkFiles[j]); + assetTags.headTags.unshift( + createTag( + ...isProduction + ? [ + 'style', + {'data-href': `/${styleChunkFiles[j]}`}, + asset.source(), + ] + : [ + 'link', + { + href: `/${styleChunkFiles[j]}`, + rel: 'stylesheet', + type: 'text/css', + }, + ], + ), + ); + } } } } - } - return { - compilation, - webpackConfig: compilation.options, - htmlWebpackPlugin: { - tags: assetTags, - files: assets, - options, - }, - }; - }, - title: flecks.interpolate(title), - ...htmlTemplateConfig, - })); - }); + return { + compilation, + webpackConfig: compilation.options, + htmlWebpackPlugin: { + tags: assetTags, + files: assets, + options, + }, + }; + }, + title: flecks.interpolate(title), + ...htmlTemplateConfig, + })); + }), + ); // @todo dynamic extensions const styleExtensionsRegex = regexFromExtensions( ['.css', '.sass', '.scss'].map((ext) => [ext, `.module${ext}`]).flat(), @@ -216,17 +224,17 @@ module.exports = async (env, argv, flecks) => { assets: '.', }, fallback: { - buffer: R.resolve('buffer'), + buffer: require.resolve('buffer'), child_process: false, fs: false, - path: R.resolve('path-browserify'), - process: R.resolve('process/browser'), - stream: R.resolve('stream-browserify'), - zlib: R.resolve('browserify-zlib'), + path: require.resolve('path-browserify'), + process: require.resolve('process/browser'), + stream: require.resolve('stream-browserify'), + zlib: require.resolve('browserify-zlib'), }, }, stats: { - ...flecks.get('@flecks/web/server.stats'), + ...flecks.get('@flecks/web.stats'), warningsFilter: [ /Failed to parse source map/, ], diff --git a/packages/web/package.json b/packages/web/package.json index 12b571f..4f7ec9d 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -8,7 +8,7 @@ "publishConfig": { "access": "public" }, - "version": "2.0.3", + "version": "3.0.0", "main": "index.js", "scripts": { "build": "flecks build", @@ -22,16 +22,12 @@ "runtime.js", "server.js", "server/build/entry.js", - "server/build/postcss.config.js", - "server/build/template.ejs", - "server/build/tests.js", - "server/build/web.webpack.config.js", - "server/build/web-vendor.webpack.config.js" + "server/build/tests.js" ], "dependencies": { "@babel/parser": "^7.17.0", "@babel/types": "^7.17.0", - "@flecks/core": "^2.0.3", + "@flecks/core": "^3.0.0", "@webpack-cli/serve": "^2.0.5", "add-asset-html-webpack-plugin": "^3.2.2", "assert": "^2.1.0", @@ -61,6 +57,6 @@ "webpack-dev-server": "^4.15.1" }, "devDependencies": { - "@flecks/fleck": "^2.0.3" + "@flecks/fleck": "^3.0.0" } } diff --git a/packages/web/src/server/augment-build.js b/packages/web/src/server/augment-build.js deleted file mode 100644 index 46e87cc..0000000 --- a/packages/web/src/server/augment-build.js +++ /dev/null @@ -1,98 +0,0 @@ -import {regexFromExtensions} from '@flecks/core/server'; -import MiniCssExtractPlugin from 'mini-css-extract-plugin'; - -const augmentBuild = (target, config, env, argv, flecks) => { - const isProduction = 'production' === argv.mode; - let finalLoader; - switch (target) { - case 'fleck': { - finalLoader = {loader: MiniCssExtractPlugin.loader}; - config.plugins.push(new MiniCssExtractPlugin({filename: 'assets/[name].css'})); - break; - } - case 'server': { - finalLoader = {loader: MiniCssExtractPlugin.loader, options: {emit: false}}; - config.plugins.push(new MiniCssExtractPlugin({filename: 'assets/[name].css'})); - break; - } - case 'web': { - finalLoader = {loader: MiniCssExtractPlugin.loader}; - config.plugins.push(new MiniCssExtractPlugin({filename: 'assets/[name].css'})); - break; - } - default: break; - } - const buildOneOf = (test, loaders, cssOptions = {}) => ({ - test, - use: [ - finalLoader, - { - loader: 'css-loader', - options: { - ...cssOptions, - importLoaders: loaders.length, - }, - }, - ...loaders, - 'source-map-loader', - ], - }); - const stylesWithModulesRule = (extensions, loaders) => ({ - oneOf: [ - // `.module.*` must match first. - buildOneOf( - regexFromExtensions(extensions.map((ext) => `module${ext}`)), - loaders, - { - modules: { - localIdentName: isProduction - ? '[hash:base64:5]' - : '[path][name]__[local]', - }, - }, - ), - buildOneOf( - regexFromExtensions(extensions), - loaders, - ), - ], - }); - const postcss = { - loader: 'postcss-loader', - options: { - postcssOptions: { - config: flecks.buildConfig('postcss.config.js'), - }, - }, - }; - // Originally separated because Sass can't handle incoming source maps, but probably more - // performant with 3rd-party CSS anyway. - config.module.rules.push(stylesWithModulesRule(['.css'], [postcss])); - config.module.rules.push(stylesWithModulesRule(['.sass', '.scss'], [postcss, 'sass-loader'])); - // Fonts. - if (isProduction) { - config.module.rules.push({ - generator: { - filename: 'assets/[hash][ext][query]', - }, - test: /\.(eot|ttf|woff|woff2)(\?v=\d+\.\d+\.\d+)?$/, - type: 'asset', - }); - } - else { - config.module.rules.push({ - test: /\.(eot|ttf|woff|woff2)(\?v=\d+\.\d+\.\d+)?$/, - type: 'asset/inline', - }); - } - // Images. - config.module.rules.push({ - generator: { - filename: 'assets/[hash][ext][query]', - }, - test: /\.(ico|png|jpg|jpeg|gif|svg|webp)(\?v=\d+\.\d+\.\d+)?$/, - type: 'asset', - }); -}; - -export default augmentBuild; diff --git a/packages/web/src/server/build/entry.js b/packages/web/src/server/build/entry.js index 344444b..af95243 100644 --- a/packages/web/src/server/build/entry.js +++ b/packages/web/src/server/build/entry.js @@ -59,7 +59,7 @@ const {version} = require('@flecks/web/package.json'); try { await Promise.all(flecks.invokeFlat('@flecks/core.starting')); await flecks.invokeSequentialAsync('@flecks/web/client.up'); - const appMountContainerId = `#${config['@flecks/web/client'].appMountId}-container`; + const appMountContainerId = `#${config['@flecks/web'].appMountId}-container`; window.document.querySelector(appMountContainerId).style.display = 'block'; debug('up!'); } diff --git a/packages/web/src/server/config.js b/packages/web/src/server/config.js index b8c94fc..e024e4a 100644 --- a/packages/web/src/server/config.js +++ b/packages/web/src/server/config.js @@ -1,35 +1,27 @@ import {Transform} from 'stream'; -const config = async (flecks, req) => { +export const configSource = async (flecks, req) => { + req.onlyAllow = (object, keys) => ( + Object.fromEntries( + Object.entries(object) + .map(([key, value]) => [key, keys.includes(key) ? value : undefined]), + ) + ); const httpConfig = await flecks.invokeMergeAsync('@flecks/web.config', req); - const {config} = flecks.web.flecks; - const reducedConfig = Object.keys(config) - .filter((path) => !path.startsWith('$')) - .filter((path) => !path.endsWith('/server')) - .reduce( - (r, key) => ({ - ...r, - [key]: { - ...(config[key] || {}), - ...(httpConfig[key] || {}), - }, - }), - {}, - ); + const config = Object.fromEntries( + Object.entries(flecks.web.flecks.config) + .filter(([path]) => !path.endsWith('/server')) + .map(([path, config]) => [path, {...config, ...httpConfig[path]}]), + ); // Fold in any bespoke configuration. Object.keys(httpConfig) .forEach((key) => { - if (!(key in reducedConfig)) { - reducedConfig[key] = httpConfig[key]; + if (!(key in config)) { + config[key] = httpConfig[key]; } }); - return reducedConfig; -}; - -export const configSource = async (flecks, req) => { - const codedConfig = encodeURIComponent(JSON.stringify(await config(flecks, req))); return `window[Symbol.for('@flecks/web.config')] = JSON.parse(decodeURIComponent("${ - codedConfig + encodeURIComponent(JSON.stringify(config)) }"));`; }; @@ -44,7 +36,7 @@ class InlineConfig extends Transform { // eslint-disable-next-line no-underscore-dangle async _transform(chunk, encoding, done) { const string = chunk.toString('utf8'); - const {appMountId} = this.flecks.get('@flecks/web/server'); + const {appMountId} = this.flecks.get('@flecks/web'); const rendered = string.replaceAll( '', [ diff --git a/packages/web/src/server/http.js b/packages/web/src/server/http.js index 7d33da5..3bfde83 100644 --- a/packages/web/src/server/http.js +++ b/packages/web/src/server/http.js @@ -20,13 +20,13 @@ const deliverHtmlStream = async (stream, flecks, req, res) => { }; export const createHttpServer = async (flecks) => { - const {trust} = flecks.get('@flecks/web/server'); + const {trust} = flecks.get('@flecks/web'); const { devHost, devPort, host, port, - } = flecks.get('@flecks/web/server'); + } = flecks.get('@flecks/web'); const app = express(); app.set('trust proxy', trust); const httpServer = createServer(app); diff --git a/packages/web/src/server/index.js b/packages/web/src/server/index.js index 6263132..0b73667 100644 --- a/packages/web/src/server/index.js +++ b/packages/web/src/server/index.js @@ -1,239 +1,28 @@ -import {stat, unlink} from 'fs/promises'; -import {join} from 'path'; - import {D} from '@flecks/core'; -import {Flecks, spawnWith} from '@flecks/core/server'; +import Server from '@flecks/core/build/server'; -import augmentBuild from './augment-build'; import {configSource, inlineConfig} from './config'; import {createHttpServer} from './http'; -const { - FLECKS_CORE_ROOT = process.cwd(), -} = process.env; - const debug = D('@flecks/web/server'); -export {augmentBuild, configSource}; +export {configSource}; export const hooks = { - '@flecks/core.build': augmentBuild, - '@flecks/core.build.alter': async (configs, env, argv, flecks) => { - // Don't build if there's a fleck target. - if (configs.fleck && !flecks.get('@flecks/web/server.forceBuildWithFleck')) { - delete configs.web; - return; - } - const isProduction = 'production' === argv.mode; - // Only build vendor in dev. - if (configs['web-vendor']) { - if (isProduction) { - delete configs['web-vendor']; - } - // Only build if something actually changed. - const dll = flecks.get('@flecks/web/server.dll'); - if (dll.length > 0) { - const manifest = join( - FLECKS_CORE_ROOT, - 'node_modules', - '.cache', - '@flecks', - 'web', - 'vendor', - 'manifest.json', - ); - let timestamp = 0; - try { - const stats = await stat(manifest); - timestamp = stats.mtime; - } - // eslint-disable-next-line no-empty - catch (error) {} - let latest = 0; - for (let i = 0; i < dll.length; ++i) { - const path = dll[i]; - try { - // eslint-disable-next-line no-await-in-loop - const stats = await stat(join(FLECKS_CORE_ROOT, 'node_modules', path)); - if (stats.mtime > latest) { - latest = stats.mtime; - } - } - // eslint-disable-next-line no-empty - catch (error) {} - } - if (timestamp > latest) { - delete configs['web-vendor']; - } - else if (timestamp > 0) { - await unlink(manifest); - } - } - } - // Bail if there's no web build. - if (!configs.web) { - return; - } - // Bail if the build isn't watching. - if (!process.argv.find((arg) => '--watch' === arg)) { - return; - } - // Otherwise, spawn `webpack-dev-server` (WDS). - const cmd = [ - // `npx` doesn't propagate signals! - // 'npx', 'webpack', - join(FLECKS_CORE_ROOT, 'node_modules', '.bin', 'webpack'), - 'serve', - '--mode', 'development', - '--hot', - '--config', flecks.buildConfig('fleckspack.config.js'), - ]; - const child = spawnWith( - cmd, - { - env: { - FLECKS_CORE_BUILD_LIST: 'web', - }, - }, - ); - // Clean up on exit. - process.on('exit', () => { - child.kill(); - }); - // Remove the build config since we're handing off to WDS. - delete configs.web; - }, - '@flecks/core.build.config': () => [ - /** - * Template file used to generate the client HTML. - * - * See: https://github.com/jantimon/html-webpack-plugin/blob/main/docs/template-option.md - */ - 'template.ejs', - /** - * PostCSS config file. - * - * See: https://github.com/postcss/postcss#usage - */ - 'postcss.config.js', - ], - '@flecks/core.config': () => ({ - /** - * The ID of the root element on the page. - */ - appMountId: 'root', - /** - * Base tag path. - */ - base: '/', - /** - * (webpack-dev-server) Disable the host check. - * - * See: https://github.com/webpack/webpack-dev-server/issues/887 - */ - devDisableHostCheck: false, - /** - * (webpack-dev-server) Host to bind. - */ - devHost: 'localhost', - /** - * (webpack-dev-server) Port to bind. - */ - devPort: undefined, - /** - * (webpack-dev-server) Public path to serve. - * - * Defaults to `flecks.get('@flecks/web/server.public')`. - */ - devPublic: undefined, - /** - * (webpack-dev-server) Webpack stats output. - */ - devStats: { - colors: true, - errorDetails: true, - }, - /** - * Modules to externalize using `webpack.DllPlugin`. - */ - dll: [], - /** - * Force building http target even if there's a fleck target. - */ - forceBuildWithFleck: false, - /** - * Host to bind. - */ - host: '0.0.0.0', - /** - * Path to icon. - */ - icon: '', - /** - * Port to bind. - */ - port: 32340, - /** - * Meta tags. - */ - meta: { - charset: 'utf-8', - viewport: 'width=device-width, user-scalable=no', - }, - /** - * Public path to server. - */ - public: 'localhost:32340', - /** - * Webpack stats configuration. - */ - stats: { - colors: true, - errorDetails: true, - }, - /** - * HTML title. - */ - title: '[@flecks/core.id]', - /** - * Proxies to trust. - * - * See: https://www.npmjs.com/package/proxy-addr - */ - trust: false, - }), '@flecks/core.mixin': (Flecks) => ( class FlecksWithWeb extends Flecks { - web = { - flecks: undefined, - server: undefined, + constructor(...args) { + super(...args); + if (!this.web) { + this.web = {flecks: undefined, server: undefined}; + } } } ), - '@flecks/core.starting': (flecks) => { - debug('bootstrapping flecks...'); - const webFlecks = Flecks.bootstrap({ - config: flecks.aliasedConfig, - platforms: ['client', '!server'], - }); - debug('bootstrapped'); - flecks.web.flecks = webFlecks; - }, - '@flecks/core.targets': (flecks) => [ - 'web', - ...(flecks.get('@flecks/web/server.dll').length > 0 ? ['web-vendor'] : []), - ], - '@flecks/fleck/server.packageJson': (json, compilation) => { - if (Object.keys(compilation.assets).some((filename) => filename.match(/^assets\//))) { - json.files.push('assets'); - } - }, - '@flecks/web.config': async (req, flecks) => ({ - '@flecks/web/client': { - appMountId: flecks.get('@flecks/web/server.appMountId'), - }, + '@flecks/web.config': async ({onlyAllow}, flecks) => ({ + '@flecks/web': onlyAllow(flecks.get('@flecks/web'), ['appMountId', 'title']), }), '@flecks/web.routes': (flecks) => [ { @@ -246,5 +35,13 @@ export const hooks = { }, ], '@flecks/web/server.stream.html': inlineConfig, + '@flecks/web/server.up': async (server, flecks) => { + debug('bootstrapping flecks...'); + flecks.web.flecks = await Server.from({ + config: flecks.config, + platforms: ['client', '!server'], + }); + debug('bootstrapped'); + }, '@flecks/server.up': (flecks) => createHttpServer(flecks), }; diff --git a/website/docs/environment.mdx b/website/docs/environment.mdx index 5a236f4..cf85689 100644 --- a/website/docs/environment.mdx +++ b/website/docs/environment.mdx @@ -28,9 +28,10 @@ override configuration such as this. This is done by like so: :::tip[How do you identify?] -As an example, `@flecks/core`'s `id` key is set using the following variable: +As an example, `@flecks/core`'s `id` key may be set using the following variable: ```bash +# Equivalent to above. FLECKS_ENV__flecks_core__id=foobar ```