diff --git a/package.json b/package.json index a5c4346fcd..d6d1112b02 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "eslint-plugin-react-hooks": "4.6.0", "eslint-plugin-simple-import-sort": "10.0.0", "execa": "7.1.1", - "import-meta-resolve": "3.0.0", + "import-meta-resolve": "2.2.2", "lodash": "4.17.21", "playwright": "1.33.0", "pm2": "^5.3.0", diff --git a/sources/@repo/test-kit/bud.ts b/sources/@repo/test-kit/bud.ts index 835192f1a4..dd32d2f206 100644 --- a/sources/@repo/test-kit/bud.ts +++ b/sources/@repo/test-kit/bud.ts @@ -14,25 +14,19 @@ export const repoPath = (...path: Array) => export const basedir = repoPath(`tests`, `util`, `project`) export const factory = async ( - overrides?: Partial, - run?: boolean, + overrides: Partial = {}, ): Promise => { const bud = await makeInstance({ basedir, + force: true, + dry: true, + log: false, + notify: false, + mode: `production`, ...overrides, - args: { - force: true, - dry: true, - log: false, - notify: false, - ...(overrides?.args ?? {}), - }, }) - if (!bud.isCLI()) - throw new Error(`test error: bud is not a CLI instance`) - - if (run) await bud.run() + await bud.run() return bud } diff --git a/sources/@roots/bud-api/src/methods/html/helpers.ts b/sources/@roots/bud-api/src/methods/html/helpers.ts index 319c1862ee..cf93b7785f 100644 --- a/sources/@roots/bud-api/src/methods/html/helpers.ts +++ b/sources/@roots/bud-api/src/methods/html/helpers.ts @@ -2,7 +2,6 @@ import {dirname, resolve} from 'node:path' import {fileURLToPath} from 'node:url' import type * as HTMLExtension from '@roots/bud-extensions/html-webpack-plugin' -import type * as InterpolateHTMLExtension from '@roots/bud-extensions/interpolate-html-webpack-plugin' import type {Bud} from '@roots/bud-framework' import isObject from '@roots/bud-support/lodash/isObject' import isUndefined from '@roots/bud-support/lodash/isUndefined' @@ -34,18 +33,3 @@ export const getHtmlPluginOptions = ( return {template, ...omit(options, `replace`, `template`)} } - -export const getInterpolatePluginOptions = ( - bud: Bud, - options: Parameters[0], -): InterpolateHTMLExtension.Options => { - if ( - !isObject(options) || - isUndefined(options) || - isUndefined(options.replace) - ) { - return bud.env.getPublicEnv() - } - - return {...bud.env.getPublicEnv(), ...options.replace} -} diff --git a/sources/@roots/bud-api/src/methods/html/index.test.ts b/sources/@roots/bud-api/src/methods/html/index.test.ts index 142f4b372d..ae52618bae 100644 --- a/sources/@roots/bud-api/src/methods/html/index.test.ts +++ b/sources/@roots/bud-api/src/methods/html/index.test.ts @@ -31,9 +31,9 @@ describe(`bud.html`, () => { `@roots/bud-extensions/interpolate-html-webpack-plugin`, ) htmlEnableSpy = vi.spyOn(htmlPlugin, `enable`) - htmlSetOptionsSpy = vi.spyOn(htmlPlugin, `setOptions`) + htmlSetOptionsSpy = vi.spyOn(htmlPlugin, `set`) interpolateEnableSpy = vi.spyOn(interpolatePlugin, `enable`) - interpolateSetOptionsSpy = vi.spyOn(interpolatePlugin, `setOptions`) + interpolateSetOptionsSpy = vi.spyOn(interpolatePlugin, `set`) html = source.html.bind(bud) }) @@ -50,14 +50,9 @@ describe(`bud.html`, () => { const returned = await html(false) expect(htmlEnableSpy).toHaveBeenCalledWith(false) - expect(htmlSetOptionsSpy).toHaveBeenCalledWith( - helpers.defaultHtmlPluginOptions, + Object.entries(helpers.defaultHtmlPluginOptions).forEach(v => + expect(htmlSetOptionsSpy).toHaveBeenCalledWith(...v), ) - expect(interpolateEnableSpy).toHaveBeenCalledWith(false) - expect(interpolateSetOptionsSpy).toHaveBeenCalledWith({ - APP_DESCRIPTION: `test app description`, - APP_TITLE: `bud.js test app`, - }) expect(returned).toBe(bud) }) @@ -66,14 +61,9 @@ describe(`bud.html`, () => { const returned = await html(true) expect(htmlEnableSpy).toHaveBeenCalledWith(true) - expect(htmlSetOptionsSpy).toHaveBeenCalledWith( - helpers.defaultHtmlPluginOptions, + Object.entries(helpers.defaultHtmlPluginOptions).forEach(v => + expect(htmlSetOptionsSpy).toHaveBeenCalledWith(...v), ) - expect(interpolateEnableSpy).toHaveBeenCalledWith(true) - expect(interpolateSetOptionsSpy).toHaveBeenCalledWith({ - APP_DESCRIPTION: `test app description`, - APP_TITLE: `bud.js test app`, - }) expect(returned).toBe(bud) }) @@ -82,14 +72,9 @@ describe(`bud.html`, () => { const returned = await html() expect(htmlEnableSpy).toHaveBeenCalledWith(true) - expect(htmlSetOptionsSpy).toHaveBeenCalledWith( - helpers.defaultHtmlPluginOptions, + Object.entries(helpers.defaultHtmlPluginOptions).forEach(v => + expect(htmlSetOptionsSpy).toHaveBeenCalledWith(...v), ) - expect(interpolateEnableSpy).toHaveBeenCalledWith(true) - expect(interpolateSetOptionsSpy).toHaveBeenCalledWith({ - APP_DESCRIPTION: `test app description`, - APP_TITLE: `bud.js test app`, - }) expect(returned).toBe(bud) }) @@ -98,14 +83,9 @@ describe(`bud.html`, () => { const returned = await html({}) expect(htmlEnableSpy).toHaveBeenCalledWith(true) - expect(htmlSetOptionsSpy).toHaveBeenCalledWith( - helpers.defaultHtmlPluginOptions, + Object.entries(helpers.defaultHtmlPluginOptions).forEach(v => + expect(htmlSetOptionsSpy).toHaveBeenCalledWith(...v), ) - expect(interpolateEnableSpy).toHaveBeenCalledWith(true) - expect(interpolateSetOptionsSpy).toHaveBeenCalledWith({ - APP_DESCRIPTION: `test app description`, - APP_TITLE: `bud.js test app`, - }) expect(returned).toBe(bud) }) @@ -114,33 +94,20 @@ describe(`bud.html`, () => { await html({template: `test`, replace: {foo: `bar`}}) expect(interpolateEnableSpy).toHaveBeenCalledWith(true) - expect(interpolateSetOptionsSpy).toHaveBeenCalledWith({ - APP_DESCRIPTION: `test app description`, - APP_TITLE: `bud.js test app`, - foo: `bar`, - }) + expect(interpolateSetOptionsSpy).toHaveBeenCalledWith(`foo`, `bar`) expect(htmlEnableSpy).toHaveBeenCalledWith(true) expect(budPathSpy).toHaveBeenCalledWith(`test`) expect(htmlSetOptionsSpy).toHaveBeenCalledWith( - expect.objectContaining({ - template: expect.stringMatching(/\/test$/), - }), + `template`, + bud.path(`test`), ) }) it(`should leave absolute paths alone when passed as options.template`, async () => { await html({template: `/test`, replace: {foo: `bar`}}) - expect(interpolateSetOptionsSpy).toHaveBeenCalledWith({ - APP_DESCRIPTION: `test app description`, - APP_TITLE: `bud.js test app`, - foo: `bar`, - }) - expect(htmlSetOptionsSpy).toHaveBeenCalledWith( - expect.objectContaining({ - template: expect.stringMatching(/\/test$/), - }), - ) + expect(interpolateSetOptionsSpy).toHaveBeenCalledWith(`foo`, `bar`) + expect(htmlSetOptionsSpy).toHaveBeenCalledWith(`template`, `/test`) }) it(`getHtmlPluginOptions returns normalized options with \`template\` when it is not included`, async () => { @@ -193,32 +160,4 @@ describe(`bud.html`, () => { helpers.getHtmlPluginOptions(bud, {template: `foo`}) expect(budPathSpy).toHaveBeenCalledWith(`foo`) }) - - it.each([true, false, undefined, {}])( - `getInterpolatePluginOptions calls bud env`, - async value => { - const envSpy = vi.spyOn(bud.env, `getPublicEnv`) - helpers.getInterpolatePluginOptions(bud, value) - expect(envSpy).toHaveBeenCalledOnce() - }, - ) - - it(`appends options.replace values to publicEnv`, async () => { - const envSpy = vi.spyOn(bud.env, `getPublicEnv`) - - const result = helpers.getInterpolatePluginOptions(bud, { - replace: { - foo: `bar`, - }, - }) - - expect(envSpy).toHaveBeenCalledOnce() - expect(result).toEqual( - expect.objectContaining({ - foo: `bar`, - APP_DESCRIPTION: `test app description`, - APP_TITLE: `bud.js test app`, - }), - ) - }) }, 60000) diff --git a/sources/@roots/bud-api/src/methods/html/index.ts b/sources/@roots/bud-api/src/methods/html/index.ts index da8ccdc0a3..90b3a22dbc 100644 --- a/sources/@roots/bud-api/src/methods/html/index.ts +++ b/sources/@roots/bud-api/src/methods/html/index.ts @@ -1,6 +1,7 @@ import type * as HTMLExtension from '@roots/bud-extensions/html-webpack-plugin' import type * as InterpolateHTMLExtension from '@roots/bud-extensions/interpolate-html-webpack-plugin' import type {Bud} from '@roots/bud-framework' +import isObject from '@roots/bud-support/lodash/isObject' type Options = HTMLExtension.Options & { replace?: InterpolateHTMLExtension.Options @@ -16,21 +17,31 @@ export interface html { * Set HTML template */ export const html: html = async function (this: Bud, options) { - const {getHtmlPluginOptions, getInterpolatePluginOptions} = await import( - `./helpers.js` - ) + const {getHtmlPluginOptions} = await import(`./helpers.js`) const enabled = options !== false - this.extensions - .get(`@roots/bud-extensions/html-webpack-plugin`) - ?.setOptions(getHtmlPluginOptions(this, options)) - .enable(enabled) + const htmlExtension = this.extensions.get( + `@roots/bud-extensions/html-webpack-plugin`, + ) + htmlExtension.enable(enabled) + + const htmlOptions = getHtmlPluginOptions(this, options) + if (isObject(htmlOptions)) { + Object.entries(htmlOptions).forEach(v => htmlExtension.set(...v)) + } - this.extensions - .get(`@roots/bud-extensions/interpolate-html-webpack-plugin`) - ?.setOptions(getInterpolatePluginOptions(this, options)) - .enable(enabled) + const interpolateVariablesExtension = this.extensions.get( + `@roots/bud-extensions/interpolate-html-webpack-plugin`, + ) + interpolateVariablesExtension.enable(enabled) + + if (isObject(options) && isObject(options.replace)) { + Object.entries(options.replace).forEach( + (v: [string, string | RegExp]) => + interpolateVariablesExtension.set(...v), + ) + } return this } diff --git a/sources/@roots/bud-api/vendor/template.html b/sources/@roots/bud-api/vendor/template.html index faf9a83609..b90e225130 100644 --- a/sources/@roots/bud-api/vendor/template.html +++ b/sources/@roots/bud-api/vendor/template.html @@ -4,12 +4,12 @@ - %APP_TITLE% +
diff --git a/sources/@roots/bud-babel/src/extension.ts b/sources/@roots/bud-babel/src/extension.ts index fdcbb23c2d..47f726d92e 100644 --- a/sources/@roots/bud-babel/src/extension.ts +++ b/sources/@roots/bud-babel/src/extension.ts @@ -143,7 +143,7 @@ export default class BabelExtension extends Extension { ) this.configFileOptions = - this.configFile.module.default ?? this.configFile.module + this.configFile.module?.default ?? this.configFile.module hooks.on(`build.cache.buildDependencies`, paths => { if (isString(this.configFile)) { diff --git a/sources/@roots/bud-build/src/config/experiments.ts b/sources/@roots/bud-build/src/config/experiments.ts index 496518a3d7..cf8a732205 100644 --- a/sources/@roots/bud-build/src/config/experiments.ts +++ b/sources/@roots/bud-build/src/config/experiments.ts @@ -1,4 +1,6 @@ import type {Factory} from './index.js' export const experiments: Factory<`experiments`> = async ({hooks}) => - hooks.filter(`build.experiments`, undefined) + hooks.filter(`build.experiments`, { + backCompat: false, + }) diff --git a/sources/@roots/bud-build/src/config/output/index.ts b/sources/@roots/bud-build/src/config/output/index.ts index 245c9c2235..a8e36913bd 100644 --- a/sources/@roots/bud-build/src/config/output/index.ts +++ b/sources/@roots/bud-build/src/config/output/index.ts @@ -18,7 +18,7 @@ export const output: Factory<`output`> = async ({ filename: filename({filter, relPath}), module: filter(`build.output.module`, false), path: filter(`build.output.path`, path(`@dist`)), - pathinfo: filter(`build.output.pathinfo`), + pathinfo: filter(`build.output.pathinfo`, false), publicPath: filter(`build.output.publicPath`, `auto`), scriptType: filter( `build.output.scriptType`, diff --git a/sources/@roots/bud-build/src/config/profile.ts b/sources/@roots/bud-build/src/config/profile.ts index 3150c11b12..9dfb5dce61 100644 --- a/sources/@roots/bud-build/src/config/profile.ts +++ b/sources/@roots/bud-build/src/config/profile.ts @@ -1,7 +1,4 @@ import type {Factory} from './index.js' export const profile: Factory<`profile`> = async bud => - bud.hooks.filter( - `build.profile`, - bud.isCLI() && bud.context.args[`debug`], - ) + bud.hooks.filter(`build.profile`, bud.context.debug) diff --git a/sources/@roots/bud-build/src/handlers/items/items.ts b/sources/@roots/bud-build/src/handlers/items/items.ts index 6dff39c19b..c6e5c7e2bb 100644 --- a/sources/@roots/bud-build/src/handlers/items/items.ts +++ b/sources/@roots/bud-build/src/handlers/items/items.ts @@ -65,7 +65,7 @@ export const minicss: Factory = async ({makeItem}) => .setLoader(`minicss`) .setIdent(`minicss`) .setOptions(app => ({ - publicPath: app.hooks.filter(`build.output.publicPath`), + publicPath: app.publicPath(), })) /** diff --git a/sources/@roots/bud-build/src/handlers/rules/json.ts b/sources/@roots/bud-build/src/handlers/rules/json.ts index c0da45ec06..e330f569e9 100644 --- a/sources/@roots/bud-build/src/handlers/rules/json.ts +++ b/sources/@roots/bud-build/src/handlers/rules/json.ts @@ -7,4 +7,6 @@ export const json: Factory = async ({filter, makeRule, path}) => .setType(`json`) .setInclude([() => path()]) .setTest(filter(`pattern.json`)) - .setParser({parse: json5.parse}) + .setParser({ + parse: json5.parse, + }) diff --git a/sources/@roots/bud-build/src/handlers/rules/toml.ts b/sources/@roots/bud-build/src/handlers/rules/toml.ts index 56b50c26a6..afc7d2929a 100644 --- a/sources/@roots/bud-build/src/handlers/rules/toml.ts +++ b/sources/@roots/bud-build/src/handlers/rules/toml.ts @@ -7,4 +7,6 @@ export const toml: Factory = async ({filter, makeRule, path}) => .setType(`json`) .setInclude([() => path()]) .setTest(() => filter(`pattern.toml`)) - .setParser({parse: tomlParser.parse}) + .setParser({ + parse: tomlParser.parse, + }) diff --git a/sources/@roots/bud-cache/src/invalidate-cache/index.ts b/sources/@roots/bud-cache/src/invalidate-cache/index.ts index 607339a16d..d61ce84d99 100644 --- a/sources/@roots/bud-cache/src/invalidate-cache/index.ts +++ b/sources/@roots/bud-cache/src/invalidate-cache/index.ts @@ -30,7 +30,7 @@ export default class InvalidateCacheExtension extends Extension { public override async register(bud: Bud) { const invalidate = await bud.fs?.exists(this.invalidationFile) - if (invalidate || (bud.isCLI() && bud.context.args.force)) { + if (invalidate || bud.context.force) { await bud.fs.remove(this.invalidationFile) await bud.fs.remove(bud.cache.cacheDirectory) } diff --git a/sources/@roots/bud-cache/src/service/index.ts b/sources/@roots/bud-cache/src/service/index.ts index 43bcae5375..0fe4c842f0 100644 --- a/sources/@roots/bud-cache/src/service/index.ts +++ b/sources/@roots/bud-cache/src/service/index.ts @@ -42,8 +42,8 @@ export default class Cache public get type(): 'memory' | 'filesystem' { return this.app.hooks.filter( `build.cache.type`, - this.app.isCLI() && isString(this.app.context.args.cache) - ? this.app.context.args.cache + isString(this.app.context.cache) + ? this.app.context.cache : `filesystem`, ) } diff --git a/sources/@roots/bud-client/src/hot/client.ts b/sources/@roots/bud-client/src/hot/client.ts index cdb2ffe6e4..8afb882628 100644 --- a/sources/@roots/bud-client/src/hot/client.ts +++ b/sources/@roots/bud-client/src/hot/client.ts @@ -111,12 +111,14 @@ export const client = async ( } /* Instantiate indicator, overlay */ - await components.make(options) + try { + await components.make(options) + } catch (error) {} /* Instantiate eventSource */ const events = injectEvents(EventSource).make(options) - if (!window.bud.listeners[options.name]) { + if (!window.bud.listeners?.[options.name]) { window.bud.listeners[options.name] = async payload => { if (!payload) return diff --git a/sources/@roots/bud-compiler/src/compiler.service.tsx b/sources/@roots/bud-compiler/src/compiler.service.tsx index c6a189fb07..e3da5be03d 100644 --- a/sources/@roots/bud-compiler/src/compiler.service.tsx +++ b/sources/@roots/bud-compiler/src/compiler.service.tsx @@ -74,13 +74,8 @@ export class Compiler extends Service implements Contract.Service { await this.app.hooks.fire(`compiler.before`, this.app) - if (this.app.isCLI() && this.app.context.args.dry) { - this.logger.timeEnd(`initialize`) - this.logger.log(`running in dry mode. exiting early.`) - return - } - this.logger.timeEnd(`initialize`) + this.logger.await(`compilation`) this.instance = this.implementation(this.config) diff --git a/sources/@roots/bud-compiler/src/compiler.test.ts b/sources/@roots/bud-compiler/src/compiler.test.ts index 952adfa2c9..71247b4a5c 100644 --- a/sources/@roots/bud-compiler/src/compiler.test.ts +++ b/sources/@roots/bud-compiler/src/compiler.test.ts @@ -43,14 +43,6 @@ describe(`@roots/bud-compiler`, function () { expect(compiler.config).toHaveLength(2) }) - it(`should log early exit (--dry)`, async () => { - // @ts-ignore - bud.context.args.dry = true - const logSpy = vi.spyOn(compiler.logger, `log`) - await compiler.compile() - expect(logSpy).toHaveBeenCalledTimes(3) - }) - it(`should set done tap`, async () => { try { await compiler.compile() diff --git a/sources/@roots/bud-dashboard/src/service.test.tsx b/sources/@roots/bud-dashboard/src/service.test.tsx index 0bb4e49fe3..8edfcda1c4 100644 --- a/sources/@roots/bud-dashboard/src/service.test.tsx +++ b/sources/@roots/bud-dashboard/src/service.test.tsx @@ -4,16 +4,13 @@ import {beforeEach, describe, expect, it} from 'vitest' import Dashboard from './index.js' -describe(`Dashboard`, () => { +describe(`@roots/bud-dashboard`, () => { let bud: Bud let dashboard: Dashboard beforeEach(async () => { bud = await factory() - if (bud.isCLI()) { - bud.context.args.log = true - } else throw new Error(`bud.isCli() should be true`) - + bud.context.log = true dashboard = new Dashboard(() => bud) }) diff --git a/sources/@roots/bud-dashboard/src/service.tsx b/sources/@roots/bud-dashboard/src/service.tsx index bd1e19b80c..643d4813ea 100644 --- a/sources/@roots/bud-dashboard/src/service.tsx +++ b/sources/@roots/bud-dashboard/src/service.tsx @@ -29,7 +29,7 @@ export class Dashboard extends Service implements Contract { * Is silent? */ public get silent() { - return this.app.isCLI() && this.app.context.args.log === false + return this.app.context.silent === true } /** @@ -46,7 +46,7 @@ export class Dashboard extends Service implements Contract { return this } - if (!this.app.isCLI() || this.app.context.args.ci === true) { + if (this.app.context.ci === true) { this.renderString(stats) return this } diff --git a/sources/@roots/bud-esbuild/src/extension.ts b/sources/@roots/bud-esbuild/src/extension.ts index 28fa1b14d0..4f47204a57 100644 --- a/sources/@roots/bud-esbuild/src/extension.ts +++ b/sources/@roots/bud-esbuild/src/extension.ts @@ -42,7 +42,7 @@ export interface Options { ts: { loader: `tsx` | `ts` target: string - tsconfigRaw: Record + tsconfig: string } } @@ -68,7 +68,7 @@ export interface Options { ts: ({context}) => ({ loader: `tsx`, target: `es2015`, - tsconfigRaw: context.files?.[`tsconfig.json`]?.module ?? null, + tsconfig: context.files?.[`tsconfig.json`]?.path ?? null, }), }) export default class BudEsbuild extends Extension { diff --git a/sources/@roots/bud-esbuild/test/extension.test.ts b/sources/@roots/bud-esbuild/test/extension.test.ts index bd35c82feb..5ede52b054 100644 --- a/sources/@roots/bud-esbuild/test/extension.test.ts +++ b/sources/@roots/bud-esbuild/test/extension.test.ts @@ -115,7 +115,7 @@ describe(`@roots/bud-esbuild`, () => { ts: { loader: `tsx`, target: `es2015`, - tsconfigRaw: expect.any(Object), + tsconfig: expect.stringContaining(`tsconfig.json`), }, }) }) diff --git a/sources/@roots/bud-eslint/src/bud/commands/bud.eslint.command.tsx b/sources/@roots/bud-eslint/src/bud/commands/bud.eslint.command.tsx index 0f921fa69c..450761d9ef 100644 --- a/sources/@roots/bud-eslint/src/bud/commands/bud.eslint.command.tsx +++ b/sources/@roots/bud-eslint/src/bud/commands/bud.eslint.command.tsx @@ -17,7 +17,7 @@ export class BudEslintCommand extends BudCommand { public options = Option.Proxy({name: `eslint passthrough options`}) public override async execute() { - await this.makeBud(this) + await this.makeBud() await this.bud.run() const eslint = join( diff --git a/sources/@roots/bud-extensions/src/extensions/index.ts b/sources/@roots/bud-extensions/src/extensions/index.ts index 6209a16f35..796c6b43eb 100644 --- a/sources/@roots/bud-extensions/src/extensions/index.ts +++ b/sources/@roots/bud-extensions/src/extensions/index.ts @@ -9,6 +9,7 @@ import MiniCssExtractPlugin from './mini-css-extract-plugin/index.js' import BudTsConfigValues from './tsconfig-values/index.js' import WebpackDefinePlugin from './webpack-define-plugin/index.js' import WebpackHotModuleReplacementPlugin from './webpack-hot-module-replacement-plugin/index.js' +import BudWebpackLifecyclePlugin from './webpack-lifecycle-plugin/index.js' import WebpackManifestPlugin from './webpack-manifest-plugin/index.js' import WebpackProvidePlugin from './webpack-provide-plugin/index.js' @@ -24,6 +25,7 @@ export { MiniCssExtractPlugin, WebpackDefinePlugin, WebpackHotModuleReplacementPlugin, + BudWebpackLifecyclePlugin, WebpackManifestPlugin, WebpackProvidePlugin, } diff --git a/sources/@roots/bud-extensions/src/extensions/interpolate-html-webpack-plugin/index.ts b/sources/@roots/bud-extensions/src/extensions/interpolate-html-webpack-plugin/index.ts index 94220929e7..520228e4ab 100644 --- a/sources/@roots/bud-extensions/src/extensions/interpolate-html-webpack-plugin/index.ts +++ b/sources/@roots/bud-extensions/src/extensions/interpolate-html-webpack-plugin/index.ts @@ -17,7 +17,10 @@ export type {Options} * Interpolate html webpack plugin configuration */ @label(`@roots/bud-extensions/interpolate-html-webpack-plugin`) -@options({}) +@options({ + APP_TITLE: app => app.label, + NO_SCRIPT: `You need to enable JavaScript to run this app`, +}) @disabled export default class BudInterpolateHtmlExtension extends Extension< Options, @@ -41,11 +44,11 @@ export default class BudInterpolateHtmlExtension extends Extension< return new InterpolateHtmlWebpackPlugin( HTMLWebpackPlugin.Plugin.getHooks, { + ...(this.options ?? {}), ...(bud.extensions.get( `@roots/bud-extensions/webpack-define-plugin`, )?.options ?? {}), ...(bud.env.getPublicEnv() ?? {}), - ...(this.options ?? {}), }, ) } diff --git a/sources/@roots/bud-extensions/src/extensions/mini-css-extract-plugin/index.ts b/sources/@roots/bud-extensions/src/extensions/mini-css-extract-plugin/index.ts index ae12657d17..cb7859d085 100644 --- a/sources/@roots/bud-extensions/src/extensions/mini-css-extract-plugin/index.ts +++ b/sources/@roots/bud-extensions/src/extensions/mini-css-extract-plugin/index.ts @@ -1,6 +1,7 @@ import {join} from 'node:path' -import {Bud, Extension} from '@roots/bud-framework' +import type {Bud} from '@roots/bud-framework' +import {Extension} from '@roots/bud-framework/extension' import { label, options, diff --git a/sources/@roots/bud-extensions/src/extensions/webpack-hot-module-replacement-plugin/index.ts b/sources/@roots/bud-extensions/src/extensions/webpack-hot-module-replacement-plugin/index.ts index c8fe0f3262..e98e4588af 100644 --- a/sources/@roots/bud-extensions/src/extensions/webpack-hot-module-replacement-plugin/index.ts +++ b/sources/@roots/bud-extensions/src/extensions/webpack-hot-module-replacement-plugin/index.ts @@ -26,7 +26,7 @@ export default class BudHMR extends Extension< */ public override when(bud: Bud) { if (bud.isProduction) return false - if (bud.isCLI() && bud.context.args.hot === false) return false + if (bud.context.hot === false) return false return true } diff --git a/sources/@roots/bud-extensions/src/extensions/webpack-lifecycle-plugin/index.ts b/sources/@roots/bud-extensions/src/extensions/webpack-lifecycle-plugin/index.ts new file mode 100644 index 0000000000..403ac708d8 --- /dev/null +++ b/sources/@roots/bud-extensions/src/extensions/webpack-lifecycle-plugin/index.ts @@ -0,0 +1,164 @@ +import {Extension} from '@roots/bud-framework/extension' +import {bind, label} from '@roots/bud-framework/extension/decorators' +import type {Compilation, Compiler, StatsCompilation} from 'webpack' + +/** + * Webpack provide plugin configuration + */ +@label(`@roots/bud-extensions/webpack-lifecycle-plugin`) +export default class BudWebpackLifecyclePlugin extends Extension { + /** + * {@link Extension.apply} + */ + @bind + public override apply(compiler: Compiler) { + ;[ + `environment`, + `afterEnvironment`, + `afterResolvers`, + `afterPlugins`, + `compile`, + `failed`, + `invalid`, + `initialize`, + `shouldEmit`, + `thisCompilation`, + ] + .filter(k => compiler.hooks[k]) + .filter(k => this[k]) + .map(k => compiler.hooks[k].tap(this.label, this[k])) + ;[ + `additionalPass`, + `afterCompile`, + `afterEmit`, + `assetEmitted`, + `beforeCompile`, + `beforeRun`, + `emit`, + `done`, + `run`, + ] + .filter(k => compiler.hooks[k]) + .filter(k => this[k]) + .map(k => + compiler.hooks[k].tapPromise( + this.label, + async (...args: any[]) => { + try { + await this[k](...args) + } catch (error) { + this.logger.error(error) + } + }, + ), + ) + } + + @bind + public environment() {} + + @bind + public afterEnvironment() {} + + @bind + public afterPlugins() {} + + @bind + public failed(error: Error) { + this.logger.error(`compilation failed`) + + if ( + error.message.includes( + `Module not found: Error: Can't resolve 'index' in '${this.app.path( + `@src`, + )}'`, + ) && + !this.app.hooks.filter(`build.entry`) + ) { + this.logger.error( + `\n\nNo entrypoints found.\n\nEither create a file at ${this.app.relPath( + `@src`, + `index.js`, + )} or use the bud.entry method to specify an entrypoint.`, + ) + } + } + + @bind + public initialize() {} + + @bind + public invalid() {} + + @bind + public shouldEmit() { + // @ts-ignore + const emitCheck = this.app.context.dry !== true + emitCheck ? this.logger.success(`emit`) : this.logger.log(`dry run`) + return emitCheck + } + + @bind + public thisCompilation(compilation: Compilation) { + this.logger.time(`compile`) + } + + @bind + public compile(...compilationParams: any[]) {} + + @bind + public async additionalPass() {} + + @bind + public async afterCompile(compilation: Compilation) { + this.logger.log(`compilation completed:`, compilation.hash) + this.logger.timeEnd(`compile`) + } + + @bind + public async afterEmit(compilation: Compilation) { + this.logger.timeEnd(`emit`) + } + + @bind + public async assetEmitted( + file: string, + info: { + content: string + source: string + outputPath: string + compilation: Compilation + targetPath: string + }, + ) { + this.logger.log( + `asset emitted:`, + file, + `=>`, + this.app.relPath(info.targetPath), + ) + } + + @bind + public async beforeCompile(compilationParams: any) {} + + @bind + public async beforeRun(compiler: Compiler) { + this.logger.log(`beforeRun`, compiler.name) + } + + @bind + public async done(stats: StatsCompilation) { + this.logger.info(`done`) + } + + @bind + public async emit(compilation: Compilation) { + this.logger.time(`emit`) + } + + @bind + public async run(compiler: Compiler) { + this.logger.log(`run`, compiler.name) + } +} diff --git a/sources/@roots/bud-extensions/src/extensions/webpack-manifest-plugin/index.ts b/sources/@roots/bud-extensions/src/extensions/webpack-manifest-plugin/index.ts index ba72fe7b21..b2e81e3280 100644 --- a/sources/@roots/bud-extensions/src/extensions/webpack-manifest-plugin/index.ts +++ b/sources/@roots/bud-extensions/src/extensions/webpack-manifest-plugin/index.ts @@ -14,9 +14,8 @@ import {Plugin} from '@roots/bud-support/webpack-manifest-plugin' @label(`@roots/bud-extensions/webpack-manifest-plugin`) @expose(`manifest`) @plugin(Plugin) -@options({ +@options({ fileName: `manifest.json`, - writeToFileEmit: true, publicPath: ({hooks}: Bud) => (hooks.filter(`build.output.publicPath`) ?? ``).replace(`auto`, ``), }) diff --git a/sources/@roots/bud-extensions/src/service/index.ts b/sources/@roots/bud-extensions/src/service/index.ts index e1696ad2af..e8d5935415 100644 --- a/sources/@roots/bud-extensions/src/service/index.ts +++ b/sources/@roots/bud-extensions/src/service/index.ts @@ -75,14 +75,10 @@ export default class Extensions const {extensions, manifest} = bud.context if (manifest?.bud?.extensions) { - const { - discover: _discover, - discovery: _discovery, - allowlist, - denylist, - } = manifest.bud.extensions - - const discover = _discover ?? _discovery + const {allowlist, denylist} = manifest.bud.extensions + const discover = + manifest.bud.extensions.discover ?? + manifest.bud.extensions.discovery if (!isUndefined(discover)) this.options.set(`discover`, discover) if (!isUndefined(allowlist)) @@ -90,38 +86,57 @@ export default class Extensions if (!isUndefined(denylist)) this.options.merge(`denylist`, denylist) } - if (manifest?.[this.app.label]?.extensions) { - const {discover, allowlist, denylist} = - manifest[this.app.label].extensions + if (manifest?.bud?.[this.app.label]?.extensions) { + const {allowlist, denylist} = manifest.bud[this.app.label].extensions + + const discover = + manifest.bud[this.app.label].extensions.discover ?? + manifest.bud[this.app.label].extensions.discovery + if (!isUndefined(discover)) this.options.set(`discover`, discover) if (!isUndefined(allowlist)) - this.options.merge(`allowlist`, allowlist) - if (!isUndefined(denylist)) this.options.merge(`denylist`, denylist) + this.options.set(`allowlist`, (allowed = []) => [ + ...allowed, + ...allowlist, + ]) + if (!isUndefined(denylist)) + this.options.set(`denylist`, (denied = []) => [ + ...denied, + ...allowlist, + ]) } if ( - !isUndefined(extensions.builtIn) && + !isUndefined(extensions?.builtIn) && Array.isArray(extensions.builtIn) ) await Promise.all( - extensions.builtIn.filter(Boolean).map(this.import), + extensions.builtIn + .filter(Boolean) + .map( + async signifier => + await this.import(signifier, true, `@roots/bud`), + ), ) - if (bud.isCLI() && !isUndefined(bud.context.args.discover)) { - this.options.set(`discover`, bud.context.args.discover) + if (!isUndefined(bud.context.discover)) { + this.options.set(`discover`, bud.context.discover) } if ( this.options.is(`discover`, true) && this.options.isEmpty(`allowlist`) && - !isUndefined(extensions.discovered) && + !isUndefined(extensions?.discovered) && Array.isArray(extensions.discovered) ) await Promise.all( extensions.discovered .filter(Boolean) .filter(this.isAllowed) - .map(this.import), + .map( + async signifier => + await this.import(signifier, true, this.app.label), + ), ) else if (this.options.isNotEmpty(`allowlist`)) await Promise.all( @@ -129,7 +144,10 @@ export default class Extensions .get(`allowlist`) .filter(Boolean) .filter(this.isAllowed) - .map(this.import), + .map( + async (signifier: string) => + await this.import(signifier, true, this.app.label), + ), ) await this.runAll(`register`) @@ -244,36 +262,45 @@ export default class Extensions @bind public async import( signifier: string, - fatalOnError: boolean | number = true, + required: boolean | number = true, + context: string, ): Promise { - if (fatalOnError && this.unresolvable.has(signifier)) + if (required && this.unresolvable.has(signifier)) throw new Error(`Extension ${signifier} is not importable`) - this.logger.info(`importing`, signifier) - if (signifier.startsWith(`.`)) { signifier = this.app.path(signifier) this.logger.info(`path resolved to`, signifier) } if (this.has(signifier)) { - this.logger.info(signifier, `extension already imported`) - return + this.logger.info(signifier, `already imported`) + return this.get(signifier) } - const extensionClass: Extension = fatalOnError - ? await this.app.module.import(signifier, import.meta.url) - : await this.app.module.tryImport(signifier, import.meta.url) + let extension: Extension + + try { + this.logger.await(`import`, signifier) + extension = await this.app.module.import(signifier, import.meta.url) + this.logger.success(`import`, signifier) + } catch (error) { + this.unresolvable.add(signifier) + if (required) throw error + } - if (!extensionClass) return + if (!extension) return - const instance = await this.instantiate(extensionClass) + const instance = await this.instantiate(extension) if (instance.dependsOn) await Promise.all( Array.from(instance.dependsOn) .filter(dependency => !this.has(dependency)) - .map(async dependency => await this.import(dependency)), + .map( + async dependency => + await this.import(dependency, true, signifier), + ), ) if (this.options.is(`discover`, true) && instance.dependsOnOptional) @@ -286,7 +313,7 @@ export default class Extensions ) .filter(optionalDependency => !this.has(optionalDependency)) .map(async optionalDependency => { - await this.import(optionalDependency, false) + await this.import(optionalDependency, false, signifier) if (!this.has(optionalDependency)) this.unresolvable.add(optionalDependency) }), @@ -391,16 +418,17 @@ export default class Extensions extension: Extension | K, methodName: Contract.LifecycleMethods, ): Promise { - extension = + const instance: Extension = typeof extension === `string` ? this.get(extension) : extension - if (extension.dependsOn) { - await Array.from(extension.dependsOn) + if (instance.dependsOn) { + await Array.from(instance.dependsOn) .filter(this.isAllowed) .filter((signifier: K) => !this.unresolvable.has(signifier)) .reduce(async (promised, signifier: K) => { await promised - if (!this.has(signifier)) await this.import(signifier) + if (!this.has(signifier)) + await this.import(signifier, true, instance.label) if ( this.get(signifier) && @@ -410,13 +438,14 @@ export default class Extensions }, Promise.resolve()) } - if (this.options.is(`discover`, true) && extension.dependsOnOptional) - await Array.from(extension.dependsOnOptional) + if (this.options.is(`discover`, true) && instance.dependsOnOptional) + await Array.from(instance.dependsOnOptional) .filter(this.isAllowed) .filter((signifier: K) => !this.unresolvable.has(signifier)) .reduce(async (promised, signifier: K) => { await promised - if (!this.has(signifier)) await this.import(signifier, false) + if (!this.has(signifier)) + await this.import(signifier, false, instance.label) if (!this.has(signifier)) { this.unresolvable.add(signifier) return diff --git a/sources/@roots/bud-extensions/src/service/util/isConstructor.ts b/sources/@roots/bud-extensions/src/service/util/isConstructor.ts index 98d4f828fd..9178c76461 100644 --- a/sources/@roots/bud-extensions/src/service/util/isConstructor.ts +++ b/sources/@roots/bud-extensions/src/service/util/isConstructor.ts @@ -1,3 +1,9 @@ +/** + * isConstructor + * + * @param subject Any js entity + * @returns true if subject is a constructor + */ export const isConstructor = ( subject: any, ): subject is new (...args: any[]) => any => { diff --git a/sources/@roots/bud-extensions/src/types.ts b/sources/@roots/bud-extensions/src/types.ts index 7b31fca22c..3b7cf7ef03 100644 --- a/sources/@roots/bud-extensions/src/types.ts +++ b/sources/@roots/bud-extensions/src/types.ts @@ -11,6 +11,7 @@ import type MiniCssExtractPlugin from './extensions/mini-css-extract-plugin/inde import type BudTsConfigValues from './extensions/tsconfig-values/index.js' import type WebpackDefinePlugin from './extensions/webpack-define-plugin/index.js' import type WebpackHotModuleReplacementPlugin from './extensions/webpack-hot-module-replacement-plugin/index.js' +import type BudWebpackLifecyclePlugin from './extensions/webpack-lifecycle-plugin/index.js' import type WebpackManifestPlugin from './extensions/webpack-manifest-plugin/index.js' import type WebpackProvidePlugin from './extensions/webpack-provide-plugin/index.js' @@ -34,6 +35,7 @@ declare module '@roots/bud-framework' { '@roots/bud-extensions/tsconfig-values': BudTsConfigValues '@roots/bud-extensions/webpack-define-plugin': WebpackDefinePlugin '@roots/bud-extensions/webpack-hot-module-replacement-plugin': WebpackHotModuleReplacementPlugin + '@roots/bud-extensions/webpack-lifecycle-plugin': BudWebpackLifecyclePlugin '@roots/bud-extensions/webpack-manifest-plugin': WebpackManifestPlugin '@roots/bud-extensions/webpack-provide-plugin': WebpackProvidePlugin } diff --git a/sources/@roots/bud-extensions/test/extensions/webpack-hot-module-replacement.test.ts b/sources/@roots/bud-extensions/test/extensions/webpack-hot-module-replacement.test.ts index b92d10159b..57f2c8838c 100644 --- a/sources/@roots/bud-extensions/test/extensions/webpack-hot-module-replacement.test.ts +++ b/sources/@roots/bud-extensions/test/extensions/webpack-hot-module-replacement.test.ts @@ -3,16 +3,16 @@ import {Extension} from '@roots/bud-framework/extension' import webpack from '@roots/bud-support/webpack' import {describe, expect, it, test, vi} from 'vitest' -import hmrExtension from '../../src/extensions/webpack-hot-module-replacement-plugin/index.js' +import HmrExtension from '../../src/extensions/webpack-hot-module-replacement-plugin/index.js' describe(`webpack-hot-module-replacement-plugin`, () => { it(`is an instance of Extension`, () => { - expect(hmrExtension).toBeInstanceOf(Function) + expect(HmrExtension).toBeInstanceOf(Function) }) it(`is an instance of Extension`, async () => { const bud = await factory() - const extension = new hmrExtension(bud) + const extension = new HmrExtension(bud) expect(extension).toBeInstanceOf(Extension) }) @@ -21,7 +21,7 @@ describe(`webpack-hot-module-replacement-plugin`, () => { const bud = await factory({mode: `production`}) // @ts-ignore - const extension = new hmrExtension(bud) + const extension = new HmrExtension(bud) expect(bud.mode).toBe(`production`) expect(await extension.isEnabled()).toBe(false) }) @@ -33,15 +33,15 @@ describe(`webpack-hot-module-replacement-plugin`, () => { expect(bud.mode).toBe(`development`) // @ts-ignore - const extension = new hmrExtension(bud) + const extension = new HmrExtension(bud) expect(await extension.isEnabled()).toBe(true) }) it(`produces webpack hmr plugin`, async () => { const bud = await factory({mode: `development`}) - const extension = new hmrExtension(bud) + const extension = new HmrExtension(bud) - expect(await extension.make(bud)).toBeInstanceOf( + expect(await extension.make()).toBeInstanceOf( webpack.HotModuleReplacementPlugin, ) }) diff --git a/sources/@roots/bud-extensions/test/service/__snapshots__/index.test.ts.snap b/sources/@roots/bud-extensions/test/service/__snapshots__/index.test.ts.snap index 1c8f677c6e..f2becbd39c 100644 --- a/sources/@roots/bud-extensions/test/service/__snapshots__/index.test.ts.snap +++ b/sources/@roots/bud-extensions/test/service/__snapshots__/index.test.ts.snap @@ -14,6 +14,7 @@ exports[`@roots/bud-extensions > bud.extensions.repository options should match "@roots/bud-extensions/tsconfig-values", "@roots/bud-extensions/webpack-define-plugin", "@roots/bud-extensions/webpack-hot-module-replacement-plugin", + "@roots/bud-extensions/webpack-lifecycle-plugin", "@roots/bud-extensions/webpack-manifest-plugin", "@roots/bud-extensions/webpack-provide-plugin", "@roots/bud-postcss", diff --git a/sources/@roots/bud-extensions/test/service/index.test.ts b/sources/@roots/bud-extensions/test/service/index.test.ts index a739c7e542..c2d4df8b26 100644 --- a/sources/@roots/bud-extensions/test/service/index.test.ts +++ b/sources/@roots/bud-extensions/test/service/index.test.ts @@ -38,6 +38,7 @@ describe(`@roots/bud-extensions`, () => { const instance = extensions.get(`mock_extension` as keyof Modules) if (!instance) throw new Error(`Extension not found`) + if (!(`label` in instance)) throw new Error(`Label not found`) expect(instance.label).toBe(`mock_extension`) diff --git a/sources/@roots/bud-extensions/tsconfig.json b/sources/@roots/bud-extensions/tsconfig.json index 0464806a3c..bc1646732f 100644 --- a/sources/@roots/bud-extensions/tsconfig.json +++ b/sources/@roots/bud-extensions/tsconfig.json @@ -1,14 +1,10 @@ { "extends": "../../../config/tsconfig.json", "compilerOptions": { - "paths": { - "@roots/bud-extensions/*": ["./src/*"] - }, "rootDir": "./src", "outDir": "./lib" }, - "include": ["./src"], - "exclude": ["./lib", "./node_modules", "**/*.test.ts"], + "include": ["src"], "references": [ {"path": "../bud-framework/tsconfig.json"}, {"path": "../bud-support/tsconfig.json"}, diff --git a/sources/@roots/bud-framework/package.json b/sources/@roots/bud-framework/package.json index 41cbacbcc5..8dedb43f5f 100644 --- a/sources/@roots/bud-framework/package.json +++ b/sources/@roots/bud-framework/package.json @@ -59,6 +59,10 @@ "import": "./lib/bud.js", "default": "./lib/bud.js" }, + "./extension": { + "import": "./lib/extension/index.js", + "default": "./lib/extension/index.js" + }, "./extension/decorators": { "import": "./lib/extension/decorators/index.js", "default": "./lib/extension/decorators/index.js" diff --git a/sources/@roots/bud-framework/src/bud.ts b/sources/@roots/bud-framework/src/bud.ts index d6f3ee1032..69ca54d387 100644 --- a/sources/@roots/bud-framework/src/bud.ts +++ b/sources/@roots/bud-framework/src/bud.ts @@ -21,10 +21,7 @@ export class Bud { /** * Context */ - public context: - | Options.CommandContext - | Options.CLIContext - | Options.Context + public context: Options.Context /** * Implementation @@ -92,13 +89,6 @@ export class Bud { return this.root.label !== this.label } - /** - * True when current instance has context set by CLI - */ - public isCLI(): this is Bud & {context: Options.CommandContext} { - return `args` in this.context - } - /** * {@link Bud} instances */ @@ -224,9 +214,8 @@ export class Bud { : {...this.context, ...request, root: this} if ( - this.isCLI() && - !isUndefined(this.context.args.filter) && - !this.context.args.filter.includes(context.label) + !isUndefined(this.context.filter) && + !this.context.filter.includes(context.label) ) { this.log( `skipping child instance based on --filter flag:`, diff --git a/sources/@roots/bud-framework/src/configuration/index.ts b/sources/@roots/bud-framework/src/configuration/index.ts index 26f91c25f4..263aa56cc7 100644 --- a/sources/@roots/bud-framework/src/configuration/index.ts +++ b/sources/@roots/bud-framework/src/configuration/index.ts @@ -8,6 +8,9 @@ import Configuration from './configuration.js' * Process configurations */ export const process = async (app: Bud) => { + const configuration = new Configuration(app) + const configs = Object.values(app.context.files).filter(({bud}) => bud) + const findConfigs = (ofType: string, isLocal: boolean) => sortBy( configs @@ -16,9 +19,6 @@ export const process = async (app: Bud) => { `name`, ) - const configuration = new Configuration(app) - const configs = Object.values(app.context.files).filter(({bud}) => bud) - // process any queued api calls await app.api.processQueue() diff --git a/sources/@roots/bud-framework/src/extension/decorators/development.test.ts b/sources/@roots/bud-framework/src/extension/decorators/development.test.ts index 889b86e29c..5f76d4fbeb 100644 --- a/sources/@roots/bud-framework/src/extension/decorators/development.test.ts +++ b/sources/@roots/bud-framework/src/extension/decorators/development.test.ts @@ -3,8 +3,8 @@ import {beforeEach, describe, expect, it} from 'vitest' import {development} from './development' -@development // @ts-ignore +@development class TestClass { public app: Bud public constructor(bud: Bud) { diff --git a/sources/@roots/bud-framework/src/extension/index.ts b/sources/@roots/bud-framework/src/extension/index.ts index c89e1afaff..5af6ae93e4 100644 --- a/sources/@roots/bud-framework/src/extension/index.ts +++ b/sources/@roots/bud-framework/src/extension/index.ts @@ -4,6 +4,7 @@ import get from '@roots/bud-support/lodash/get' import isFunction from '@roots/bud-support/lodash/isFunction' import isUndefined from '@roots/bud-support/lodash/isUndefined' import set from '@roots/bud-support/lodash/set' +import type {Compiler} from 'webpack' import type {Bud} from '../bud.js' import type {Modules} from '../index.js' @@ -26,7 +27,7 @@ export interface ApplyPlugin { /** * @see {@link https://webpack.js.org/contribute/writing-a-plugin/#basic-plugin-architecture} */ - apply: (...args: any[]) => unknown + apply?(...args: any[]): unknown } export interface Constructor { @@ -92,8 +93,16 @@ export class Extension< /** * {@link ApplyPlugin.apply} */ - public declare apply?: ApplyPlugin[`apply`] + public apply?(compiler: Compiler): unknown + /** + * Is extension enabled + * + * @remarks + * The following methods are skipped if `enabled` is false: + * - {@link Extension.buildBefore} + * - {@link Extension.make} + */ public enabled: boolean = true /** @@ -127,7 +136,7 @@ export class Extension< /** * The module name */ - public label: keyof Modules & string + public label: `${keyof Modules & string}` /** * Logger instance @@ -513,7 +522,7 @@ export class Extension< try { const result = await this.app.module.import(signifier, context) this.logger.success(`imported`, signifier) - return result?.default ?? result ?? undefined + return result } catch (error) { throw new ImportError(`could not import ${signifier}`, { props: { diff --git a/sources/@roots/bud-framework/src/lifecycle/init.ts b/sources/@roots/bud-framework/src/lifecycle/init.ts index de51145b26..d23edcb29d 100644 --- a/sources/@roots/bud-framework/src/lifecycle/init.ts +++ b/sources/@roots/bud-framework/src/lifecycle/init.ts @@ -12,7 +12,7 @@ import type {Bud} from '../bud.js' export const initialize = (bud: Bud): Bud => bud.hooks .fromMap({ - 'feature.hash': () => false, + 'feature.hash': () => bud.context.hash, }) .hooks.fromMap({ 'pattern.js': /\.(mjs|jsx?)$/, @@ -37,19 +37,16 @@ export const initialize = (bud: Bud): Bud => 'pattern.json5': /\.json5$/, }) .hooks.fromMap({ - 'location.@src': - bud.isCLI() && isString(bud.context.args.input) - ? bud.context.args.input - : `src`, - 'location.@dist': - bud.isCLI() && isString(bud.context.args.output) - ? bud.context.args.output - : `dist`, + 'location.@src': isString(bud.context.input) + ? bud.context.input + : `src`, + 'location.@dist': isString(bud.context.output) + ? bud.context.output + : `dist`, 'location.@storage': paths.get(bud.context.basedir)[`storage`], - 'location.@modules': - bud.isCLI() && isString(bud.context.args.modules) - ? bud.context.args.modules - : `node_modules`, + 'location.@modules': isString(bud.context.modules) + ? bud.context.modules + : `node_modules`, 'location.@os-cache': paths.get(bud.context.basedir)[`os-cache`], 'location.@os-config': paths.get(bud.context.basedir)[`os-config`], 'location.@os-data': paths.get(bud.context.basedir)[`os-data`], diff --git a/sources/@roots/bud-framework/src/methods/close.ts b/sources/@roots/bud-framework/src/methods/close.ts index e7e5863f5e..4f3de692cd 100644 --- a/sources/@roots/bud-framework/src/methods/close.ts +++ b/sources/@roots/bud-framework/src/methods/close.ts @@ -20,7 +20,7 @@ export interface close { * bud.close() * ``` */ -export function close(onComplete?: any) { +export function close(onComplete?: () => unknown) { const application = this as Bud try { diff --git a/sources/@roots/bud-framework/src/module.ts b/sources/@roots/bud-framework/src/module.ts index f8e50bffad..f19e597f07 100644 --- a/sources/@roots/bud-framework/src/module.ts +++ b/sources/@roots/bud-framework/src/module.ts @@ -5,9 +5,11 @@ import {fileURLToPath, pathToFileURL} from 'node:url' import {bind} from '@roots/bud-support/decorators/bind' import {ImportError} from '@roots/bud-support/errors' import {resolve} from '@roots/bud-support/import-meta-resolve' +import get from '@roots/bud-support/lodash/get' +import set from '@roots/bud-support/lodash/set' import args from '@roots/bud-support/utilities/args' import logger from '@roots/bud-support/utilities/logger' -import * as paths from '@roots/bud-support/utilities/paths' +import {paths} from '@roots/bud-support/utilities/paths' import type {Bud} from './bud.js' import {Service} from './service.js' @@ -30,10 +32,7 @@ export class Module extends Service { * Cache location */ public get cacheLocation(): string { - return join( - paths.get(this.app.context.basedir).storage, - `resolutions.yml`, - ) + return join(paths.storage, `resolutions.yml`) } /** @@ -58,16 +57,17 @@ export class Module extends Service { public override async bootstrap(bud: Bud) { this.require = createRequire(this.makeContextURL(bud.context.basedir)) - if (this.cacheEnabled && !!(await bud.fs.exists(this.cacheLocation))) { - logger - .scope(`module`) - .info(`cache is enabled and cached resolutions exist`) - + if (this.cacheEnabled && (await bud.fs.exists(this.cacheLocation))) { try { const data = await bud.fs.read(this.cacheLocation) + logger + .scope(`module`) + .info(`cache is enabled and cached resolutions exist`) + .info(data) + if (data.version && data.version === bud.context?.bud?.version) { - this.resolved = data.resolutions - this.cacheValid = true + set(this, `resolved`, data.resolutions) + set(this, `cacheValid`, true) return } } catch (e) { @@ -75,7 +75,7 @@ export class Module extends Service { } } - this.cacheValid = false + set(this, `cacheValid`, false) } /** @@ -84,7 +84,7 @@ export class Module extends Service { @bind public async getDirectory(signifier: string, context?: string) { return await this.resolve(signifier, context) - .then(path => relative(context ?? this.app.context.basedir, path)) + .then(path => relative(this.app.context.basedir, path)) .then(path => path.split(signifier).shift()) .then(path => this.app.path(path as any, signifier)) } @@ -118,59 +118,45 @@ export class Module extends Service { signifier: string, context?: string, ): Promise { + let errors = [] + if (signifier in this.resolved) { logger .scope(`module`) - .info( - `resolved ${signifier} to ${relative( - this.app.context.basedir, - this.resolved[signifier], - )} from cache`, - ) + .info(`[cache hit] ${signifier} => ${this.resolved[signifier]}`) - return this.resolved[signifier] + return get(this.resolved, [signifier]) } - logger.scope(`module`).info(`resolving ${signifier} from ${context}`) - - let errors = [] + logger.scope(`module`).info(`resolving`, signifier) try { const path = await resolve(signifier, this.makeContextURL()) - const normal = normalize(fileURLToPath(path)) + set(this.resolved, [signifier], normalize(fileURLToPath(path))) + logger .scope(`module`) .info( `[cache miss]`, - `resolved ${signifier} to ${relative( - this.app.context.basedir, - normal, - )}`, + `resolved ${signifier} to ${get(this.resolved, [signifier])}`, ) - this.resolved[signifier] = normal - return this.resolved[signifier] + return get(this.resolved, [signifier]) } catch (err) { errors.push(err.toString()) } try { - const path = await resolve(signifier, this.makeContextURL(context)) - - const normal = normalize(fileURLToPath(path)) - + const path = await resolve(signifier, this.makeContextURL()) + set(this.resolved, [signifier], normalize(fileURLToPath(path))) logger .scope(`module`) .info( `[cache miss]`, - `resolved ${signifier} to ${relative( - this.app.context.basedir, - normal, - )}`, + `resolved ${signifier} to ${get(this.resolved, [signifier])}`, ) - this.resolved[signifier] = normal - return this.resolved[signifier] + return get(this.resolved, [signifier]) } catch (err) { errors.push(err.toString()) } @@ -190,46 +176,31 @@ export class Module extends Service { signifier: string, context?: string, ): Promise { + if (signifier in this.resolved) { + const m = await import(get(this.resolved, [signifier])) + return m?.default ?? m + } + try { - const modulePath = await this.resolve(signifier, context) - const result = await import(modulePath) - logger.scope(`module`).info(`imported ${signifier}`) + const path = await this.resolve(signifier, context) + const result = await import(path) + logger.scope(`module`).info(`imported`, signifier) return result?.default ?? result - } catch (cause) { - throw new ImportError(`could not resolve ${signifier}`, { + } catch (error) { + throw new ImportError(`could not import ${signifier}`, { props: { - details: `Could not resolve/import ${signifier}. Context: ${context}`, - origin: cause, + details: `Could not import ${signifier}`, + origin: error, }, }) } } - /** - * Import a module from its signifier - */ - @bind - public async tryImport( - signifier: string, - context?: string, - ): Promise | undefined | null { - try { - const modulePath = await this.resolve(signifier, context) - const result = await import(modulePath) - logger.scope(`module`).info(`imported ${signifier} (optional)`) - return result?.default ?? result - } catch (err) { - logger - .scope(`module`) - .info(`${signifier} could not be imported (optional)`, err) - } - } - /** * Make context URL */ @bind - protected makeContextURL(context?: string): string { + public makeContextURL(context?: string): string { return ( context ?? (pathToFileURL( diff --git a/sources/@roots/bud-framework/src/service.ts b/sources/@roots/bud-framework/src/service.ts index bf0987f39f..dfeef85152 100644 --- a/sources/@roots/bud-framework/src/service.ts +++ b/sources/@roots/bud-framework/src/service.ts @@ -2,11 +2,6 @@ import camelCase from '@roots/bud-support/lodash/camelCase' import Container from '@roots/container' import type {Bud} from './index.js' -import type { - CLIContext, - CommandContext, - Context, -} from './types/options/context.js' import type {Logger} from './types/services/logger/index.js' interface Contract { @@ -15,7 +10,7 @@ interface Contract { /** * Bud instance */ - app: Bud & {context: CommandContext | CLIContext | Context} + app: Bud /** * Scoped logger @@ -33,7 +28,7 @@ interface Contract { * @remarks * `bootstrap` is called when the Service is instantiated (but before all services are guaranteed to be instantiated). */ - bootstrap(app: Bud): Promise + bootstrap(app: Bud): Promise /** * Lifecycle method: register @@ -56,27 +51,27 @@ interface Contract { /** * After config callback */ - configAfter?(app?: Bud): Promise + configAfter?(app?: Bud): Promise /** * Before build service */ - buildBefore?(app?: Bud): Promise + buildBefore?(app?: Bud): Promise /** * After build service */ - buildAfter?(app?: Bud): Promise + buildAfter?(app?: Bud): Promise /** * Before Compiler service */ - compilerBefore?(app?: Bud): Promise + compilerBefore?(app?: Bud): Promise /** * After Compiler service */ - compilerAfter?(app?: Bud): Promise + compilerAfter?(app?: Bud): Promise } /** @@ -92,9 +87,7 @@ abstract class Base implements Partial { * Bud instance * @readonly */ - public get app(): Bud & { - context: CommandContext | CLIContext | Context - } { + public get app(): Bud { return this._app() } @@ -149,27 +142,27 @@ abstract class Base implements Partial { /** * After config callback */ - public configAfter?(app?: Bud): Promise + public configAfter?(app?: Bud): Promise /** * Before build service */ - public buildBefore?(app?: Bud): Promise + public buildBefore?(app?: Bud): Promise /** * After build service */ - public buildAfter?(app?: Bud): Promise + public buildAfter?(app?: Bud): Promise /** * Before Compiler service */ - public compilerBefore?(app?: Bud): Promise + public compilerBefore?(app?: Bud): Promise /** * After Compiler service */ - public compilerAfter?(app?: Bud): Promise + public compilerAfter?(app?: Bud): Promise /** * Class constructor @@ -193,9 +186,7 @@ abstract class BaseContainer * Bud instance * @readonly */ - public get app(): Bud & { - context: CommandContext | CLIContext | Context - } { + public get app(): Bud { return this._app() } diff --git a/sources/@roots/bud-framework/src/services/console.test.ts b/sources/@roots/bud-framework/src/services/console.test.ts index d912601a2f..b7573eded3 100644 --- a/sources/@roots/bud-framework/src/services/console.test.ts +++ b/sources/@roots/bud-framework/src/services/console.test.ts @@ -12,8 +12,7 @@ describe(`ConsoleBuffer`, () => { }) it(`should not patch the console in ci`, async () => { - if (!bud.isCLI()) throw new Error(`bud.isCLI() returned false`) - bud.context.args.ci = true + bud.context.ci = true await consoleBuffer.register(bud) expect(consoleBuffer.restore).not.toBeDefined() diff --git a/sources/@roots/bud-framework/src/services/console.ts b/sources/@roots/bud-framework/src/services/console.ts index 2facb8b5be..d666297f9e 100644 --- a/sources/@roots/bud-framework/src/services/console.ts +++ b/sources/@roots/bud-framework/src/services/console.ts @@ -13,7 +13,7 @@ type MessagesCache = Array<{ }> /** - * ConsoleBuffer service class + * ConsoleBuffer {@link Service} * * @remarks * Intercepts console function calls and emits them using the bud logger. @@ -39,6 +39,12 @@ export default class ConsoleBuffer extends Service { */ public restore?: () => any + /** + * Fetch and remove + * + * @remarks + * Transfers messages from {@link ConsoleBuffer.queue} to {@link ConsoleBuffer.stack} + */ public fetchAndRemove() { const queue = [...this.queue] this.queue = [] @@ -47,11 +53,11 @@ export default class ConsoleBuffer extends Service { } /** - * {@link Extension.register} + * {@link Service.register} */ @bind public override async register(bud: Bud) { - if (!bud.isCLI() || (bud.isCLI() && bud.context.args?.ci)) return + if (bud.context.ci) return // Patch the console, and assign the restore function this.restore = patchConsole((stream, data) => { diff --git a/sources/@roots/bud-framework/src/services/fs.ts b/sources/@roots/bud-framework/src/services/fs.ts index f5863c5517..95dd592733 100644 --- a/sources/@roots/bud-framework/src/services/fs.ts +++ b/sources/@roots/bud-framework/src/services/fs.ts @@ -7,45 +7,47 @@ import isUndefined from '@roots/bud-support/lodash/isUndefined' import {S3} from '@roots/filesystem/s3' import type {Bud} from '../bud.js' -import type * as Service from '../service.js' +import type {Contract} from '../service.js' /** - * Filesystem service + * {@link Filesystem} service */ -export default class FS extends Filesystem implements Service.Contract { +export default class FS extends Filesystem implements Contract { /** - * Service identifier + * {@link Contract.label} */ public label = `fs` /** - * Access {@link Bud} - * - * @public @readonly + * {@link Contract.app} */ public get app(): Bud { return this._app() } /** - * Logger instance + * {@link Contract.logger} */ public get logger() { return this.app.context.logger.scope(`fs`) } /** - * JSON handling + * JSON + * + * @see {@link https://bud.js.org/docs/bud.fs/json} */ public json: typeof json = json /** - * S3 instance + * S3 + * + * @see {@link https://bud.js.org/docs/bud.fs/s3} */ public s3: S3 /** - * YML handling + * @see {@link https://bud.js.org/docs/bud.fs/yml} */ public yml: typeof yml = yml @@ -57,9 +59,9 @@ export default class FS extends Filesystem implements Service.Contract { } /** - * {@link Service.Contract.bootstrap} + * {@link Contract.bootstrap} */ - public async bootstrap(bud: Bud) { + public async register(bud: Bud) { try { const s3IsResolvable = await bud.module.resolve( `@aws-sdk/client-s3`, @@ -74,12 +76,12 @@ export default class FS extends Filesystem implements Service.Contract { } /** - * Fulfills {@link Service.Contract.register} + * Fulfills {@link Contract.bootstrap} */ - public async register() {} + public async bootstrap() {} /** - * Fulfills {@link Service.Contract.boot} + * Fulfills {@link Contract.boot} */ public async boot() {} @@ -87,6 +89,8 @@ export default class FS extends Filesystem implements Service.Contract { * Set bucket * * @param bucket - {@link S3.bucket} + * + * @see {@link https://bud.js.org/docs/bud.fs/s3#setup} */ public setBucket(bucket: string) { if (!this.s3) this.throwS3Error() @@ -102,6 +106,8 @@ export default class FS extends Filesystem implements Service.Contract { * Set credentials * * @param credentials - {@link S3.credentials} + * + * @see {@link https://bud.js.org/docs/bud.fs/s3#setup} */ public setCredentials(credentials: S3[`config`][`credentials`]) { if (!this.s3) this.throwS3Error() @@ -116,7 +122,9 @@ export default class FS extends Filesystem implements Service.Contract { /** * Set endpoint * - * @param options - upload options + * @param endpoint - S3 endpoint + * + * @see {@link https://bud.js.org/docs/bud.fs/s3#setup} */ public setEndpoint(endpoint: S3[`config`][`endpoint`]) { if (!this.s3) this.throwS3Error() @@ -131,7 +139,9 @@ export default class FS extends Filesystem implements Service.Contract { /** * Set S3 region * - * @param options - upload options + * @param region - S3 region + * + * @see {@link https://bud.js.org/docs/bud.fs/s3#setup} */ public setRegion(region: S3[`config`][`region`]) { if (!this.s3) this.throwS3Error() @@ -147,6 +157,8 @@ export default class FS extends Filesystem implements Service.Contract { * Upload files to S3 * * @param options - upload options + * + * @see {@link https://bud.js.org/docs/bud.fs/s3#uploading-files} */ public upload(options?: { source?: string @@ -258,6 +270,9 @@ export default class FS extends Filesystem implements Service.Contract { return this } + /** + * Throw S3 error + */ @bind public throwS3Error() { const dependencies = { diff --git a/sources/@roots/bud-framework/src/services/notifier/index.test.ts b/sources/@roots/bud-framework/src/services/notifier/index.test.ts index fa2fbd9f16..c997b5fe2b 100644 --- a/sources/@roots/bud-framework/src/services/notifier/index.test.ts +++ b/sources/@roots/bud-framework/src/services/notifier/index.test.ts @@ -12,7 +12,7 @@ describe(`notifier`, () => { vi.mock('open', () => ({default: vi.fn()})) vi.mock('open-editor', () => ({default: vi.fn()})) - bud = await factory({args: {notify: true}}) + bud = await factory({notify: true}) notifier = new Notifier(() => bud) await notifier.boot(bud) notifier.notify = vi.fn() diff --git a/sources/@roots/bud-framework/src/services/notifier/notifier.ts b/sources/@roots/bud-framework/src/services/notifier/notifier.ts index 5b9faf6c00..d842b07b06 100644 --- a/sources/@roots/bud-framework/src/services/notifier/notifier.ts +++ b/sources/@roots/bud-framework/src/services/notifier/notifier.ts @@ -66,21 +66,21 @@ export class Notifier extends Service { * True if notifications are enabled */ public get notificationsEnabled(): boolean { - return this.app.isCLI() && this.app?.context.args.notify === true + return this.app?.context.notify === true } /** * True if editor opening is enabled */ public get openEditorEnabled(): boolean { - return this.app.isCLI() && this.app?.context.args.editor === true + return this.app?.context.editor === true } /** * True if browser opening is enabled */ public get openBrowserEnabled(): boolean { - return this.app.isCLI() && this.app?.context.args.browser === true + return this.app?.context.browser === true } /** diff --git a/sources/@roots/bud-framework/src/types/options/context.ts b/sources/@roots/bud-framework/src/types/options/context.ts index deb321858a..b0c2d60722 100644 --- a/sources/@roots/bud-framework/src/types/options/context.ts +++ b/sources/@roots/bud-framework/src/types/options/context.ts @@ -1,114 +1,583 @@ -import type {Readable, Writable} from 'node:stream' - import type {Bud} from '../../bud.js' import type {Logger} from '../services/logger/index.js' +/** + * Bud context object + * + * @remarks + * The context object is constructing during bootstrapping. You shouldn't modify + * or reference this object unless you know what you're doing and are okay with updating + * your code when the context object changes between releases. + * + * @internal + */ export interface Context { - label: string + /** + * All received positional arguments + */ + _?: Record + + /** + * Root bud.js instance + * + * @remarks + * In the case of a nested bud.js instance, this will be the parent bud.js instance. + */ + root?: Bud + + /** + * Bud.js instance dependencies + * + * @remarks + * In the case of a nested bud.js instance, this will + * be an array of {@link Bud.label} referencing the parent and previously + * defined child instances. + */ + dependsOn?: Array + + /** + * The instance label + * + * @remarks + * Can be accessed via {@link Bud.label} + */ + label?: string + + /** + * The instance base directory + * + * @remarks + * Set with `--basedir` or `--cwd` CLI flags + */ basedir: string + + /** + * The build mode + * + * @remarks + * Set with `--mode` CLI flag. + */ mode: 'development' | 'production' - bud: Record + + /** + * Information on the installed version of bud.js + */ + bud: { + /** + * The name of the bud.js implementation, without the package namespace (e.g. `bud` from `@roots/bud`) + */ + label: string + /** + * Path to the bud.js package.json file + */ + manifestPath: string + /** + * The version of the bud.js implementation + */ + version: string + } + /** + * Config files and directories + * + * @remarks + * Files are sourced from the {@link Context.basedir} and `./config` directories. + */ files: Record - env: Record + + /** + * Environment variable data + * + * @remarks + * Can be accessed with {@link Bud.env}, which reads from these records. + */ + env: Record + + /** + * Extension data + * + * @remarks + * Can be accessed with {@link Bud.extensions} which reads from these records. + */ extensions: { + /** + * Built-in extensions (always loaded) + */ builtIn: Array + /** + * Discovered extensions (loaded unless `--no-discover` flag is set) + */ discovered: Array } + /** + * The application manifest + * + * @remarks + * Sourced from the `package.json` file in the {@link Context.basedir} directory. + */ manifest: Record + /** + * bud.js {@link Service} instances + * + * @remarks + * Services from this array are imported and instantiated during {@link Bud.lifecycle} + */ services: Array + /** + * Logger instance + * + * @remarks + * Can be accessed with {@link Bud.log}, {@link Bud.info}, etc. + */ logger: Logger - root?: Bud | undefined - dependsOn?: Array | undefined -} -export interface CLIContext extends Context { - stdin: Readable - stdout: Writable - stderr: Writable - colorDepth: number -} + /** + * Initial paths + * + * @remarks + * Use {@link Bud.path} and {@link Bud.setPath} to interact with bud.js paths. + * + * @remarks + * This is a record of paths that are set by default or by flags/env. It is not + * necesssarily a complete or accurate representation of the paths that are + * available to bud.js. + */ + paths?: { + /** + * Hash of paths + */ + hash: string + + /** + * Base directory for all paths + */ + basedir: string + + /** + * Directory for temporary files + * + * @default os-cache + */ + storage: string + + /** + * OS reported directory for cache files + */ + [`os-data`]: string + + /** + * OS reported directory for configuration files + */ + [`os-config`]: string + + /** + * OS reported directory for cache files + */ + [`os-cache`]: string + + /** + * OS reported directory for log files + */ + [`os-log`]: string + + /** + * OS reported directory for temporary files + */ + [`os-temp`]: string + } + + /** + * Open application in browser when a development build is ran + * + * @remarks + * Set with `--browser` CLI flag. + */ + browser?: string | boolean + /** + * Cache option + * + * @remarks + * Can be accessed with {@link Bud.cache} + * Set with `--cache` CLI flag. + */ + cache?: `filesystem` | `memory` | true | false + /** + * CI option + * + * @remarks + * Set with `--ci` CLI flag. + */ + ci?: boolean + /** + * Clean option + * + * @remarks + * Set with `--clean` CLI flag. + */ + clean?: boolean + /** + * Debug option + * + * @remarks + * Set with `--debug` CLI flag. + */ + debug?: boolean + /** + * Devtool option + * + * @remarks + * Set with `--devtool` CLI flag. + */ + devtool?: + | false + | `eval` + | `eval-cheap-source-map` + | `eval-cheap-module-source-map` + | `eval-source-map` + | `cheap-source-map` + | `cheap-module-source-map` + | `source-map` + | `inline-cheap-source-map` + | `inline-cheap-module-source-map` + | `inline-source-map` + | `eval-nosources-cheap-source-map` + | `eval-nosources-cheap-modules-source-map` + | `eval-nosources-source-map` + | `inline-nosources-cheap-source-map` + | `inline-nosources-cheap-module-source-map` + | `inline-nosources-source-map` + | `nosources-cheap-source-map` + | `nosources-cheap-module-source-map` + | `hidden-nosources-cheap-source-map` + | `hidden-nosources-cheap-module-source-map` + | `hidden-nosources-source-map` + | `hidden-cheap-source-map` + | `hidden-cheap-module-source-map` + | `hidden-source-map` + + /** + * Discover option + * + * @remarks + * Set with `--no-discover` CLI flag. + */ + discover?: boolean + + /** + * Dry option + * + * @remarks + * Set with `--dry` CLI flag. + */ + dry?: boolean + + /** + * Output directory + * + * @remarks + * Set with `--output` CLI flag. + */ + output?: string + + /** + * Open editor on error + * + * @remarks + * Set with `--editor` CLI flag. + */ + editor?: string | boolean + + /** + * ESM option + * + * @remarks + * Set with `--esm` CLI flag. + */ + esm?: boolean + + /** + * Filter option + * + * @remarks + * In the case of a nested bud.js instance, this will limit the instances + * that are compiled to those that match the filter. Keyed by {@link Bud.label}. + * + * Set with `--filter` CLI flag. + */ + filter?: Array + + /** + * Force option + * + * @remarks + * Set with the `--force` CLI flag. + */ + force?: boolean + + /** + * Hash option + * + * @remarks + * Set with the `--hash` CLI flag. + */ + hash?: boolean + + /** + * Hot option + * + * @remarks + * Set with the `--hot` CLI flag. + */ + hot?: boolean -export interface CommandContext extends CLIContext { - args: Partial<{ - basedir: string | undefined - browser: string | boolean | undefined - cache: `filesystem` | `memory` | true | false | undefined - ci: boolean | undefined - clean: boolean | undefined | undefined - debug: boolean | undefined - devtool?: - | false - | `eval` - | `eval-cheap-source-map` - | `eval-cheap-module-source-map` - | `eval-source-map` - | `cheap-source-map` - | `cheap-module-source-map` - | `source-map` - | `inline-cheap-source-map` - | `inline-cheap-module-source-map` - | `inline-source-map` - | `eval-nosources-cheap-source-map` - | `eval-nosources-cheap-modules-source-map` - | `eval-nosources-source-map` - | `inline-nosources-cheap-source-map` - | `inline-nosources-cheap-module-source-map` - | `inline-nosources-source-map` - | `nosources-cheap-source-map` - | `nosources-cheap-module-source-map` - | `hidden-nosources-cheap-source-map` - | `hidden-nosources-cheap-module-source-map` - | `hidden-nosources-source-map` - | `hidden-cheap-source-map` - | `hidden-cheap-module-source-map` - | `hidden-source-map` - | undefined - discover: boolean | undefined - dry: boolean | undefined - output: string | undefined - editor: string | boolean | undefined - esm: boolean | undefined - filter: Array | undefined - force: boolean | undefined - hash: boolean | undefined - hot: boolean | undefined - html: boolean | string | undefined - immutable: boolean | undefined - indicator: boolean | undefined - input: string | undefined - log: boolean | undefined - manifest: boolean | undefined - minimize: boolean | undefined - modules: string | undefined - notify: boolean | undefined - overlay: boolean | undefined - publicPath: string | undefined - port: string | undefined - proxy: string | undefined - reload: boolean | undefined - runtime: `single` | `multiple` | boolean | undefined - splitChunks: boolean | undefined - storage: string | undefined - target: Array | undefined - use: Array | undefined - verbose: boolean | undefined - }> + /** + * HTML option + * + * @remarks + * Set with the `--html` CLI flag. + */ + html?: boolean | string + + /** + * Immutable option + * + * @remarks + * Set with the `--immutable` CLI flag. + */ + immutable?: boolean + + /** + * Indicator option + * + * @remarks + * Set with the `--indicator` CLI flag. + */ + indicator?: boolean + + /** + * Input option + * + * @remarks + * Set with the `--input` CLI flag. + */ + input?: string + + /** + * Log option + * + * @remarks + * Set with the `--log` CLI flag. + */ + log?: boolean + + /** + * Minimize option + * + * @remarks + * Set with the `--minimize` CLI flag. + */ + minimize?: boolean + + /** + * Modules option + * + * @remarks + * Set with the `--modules` CLI flag. + */ + modules?: string + + /** + * Notify option + * + * @remarks + * Set with the `--notify` CLI flag. + */ + notify?: boolean + + /** + * Overlay option + * + * @remarks + * Set with the `--overlay` CLI flag. + */ + overlay?: boolean + + /** + * Public path option + * + * @remarks + * Set with the `--publicPath` CLI flag. + */ + publicPath?: string + + /** + * Port option + * + * @remarks + * Set with the `--port` CLI flag. + */ + port?: string + + /** + * Proxy option + * + * @remarks + * Set with the `--proxy` CLI flag. + */ + proxy?: string + + /** + * Reload option + * + * @remarks + * Set with the `--reload` CLI flag. + */ + reload?: boolean + + /** + * Runtime option + * + * @remarks + * Set with the `--runtime` CLI flag. + */ + runtime?: `single` | `multiple` | boolean + + /** + * Silent option + * + * @remarks + * Set with the `--silent` CLI flag. + */ + silent?: boolean + + /** + * Split chunks option + * + * @remarks + * Set with the `--splitChunks` CLI flag. + */ + splitChunks?: boolean + + /** + * Storage option + * + * @remarks + * Set with the `--storage` CLI flag. + */ + storage?: string + + /** + * Target option + * + * @todo Is this used anywhere? + */ + target?: Array + + /** + * Use option + * + * @remarks + * Additional extensions to load. + * + * Set with the `--use` CLI flag. + */ + use?: Array + + /** + * Verbose logging option + * + * @remarks + * Set with `--verbose` CLI flag. + */ + verbose?: boolean + + /** + * Bin option + * + * @remarks + * Set with `--bin` CLI flag. + */ bin?: `node` | `ts-node` | `bun` } +/** + * Virtual file system file + */ export interface File { + /** + * Filename + */ name: string + /** + * Absolute filepath + */ path: string + /** + * Is a bud configuration file + * + * @remarks + * File name contains `bud`. + */ bud: boolean + /** + * Is a local configuration file + * + * @remarks + * File name includes `.local`. + */ local: boolean + /** + * Is a dynamic configuration file + * + * @remarks + * File extension is `.cjs`, `mjs`, `.js` or `.ts` + */ dynamic: boolean + + /** + * File extension + */ extension: string | null + + /** + * Target environment config + * + * @remarks + * File name includes `.production` or `.development` + */ type: `production` | `development` | `base` + + /** + * Module (if file is a dynamic configuration file) + */ module: any + + /** + * Is a `file` (as opposed to a directory) + */ file: boolean + + /** + * Is a `directory` (as opposed to a file) + */ dir: boolean + + /** + * Is a symlink + */ symlink: boolean + + /** + * File size + */ size: number + + /** + * SHA1 hash of file contents + */ sha1: string + + /** + * File mode + */ mode: number } diff --git a/sources/@roots/bud-framework/src/types/options/index.ts b/sources/@roots/bud-framework/src/types/options/index.ts index 2f8e066fd2..6f6c679b68 100644 --- a/sources/@roots/bud-framework/src/types/options/index.ts +++ b/sources/@roots/bud-framework/src/types/options/index.ts @@ -1,3 +1 @@ -import type {CLIContext, CommandContext, Context, File} from './context.js' - -export type {CLIContext, CommandContext, Context, File} +export type * from './context.js' diff --git a/sources/@roots/bud-framework/src/types/registry/events.ts b/sources/@roots/bud-framework/src/types/registry/events.ts index a8fe505ab7..146a2579e4 100644 --- a/sources/@roots/bud-framework/src/types/registry/events.ts +++ b/sources/@roots/bud-framework/src/types/registry/events.ts @@ -1,4 +1,4 @@ -import type {MultiStats} from '@roots/bud-support/webpack' +import type {Compilation, MultiStats} from '@roots/bud-support/webpack' import type {Bud} from '../../bud.js' @@ -18,6 +18,7 @@ export interface Events { 'compiler.stats': MultiStats 'compiler.error': Error 'project.write': Bud + 'compilation.afterEmit': Compilation 'config.after': Bud run: Bud 'server.before': Bud diff --git a/sources/@roots/bud-framework/src/types/services/dashboard/index.ts b/sources/@roots/bud-framework/src/types/services/dashboard/index.ts index a674530af7..1797b418f5 100644 --- a/sources/@roots/bud-framework/src/types/services/dashboard/index.ts +++ b/sources/@roots/bud-framework/src/types/services/dashboard/index.ts @@ -8,8 +8,6 @@ import type {Service as Contract} from '../../../service.js' export interface Service extends Contract { silent: boolean - renderLog?: any - /** * Update the dashboard */ diff --git a/sources/@roots/bud-framework/src/types/services/extensions/index.ts b/sources/@roots/bud-framework/src/types/services/extensions/index.ts index 75fad99d5b..614e103db6 100644 --- a/sources/@roots/bud-framework/src/types/services/extensions/index.ts +++ b/sources/@roots/bud-framework/src/types/services/extensions/index.ts @@ -73,7 +73,8 @@ export interface Service extends BaseService { import( signifier: K, - fatalOnError?: boolean, + fatalOnError: boolean, + context: string, ): Promise runAll(methodName: LifecycleMethods): Promise> diff --git a/sources/@roots/bud-preset-recommend/src/extension.ts b/sources/@roots/bud-preset-recommend/src/extension.ts index 4d607a9ae2..a25fdd2dd7 100644 --- a/sources/@roots/bud-preset-recommend/src/extension.ts +++ b/sources/@roots/bud-preset-recommend/src/extension.ts @@ -1,4 +1,4 @@ -import type {Bud} from '@roots/bud' +import type {Bud} from '@roots/bud-framework' import {Extension} from '@roots/bud-framework/extension' import { bind, diff --git a/sources/@roots/bud-preset-wordpress/src/types.ts b/sources/@roots/bud-preset-wordpress/src/types.ts index 31ffdfb7d8..cdf7681cfd 100644 --- a/sources/@roots/bud-preset-wordpress/src/types.ts +++ b/sources/@roots/bud-preset-wordpress/src/types.ts @@ -1,9 +1,9 @@ import '@roots/bud/types' import '@roots/bud-preset-recommend/types' import '@roots/bud-react/types' -import '@roots/bud-tailwindcss-theme-json/types' import '@roots/bud-wordpress-manifests/types' import '@roots/bud-wordpress-theme-json/types' +import '@roots/bud-tailwindcss-theme-json/types' import '@roots/wordpress-hmr/types' import type {PublicExtensionApi} from '@roots/bud-framework/extension' @@ -17,7 +17,6 @@ declare module '@roots/bud-framework' { interface Modules { '@roots/bud-preset-wordpress': {} - '@roots/bud-tailwind-theme-json'?: {} } interface Loaders { '@roots/wordpress-hmr/loader': any diff --git a/sources/@roots/bud-preset-wordpress/test/extension.test.ts b/sources/@roots/bud-preset-wordpress/test/extension.test.ts index ccb2b5482d..c29e569887 100644 --- a/sources/@roots/bud-preset-wordpress/test/extension.test.ts +++ b/sources/@roots/bud-preset-wordpress/test/extension.test.ts @@ -1,6 +1,7 @@ -import '@roots/bud' -import '@roots/bud-preset-wordpress' +import '../src/index.js' + import '@roots/bud-tailwindcss' +import '@roots/bud-wordpress-theme-json' import {Bud, factory} from '@repo/test-kit/bud' import {beforeEach, describe, expect, it, vi} from 'vitest' diff --git a/sources/@roots/bud-prettier/src/bud/commands/bud.prettier.command.tsx b/sources/@roots/bud-prettier/src/bud/commands/bud.prettier.command.tsx index bfb52f4944..a180483f1e 100644 --- a/sources/@roots/bud-prettier/src/bud/commands/bud.prettier.command.tsx +++ b/sources/@roots/bud-prettier/src/bud/commands/bud.prettier.command.tsx @@ -16,7 +16,7 @@ export class BudPrettierCommand extends BudCommand { public options = Option.Proxy({name: `prettier passthrough options`}) public override async execute() { - await this.makeBud(this) + await this.makeBud() await this.bud.run() const prettier = join( diff --git a/sources/@roots/bud-react/src/react-refresh/extension.ts b/sources/@roots/bud-react/src/react-refresh/extension.ts index bdd64d8a6c..bbfc385bdc 100644 --- a/sources/@roots/bud-react/src/react-refresh/extension.ts +++ b/sources/@roots/bud-react/src/react-refresh/extension.ts @@ -50,7 +50,7 @@ export default class BudReactRefresh extends Extension< */ @bind public override async configAfter(bud: Bud) { - if (bud.isCLI() && bud.context.args.hot === false) return + if (bud.context.hot === false) return if (!this.transformExtension) { const signifier = bud.react.useBabel diff --git a/sources/@roots/bud-react/src/babel-refresh/extension.test.ts b/sources/@roots/bud-react/test/babel-refresh/extension.test.ts similarity index 81% rename from sources/@roots/bud-react/src/babel-refresh/extension.test.ts rename to sources/@roots/bud-react/test/babel-refresh/extension.test.ts index d677c3116e..128295bad9 100644 --- a/sources/@roots/bud-react/src/babel-refresh/extension.test.ts +++ b/sources/@roots/bud-react/test/babel-refresh/extension.test.ts @@ -1,6 +1,6 @@ import {describe, expect, it} from 'vitest' -import Extension from './index.js' +import Extension from '../../src/index.js' describe(`@roots/bud-react/babel-refresh`, () => { it(`should be constructable`, () => { diff --git a/sources/@roots/bud-react/src/extension.test.ts b/sources/@roots/bud-react/test/extension.test.ts similarity index 76% rename from sources/@roots/bud-react/src/extension.test.ts rename to sources/@roots/bud-react/test/extension.test.ts index 884078fabc..612d669cdc 100644 --- a/sources/@roots/bud-react/src/extension.test.ts +++ b/sources/@roots/bud-react/test/extension.test.ts @@ -1,6 +1,6 @@ import {describe, expect, it} from 'vitest' -import Extension from './extension/extension.js' +import Extension from '../src/extension/extension.js' describe(`@roots/bud-react`, () => { it(`should be constructable`, () => { diff --git a/sources/@roots/bud-react/src/extension/extension.test.ts b/sources/@roots/bud-react/test/extension/extension.test.ts similarity index 70% rename from sources/@roots/bud-react/src/extension/extension.test.ts rename to sources/@roots/bud-react/test/extension/extension.test.ts index 6c06ad5e4a..51b1a764b7 100644 --- a/sources/@roots/bud-react/src/extension/extension.test.ts +++ b/sources/@roots/bud-react/test/extension/extension.test.ts @@ -1,6 +1,8 @@ +import '@roots/bud-react/types' + import {describe, expect, it} from 'vitest' -import Extension from '../index' +import Extension from '../../src/index' describe(`@roots/bud-react`, () => { it(`should be constructable`, () => { diff --git a/sources/@roots/bud-react/src/react-refresh/extension.test.ts b/sources/@roots/bud-react/test/react-refresh/extension.test.ts similarity index 69% rename from sources/@roots/bud-react/src/react-refresh/extension.test.ts rename to sources/@roots/bud-react/test/react-refresh/extension.test.ts index 2bc8ac20c4..ad216f8ef1 100644 --- a/sources/@roots/bud-react/src/react-refresh/extension.test.ts +++ b/sources/@roots/bud-react/test/react-refresh/extension.test.ts @@ -1,6 +1,8 @@ +import '@roots/bud-react/types' + import {describe, expect, it} from 'vitest' -import Extension from './extension.js' +import Extension from '../../src/extension/index.js' describe(`@roots/bud-react/react-refresh`, () => { it(`should be constructable`, () => { diff --git a/sources/@roots/bud-react/src/swc-refresh/extension.test.ts b/sources/@roots/bud-react/test/swc-refresh/extension.test.ts similarity index 64% rename from sources/@roots/bud-react/src/swc-refresh/extension.test.ts rename to sources/@roots/bud-react/test/swc-refresh/extension.test.ts index ad303bd2c7..c07b8958c1 100644 --- a/sources/@roots/bud-react/src/swc-refresh/extension.test.ts +++ b/sources/@roots/bud-react/test/swc-refresh/extension.test.ts @@ -1,6 +1,9 @@ +import '@roots/bud-react/types' +import '@roots/bud-swc/types' + import {describe, expect, it} from 'vitest' -import Extension from './index.js' +import Extension from '../../src/index.js' describe(`@roots/bud-react/swc-refresh`, () => { it(`should be constructable`, () => { diff --git a/sources/@roots/bud-react/src/typescript-refresh/extension.test.ts b/sources/@roots/bud-react/test/typescript-refresh/extension.test.ts similarity index 63% rename from sources/@roots/bud-react/src/typescript-refresh/extension.test.ts rename to sources/@roots/bud-react/test/typescript-refresh/extension.test.ts index 8f691bb236..d5ad35ed8a 100644 --- a/sources/@roots/bud-react/src/typescript-refresh/extension.test.ts +++ b/sources/@roots/bud-react/test/typescript-refresh/extension.test.ts @@ -1,6 +1,9 @@ +import '@roots/bud-react/types' +import '@roots/bud-typescript/types' + import {describe, expect, it} from 'vitest' -import Extension from './index.js' +import Extension from '../../src/index.js' describe(`@roots/bud-react/typescript-refresh`, () => { it(`should be constructable`, () => { diff --git a/sources/@roots/bud-react/tsconfig.json b/sources/@roots/bud-react/tsconfig.json index aa2066eb2f..3626f7094d 100644 --- a/sources/@roots/bud-react/tsconfig.json +++ b/sources/@roots/bud-react/tsconfig.json @@ -7,7 +7,7 @@ "module": "esnext", }, "include": ["src"], - "exclude": ["**/*.test.ts"], + "exclude": ["test"], "references": [ {"path": "./../bud-api/tsconfig.json"}, {"path": "./../bud-babel/tsconfig.json"}, diff --git a/sources/@roots/bud-server/src/hooks/dev.client.scripts.ts b/sources/@roots/bud-server/src/hooks/dev.client.scripts.ts index c176aff944..6e98a106c1 100644 --- a/sources/@roots/bud-server/src/hooks/dev.client.scripts.ts +++ b/sources/@roots/bud-server/src/hooks/dev.client.scripts.ts @@ -37,31 +37,25 @@ export const proxyClickInterceptor = (app: Bud) => { * @returns string */ export const hmrClient = (app: Bud) => { - if (app.isCLI() && app.context.args.hot === false) return + if (app.context.hot === false) return const params = new URLSearchParams({ name: app.label, indicator: - !app.isCLI() || - isUndefined(app.context.args.indicator) || - isNull(app.context.args.indicator) + isUndefined(app.context.indicator) || isNull(app.context.indicator) ? `true` - : app.context.args.indicator.toString(), + : app.context.indicator.toString(), overlay: - !app.isCLI() || - isUndefined(app.context.args.overlay) || - isNull(app.context.args.overlay) + isUndefined(app.context.overlay) || isNull(app.context.overlay) ? `true` - : app.context.args.overlay.toString(), + : app.context.overlay.toString(), reload: - !app.isCLI() || - isUndefined(app.context.args.reload) || - isNull(app.context.args.reload) + isUndefined(app.context.reload) || isNull(app.context.reload) ? `true` - : app.context.args.reload.toString(), + : app.context.reload.toString(), }) return `@roots/bud-client/lib/hot/index.mjs?${params.toString()}` diff --git a/sources/@roots/bud-server/src/middleware/hot/factory.test.ts b/sources/@roots/bud-server/src/middleware/hot/factory.test.ts index f1adb6a997..cb685d4062 100644 --- a/sources/@roots/bud-server/src/middleware/hot/factory.test.ts +++ b/sources/@roots/bud-server/src/middleware/hot/factory.test.ts @@ -3,8 +3,6 @@ import {afterAll, beforeAll, describe, expect, it, vi} from 'vitest' import * as hot from './index.js' -vi.mock(`@roots/bud-compiler`) - describe(`@roots/bud-server/middleware/hot`, () => { let bud: Bud @@ -12,8 +10,6 @@ describe(`@roots/bud-server/middleware/hot`, () => { bud = await factory({mode: `development`}) }) - afterAll(async () => bud.close()) - it(`should be a function`, () => { expect(hot.factory).toBeDefined() }) diff --git a/sources/@roots/bud-server/src/middleware/proxy/factory.ts b/sources/@roots/bud-server/src/middleware/proxy/factory.ts index 1ddd973d29..056d2aea77 100644 --- a/sources/@roots/bud-server/src/middleware/proxy/factory.ts +++ b/sources/@roots/bud-server/src/middleware/proxy/factory.ts @@ -67,11 +67,11 @@ export const makeOptions = (app: Bud): Options => { ), logger: app.hooks.filter( `dev.middleware.proxy.options.logger`, - app.isCLI() && app.context.args.log + app.context.log ? app.context.logger.scope(app.label, `proxy`) - : app.isCLI() - ? undefined - : console, + : app.context.ci + ? console + : undefined, ), on: filterUndefined( app.hooks.filter(`dev.middleware.proxy.options.on`, { diff --git a/sources/@roots/bud-server/src/middleware/proxy/middleware.test.ts b/sources/@roots/bud-server/src/middleware/proxy/middleware.test.ts index b5c2ea058a..b463990d69 100644 --- a/sources/@roots/bud-server/src/middleware/proxy/middleware.test.ts +++ b/sources/@roots/bud-server/src/middleware/proxy/middleware.test.ts @@ -33,9 +33,7 @@ describe(`proxy middleware`, () => { }) it(`should have logger when --log flag is used`, async () => { - if (bud.isCLI()) { - bud.context.args.log = true - } + bud.context.log = true // @ts-ignore expect(middleware.makeOptions(bud).logger).toBe(logger) diff --git a/sources/@roots/bud-server/src/server/watcher.ts b/sources/@roots/bud-server/src/server/watcher.ts index be5cd1088c..146b2482be 100644 --- a/sources/@roots/bud-server/src/server/watcher.ts +++ b/sources/@roots/bud-server/src/server/watcher.ts @@ -67,7 +67,7 @@ export class Watcher implements Server.Watcher { */ @bind public async watch(): Promise { - if (this.app.isCLI() && this.app.context.args.dry) return + if (this.app.context.dry) return this.files = this.app.hooks.filter(`dev.watch.files`, new Set([])) this.options = this.app.hooks.filter(`dev.watch.options`, { diff --git a/sources/@roots/bud-server/src/service/index.ts b/sources/@roots/bud-server/src/service/index.ts index 53ad552ce7..61153c0b27 100644 --- a/sources/@roots/bud-server/src/service/index.ts +++ b/sources/@roots/bud-server/src/service/index.ts @@ -166,18 +166,11 @@ export class Server extends Service implements BaseService { */ @bind public async applyMiddleware() { - if (this.app.isCLI() && this.app.context.args.dry) return - try { await Promise.all( Object.entries(this.enabledMiddleware).map( async ([key, signifier]) => { - if ( - this.app.isCLI() && - this.app.context.args.hot === false && - key === `hot` - ) - return + if (this.app.context.hot === false && key === `hot`) return /** import middleware */ const {factory} = await this.app.module.import( @@ -208,7 +201,7 @@ export class Server extends Service implements BaseService { public async run() { await this.app.hooks.fire(`server.before`, this.app) - if (!this.app.isCLI() || !this.app.context.args.dry) { + if (this.app.context.dry !== true) { await this.connection.createServer(this.application) await this.connection.listen() } diff --git a/sources/@roots/bud-server/src/service/service.test.ts b/sources/@roots/bud-server/src/service/service.test.ts index 90f850b93c..1e5f9f0141 100644 --- a/sources/@roots/bud-server/src/service/service.test.ts +++ b/sources/@roots/bud-server/src/service/service.test.ts @@ -10,7 +10,7 @@ describe(`@roots/bud-server`, () => { beforeEach(async () => { bud = await factory({mode: `development`}) - expect(bud.context.args.dry).toBe(true) + expect(bud.context.dry).toBe(true) expect(bud.mode).toBe(`development`) instance = new Server(() => bud) diff --git a/sources/@roots/bud-support/package.json b/sources/@roots/bud-support/package.json index 13c3727d2e..151ba75326 100644 --- a/sources/@roots/bud-support/package.json +++ b/sources/@roots/bud-support/package.json @@ -155,7 +155,7 @@ "html-webpack-plugin": "5.5.1", "http-proxy-middleware": "3.0.0-beta.1", "human-readable": "0.2.1", - "import-meta-resolve": "3.0.0", + "import-meta-resolve": "2.2.2", "json5": "2.2.3", "lodash": "4.17.21", "mini-css-extract-plugin": "2.7.5", diff --git a/sources/@roots/bud-support/src/esbuild/index.ts b/sources/@roots/bud-support/src/esbuild/index.ts index 02a73335fa..7e166233fa 100644 --- a/sources/@roots/bud-support/src/esbuild/index.ts +++ b/sources/@roots/bud-support/src/esbuild/index.ts @@ -1,12 +1,19 @@ +import {type build} from 'esbuild-wasm' import {resolve} from 'import-meta-resolve' import logger from '../logger/index.js' let path: string -let transformer: typeof import('esbuild-wasm') +interface transformer { + build: typeof build +} + +let transformer: transformer -export const getImplementation = async (context: string) => { +export const getImplementation = async ( + context: string, +): Promise => { if (transformer) return transformer const sources: Array<[string, string]> = [ @@ -29,6 +36,8 @@ export const getImplementation = async (context: string) => { return transformer } +export {transformer} + async function trySource(signifier: string, context: any) { try { path = await resolve(signifier, context) diff --git a/sources/@roots/bud-support/src/utilities/files.ts b/sources/@roots/bud-support/src/utilities/files.ts index e1a030dc5c..2f611d4e19 100644 --- a/sources/@roots/bud-support/src/utilities/files.ts +++ b/sources/@roots/bud-support/src/utilities/files.ts @@ -1,6 +1,7 @@ import {randomUUID} from 'node:crypto' import {dirname, join, parse} from 'node:path' +import type * as esbuild from '@roots/bud-support/esbuild' import omit from '@roots/bud-support/lodash/omit' import {BudError, FileReadError, ImportError} from '../errors/index.js' @@ -18,7 +19,7 @@ const COMPATIBLE_EXTENSIONS = [...DYNAMIC_EXTENSIONS, ...STATIC_EXTENSIONS] const uid = randomUUID() // prevents conflicting fs operations when testing -let transformer: any +let transformer: esbuild.transformer let fs: Filesystem let data: Record let paths: ReturnType @@ -253,6 +254,9 @@ async function transformConfig({ platform: `node`, outfile: cachePath, }) + + file.path = cachePath + logger.timeEnd(`compiling ${file.name}`) } diff --git a/sources/@roots/bud-support/src/utilities/logger.ts b/sources/@roots/bud-support/src/utilities/logger.ts index b1df1847ad..4b378dab66 100644 --- a/sources/@roots/bud-support/src/utilities/logger.ts +++ b/sources/@roots/bud-support/src/utilities/logger.ts @@ -28,6 +28,9 @@ class Logger { this.instance = new Signale.Signale(options) this.instance.config({displayLabel: false}) + if (args.silent && !args.log && !args.verbose) { + this.instance.disable() + } } @bind diff --git a/sources/@roots/bud-tailwindcss-theme-json/src/extension.ts b/sources/@roots/bud-tailwindcss-theme-json/src/extension.ts index e814861db6..e9cd223a03 100644 --- a/sources/@roots/bud-tailwindcss-theme-json/src/extension.ts +++ b/sources/@roots/bud-tailwindcss-theme-json/src/extension.ts @@ -1,21 +1,16 @@ import type {Bud} from '@roots/bud-framework' import {Extension} from '@roots/bud-framework/extension' -import { - bind, - dependsOn, - label, -} from '@roots/bud-framework/extension/decorators' +import {bind, label} from '@roots/bud-framework/extension/decorators' import * as tailwindAdapter from './tailwind/index.js' /** - * Acorn v2 public path fix + * Support Tailwind values in {@link Bud.wpjson} */ @label(`@roots/bud-tailwindcss-theme-json`) -@dependsOn([`@roots/bud-wordpress-theme-json`]) export class TailwindThemeJSON extends Extension { /** - * `register` callback + * {@link Extension.register} */ @bind public override async register(bud: Bud) { @@ -30,6 +25,9 @@ export class TailwindThemeJSON extends Extension { ) } + /** + * Use tailwind colors in theme.json + */ public useTailwindColors( this: Bud[`wpjson`], extendOnly?: boolean, @@ -47,6 +45,9 @@ export class TailwindThemeJSON extends Extension { return this.enable() } + /** + * Use tailwind fontFamily in theme.json + */ public useTailwindFontFamily( this: Bud[`wpjson`], extendOnly?: boolean, @@ -72,6 +73,9 @@ export class TailwindThemeJSON extends Extension { return this.enable() } + /** + * Use tailwind fontSize in theme.json + */ public useTailwindFontSize( this: Bud[`wpjson`], extendOnly?: boolean, diff --git a/sources/@roots/bud-tailwindcss-theme-json/src/types.ts b/sources/@roots/bud-tailwindcss-theme-json/src/types.ts index ec293efbbb..5b47dc2cf2 100644 --- a/sources/@roots/bud-tailwindcss-theme-json/src/types.ts +++ b/sources/@roots/bud-tailwindcss-theme-json/src/types.ts @@ -1,14 +1,8 @@ import '@roots/bud/types' import '@roots/bud-wordpress-theme-json/types' -import type {PublicExtensionApi} from '@roots/bud-framework/extension' -import type WordPressThemeJSON from '@roots/bud-wordpress-theme-json' - -interface PublicWordPressThemeJSON - extends PublicExtensionApi {} - declare module '@roots/bud-framework' { interface Modules { - '@roots/bud-tailwindcss-theme-json': PublicWordPressThemeJSON + '@roots/bud-tailwindcss-theme-json'?: any } } diff --git a/sources/@roots/bud-tailwindcss/src/bud/commands/tailwindcss/index.tsx b/sources/@roots/bud-tailwindcss/src/bud/commands/tailwindcss/index.tsx index 892b893c5f..115e006d2b 100644 --- a/sources/@roots/bud-tailwindcss/src/bud/commands/tailwindcss/index.tsx +++ b/sources/@roots/bud-tailwindcss/src/bud/commands/tailwindcss/index.tsx @@ -1,4 +1,4 @@ -import {join} from 'node:path' +import {join, relative} from 'node:path' import BudCommand from '@roots/bud/cli/commands/bud' import {dry} from '@roots/bud/cli/decorators/command.dry' @@ -19,7 +19,7 @@ export class BudTailwindCommand extends BudCommand { public options = Option.Proxy({name: `tailwindcss passthrough options`}) public override async execute() { - await this.makeBud(this) + await this.makeBud() await this.bud.run() const tw = join( @@ -37,9 +37,7 @@ export class BudTailwindCommand extends BudCommand { `--input`, input, `--output`, - this.bud.relPath( - this.bud.path(`@dist`, this.bud.relPath(`@src`, input)), - ), + this.bud.relPath(`@dist`, relative(this.bud.path(`@src`), input)), ) const config = this.bud.context.files diff --git a/sources/@roots/bud-tailwindcss/src/extension/index.ts b/sources/@roots/bud-tailwindcss/src/extension/index.ts index 313acbb0fe..a195e7bb3b 100644 --- a/sources/@roots/bud-tailwindcss/src/extension/index.ts +++ b/sources/@roots/bud-tailwindcss/src/extension/index.ts @@ -77,7 +77,7 @@ export class BudTailwindCss extends Extension { */ public get config() { return { - ...(resolveConfig(this.file.module.default ?? this.file.module) ?? + ...(resolveConfig(this.file.module?.default ?? this.file.module) ?? {}), } } @@ -180,6 +180,7 @@ export class BudTailwindCss extends Extension { bud.hooks.on(`build.cache.buildDependencies`, paths => { if (isString(this.file.path)) { paths.tailwind = [this.file.path] + this.logger.success( `tailwind config added to webpack build dependencies`, ) diff --git a/sources/@roots/bud-tailwindcss/test/extension/__snapshots__/index.test.ts.snap b/sources/@roots/bud-tailwindcss/test/extension/__snapshots__/index.test.ts.snap index 5f9ec5e0bf..a66ef31642 100644 --- a/sources/@roots/bud-tailwindcss/test/extension/__snapshots__/index.test.ts.snap +++ b/sources/@roots/bud-tailwindcss/test/extension/__snapshots__/index.test.ts.snap @@ -320,6 +320,8 @@ exports[`@roots/bud-tailwindcss extension > should throw when key does not exist } } }, - \\"plugins\\": [] + \\"plugins\\": [ + null + ] }" `; diff --git a/sources/@roots/bud-typescript/src/bud/commands/bud.ts.check.command.tsx b/sources/@roots/bud-typescript/src/bud/commands/bud.ts.check.command.tsx index d1fed28fe7..821ccafdb1 100644 --- a/sources/@roots/bud-typescript/src/bud/commands/bud.ts.check.command.tsx +++ b/sources/@roots/bud-typescript/src/bud/commands/bud.ts.check.command.tsx @@ -24,11 +24,11 @@ export class BudTSCheckCommand extends BudCommand { */ @bind public override async execute() { - await this.makeBud(this) + await this.makeBud() await this.bud.run() const tsc = join( - await this.bud.module.getDirectory(`tsc`), + await this.bud.module.getDirectory(`typescript`, import.meta.url), `bin`, `tsc`, ) diff --git a/sources/@roots/bud-typescript/src/bud/commands/bud.tsc.command.tsx b/sources/@roots/bud-typescript/src/bud/commands/bud.tsc.command.tsx index 928d715981..c40ac4a621 100644 --- a/sources/@roots/bud-typescript/src/bud/commands/bud.tsc.command.tsx +++ b/sources/@roots/bud-typescript/src/bud/commands/bud.tsc.command.tsx @@ -17,11 +17,11 @@ export class BudTSCCommand extends BudCommand { public options = Option.Proxy({name: `tsc passthrough options`}) public override async execute() { - await this.makeBud(this) + await this.makeBud() await this.bud.run() const tsc = join( - await this.bud.module.getDirectory(`tsc`), + await this.bud.module.getDirectory(`typescript`, import.meta.url), `bin`, `tsc`, ) diff --git a/sources/@roots/bud-wordpress-theme-json/src/extension.ts b/sources/@roots/bud-wordpress-theme-json/src/extension.ts index 22015d7996..08f55b4df2 100644 --- a/sources/@roots/bud-wordpress-theme-json/src/extension.ts +++ b/sources/@roots/bud-wordpress-theme-json/src/extension.ts @@ -68,6 +68,12 @@ export class WordPressThemeJSON extends Extension< Options, ThemeJsonWebpackPlugin > { + @bind + public override async register() {} + + @bind + public override async boot() {} + @bind public settings( input?: diff --git a/sources/@roots/bud-wordpress-theme-json/src/types.ts b/sources/@roots/bud-wordpress-theme-json/src/types.ts index 16f41ab128..7195040880 100644 --- a/sources/@roots/bud-wordpress-theme-json/src/types.ts +++ b/sources/@roots/bud-wordpress-theme-json/src/types.ts @@ -1,5 +1,3 @@ -import '@roots/bud/types' - import type {PublicExtensionApi} from '@roots/bud-framework/extension' import type {WordPressThemeJSON} from './extension.js' @@ -10,7 +8,7 @@ import type {WordPressThemeJSON} from './extension.js' * @see {@link https://bud.js.org/extensions/sage/theme.json/} * @see {@link https://developer.wordpress.org/block-editor/how-to-guides/themes/theme-json/} */ -interface WPJSONApi extends PublicExtensionApi { +type WPJSONApi = PublicExtensionApi & { /** * ## bud.wpjson.settings * @@ -26,7 +24,7 @@ interface WPJSONApi extends PublicExtensionApi { * @note * Requires {@link https://bud.js.org/extensions/bud-tailwindcss/ @roots/bud-tailwindcss} to be installed. */ - useTailwindColors?: (value?: boolean, extendOnly?: boolean) => this + useTailwindColors?: (value?: boolean, extendOnly?: boolean) => WPJSONApi /** * ## bud.wpjson.useTailwindFontFamily @@ -36,7 +34,10 @@ interface WPJSONApi extends PublicExtensionApi { * @note * Requires {@link https://bud.js.org/extensions/bud-tailwindcss/ @roots/bud-tailwindcss} to be installed. */ - useTailwindFontFamily?: (value?: boolean, extendOnly?: boolean) => this + useTailwindFontFamily?: ( + value?: boolean, + extendOnly?: boolean, + ) => WPJSONApi /** * ## bud.wpjson.useTailwindFontSize @@ -46,7 +47,10 @@ interface WPJSONApi extends PublicExtensionApi { * @note * Requires {@link https://bud.js.org/extensions/bud-tailwindcss/ @roots/bud-tailwindcss} to be installed. */ - useTailwindFontSize?: (value?: boolean, extendOnly?: boolean) => this + useTailwindFontSize?: ( + value?: boolean, + extendOnly?: boolean, + ) => WPJSONApi } declare module '@roots/bud-framework' { @@ -56,5 +60,6 @@ declare module '@roots/bud-framework' { interface Modules { '@roots/bud-wordpress-theme-json': WPJSONApi + '@roots/bud-tailwindcss-theme-json'?: any } } diff --git a/sources/@roots/bud/package.json b/sources/@roots/bud/package.json index 33e0d94c0e..ba20bd8c52 100644 --- a/sources/@roots/bud/package.json +++ b/sources/@roots/bud/package.json @@ -84,6 +84,10 @@ "import": "./lib/cli/decorators/*.js", "default": "./lib/cli/decorators/*.js" }, + "./cli/flags": { + "import": "./lib/cli/flags/index.js", + "default": "./lib/cli/flags/index.js" + }, "./cli/helpers/*": { "import": "./lib/cli/helpers/*.js", "default": "./lib/cli/helpers/*.js" @@ -133,6 +137,9 @@ "cli/decorators": [ "./lib/cli/decorators/index.d.ts" ], + "cli/flags": [ + "./lib/cli/flags/index.d.ts" + ], "context": [ "./lib/context/index.d.ts" ], diff --git a/sources/@roots/bud/src/cli/app.tsx b/sources/@roots/bud/src/cli/app.tsx index 2beb1ad2ab..fa29eba0ad 100644 --- a/sources/@roots/bud/src/cli/app.tsx +++ b/sources/@roots/bud/src/cli/app.tsx @@ -7,8 +7,7 @@ import BudUpgradeCommand from '@roots/bud/cli/commands/bud.upgrade' import BudViewCommand from '@roots/bud/cli/commands/bud.view' import BudWebpackCommand from '@roots/bud/cli/commands/bud.webpack' import {Commands} from '@roots/bud/cli/finder' -import type {Context} from '@roots/bud/context' -import getContext from '@roots/bud/context' +import getContext, {type Context} from '@roots/bud/context' import {Error} from '@roots/bud-dashboard/app' import type {CommandClass} from '@roots/bud-support/clipanion' import {Builtins, Cli} from '@roots/bud-support/clipanion' @@ -17,23 +16,40 @@ import {render} from 'ink' import BudDoctorCommand from './commands/doctor/index.js' import BudReplCommand from './commands/repl/index.js' +import type {CLIContext} from './index.js' -let context: Partial +let application: Cli +let context: Context | CLIContext + +const isCLIContext = ( + context: Context & { + stdout?: NodeJS.WriteStream + stderr?: NodeJS.WriteStream + stdin?: NodeJS.ReadStream + stdio?: NodeJS.WriteStream + }, +): context is CLIContext => + context.basedir !== undefined && + context.stdout !== undefined && + context.stderr !== undefined && + context.stdin !== undefined try { - context = await getContext({ - stdin: process.stdin, - stdout: process.stdout, - stderr: process.stderr, + context = { + ...(await getContext()), + stdin: global.process.stdin, + stdout: global.process.stdout, + stderr: global.process.stderr, colorDepth: 256, - }) + } + + if (!isCLIContext(context)) throw `Invalid context` } catch (err) { render() - // eslint-disable-next-line - process.exit(1) + global.process.exit(1) } -const application = new Cli({ +application = new Cli({ binaryLabel: `bud`, binaryName: `bud`, binaryVersion: context.bud?.version ?? undefined, @@ -64,7 +80,7 @@ await Commands.get(application, context) await Promise.all(fns.map(async fn => await fn(application))), ) -application.runExit(args.raw, context) +application.runExit(args.raw, context).catch(err => ) export {application, Builtins, Cli} export type {CommandClass} diff --git a/sources/@roots/bud/src/cli/commands/bud.build.development.tsx b/sources/@roots/bud/src/cli/commands/bud.build.development.tsx index 3b5feb00a0..22a05de3a5 100644 --- a/sources/@roots/bud/src/cli/commands/bud.build.development.tsx +++ b/sources/@roots/bud/src/cli/commands/bud.build.development.tsx @@ -1,22 +1,38 @@ -import {Option} from '@roots/bud/cli/commands/bud' import BuildCommand from '@roots/bud/cli/commands/bud.build' -import type {CommandContext} from '@roots/bud-framework/options' +import browser from '@roots/bud/cli/flags/browser' +import hot from '@roots/bud/cli/flags/hot' +import indicator from '@roots/bud/cli/flags/indicator' +import overlay from '@roots/bud/cli/flags/overlay' +import port from '@roots/bud/cli/flags/port' +import proxy from '@roots/bud/cli/flags/proxy' +import reload from '@roots/bud/cli/flags/reload' +import type {Context} from '@roots/bud-framework/options' /** * `bud build development` command */ export default class BuildDevelopmentCommand extends BuildCommand { + /** + * {@link Command.paths} + */ public static override paths = [ [`build`, `development`], [`dev`], [`development`], ] + + /** + * {@link Command.usage} + */ public static override usage = BuildCommand.Usage({ category: `build`, + description: `Compiles source assets in \`development\` mode.`, + details: `\ \`bud build development\` compiles source assets in \`development\` mode. `, + examples: [ [`compile source and serve`, `$0 build development`], [ @@ -34,52 +50,30 @@ export default class BuildDevelopmentCommand extends BuildCommand { ], }) - public hot = Option.Boolean(`--hot`, undefined, { - description: `Enable hot module replacement`, - }) + public browser = browser - public port = Option.String(`--port`, undefined, { - description: `Port to serve on`, - }) + public hot = hot - public proxy = Option.String(`--proxy`, undefined, { - description: `Proxy request URL`, - }) + public overlay = overlay - public reload = Option.Boolean(`--reload`, undefined, { - description: `Reload browser on unrecoverable errors`, - }) + public port = port - public overlay = Option.Boolean(`--overlay`, undefined, { - description: `Display error overlay in the browser`, - }) + public proxy = proxy - public indicator = Option.Boolean(`--indicator`, undefined, { - description: `Display status in the browser`, - }) + public reload = reload - public browser = Option.String(`--browser`, undefined, { - description: `Open browser on successful development build.`, - tolerateBoolean: true, - }) + public indicator = indicator - public override withSubcommandContext = async ( - context: CommandContext, - ) => { + /** + * {@link Command.withSubcommandContext} + */ + public override withSubcommandContext = async (context: Context) => { return { ...context, - mode: `development` as `development`, - } - } - - public override withSubcommandArguments = async ( - args: CommandContext[`args`], - ) => { - return { - ...args, browser: this.browser, hot: this.hot, indicator: this.indicator, + mode: `development` as `development`, overlay: this.overlay, port: this.port, proxy: this.proxy, diff --git a/sources/@roots/bud/src/cli/commands/bud.build.production.tsx b/sources/@roots/bud/src/cli/commands/bud.build.production.tsx index 1a71b05f45..3215643aa4 100644 --- a/sources/@roots/bud/src/cli/commands/bud.build.production.tsx +++ b/sources/@roots/bud/src/cli/commands/bud.build.production.tsx @@ -1,19 +1,19 @@ import {Command} from '@roots/bud-support/clipanion' import BuildCommand from './bud.build.js' -import type {CommandContext} from './bud.js' +import type {Context} from './bud.js' /** * `bud build production` command */ export default class BuildProductionCommand extends BuildCommand { /** - * Command paths + * {@link Command.paths} */ public static override paths = [[`build`, `production`], [`production`]] /** - * Usage + * {@link Command.usage} */ public static override usage = Command.Usage({ category: `build`, @@ -28,10 +28,10 @@ export default class BuildProductionCommand extends BuildCommand { }) /** - * Subcommand context + * {@link BuildCommand.withSubcommandContext} */ public override withSubcommandContext: BuildCommand[`withSubcommandContext`] = - async (context: CommandContext) => { + async (context: Context) => { return { ...context, mode: `production` as `production`, diff --git a/sources/@roots/bud/src/cli/commands/bud.build.tsx b/sources/@roots/bud/src/cli/commands/bud.build.tsx index 005c5cd154..4b475ae8c9 100644 --- a/sources/@roots/bud/src/cli/commands/bud.build.tsx +++ b/sources/@roots/bud/src/cli/commands/bud.build.tsx @@ -1,16 +1,43 @@ -import type {CommandContext} from '@roots/bud/cli/commands/bud' import BudCommand from '@roots/bud/cli/commands/bud' +import cache from '@roots/bud/cli/flags/cache' +import ci from '@roots/bud/cli/flags/ci' +import clean from '@roots/bud/cli/flags/clean' +import devtool from '@roots/bud/cli/flags/devtool' +import discover from '@roots/bud/cli/flags/discover' +import editor from '@roots/bud/cli/flags/editor' +import esm from '@roots/bud/cli/flags/esm' +import force from '@roots/bud/cli/flags/force' +import hash from '@roots/bud/cli/flags/hash' +import html from '@roots/bud/cli/flags/html' +import immutable from '@roots/bud/cli/flags/immutable' +import input from '@roots/bud/cli/flags/input' +import minimize from '@roots/bud/cli/flags/minimize' +import output from '@roots/bud/cli/flags/output' +import publicPath from '@roots/bud/cli/flags/publicPath' +import runtime from '@roots/bud/cli/flags/runtime' +import splitChunks from '@roots/bud/cli/flags/splitChunks' +import storage from '@roots/bud/cli/flags/storage' +import use from '@roots/bud/cli/flags/use' +import type {Context} from '@roots/bud-framework/options/context' import {Command, Option} from '@roots/bud-support/clipanion' -import * as t from '@roots/bud-support/typanion' /** - * Build command + * `bud build` command */ export default class BudBuildCommand extends BudCommand { + /** + * {@link Command.paths} + */ public static override paths = [[`build`]] + + /** + * {@link Command.usage} + */ public static override usage = Command.Usage({ category: `build`, + description: `Compile source assets`, + details: `\ \`bud build production\` compiles source assets in \`production\` mode. Run \`bud build production --help\` for usage. @@ -18,144 +45,66 @@ export default class BudBuildCommand extends BudCommand { If you run this command without a configuration file \`bud\` will look for an entrypoint at \`@src/index.js\`. `, + examples: [[`compile source assets`, `$0 build`]], }) - public cache = Option.String(`--cache`, undefined, { - description: `Utilize compiler's filesystem cache`, - tolerateBoolean: true, - validator: t.isOneOf([ - t.isLiteral(`filesystem`), - t.isLiteral(`memory`), - t.isLiteral(true), - t.isLiteral(false), - ]), - env: `APP_CACHE`, - }) + public cache = cache - public clean = Option.Boolean(`--clean`, undefined, { - description: `Clean artifacts and distributables prior to compilation`, - }) + public ci = ci - public devtool = Option.String(`--devtool`, undefined, { - description: `Set devtool option`, - validator: t.isOneOf([ - t.isLiteral(false), - t.isLiteral(`eval`), - t.isLiteral(`eval-cheap-source-map`), - t.isLiteral(`eval-cheap-module-source-map`), - t.isLiteral(`eval-source-map`), - t.isLiteral(`cheap-source-map`), - t.isLiteral(`cheap-module-source-map`), - t.isLiteral(`source-map`), - t.isLiteral(`inline-cheap-source-map`), - t.isLiteral(`inline-cheap-module-source-map`), - t.isLiteral(`inline-source-map`), - t.isLiteral(`eval-nosources-cheap-source-map`), - t.isLiteral(`eval-nosources-cheap-modules-source-map`), - t.isLiteral(`eval-nosources-source-map`), - t.isLiteral(`inline-nosources-cheap-source-map`), - t.isLiteral(`inline-nosources-cheap-module-source-map`), - t.isLiteral(`inline-nosources-source-map`), - t.isLiteral(`nosources-cheap-source-map`), - t.isLiteral(`nosources-cheap-module-source-map`), - t.isLiteral(`hidden-nosources-cheap-source-map`), - t.isLiteral(`hidden-nosources-cheap-module-source-map`), - t.isLiteral(`hidden-nosources-source-map`), - t.isLiteral(`hidden-cheap-source-map`), - t.isLiteral(`hidden-cheap-module-source-map`), - t.isLiteral(`hidden-source-map`), - ]), - env: `APP_DEVTOOL`, - }) - public editor = Option.String(`--editor`, undefined, { - description: `Open editor to file containing errors on unsuccessful development build`, - tolerateBoolean: true, - }) - public esm = Option.Boolean(`--esm`, undefined, { - description: `build as es modules`, - }) - public force = Option.Boolean(`--force,--flush`, undefined, { - description: `Force clearing bud internal cache`, - }) - public hash = Option.Boolean(`--hash`, undefined, { - description: `Hash compiled filenames`, - }) - public html = Option.String(`--html`, undefined, { - description: `Generate an html template`, - tolerateBoolean: true, - }) - public immutable = Option.Boolean(`--immutable`, undefined, { - description: `bud.http: immutable module lockfile`, - }) - public discover = Option.Boolean(`--discover,--discovery`, undefined, { - description: `Automatically register extensions`, - }) - public manifest = Option.Boolean(`--manifest`, undefined, { - description: `Generate a manifest of compiled assets`, - }) - public minimize = Option.Boolean(`--minimize`, undefined, { - description: `Minimize compiled assets`, - }) - public input = Option.String(`--input,-i,--@src,--src`, undefined, { - description: `Source directory (relative to project)`, - env: `APP_PATH_INPUT`, - }) - public output = Option.String(`--output,-o,--@dist,--dist`, undefined, { - description: `Distribution directory (relative to project)`, - env: `APP_PATH_OUTPUT`, - }) - public publicPath = Option.String(`--publicPath`, undefined, { - description: `public path of emitted assets`, - env: `APP_PUBLIC_PATH`, - }) - public runtime: `single` | `multiple` | boolean = Option.String( - `--runtime`, - undefined, - { - description: `Set runtime chunk`, - validator: t.isOneOf([ - t.isLiteral(`single`), - t.isLiteral(`multiple`), - t.isBoolean(), - ]), - tolerateBoolean: true, - }, - ) - public splitChunks = Option.Boolean( - `--splitChunks,--vendor`, - undefined, - { - description: `Separate vendor bundle`, - }, - ) - public storage = Option.String(`--storage`, undefined, { - description: `Storage directory (relative to project)`, - env: `APP_PATH_STORAGE`, - }) - public ci = Option.Boolean(`--ci`, undefined, { - description: `Simple build summaries for CI`, + public clean = clean + + public devtool = devtool + + public discover = discover + + public override dry = Option.Boolean(`--dry`, false, { + description: `run in dry mode`, }) - public use = Option.Array(`--use`, undefined, { - description: `Enable an extension`, + + public editor = editor + + public esm = esm + + public force = force + + public hash = hash + + public html = html + + public immutable = immutable + + public input = input + + public minimize = minimize + + public output = output + + public publicPath = publicPath + + public runtime = runtime + + public override silent = Option.Boolean(`--silent`, false, { + description: `suppress stdout output`, }) - public override withContext = async (context: CommandContext) => { - if (this.withSubcommandContext) { - context = await this.withSubcommandContext(context) - } - return context - } + public splitChunks = splitChunks - public override withArguments = async ( - args: BudCommand[`bud`][`context`][`args`], - ) => { - if (this.withSubcommandArguments) { - args = await this.withSubcommandArguments(args) + public storage = storage + + public use = use + + /** + * {@link Command.withContext} + */ + public override withContext = async (context: Context) => { + if (`withSubcommandContext` in this) { + context = await this.withSubcommandContext(context) } return { - ...args, + ...context, cache: this.cache, ci: this.ci, clean: this.clean, @@ -170,11 +119,10 @@ export default class BudBuildCommand extends BudCommand { immutable: this.immutable, input: this.input, output: this.output, - manifest: this.manifest, minimize: this.minimize, - mode: this.mode, publicPath: this.publicPath, runtime: this.runtime, + silent: this.silent, splitChunks: this.splitChunks, storage: this.storage, target: this.filter, @@ -183,11 +131,12 @@ export default class BudBuildCommand extends BudCommand { } /** - * Execute command + * {@link Command.execute} */ public override async execute() { - await this.makeBud(this) + await this.makeBud() await this.healthcheck(this) - await this.bud.run() + await this.bud?.run() + await this.bud?.dashboard?.renderQueuedMessages() } } diff --git a/sources/@roots/bud/src/cli/commands/bud.clean.tsx b/sources/@roots/bud/src/cli/commands/bud.clean.tsx index 7ad48a434d..f0727cefcd 100644 --- a/sources/@roots/bud/src/cli/commands/bud.clean.tsx +++ b/sources/@roots/bud/src/cli/commands/bud.clean.tsx @@ -1,6 +1,7 @@ import type {Bud} from '@roots/bud' import BudCommand from '@roots/bud/cli/commands/bud' import {dry} from '@roots/bud/cli/decorators/command.dry' +import storage from '@roots/bud/cli/flags/storage' import {Command, Option} from '@roots/bud-support/clipanion' import {bind} from '@roots/bud-support/decorators/bind' import * as Ink from 'ink' @@ -28,38 +29,39 @@ export default class BudCleanCommand extends BudCommand { ], }) - public storage = Option.String(`--storage`, undefined, { - description: `Storage directory (relative to project)`, - env: `APP_PATH_STORAGE`, - }) - public storageArg = Option.Boolean(`@storage,storage`, false, { + public storage = storage + + public storagePositional = Option.Boolean(`@storage,storage`, false, { description: `empty @storage`, }) - public outputArg = Option.Boolean(`@dist,dist,output`, false, { + public outputPositional = Option.Boolean(`@dist,dist,output`, false, { description: `empty @dist`, }) - public cacheArg = Option.Boolean(`@cache,cache`, false, { + public cachePositional = Option.Boolean(`@cache,cache`, false, { description: `empty @cache`, }) /** - * Execute command + * {@link Command.execute} */ public override async execute() { - await this.makeBud(this) + await this.makeBud() await this.healthcheck(this) - const cleanAll = !this.outputArg && !this.storageArg && !this.cacheArg + const cleanAll = + !this.outputPositional && + !this.storagePositional && + !this.cachePositional - if (this.storageArg || cleanAll) { + if (this.storagePositional || cleanAll) { await this.cleanStorage() } - if (this.outputArg || cleanAll) { + if (this.outputPositional || cleanAll) { await this.cleanOutput() } - if (this.cacheArg || cleanAll) { + if (this.cachePositional || cleanAll) { await this.cleanCache() } } diff --git a/sources/@roots/bud/src/cli/commands/bud.tsx b/sources/@roots/bud/src/cli/commands/bud.tsx index c410be01c9..3c10ac6b30 100644 --- a/sources/@roots/bud/src/cli/commands/bud.tsx +++ b/sources/@roots/bud/src/cli/commands/bud.tsx @@ -1,51 +1,52 @@ -/* eslint-disable n/no-process-exit */ -import {platform} from 'node:os' - +import basedir from '@roots/bud/cli/flags/basedir' +import color from '@roots/bud/cli/flags/color' +import debug from '@roots/bud/cli/flags/debug' +import dry from '@roots/bud/cli/flags/dry' +import filter from '@roots/bud/cli/flags/filter' +import log from '@roots/bud/cli/flags/log' +import mode from '@roots/bud/cli/flags/mode' +import notify from '@roots/bud/cli/flags/notify' +import silent from '@roots/bud/cli/flags/silent' +import verbose from '@roots/bud/cli/flags/verbose' import {checkDependencies} from '@roots/bud/cli/helpers/checkDependencies' import {checkPackageManagerErrors} from '@roots/bud/cli/helpers/checkPackageManagerErrors' import {isset} from '@roots/bud/cli/helpers/isset' import * as instances from '@roots/bud/instances' import {Console} from '@roots/bud-dashboard/console' import {Bud} from '@roots/bud-framework' -import type { - CommandContext, - Context, -} from '@roots/bud-framework/options/context' +import type {Context} from '@roots/bud-framework/options/context' import type {BaseContext} from '@roots/bud-support/clipanion' import {Command, Option} from '@roots/bud-support/clipanion' import {bind} from '@roots/bud-support/decorators/bind' import {BudError, BudHandler} from '@roots/bud-support/errors' import isString from '@roots/bud-support/lodash/isString' -import * as t from '@roots/bud-support/typanion' +import omit from '@roots/bud-support/lodash/omit' import * as Ink from 'ink' import * as Display from '../components/Error.js' import {Menu} from '../components/Menu.js' import {WinError} from '../components/WinError.js' import {isWindows} from '../helpers/isWindows.js' +import type {CLIContext} from '../index.js' -export type {BaseContext, CommandContext, Context} +export type {BaseContext, Context} export {Option} -export interface ArgsModifier { - (from: T): (on: T) => Promise -} -export const ArgsModifier: ArgsModifier = from => async on => ({ - ...from, - ...on, -}) - /** * Bud command */ -export default class BudCommand extends Command { +export default class BudCommand extends Command { /** * Bud instance */ - public declare bud?: (Bud & {context: CommandContext}) | undefined + public declare bud?: Bud | undefined /** * Binary (node, ts-node, bun) + * + * @remarks + * String like `node`, `ts-node`, or `bun`. For executing child + * processes with the same binary as the parent. */ public get bin() { // eslint-disable-next-line n/no-process-env @@ -55,7 +56,7 @@ export default class BudCommand extends Command { /** * {@link Command.context} */ - public declare context: CommandContext + public declare context: CLIContext /** * {@link Command.paths} @@ -75,67 +76,47 @@ export default class BudCommand extends Command { examples: [[`compile source assets`, `$0 build`]], }) - public declare withArguments?: ( - args: CommandContext[`args`], - ) => Promise - - public declare withSubcommandArguments?: ( - args: CommandContext[`args`], - ) => Promise - + /** + * withContext + * + * @remarks + * For extending the context object from subcommands + */ public declare withContext?: ( - context: CommandContext, - ) => Promise + context: Omit, + ) => Promise + /** + * withSubcommandContext + * + * @remaks + * For extending the context object from subcommands + */ public declare withSubcommandContext?: ( - context: CommandContext, - ) => Promise - - public declare withBud?: ( - bud: BudCommand[`bud`], - ) => Promise - - public notify: boolean = Option.Boolean( - `--notify`, - platform() === `darwin`, - { - description: `Enable notification (default on macOS, experimental on other platforms)`, - }, - ) - - public basedir = Option.String(`--basedir,--cwd`, undefined, { - description: `project base directory`, - hidden: true, - }) + context: Context, + ) => Promise - public debug = Option.Boolean(`--debug`, undefined, { - description: `Enable debug mode`, - }) + public basedir = basedir - public log = Option.Boolean(`--log`, undefined, { - description: `Enable logging`, - hidden: true, - }) + public color = color - public verbose = Option.Boolean(`--verbose`, undefined, { - description: `Log verbose output`, - }) + public dry = dry - public mode = Option.String(`--mode`, undefined, { - description: `Compilation mode`, - validator: t.isOneOf([ - t.isLiteral(`production`), - t.isLiteral(`development`), - ]), - }) + public silent = silent - public filter = Option.Array(`--filter`, undefined, { - description: `Limit command to particular compilers`, - }) + public debug = debug - public render(children: React.ReactElement) { - Ink?.render(children) - } + public filter = filter + + public log = log + + public mode = mode + + public notify = notify + + public verbose = verbose + + public render = Ink.render public async renderStatic(...children: Array) { return Ink?.render( @@ -145,69 +126,67 @@ export default class BudCommand extends Command { ).unmount() } - public async makeBud(command?: T) { - this.context.mode = this.mode ?? this.context.mode - this.context.args = Object.entries({ - ...this.context.args, - basedir: this.context.basedir, + public async makeBud() { + const context = { + basedir: this.basedir, + color: this.color, + dry: this.dry, debug: this.debug, filter: this.filter, log: this.log, + mode: this.mode, notify: this.notify, + silent: this.silent, target: this.filter, verbose: this.verbose, - }) - .filter(([k, v]) => v !== undefined) - .reduce( - (acc, [k, v]) => ({ - ...acc, - [k]: v, - }), - {}, - ) as CommandContext[`args`] - - if (this.withArguments) { - this.context.args = await this.withArguments(this.context.args) + ...omit(this.context, [`stdin`, `stdout`, `stderr`, `colorDepth`]), } - if (this.withContext) { - this.context = await this.withContext(this.context) - } + Object.assign( + this.context, + this.withContext ? await this.withContext(context) : context, + ) + await import(`../env.${this.context.mode}.js`) - if (this.context.mode === `development`) { - await import(`../env.development.js`) - } else { - await import(`../env.production.js`) - } + this.bud = instances.get() - this.bud = instances.get() as Bud & { - context: CommandContext - } + this.context.logger.info(`bud.js configured with`, context) try { - await this.bud.lifecycle(this.context) - } catch (err) { - if (err.isBudError) throw err - throw BudError.normalize(err) + await this.bud.lifecycle( + omit(this.context, [ + `stdin`, + `stdout`, + `stderr`, + `colorDepth`, + ]) as Context, + ) + } catch (error) { + if (error.isBudError) throw error + throw BudError.normalize(error) } this.bud.hooks.action(`build.before`, async bud => { - if (!bud.isCLI()) return await this.applyBudEnv(bud) await bud.api.processQueue() }) - await this.applyBudEnv(this.bud) - await this.applyBudManifestOptions(this.bud) - await this.applyBudArguments(this.bud) + await Promise.all([ + this.applyBudEnv(this.bud), + this.applyBudManifestOptions(this.bud), + this.applyBudArguments(this.bud), + ]) - if (this.withBud) await this.withBud(this.bud) await this.bud.processConfigs() await this.applyBudArguments(this.bud) + return this.bud } + /** + * Execute arbitrary sh command with inherited stdio + */ @bind public async $(bin: string, args: Array, options = {}) { const {execa: command} = await import(`@roots/bud-support/execa`) @@ -220,13 +199,20 @@ export default class BudCommand extends Command { }) } - public async healthcheck(command: BudCommand) { + /** + * Check bud.js system and environment requirements are met + */ + @bind + public async healthcheck(_bud: any) { try { - checkPackageManagerErrors(command.bud) - await checkDependencies(command.bud) + checkPackageManagerErrors(this.bud) + await checkDependencies(this.bud) } catch (e) {} } + /** + * Apply context from env to bud.js instance + */ @bind public async applyBudEnv(bud: Bud) { bud @@ -254,6 +240,9 @@ export default class BudCommand extends Command { ) } + /** + * Apply context from manifest to bud.js instance + */ @bind public async applyBudManifestOptions(bud: Bud) { const {bud: manifest} = bud.context.manifest @@ -275,46 +264,47 @@ export default class BudCommand extends Command { } /** - * Apply context from argv + * Apply context from argv to bud.js instance */ @bind public async applyBudArguments(bud: BudCommand[`bud`]) { - const {args, logger} = bud.context + const {logger, ...context} = bud.context - isset(args.input) && bud.setPath(`@src`, args.input) - isset(args.output) && bud.setPath(`@dist`, args.output) - isset(args.publicPath) && bud.setPublicPath(args.publicPath) - isset(args.modules) && bud.setPath(`@modules`, args.modules) + isset(context.input) && bud.setPath(`@src`, context.input) + isset(context.output) && bud.setPath(`@dist`, context.output) + isset(context.publicPath) && bud.setPublicPath(context.publicPath) + isset(context.modules) && bud.setPath(`@modules`, context.modules) - if (isset(args.hot)) { + if (isset(context.hot)) { bud.log(`disabling hot module replacement`) bud.hooks.on(`dev.middleware.enabled`, (middleware = []) => middleware.filter(key => - args.hot === false ? key !== `hot` : args.hot, + context.hot === false ? key !== `hot` : context.hot, ), ) } - if (isset(args.port)) { + if (isset(context.port)) { bud.log(`overriding port from cli`) bud.hooks.on(`dev.url`, (url = new URL(`http://0.0.0.0:3000`)) => { - url.port = args.port + url.port = context.port return url }) } - if (isset(args.proxy)) { + if (isset(context.proxy)) { bud.log(`overriding proxy from cli`) bud.hooks.on( `dev.middleware.proxy.options.target`, - new URL(args.proxy), + new URL(context.proxy), ) } /** - * Override settings for either: - * - the parent (if children do not exist), or; - * - all children but not the parent (if children exist) + * Override settings: + * + * - when children: all children but not the parent + * - when no children: the parent; */ const override = (override: (bud: Bud) => void) => { bud.hasChildren @@ -322,71 +312,68 @@ export default class BudCommand extends Command { : override(bud) } - if (isset(args.manifest)) { - bud.log(`overriding manifest setting from cli`) - override(bud => bud.manifest.enable(args.manifest)) - } - - if (isset(args.cache)) { + if (isset(context.cache)) { bud.log(`overriding cache settings from cli`) - override(bud => bud.persist(args.cache)) + override(bud => bud.persist(context.cache)) } - if (isset(args.minimize)) { + if (isset(context.minimize)) { bud.log(`overriding minimize setting from cli`) - override(bud => bud.minimize(args.minimize)) + override(bud => bud.minimize(context.minimize)) } - if (isset(args.devtool)) { + if (isset(context.devtool)) { bud.log(`overriding devtool from cli`) - override(bud => bud.devtool(args.devtool)) + override(bud => bud.devtool(context.devtool)) } - if (isset(args.esm)) { + if (isset(context.esm)) { bud.log(`overriding esm from cli`) - override((bud: Bud) => bud.esm.enable(args.esm)) + override((bud: Bud) => bud.esm.enable(context.esm)) } - if (isset(args.immutable)) { + if (isset(context.immutable)) { bud.log(`overriding immutable from cli`) - override((bud: Bud) => bud.cdn.freeze(args.immutable)) + override((bud: Bud) => bud.cdn.freeze(context.immutable)) } - if (isset(args.clean)) { + if (isset(context.clean)) { bud.log(`overriding clean setting from cli`) override((bud: Bud) => { bud.extensions .get(`@roots/bud-extensions/clean-webpack-plugin`) - .enable(args.clean) + .enable(context.clean) - bud.hooks.on(`build.output.clean`, args.clean) + bud.hooks.on(`build.output.clean`, context.clean) }) } - if (isset(args.hash)) { + if (isset(context.hash)) { logger.log(`overriding hash setting from cli`) - override((bud: Bud) => bud.hash(args.hash)) + override((bud: Bud) => bud.hash(context.hash)) } - if (isset(args.html)) { + if (isset(context.html)) { logger.log(`overriding html setting from cli`) override((bud: Bud) => - isString(args.html) ? bud.html({template: args.html}) : bud.html(), + isString(context.html) + ? bud.html({template: context.html}) + : bud.html(context.html), ) } - if (isset(args.runtime)) { + if (isset(context.runtime)) { bud.log(`overriding runtime setting from cli`) - override((bud: Bud) => bud.runtime(args.runtime)) + override((bud: Bud) => bud.runtime(context.runtime)) } - if (isset(args.splitChunks)) { + if (isset(context.splitChunks)) { bud.log(`overriding splitChunks setting from cli`) - override((bud: Bud) => bud.splitChunks(args.splitChunks)) + override((bud: Bud) => bud.splitChunks(context.splitChunks)) } - if (args.use) { - await bud.extensions.add(args.use as any) + if (context.use) { + await bud.extensions.add(context.use as any) } await bud.api.processQueue() @@ -400,20 +387,20 @@ export default class BudCommand extends Command { } /** - * Execute command + * {@link Command.execute} */ public async execute() { this.render() - await this.bud?.dashboard?.renderQueuedMessages() } /** * Handle errors */ public override async catch(err: BudHandler) { - process.exitCode = 1 - let error: BudHandler + + global.process.exitCode = 1 + if (isString(err)) error = BudError.normalize(err) else if (err.isBudError) error = err @@ -440,11 +427,13 @@ export default class BudCommand extends Command { {isWindows() ? : null} , ) - if (this.bud.isProduction) process.exit(1) + if (this.bud.isProduction) global.process.exit(1) } catch (e) { - if (error.message) process.stderr.write(error.message.concat(`\n`)) - if (err.message) process.stderr.write(err.message.concat(`\n`)) - if (this.bud.isProduction) process.exit(1) + if (error.message) + global.process.stderr.write(error.message.concat(`\n`)) + if (err.message) + global.process.stderr.write(err.message.concat(`\n`)) + if (this.bud.isProduction) global.process.exit(1) } } } diff --git a/sources/@roots/bud/src/cli/commands/bud.upgrade.tsx b/sources/@roots/bud/src/cli/commands/bud.upgrade.tsx index 5aa2a34dbb..c77e6ba520 100644 --- a/sources/@roots/bud/src/cli/commands/bud.upgrade.tsx +++ b/sources/@roots/bud/src/cli/commands/bud.upgrade.tsx @@ -1,4 +1,4 @@ -import BudCommand, {ArgsModifier} from '@roots/bud/cli/commands/bud' +import BudCommand from '@roots/bud/cli/commands/bud' import {dry} from '@roots/bud/cli/decorators/command.dry' import {Command, Option} from '@roots/bud-support/clipanion' import {bind} from '@roots/bud-support/decorators/bind' @@ -11,7 +11,14 @@ import {isInternalDevelopmentEnv} from '../helpers/isInternalDevelopmentEnv.js' */ @dry export default class BudUpgradeCommand extends BudCommand { + /** + * {@link Command.paths} + */ public static override paths = [[`upgrade`], [`version`, `set`]] + + /** + * {@link Command.usage} + */ public static override usage = Command.Usage({ description: `Set bud.js version`, details: ` @@ -35,14 +42,22 @@ export default class BudUpgradeCommand extends BudCommand { ], ], }) - public override withArguments = ArgsModifier({dry: true}) + /** + * Request a specific version of bud.js + */ public version = Option.String({required: false}) + /** + * Use an alternative registry + */ public registry = Option.String(`--registry`, undefined, { description: `custom registry`, }) + /** + * Package manager + */ public get pacman(): `yarn` | `npm` { const pacman = detectPackageManager(this.bud) if (pacman === false) throw new Error(`Package manager is ambiguous`) @@ -54,7 +69,7 @@ export default class BudUpgradeCommand extends BudCommand { } public override async execute() { - await this.makeBud(this) + await this.makeBud() await this.healthcheck(this) await this.bud.run() diff --git a/sources/@roots/bud/src/cli/commands/bud.view.tsx b/sources/@roots/bud/src/cli/commands/bud.view.tsx index a2fb95be09..7eba9c24f5 100644 --- a/sources/@roots/bud/src/cli/commands/bud.view.tsx +++ b/sources/@roots/bud/src/cli/commands/bud.view.tsx @@ -1,4 +1,5 @@ import BudCommand from '@roots/bud/cli/commands/bud' +import indent from '@roots/bud/cli/flags/indent' import {Command, Option} from '@roots/bud-support/clipanion' import {highlight} from '@roots/bud-support/highlight' import get from '@roots/bud-support/lodash/get' @@ -9,7 +10,14 @@ import * as Ink from 'ink' * `bud view` command */ export default class BudViewCommand extends BudCommand { + /** + * {@link Command.paths} + */ public static override paths = [[`view`]] + + /** + * {@link Command.usage} + */ public static override usage = Command.Usage({ description: `Explore bud object`, examples: [ @@ -18,33 +26,21 @@ export default class BudViewCommand extends BudCommand { ], }) - public color = Option.Boolean(`--color,-c`, true, { - description: `use syntax highlighting`, - }) - - public indent = Option.String(`--indent,-i`, `2`, { - description: `indentation level`, - tolerateBoolean: true, - }) + public indent = indent + /** + * Positional + */ public subject = Option.String({name: `subject`, required: false}) public override async execute() { - await this.makeBud(this) + await this.makeBud() await this.bud.run() let value = this.subject ? get(this.bud, this.subject) : this.bud let indent = 0 switch (this.indent) { - case true: - indent = 2 - break - - case false: - indent = 0 - break - case undefined: indent = 2 break diff --git a/sources/@roots/bud/src/cli/commands/bud.webpack.tsx b/sources/@roots/bud/src/cli/commands/bud.webpack.tsx index 0ddcf9289d..d4796a1e14 100644 --- a/sources/@roots/bud/src/cli/commands/bud.webpack.tsx +++ b/sources/@roots/bud/src/cli/commands/bud.webpack.tsx @@ -9,8 +9,14 @@ import {Command, Option} from '@roots/bud-support/clipanion' */ @dry export default class BudWebpackCommand extends BudCommand { + /** + * {@link Command.paths} + */ public static override paths = [[`webpack`]] + /** + * {@link Command.usage} + */ public static override usage = Command.Usage({ description: `Webpack CLI passthrough`, category: `tools`, @@ -20,10 +26,10 @@ export default class BudWebpackCommand extends BudCommand { public options = Option.Proxy({name: `webpack passthrough options`}) /** - * Command execute + * {@link Command.execute} */ public override async execute() { - await this.makeBud(this) + await this.makeBud() await this.bud.run() const bin = join( @@ -32,7 +38,7 @@ export default class BudWebpackCommand extends BudCommand { `webpack.js`, ) - process.stdout.write(`\n\n$ ${this.bin} ${bin}\n\n`) + this.context.stdout.write(`\n\n$ ${this.bin} ${bin}\n\n`) await this.$(this.bin, [ bin, diff --git a/sources/@roots/bud/src/cli/commands/doctor/index.tsx b/sources/@roots/bud/src/cli/commands/doctor/index.tsx index d43ce547a0..300062ca6a 100644 --- a/sources/@roots/bud/src/cli/commands/doctor/index.tsx +++ b/sources/@roots/bud/src/cli/commands/doctor/index.tsx @@ -2,7 +2,7 @@ import BudCommand from '@roots/bud/cli/commands/bud' import {Error} from '@roots/bud-dashboard/app' import type {Extension} from '@roots/bud-framework' -import type {CommandContext} from '@roots/bud-framework/options' +import type {Context} from '@roots/bud-framework/options' import {Command} from '@roots/bud-support/clipanion' import {bind} from '@roots/bud-support/decorators/bind' import {BudError, InputError} from '@roots/bud-support/errors' @@ -41,8 +41,12 @@ for a lot of edge cases so it might return a false positive. [`Check compiled configuration against webpack`, `$0 doctor`], ], }) - public override withArguments = async (args: CommandContext[`args`]) => { - return {...args, cache: false, dry: true} + + /** + * {@link Command.withContext} + */ + public override withContext = async (context: Context) => { + return {...context, cache: false, dry: true, silent: true} } public configuration: webpack.Configuration @@ -69,8 +73,9 @@ for a lot of edge cases so it might return a false positive. const {Doctor} = await import(`./Doctor.js`) const buildTimer = this.makeTimer() - await this.makeBud(this) + await this.makeBud() await this.bud.run() + this.timings.build = buildTimer() if (isWindows()) { @@ -170,61 +175,6 @@ for a lot of edge cases so it might return a false positive. , ) - const check = - (top = false) => - async ([signifier, version]) => { - if (!top) { - this.resolvedDependencies[signifier] = version - } - - try { - const path = await this.bud.module.getManifestPath(signifier) - if (!path) return - - const manifest = await this.bud.fs.json.read(path) - if (!manifest.dependencies) return - - Object.entries(manifest.dependencies).forEach( - ([signifier, version]: [string, string]) => { - this.resolvedDependencies[signifier] = version - }, - ) - - await Promise.all( - Object.entries(manifest.dependencies) - .filter(([signifier]) => signifier.startsWith(`@roots`)) - .flatMap(check()), - ) - } catch (e) {} - } - - try { - await Promise.all( - [ - ...(this.bud.context.manifest?.dependencies - ? Object.entries(this.bud.context.manifest.dependencies) - : []), - ...(this.bud.context.manifest?.devDependencies - ? Object.entries(this.bud.context.manifest.devDependencies) - : []), - ] - .filter(Boolean) - .filter(([signifier]) => signifier.startsWith(`@roots`)) - .map(check(true)), - ) - } catch (e) {} - - this.renderStatic( - - - Checking installed package compatibility{`\n`} - - {Object.entries(this.resolvedDependencies).map( - this.formatDepCheck, - )} - , - ) - this.renderStatic( Mode diff --git a/sources/@roots/bud/src/cli/commands/repl/index.tsx b/sources/@roots/bud/src/cli/commands/repl/index.tsx index 1d3aa44685..88f3f7b471 100644 --- a/sources/@roots/bud/src/cli/commands/repl/index.tsx +++ b/sources/@roots/bud/src/cli/commands/repl/index.tsx @@ -1,56 +1,41 @@ import {dry} from '@roots/bud/cli/decorators' +import indent from '@roots/bud/cli/flags/indent' import {bind} from '@roots/bud-framework/extension/decorators' import {Command, Option} from '@roots/bud-support/clipanion' import BudCommand from '../bud.js' /** - * bud repl command + * `bud repl` */ @dry export default class BudReplCommand extends BudCommand { /** - * Command paths + * {@link Command.paths} */ public static override paths = [[`repl`]] /** - * Command usage + * {@link Command.usage} */ public static override usage = Command.Usage({ description: `Use bud in a repl`, examples: [[`repl`, `$0 repl`]], }) - /** - * `--color` - */ - public color = Option.Boolean(`--color,-c`, true, { - description: `use syntax highlighting`, - }) - - /** - * `--indent` - */ - public indent = Option.String(`--indent,-i`, `1`, { - description: `indentation level`, - tolerateBoolean: false, - }) + public indent = indent - /** - * `--depth` - */ public depth = Option.String(`--depth,-d`, `1`, { description: `recursion depth`, tolerateBoolean: false, }) /** - * Execute command + * {@link Command.execute} */ @bind public override async execute() { - await this.makeBud(this) + await this.makeBud() await this.bud.run() const {Repl} = await import(`./Repl.js`) diff --git a/sources/@roots/bud/src/cli/decorators/command.dry.ts b/sources/@roots/bud/src/cli/decorators/command.dry.ts index 9fa9c20fcc..488db1478a 100644 --- a/sources/@roots/bud/src/cli/decorators/command.dry.ts +++ b/sources/@roots/bud/src/cli/decorators/command.dry.ts @@ -3,9 +3,9 @@ export function dry(constructor: any): any { public constructor() { super() - const fn = this.withArguments?.bind(this) ?? (value => value) + const fn = this.withContext?.bind(this) ?? (value => value) - this.withArguments = async (args: any) => { + this.withContext = async (args: any) => { args = await fn(args) return {...args, dry: true} } diff --git a/sources/@roots/bud/src/cli/finder.ts b/sources/@roots/bud/src/cli/finder.ts index 36fe8851b7..3b2a03feeb 100644 --- a/sources/@roots/bud/src/cli/finder.ts +++ b/sources/@roots/bud/src/cli/finder.ts @@ -4,7 +4,6 @@ import {fileURLToPath} from 'node:url' import type {Context} from '@roots/bud-framework/options' import {bind} from '@roots/bud-support/decorators/bind' -import globby from '@roots/bud-support/globby' import {resolve} from '@roots/bud-support/import-meta-resolve' import isString from '@roots/bud-support/lodash/isString' @@ -49,7 +48,9 @@ export class Commands { public async getRegistrationModulePaths(): Promise> { return await this.resolveExtensionCommandPaths( this.getProjectDependencySignifiers(), - ).then(this.findExtensionCommandPaths) + ) + .then(this.findExtensionCommandPaths) + .then(this.resolveExtensionCommandPaths) } /** @@ -67,15 +68,10 @@ export class Commands { * Find commands shipped with a given extension */ @bind - public async findExtensionCommandPaths(paths: Array) { - return await Promise.all( - paths - .map(dirname) - .map( - async path => - await globby(join(path, join(`bud`, `commands`, `index.js`))), - ), - ).then(results => results.flat()) + public findExtensionCommandPaths(paths: Array) { + return paths + .map(dirname) + .map(path => join(path, join(`bud`, `commands`, `index.js`))) } public async resolveExtensionCommandPaths(paths: Array) { diff --git a/sources/@roots/bud/src/cli/flags/basedir.ts b/sources/@roots/bud/src/cli/flags/basedir.ts new file mode 100644 index 0000000000..a21fbf9b1c --- /dev/null +++ b/sources/@roots/bud/src/cli/flags/basedir.ts @@ -0,0 +1,6 @@ +import {Option} from '@roots/bud-support/clipanion' + +export default Option.String(`--basedir,--cwd`, undefined, { + description: `project base directory`, + hidden: true, +}) diff --git a/sources/@roots/bud/src/cli/flags/browser.ts b/sources/@roots/bud/src/cli/flags/browser.ts new file mode 100644 index 0000000000..66bf2d5638 --- /dev/null +++ b/sources/@roots/bud/src/cli/flags/browser.ts @@ -0,0 +1,6 @@ +import {Option} from '@roots/bud-support/clipanion' + +export default Option.String(`--browser`, undefined, { + description: `Open browser on successful development build.`, + tolerateBoolean: true, +}) diff --git a/sources/@roots/bud/src/cli/flags/cache.ts b/sources/@roots/bud/src/cli/flags/cache.ts new file mode 100644 index 0000000000..cca03b3f1d --- /dev/null +++ b/sources/@roots/bud/src/cli/flags/cache.ts @@ -0,0 +1,14 @@ +import {Option} from '@roots/bud-support/clipanion' +import {isLiteral, isOneOf} from '@roots/bud-support/typanion' + +export default Option.String(`--cache`, undefined, { + description: `Utilize compiler's filesystem cache`, + tolerateBoolean: true, + validator: isOneOf([ + isLiteral(`filesystem`), + isLiteral(`memory`), + isLiteral(true), + isLiteral(false), + ]), + env: `APP_CACHE`, +}) diff --git a/sources/@roots/bud/src/cli/flags/ci.ts b/sources/@roots/bud/src/cli/flags/ci.ts new file mode 100644 index 0000000000..4470f34487 --- /dev/null +++ b/sources/@roots/bud/src/cli/flags/ci.ts @@ -0,0 +1,5 @@ +import {Option} from '@roots/bud-support/clipanion' + +export default Option.Boolean(`--ci`, undefined, { + description: `Simple build summaries for CI`, +}) diff --git a/sources/@roots/bud/src/cli/flags/clean.ts b/sources/@roots/bud/src/cli/flags/clean.ts new file mode 100644 index 0000000000..0013d75bc3 --- /dev/null +++ b/sources/@roots/bud/src/cli/flags/clean.ts @@ -0,0 +1,5 @@ +import {Option} from '@roots/bud-support/clipanion' + +export default Option.Boolean(`--clean`, undefined, { + description: `Clean artifacts and distributables prior to compilation`, +}) diff --git a/sources/@roots/bud/src/cli/flags/color.ts b/sources/@roots/bud/src/cli/flags/color.ts new file mode 100644 index 0000000000..7687475658 --- /dev/null +++ b/sources/@roots/bud/src/cli/flags/color.ts @@ -0,0 +1,4 @@ +import {Option} from '@roots/bud-support/clipanion' +export default Option.Boolean(`--color,-c`, true, { + description: `Colorize output`, +}) diff --git a/sources/@roots/bud/src/cli/flags/debug.ts b/sources/@roots/bud/src/cli/flags/debug.ts new file mode 100644 index 0000000000..09d1b3766f --- /dev/null +++ b/sources/@roots/bud/src/cli/flags/debug.ts @@ -0,0 +1,5 @@ +import {Option} from '@roots/bud-support/clipanion' + +export default Option.Boolean(`--debug`, undefined, { + description: `Enable debug mode`, +}) diff --git a/sources/@roots/bud/src/cli/flags/devtool.ts b/sources/@roots/bud/src/cli/flags/devtool.ts new file mode 100644 index 0000000000..e57b334a13 --- /dev/null +++ b/sources/@roots/bud/src/cli/flags/devtool.ts @@ -0,0 +1,34 @@ +import {Option} from '@roots/bud-support/clipanion' +import {isLiteral, isOneOf} from '@roots/bud-support/typanion' + +export default Option.String(`--devtool`, undefined, { + description: `Set devtool option`, + validator: isOneOf([ + isLiteral(false), + isLiteral(`eval`), + isLiteral(`eval-cheap-source-map`), + isLiteral(`eval-cheap-module-source-map`), + isLiteral(`eval-source-map`), + isLiteral(`cheap-source-map`), + isLiteral(`cheap-module-source-map`), + isLiteral(`source-map`), + isLiteral(`inline-cheap-source-map`), + isLiteral(`inline-cheap-module-source-map`), + isLiteral(`inline-source-map`), + isLiteral(`eval-nosources-cheap-source-map`), + isLiteral(`eval-nosources-cheap-modules-source-map`), + isLiteral(`eval-nosources-source-map`), + isLiteral(`inline-nosources-cheap-source-map`), + isLiteral(`inline-nosources-cheap-module-source-map`), + isLiteral(`inline-nosources-source-map`), + isLiteral(`nosources-cheap-source-map`), + isLiteral(`nosources-cheap-module-source-map`), + isLiteral(`hidden-nosources-cheap-source-map`), + isLiteral(`hidden-nosources-cheap-module-source-map`), + isLiteral(`hidden-nosources-source-map`), + isLiteral(`hidden-cheap-source-map`), + isLiteral(`hidden-cheap-module-source-map`), + isLiteral(`hidden-source-map`), + ]), + env: `APP_DEVTOOL`, +}) diff --git a/sources/@roots/bud/src/cli/flags/discover.ts b/sources/@roots/bud/src/cli/flags/discover.ts new file mode 100644 index 0000000000..5e8740a00d --- /dev/null +++ b/sources/@roots/bud/src/cli/flags/discover.ts @@ -0,0 +1,5 @@ +import {Option} from '@roots/bud-support/clipanion' + +export default Option.Boolean(`--discover,--discovery`, undefined, { + description: `Automatically register extensions`, +}) diff --git a/sources/@roots/bud/src/cli/flags/dry.ts b/sources/@roots/bud/src/cli/flags/dry.ts new file mode 100644 index 0000000000..5b6e182741 --- /dev/null +++ b/sources/@roots/bud/src/cli/flags/dry.ts @@ -0,0 +1,5 @@ +import {Option} from '@roots/bud-support/clipanion' + +export default Option.Boolean(`--dry`, true, { + description: `run in dry mode`, +}) diff --git a/sources/@roots/bud/src/cli/flags/editor.ts b/sources/@roots/bud/src/cli/flags/editor.ts new file mode 100644 index 0000000000..781c831efb --- /dev/null +++ b/sources/@roots/bud/src/cli/flags/editor.ts @@ -0,0 +1,6 @@ +import {Option} from '@roots/bud-support/clipanion' + +export default Option.String(`--editor`, undefined, { + description: `Open editor to file containing errors on unsuccessful development build`, + tolerateBoolean: true, +}) diff --git a/sources/@roots/bud/src/cli/flags/esm.ts b/sources/@roots/bud/src/cli/flags/esm.ts new file mode 100644 index 0000000000..8d3dbeb0a6 --- /dev/null +++ b/sources/@roots/bud/src/cli/flags/esm.ts @@ -0,0 +1,5 @@ +import {Option} from '@roots/bud-support/clipanion' + +export default Option.Boolean(`--esm`, undefined, { + description: `build as es modules`, +}) diff --git a/sources/@roots/bud/src/cli/flags/filter.ts b/sources/@roots/bud/src/cli/flags/filter.ts new file mode 100644 index 0000000000..ae38d00bc3 --- /dev/null +++ b/sources/@roots/bud/src/cli/flags/filter.ts @@ -0,0 +1,5 @@ +import {Option} from '@roots/bud-support/clipanion' + +export default Option.Array(`--filter`, undefined, { + description: `Limit command to particular compilers`, +}) diff --git a/sources/@roots/bud/src/cli/flags/force.ts b/sources/@roots/bud/src/cli/flags/force.ts new file mode 100644 index 0000000000..ff4dcd1315 --- /dev/null +++ b/sources/@roots/bud/src/cli/flags/force.ts @@ -0,0 +1,5 @@ +import {Option} from '@roots/bud-support/clipanion' + +export default Option.Boolean(`--force,--flush`, undefined, { + description: `Force clearing all caches`, +}) diff --git a/sources/@roots/bud/src/cli/flags/hash.ts b/sources/@roots/bud/src/cli/flags/hash.ts new file mode 100644 index 0000000000..189eb54a11 --- /dev/null +++ b/sources/@roots/bud/src/cli/flags/hash.ts @@ -0,0 +1,5 @@ +import {Option} from '@roots/bud-support/clipanion' + +export default Option.Boolean(`--hash`, undefined, { + description: `Hash compiled filenames`, +}) diff --git a/sources/@roots/bud/src/cli/flags/hot.ts b/sources/@roots/bud/src/cli/flags/hot.ts new file mode 100644 index 0000000000..49ba02db7d --- /dev/null +++ b/sources/@roots/bud/src/cli/flags/hot.ts @@ -0,0 +1,5 @@ +import {Option} from '@roots/bud-support/clipanion' + +export default Option.Boolean(`--hot`, undefined, { + description: `Enable hot module replacement`, +}) diff --git a/sources/@roots/bud/src/cli/flags/html.ts b/sources/@roots/bud/src/cli/flags/html.ts new file mode 100644 index 0000000000..af9c752f7b --- /dev/null +++ b/sources/@roots/bud/src/cli/flags/html.ts @@ -0,0 +1,6 @@ +import {Option} from '@roots/bud-support/clipanion' + +export default Option.String(`--html`, undefined, { + description: `Generate an html template`, + tolerateBoolean: true, +}) diff --git a/sources/@roots/bud/src/cli/flags/immutable.ts b/sources/@roots/bud/src/cli/flags/immutable.ts new file mode 100644 index 0000000000..66b00a2415 --- /dev/null +++ b/sources/@roots/bud/src/cli/flags/immutable.ts @@ -0,0 +1,5 @@ +import {Option} from '@roots/bud-support/clipanion' + +export default Option.Boolean(`--immutable`, undefined, { + description: `bud.http: immutable module lockfile`, +}) diff --git a/sources/@roots/bud/src/cli/flags/indent.ts b/sources/@roots/bud/src/cli/flags/indent.ts new file mode 100644 index 0000000000..7b07fd6102 --- /dev/null +++ b/sources/@roots/bud/src/cli/flags/indent.ts @@ -0,0 +1,4 @@ +import {Option} from '@roots/bud-support/clipanion' +export default Option.String(`--indent,-i`, `2`, { + description: `indentation level of objects in output`, +}) diff --git a/sources/@roots/bud/src/cli/flags/indicator.ts b/sources/@roots/bud/src/cli/flags/indicator.ts new file mode 100644 index 0000000000..3cf1fb0eb2 --- /dev/null +++ b/sources/@roots/bud/src/cli/flags/indicator.ts @@ -0,0 +1,5 @@ +import {Option} from '@roots/bud-support/clipanion' + +export default Option.Boolean(`--indicator`, undefined, { + description: `Display status in the browser`, +}) diff --git a/sources/@roots/bud/src/cli/flags/input.ts b/sources/@roots/bud/src/cli/flags/input.ts new file mode 100644 index 0000000000..1a030237e5 --- /dev/null +++ b/sources/@roots/bud/src/cli/flags/input.ts @@ -0,0 +1,6 @@ +import {Option} from '@roots/bud-support/clipanion' + +export default Option.String(`--input,-i,--@src,--src`, undefined, { + description: `Source directory (relative to project)`, + env: `APP_PATH_INPUT`, +}) diff --git a/sources/@roots/bud/src/cli/flags/log.ts b/sources/@roots/bud/src/cli/flags/log.ts new file mode 100644 index 0000000000..653fa50a3f --- /dev/null +++ b/sources/@roots/bud/src/cli/flags/log.ts @@ -0,0 +1,6 @@ +import {Option} from '@roots/bud-support/clipanion' + +export default Option.Boolean(`--log`, undefined, { + description: `Enable logging`, + hidden: true, +}) diff --git a/sources/@roots/bud/src/cli/flags/minimize.ts b/sources/@roots/bud/src/cli/flags/minimize.ts new file mode 100644 index 0000000000..c4e682e519 --- /dev/null +++ b/sources/@roots/bud/src/cli/flags/minimize.ts @@ -0,0 +1,5 @@ +import {Option} from '@roots/bud-support/clipanion' + +export default Option.Boolean(`--minimize`, undefined, { + description: `Minimize compiled assets`, +}) diff --git a/sources/@roots/bud/src/cli/flags/mode.ts b/sources/@roots/bud/src/cli/flags/mode.ts new file mode 100644 index 0000000000..73439991a4 --- /dev/null +++ b/sources/@roots/bud/src/cli/flags/mode.ts @@ -0,0 +1,7 @@ +import {Option} from '@roots/bud-support/clipanion' +import {isLiteral, isOneOf} from '@roots/bud-support/typanion' + +export default Option.String(`--mode`, undefined, { + description: `Compilation mode`, + validator: isOneOf([isLiteral(`production`), isLiteral(`development`)]), +}) diff --git a/sources/@roots/bud/src/cli/flags/notify.ts b/sources/@roots/bud/src/cli/flags/notify.ts new file mode 100644 index 0000000000..4875585eaf --- /dev/null +++ b/sources/@roots/bud/src/cli/flags/notify.ts @@ -0,0 +1,7 @@ +import {platform} from 'node:os' + +import {Option} from '@roots/bud-support/clipanion' + +export default Option.Boolean(`--notify`, platform() === `darwin`, { + description: `Enable notification (default on macOS, experimental on other platforms)`, +}) diff --git a/sources/@roots/bud/src/cli/flags/output.ts b/sources/@roots/bud/src/cli/flags/output.ts new file mode 100644 index 0000000000..deba4bbab3 --- /dev/null +++ b/sources/@roots/bud/src/cli/flags/output.ts @@ -0,0 +1,6 @@ +import {Option} from '@roots/bud-support/clipanion' + +export default Option.String(`--output,-o,--@dist,--dist`, undefined, { + description: `Distribution directory (relative to project)`, + env: `APP_PATH_OUTPUT`, +}) diff --git a/sources/@roots/bud/src/cli/flags/overlay.ts b/sources/@roots/bud/src/cli/flags/overlay.ts new file mode 100644 index 0000000000..21acd911ec --- /dev/null +++ b/sources/@roots/bud/src/cli/flags/overlay.ts @@ -0,0 +1,5 @@ +import {Option} from '@roots/bud-support/clipanion' + +export default Option.Boolean(`--overlay`, undefined, { + description: `Display error overlay in the browser`, +}) diff --git a/sources/@roots/bud/src/cli/flags/port.ts b/sources/@roots/bud/src/cli/flags/port.ts new file mode 100644 index 0000000000..b2f7eb1d26 --- /dev/null +++ b/sources/@roots/bud/src/cli/flags/port.ts @@ -0,0 +1,8 @@ +import {Option} from '@roots/bud-support/clipanion' + +export default Option.String(`--port`, undefined, { + description: `Port to serve on`, +}) +Option.String(`--proxy`, undefined, { + description: `Proxy request URL`, +}) diff --git a/sources/@roots/bud/src/cli/flags/proxy.ts b/sources/@roots/bud/src/cli/flags/proxy.ts new file mode 100644 index 0000000000..5e5672e041 --- /dev/null +++ b/sources/@roots/bud/src/cli/flags/proxy.ts @@ -0,0 +1,5 @@ +import {Option} from '@roots/bud-support/clipanion' + +export default Option.String(`--proxy`, undefined, { + description: `Proxy request URL`, +}) diff --git a/sources/@roots/bud/src/cli/flags/publicPath.ts b/sources/@roots/bud/src/cli/flags/publicPath.ts new file mode 100644 index 0000000000..deba4bbab3 --- /dev/null +++ b/sources/@roots/bud/src/cli/flags/publicPath.ts @@ -0,0 +1,6 @@ +import {Option} from '@roots/bud-support/clipanion' + +export default Option.String(`--output,-o,--@dist,--dist`, undefined, { + description: `Distribution directory (relative to project)`, + env: `APP_PATH_OUTPUT`, +}) diff --git a/sources/@roots/bud/src/cli/flags/reload.ts b/sources/@roots/bud/src/cli/flags/reload.ts new file mode 100644 index 0000000000..c345963fd9 --- /dev/null +++ b/sources/@roots/bud/src/cli/flags/reload.ts @@ -0,0 +1,5 @@ +import {Option} from '@roots/bud-support/clipanion' + +export default Option.Boolean(`--reload`, undefined, { + description: `Reload browser on unrecoverable errors`, +}) diff --git a/sources/@roots/bud/src/cli/flags/runtime.ts b/sources/@roots/bud/src/cli/flags/runtime.ts new file mode 100644 index 0000000000..9d8b64b2ea --- /dev/null +++ b/sources/@roots/bud/src/cli/flags/runtime.ts @@ -0,0 +1,12 @@ +import {Option} from '@roots/bud-support/clipanion' +import {isBoolean, isLiteral, isOneOf} from '@roots/bud-support/typanion' + +export default Option.String(`--runtime`, undefined, { + description: `Set runtime chunk`, + validator: isOneOf([ + isLiteral(`single`), + isLiteral(`multiple`), + isBoolean(), + ]), + tolerateBoolean: true, +}) diff --git a/sources/@roots/bud/src/cli/flags/silent.ts b/sources/@roots/bud/src/cli/flags/silent.ts new file mode 100644 index 0000000000..f6f0009a81 --- /dev/null +++ b/sources/@roots/bud/src/cli/flags/silent.ts @@ -0,0 +1,4 @@ +import {Option} from '@roots/bud-support/clipanion' +export default Option.Boolean(`--silent,-s`, true, { + description: `silence stdout`, +}) diff --git a/sources/@roots/bud/src/cli/flags/splitChunks.ts b/sources/@roots/bud/src/cli/flags/splitChunks.ts new file mode 100644 index 0000000000..c7bbf5f131 --- /dev/null +++ b/sources/@roots/bud/src/cli/flags/splitChunks.ts @@ -0,0 +1,5 @@ +import {Option} from '@roots/bud-support/clipanion' + +export default Option.Boolean(`--splitChunks,--vendor`, undefined, { + description: `Separate vendor bundle`, +}) diff --git a/sources/@roots/bud/src/cli/flags/storage.ts b/sources/@roots/bud/src/cli/flags/storage.ts new file mode 100644 index 0000000000..92f32aad89 --- /dev/null +++ b/sources/@roots/bud/src/cli/flags/storage.ts @@ -0,0 +1,6 @@ +import {Option} from '@roots/bud-support/clipanion' + +export default Option.String(`--storage`, undefined, { + description: `Storage directory (relative to project)`, + env: `APP_PATH_STORAGE`, +}) diff --git a/sources/@roots/bud/src/cli/flags/use.ts b/sources/@roots/bud/src/cli/flags/use.ts new file mode 100644 index 0000000000..094bb2523b --- /dev/null +++ b/sources/@roots/bud/src/cli/flags/use.ts @@ -0,0 +1,5 @@ +import {Option} from '@roots/bud-support/clipanion' + +export default Option.Array(`--use`, undefined, { + description: `Enable an extension`, +}) diff --git a/sources/@roots/bud/src/cli/flags/verbose.ts b/sources/@roots/bud/src/cli/flags/verbose.ts new file mode 100644 index 0000000000..5ae724bf5d --- /dev/null +++ b/sources/@roots/bud/src/cli/flags/verbose.ts @@ -0,0 +1,5 @@ +import {Option} from '@roots/bud-support/clipanion' + +export default Option.Boolean(`--verbose`, undefined, { + description: `Log verbose output`, +}) diff --git a/sources/@roots/bud/src/cli/index.ts b/sources/@roots/bud/src/cli/index.ts index 68aff9881e..1d1fddfde6 100644 --- a/sources/@roots/bud/src/cli/index.ts +++ b/sources/@roots/bud/src/cli/index.ts @@ -1,2 +1,14 @@ -export {application} from './app.js' +import type {ReadStream, WriteStream} from 'node:tty' + +import type {Context} from '@roots/bud-framework/options/context' + +export interface CLIContext extends Context { + stdin: ReadStream + stdout: WriteStream + stderr: WriteStream + colorDepth: number +} + export type {Builtins, Cli, CommandClass} from './app.js' + +export {application} from './app.js' diff --git a/sources/@roots/bud/src/context/bud.ts b/sources/@roots/bud/src/context/bud.ts index 2dd8e7692a..371068a1e7 100644 --- a/sources/@roots/bud/src/context/bud.ts +++ b/sources/@roots/bud/src/context/bud.ts @@ -3,20 +3,30 @@ import {fileURLToPath} from 'node:url' import type {Context} from '@roots/bud-framework/options' -let bud: Context[`bud`] = {} +let bud: Partial = {} -export const get = async fs => { +export const get = async (fs: any): Promise => { const resolvedPath = dirname(fileURLToPath(import.meta.url)) + bud.manifestPath = resolve( join(resolvedPath, `..`, `..`, `package.json`), ) const manifest = await fs.read(bud.manifestPath) bud.label = manifest.name.split(sep).pop() - bud.version = manifest.version + if (!isBudInterface(bud)) { + throw new Error(`Context['bud'] interface not correctly implemented.`) + } + return bud } +const isBudInterface = ( + value: Partial | Context[`bud`], +): value is Context[`bud`] => { + return `label` in value && `version` in value && `manifestPath` in value +} + export default bud diff --git a/sources/@roots/bud/src/context/extensions.ts b/sources/@roots/bud/src/context/extensions.ts index 8fc2ed4902..f797197ffb 100644 --- a/sources/@roots/bud/src/context/extensions.ts +++ b/sources/@roots/bud/src/context/extensions.ts @@ -34,6 +34,7 @@ const extensions: Extensions = { `@roots/bud-extensions/mini-css-extract-plugin`, `@roots/bud-extensions/webpack-define-plugin`, `@roots/bud-extensions/webpack-hot-module-replacement-plugin`, + `@roots/bud-extensions/webpack-lifecycle-plugin`, `@roots/bud-extensions/webpack-manifest-plugin`, `@roots/bud-extensions/webpack-provide-plugin`, `@roots/bud-extensions/tsconfig-values`, @@ -42,8 +43,7 @@ const extensions: Extensions = { } export default (manifest?: Context[`manifest`]) => { - if (!manifest || args[`no-disocvery`] || args[`no-discover`]) - return extensions + if (!manifest || args.discovery === false) return extensions Object.keys({ ...(manifest?.devDependencies ?? {}), diff --git a/sources/@roots/bud/src/context/index.ts b/sources/@roots/bud/src/context/index.ts index 30ad4715f7..b0ea5afb2f 100644 --- a/sources/@roots/bud/src/context/index.ts +++ b/sources/@roots/bud/src/context/index.ts @@ -1,36 +1,43 @@ /* eslint-disable no-console */ import {join} from 'node:path' -import type {CommandContext, Context} from '@roots/bud-framework/options' +import type {Context} from '@roots/bud-framework/options' import args from '@roots/bud-support/utilities/args' import * as projectEnv from '@roots/bud-support/utilities/env' import * as projectFiles from '@roots/bud-support/utilities/files' import * as filesystem from '@roots/bud-support/utilities/filesystem' import logger from '@roots/bud-support/utilities/logger' -import * as projectPaths from '@roots/bud-support/utilities/paths' +import * as paths from '@roots/bud-support/utilities/paths' import * as budContext from './bud.js' import getExtensions from './extensions.js' import services from './services.js' export default async ( - context: Partial, + context: Partial = {}, ): Promise => { let manifest: Context[`manifest`] - let paths = projectPaths.get(context?.basedir ?? process.cwd()) - let fs = filesystem.get(paths.basedir) + context.paths = { + ...paths.get(context?.basedir ?? process.cwd()), + ...(context?.paths ?? {}), + } + + let fs = filesystem.get(context.paths.basedir) - let env: Context[`env`] = projectEnv.get(paths.basedir) + let env: Context[`env`] = projectEnv.get(context.paths.basedir) let bud: Context[`bud`] = await budContext.get(fs) try { - manifest = await fs.read(join(paths.basedir, `package.json`)) + manifest = await fs.read(join(context.paths.basedir, `package.json`)) } catch (e) { logger.scope(`bootstrap`).warn(`📦`, `no package.json found`) } - let files: Context[`files`] = await projectFiles.get(paths.basedir) + let files: Context[`files`] = await projectFiles.get( + context.paths.basedir, + ) + Object.entries(files).map(([k, v]) => logger.scope(`bootstrap`).info(`file`, k, v), ) @@ -38,12 +45,12 @@ export default async ( let extensions: Context[`extensions`] = getExtensions(manifest) const instance: Context = { + ...(args ?? {}), ...(context ?? {}), label: context?.label ?? manifest?.name ?? bud?.label ?? `default`, // eslint-disable-next-line n/no-process-env bin: process.env.BUD_JS_BIN ?? `node`, - basedir: paths.basedir, - args: {...(context?.args ?? {}), ...(args ?? {})}, + basedir: context.paths.basedir, mode: context?.mode ?? `production`, env: {...(env ?? {}), ...(context?.env ?? {})}, files: {...(files ?? {}), ...(context?.files ?? {})}, @@ -74,4 +81,4 @@ export default async ( return instance } -export type {Context, CommandContext} +export type {Context} diff --git a/sources/@roots/bud/src/factory/factory.ts b/sources/@roots/bud/src/factory/factory.ts index 3ea5b6b345..6eafa34f3a 100644 --- a/sources/@roots/bud/src/factory/factory.ts +++ b/sources/@roots/bud/src/factory/factory.ts @@ -1,10 +1,6 @@ import {Bud} from '@roots/bud' import getContext from '@roots/bud/context' -import type { - CLIContext, - CommandContext, - Context, -} from '@roots/bud-framework/options' +import type {Context} from '@roots/bud-framework/options' /** * Create a {@link Bud} instance programatically @@ -22,8 +18,8 @@ import type { * ``` */ export async function factory( - ctx: Partial = {}, + context: Partial = {}, ): Promise { const bud = new Bud() - return await bud.lifecycle(await getContext(ctx)) + return await bud.lifecycle(await getContext(context)) } diff --git a/sources/@roots/bud/src/services/project/project.test.ts b/sources/@roots/bud/src/services/project/project.test.ts index c1e8b38f29..e4daf554ee 100644 --- a/sources/@roots/bud/src/services/project/project.test.ts +++ b/sources/@roots/bud/src/services/project/project.test.ts @@ -14,10 +14,12 @@ describe(`@roots/bud/services/project`, () => { }) it(`returns early if debug not set`, async () => { - bud.context.args.debug = false + bud.context.debug = false const infoSpy = vi.spyOn(bud, `info`) const pathSpy = vi.spyOn(bud, `path`) + if (!project.buildAfter) return + await project.buildAfter(bud) expect(pathSpy).not.toHaveBeenCalled() diff --git a/sources/@roots/bud/src/services/project/project.ts b/sources/@roots/bud/src/services/project/project.ts index 74312db8b8..39c43489bd 100644 --- a/sources/@roots/bud/src/services/project/project.ts +++ b/sources/@roots/bud/src/services/project/project.ts @@ -2,6 +2,7 @@ import type {Bud} from '@roots/bud-framework' import {Service} from '@roots/bud-framework/service' import {bind} from '@roots/bud-support/decorators/bind' import {BudError, FileWriteError} from '@roots/bud-support/errors' +import omit from '@roots/bud-support/lodash/omit' import * as args from '@roots/bud-support/utilities/args' /** @@ -13,50 +14,26 @@ class Project extends Service { */ @bind public override async buildAfter?(bud: Bud) { - if (!bud.isCLI()) { - bud.info(`not a CLI build. skipping project profile.`) - return - } - - if (!bud.context.args?.debug) { - bud.info(`--debug not \`true\`. skipping fs write.`) - return - } + if (!bud.context.debug) + return bud.info(`--debug not \`true\`. skipping fs write.`) try { const path = bud.path(`@storage`, bud.label, `debug`, `profile.yml`) await bud.fs.write(path, { - basedir: bud.context.basedir, - mode: bud.mode, - isCLI: bud.isCLI(), - bud: bud.context.bud.version, - minimize: bud.hooks.filter( - `build.optimization.minimize`, - undefined, - ), - files: bud.context.files - ? Object.values(bud.context.files)?.map(file => ({ - ...file, - module: !!file.module, - })) - : [], + ...omit(bud.context, [`env`, `logger`, `stdout`, `stderr`]), + args: args.raw, env: bud.env.getKeys(), children: bud.children ? Object.keys(bud.children) : [], - args: bud.context?.args, - args_raw: args.raw, services: bud.context?.services, - extensions: { - context: bud.context?.extensions, - loaded: Object.entries(bud.extensions?.repository).map( - ([key, extension]) => ({ - key, - label: extension.label, - meta: extension.meta, - options: extension.options, - }), - ), - }, + loaded: Object.entries(bud.extensions?.repository).map( + ([key, extension]) => ({ + key, + label: extension.label, + meta: extension.meta, + options: extension.options, + }), + ), resolutions: bud.module.resolved, }) diff --git a/sources/@roots/bud/test/factory.test.ts b/sources/@roots/bud/test/factory.test.ts index 8902d8eb95..51479563fe 100644 --- a/sources/@roots/bud/test/factory.test.ts +++ b/sources/@roots/bud/test/factory.test.ts @@ -9,10 +9,8 @@ describe(`@roots/bud/factory`, () => { test(`should merge overrides`, async () => { const bud = await factory({ basedir: join(paths.tests, `util`, `project`), - args: { - dry: true, - log: false, - }, + dry: true, + log: false, }) expect(bud.label).toBe(`@tests/project`) diff --git a/sources/@roots/bud/tsconfig.json b/sources/@roots/bud/tsconfig.json index 7fc3c97093..b6c72dc82b 100644 --- a/sources/@roots/bud/tsconfig.json +++ b/sources/@roots/bud/tsconfig.json @@ -7,7 +7,14 @@ "@roots/bud": ["./src/index.ts"], "@roots/bud/*": ["./src/*"] }, - "types": ["@roots/bud-framework", "react"] + "types": [ + "@roots/bud-api", + "@roots/bud-build", + "@roots/bud-cache", + "@roots/bud-extensions", + "@roots/bud-framework", + "react" + ] }, "include": ["./src"], "exclude": ["./lib", "./node_modules", "**/*.test.ts"], diff --git a/sources/@roots/wordpress-hmr/src/editor.ts b/sources/@roots/wordpress-hmr/src/editor.ts index 7ba506cc70..a1485507a5 100644 --- a/sources/@roots/wordpress-hmr/src/editor.ts +++ b/sources/@roots/wordpress-hmr/src/editor.ts @@ -38,7 +38,7 @@ export const load = ({ context?.keys().forEach((key: string) => { const raw = context(key) - const source = raw.default || raw + const source = raw.default ?? raw if (cache.is(key, source)) return if (cache.has(key)) api.unregister(cache.get(key)) diff --git a/tests/reproductions/issue-1955/package.json b/tests/reproductions/issue-1955/package.json index 4d745ac0a0..9cc88840fe 100644 --- a/tests/reproductions/issue-1955/package.json +++ b/tests/reproductions/issue-1955/package.json @@ -11,8 +11,6 @@ "react": "18.2.0" }, "bud": { - "extensions": { - "discovery": false - } + "extensions": {} } } diff --git a/tests/reproductions/issue-1955/src/app.js b/tests/reproductions/issue-1955/src/app.js index 5d69c5a474..379cfe76ac 100644 --- a/tests/reproductions/issue-1955/src/app.js +++ b/tests/reproductions/issue-1955/src/app.js @@ -6,5 +6,3 @@ import {App} from './components/App.js' * React 18 render root */ ReactDOM.createRoot(document.getElementById('root')).render() - -import.meta.webpackHot?.accept(console.error) diff --git a/tests/reproductions/issue-2126/bud.config.js b/tests/reproductions/issue-2126/bud.config.js index 2c6e9d6cc7..b3fb3e4eb4 100644 --- a/tests/reproductions/issue-2126/bud.config.js +++ b/tests/reproductions/issue-2126/bud.config.js @@ -1,3 +1,3 @@ export default async bud => { - bud.entry('app', ['index.js', 'index.scss']).hash(false) + bud.entry('app', ['index.js', 'index.scss']).hash(false).wpjson.enable() } diff --git a/tests/reproductions/issue-2126/jsconfig.json b/tests/reproductions/issue-2126/jsconfig.json index e4bb7e2c62..c669af7cda 100644 --- a/tests/reproductions/issue-2126/jsconfig.json +++ b/tests/reproductions/issue-2126/jsconfig.json @@ -1,8 +1,7 @@ { "extends": "@roots/bud/config/jsconfig.json", "compilerOptions": { - "types": ["@roots/bud-vue"] + "types": ["@roots/bud", "@roots/bud-vue"] }, - "include": ["resources", "bud.config.js"], - "exclude": ["node_modules", "dist"] + "include": ["resources", "bud.config.js"] } diff --git a/tests/reproductions/issue-2126/package.json b/tests/reproductions/issue-2126/package.json index f857f034ea..e6a0e596dc 100644 --- a/tests/reproductions/issue-2126/package.json +++ b/tests/reproductions/issue-2126/package.json @@ -10,13 +10,6 @@ "@roots/sage": "workspace:sources/@roots/sage" }, "bud": { - "extensions": { - "allowlist": [ - "@roots/sage", - "@roots/bud-postcss", - "@roots/bud-sass", - "@roots/bud-vue" - ] - } + "extensions": {} } } diff --git a/tests/reproductions/issue-2126/theme.json b/tests/reproductions/issue-2126/theme.json new file mode 100644 index 0000000000..33e7e628bd --- /dev/null +++ b/tests/reproductions/issue-2126/theme.json @@ -0,0 +1,33 @@ +{ + "__generated__": "⚠️ This file is generated. Do not edit.", + "$schema": "https://schemas.wp.org/trunk/theme.json", + "version": 2, + "settings": { + "color": { + "custom": false, + "customGradient": false + }, + "custom": { + "spacing": {}, + "typography": { + "font-size": {}, + "line-height": {} + } + }, + "spacing": { + "padding": true, + "units": [ + "px", + "%", + "em", + "rem", + "vw", + "vh" + ] + }, + "typography": { + "customFontSize": false, + "dropCap": false + } + } +} \ No newline at end of file diff --git a/tests/util/project/config/bud.config.ts b/tests/util/project/config/bud.config.ts index 92055ec03e..2df628f856 100644 --- a/tests/util/project/config/bud.config.ts +++ b/tests/util/project/config/bud.config.ts @@ -1,21 +1,11 @@ -import type {Bud} from '@roots/bud' - -export default async (bud: Bud) => { +export default async bud => { bud - .assets(`fonts`) .entry(`app`, [`scripts/app`, `styles/app`]) .watch([bud.path(`@src`, `*.html`), bud.path(`@src`, `images`)]) .serve(3015) .splitChunks(false) .minimize(false) - .html({ - template: bud.path(`@src`, `index.html`), - replace: { - noScript: `You need to enable JavaScript to run this app`, - }, - }) - - console.log(`foo`, `bar`) + .html() // .assets([[`fonts`, `fontz`]]) // .assets([[`fonts/test.otf`, `fontz/test.otf`]]) diff --git a/tests/util/project/config/tailwind.config.ts b/tests/util/project/config/tailwind.config.ts index a7c8be400f..b5efb1b6a2 100644 --- a/tests/util/project/config/tailwind.config.ts +++ b/tests/util/project/config/tailwind.config.ts @@ -1,3 +1,5 @@ +import forms from '@tailwindcss/forms' + export default { content: [`./src/index.html`, `./src/**/*.js`], theme: { @@ -7,5 +9,5 @@ export default { }, }, }, - plugins: [], + plugins: [forms], } diff --git a/tests/util/project/package.json b/tests/util/project/package.json index 3c8201c92f..91f680b8f7 100644 --- a/tests/util/project/package.json +++ b/tests/util/project/package.json @@ -13,5 +13,8 @@ }, "browserslist": [ "extends @roots/browserslist-config" - ] + ], + "dependencies": { + "@tailwindcss/forms": "0.5.3" + } } diff --git a/tests/util/project/src/index.html b/tests/util/project/src/index.html index aec5561ee4..8b22dbbe58 100644 --- a/tests/util/project/src/index.html +++ b/tests/util/project/src/index.html @@ -11,7 +11,7 @@ -
foobar
+
diff --git a/tests/util/project/src/scripts/app.js b/tests/util/project/src/scripts/app.js index bdc7634a1a..a51b976ea9 100644 --- a/tests/util/project/src/scripts/app.js +++ b/tests/util/project/src/scripts/app.js @@ -1,15 +1,14 @@ import React from 'react' import {createRoot} from 'react-dom/client' -import {App} from '@components/App' +import {App} from '@components/app' createRoot(document.getElementById('root')).render( , ) +;(async function read() { + await import('./components/main.js').then(({main}) => main()) +})() -window.requestAnimationFrame(async function ready() { - return document.body - ? await import('./components/main.js').then(({main}) => main()) - : window.requestAnimationFrame(ready) -}) +if (import.meta.webpackHot) import.meta.webpackHot.accept(console.error) diff --git a/tests/util/project/src/scripts/components/App.tsx b/tests/util/project/src/scripts/components/App.tsx index 9cce17fe6e..8449299b3b 100644 --- a/tests/util/project/src/scripts/components/App.tsx +++ b/tests/util/project/src/scripts/components/App.tsx @@ -1,12 +1,12 @@ -import logo from './logo.svg' +import logo from '@components/logo.svg' export const App = () => { return ( -
-
- logo - Edit src/components/App.js and save to reload -
+
+ logo + + Edit src/components/app.tsx and save to reload +
) } diff --git a/tests/util/project/src/scripts/components/logo.svg b/tests/util/project/src/scripts/components/logo.svg index 092946e608..a7c8baa474 100644 --- a/tests/util/project/src/scripts/components/logo.svg +++ b/tests/util/project/src/scripts/components/logo.svg @@ -2,13 +2,17 @@ - + + - - + + + + diff --git a/tests/util/project/src/scripts/components/main.js b/tests/util/project/src/scripts/components/main.js index 438ac62d07..6f8abe9f61 100644 --- a/tests/util/project/src/scripts/components/main.js +++ b/tests/util/project/src/scripts/components/main.js @@ -1,5 +1,11 @@ -import image from '/images/image.jpeg' +import image from '../../images/image.jpeg' export const main = () => { - document.body.style.background = `url(${image})` + const root = document.querySelector(`#root`) + root.style.backgroundImage = `url(${image})` + root.style.backgroundSize = `cover` + root.style.backgroundRepeat = `no-repeat` + root.style.minHeight = `100vh` + root.style.height = `100vh` + root.style.display = `flex` } diff --git a/tests/util/project/src/styles/common/global.css b/tests/util/project/src/styles/common/global.css index 39022a1353..ea517d14b6 100644 --- a/tests/util/project/src/styles/common/global.css +++ b/tests/util/project/src/styles/common/global.css @@ -1,9 +1,15 @@ #root { - @apply text-indigo-500; + @apply flex flex-wrap flex-col justify-around h-full w-full bg-white bg-opacity-50; +} + +.app { + @apply flex flex-wrap flex-col w-full text-white; +} - background: url(@src/images/nested/image.jpeg); +.app img { + @apply w-32 h-32 flex flex-col align-middle justify-center mx-auto my-4; } -.icon { - background: url(@src/svg/icon.svg); +.app span { + @apply text-center; } diff --git a/tests/util/project/tsconfig.json b/tests/util/project/tsconfig.json index 0947394c6b..e6b9fd9e76 100644 --- a/tests/util/project/tsconfig.json +++ b/tests/util/project/tsconfig.json @@ -10,7 +10,7 @@ }, "types": ["node", "webpack-env", "@roots/bud", "@roots/bud-swc", "@roots/bud-tailwindcss"] }, - "files": ["./config/bud.config.ts", "./config/tailwind.config.ts"], + "files": ["./config/bud.config.js", "./config/tailwind.config.ts"], "include": ["./src"], "bud": { "useCompilerOptions": true diff --git a/yarn.lock b/yarn.lock index 5e6ad98c26..4a83546966 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8439,7 +8439,7 @@ __metadata: html-webpack-plugin: 5.5.1 http-proxy-middleware: 3.0.0-beta.1 human-readable: 0.2.1 - import-meta-resolve: 3.0.0 + import-meta-resolve: 2.2.2 json5: 2.2.3 lodash: 4.17.21 mini-css-extract-plugin: 2.7.5 @@ -9471,6 +9471,17 @@ __metadata: languageName: node linkType: hard +"@tailwindcss/forms@npm:0.5.3": + version: 0.5.3 + resolution: "@tailwindcss/forms@npm:0.5.3" + dependencies: + mini-svg-data-uri: ^1.2.3 + peerDependencies: + tailwindcss: ">=3.0.0 || >= 3.0.0-alpha.1" + checksum: 9eddb4dbd06d01b1068138ff52a54ed0e35a28e7bfd3c72e226fc28658ecd92a9c078c4abe9c83db090984672040644d7ae2e035933fb619dd703df1d87aa275 + languageName: node + linkType: hard + "@tannin/compile@npm:^1.1.0": version: 1.1.0 resolution: "@tannin/compile@npm:1.1.0" @@ -9621,6 +9632,7 @@ __metadata: "@roots/bud-react": "workspace:sources/@roots/bud-react" "@roots/bud-swc": "workspace:sources/@roots/bud-swc" "@roots/bud-tailwindcss": "workspace:sources/@roots/bud-tailwindcss" + "@tailwindcss/forms": 0.5.3 languageName: unknown linkType: soft @@ -14819,7 +14831,7 @@ __metadata: eslint-plugin-react-hooks: 4.6.0 eslint-plugin-simple-import-sort: 10.0.0 execa: 7.1.1 - import-meta-resolve: 3.0.0 + import-meta-resolve: 2.2.2 lodash: 4.17.21 playwright: 1.33.0 pm2: ^5.3.0 @@ -22651,10 +22663,10 @@ __metadata: languageName: node linkType: hard -"import-meta-resolve@npm:3.0.0": - version: 3.0.0 - resolution: "import-meta-resolve@npm:3.0.0" - checksum: d0428cd14915ee0093b995dc5bbc70bd01cc668822f52b62af98f728e5d6a08724f07e6aa9f5fae002d5eecbf6ec2cdcd379bf4869dd1b353bd080693f91e394 +"import-meta-resolve@npm:2.2.2": + version: 2.2.2 + resolution: "import-meta-resolve@npm:2.2.2" + checksum: 3a5910a6f914b5f06b307d7d1c25710bc56f12e21e923d5b2180dd0d53c6c2d51e7b55df26f168b63f5670babcaca9422b7a9429e877bbb8c1997d79bd65882b languageName: node linkType: hard @@ -26320,7 +26332,7 @@ __metadata: languageName: node linkType: hard -"mini-svg-data-uri@npm:1.4.4": +"mini-svg-data-uri@npm:1.4.4, mini-svg-data-uri@npm:^1.2.3": version: 1.4.4 resolution: "mini-svg-data-uri@npm:1.4.4" bin: