diff --git a/sources/@roots/bud-extensions/package.json b/sources/@roots/bud-extensions/package.json index 0f88f51ef4..aa358a77fa 100644 --- a/sources/@roots/bud-extensions/package.json +++ b/sources/@roots/bud-extensions/package.json @@ -37,74 +37,22 @@ "lib/" ], "type": "module", - "module": "./lib/index.js", - "types": "./lib/index.d.ts", "exports": { - ".": "./lib/index.js", - "./service": "./lib/service.js", - "./extensions": "./lib/extensions/index.js", - "./cdn": "./lib/extensions/cdn/index.js", - "./esm": "./lib/extensions/esm/index.js", - "./clean-webpack-plugin": "./lib/extensions/clean-webpack-plugin/index.js", - "./copy-webpack-plugin": "./lib/extensions/copy-webpack-plugin/index.js", - "./fix-style-only-entrypoints": "./lib/extensions/fix-style-only-entrypoints/index.js", - "./html-webpack-plugin": "./lib/extensions/html-webpack-plugin/index.js", - "./interpolate-html-webpack-plugin": "./lib/extensions/interpolate-html-webpack-plugin/index.js", - "./mini-css-extract-plugin": "./lib/extensions/mini-css-extract-plugin/index.js", - "./webpack-define-plugin": "./lib/extensions/webpack-define-plugin/index.js", - "./webpack-hot-module-replacement-plugin": "./lib/extensions/webpack-hot-module-replacement-plugin/index.js", - "./webpack-manifest-plugin": "./lib/extensions/webpack-manifest-plugin/index.js", - "./webpack-provide-plugin": "./lib/extensions/webpack-provide-plugin/index.js" - }, - "typesVersions": { - "*": { - ".": [ - "./lib/index.d.ts" - ], - "service": [ - "./lib/service.d.ts" - ], - "extensions": [ - "./lib/extensions/index.d.ts" - ], - "cdn": [ - "./lib/extensions/cdn/index.d.ts" - ], - "esm": [ - "./lib/extensions/esm/index.d.ts" - ], - "fix-style-only-entrypoints": [ - "./lib/extensions/fix-style-only-entrypoints/index.d.ts" - ], - "clean-webpack-plugin": [ - "./lib/extensions/clean-webpack-plugin/index.d.ts" - ], - "copy-webpack-plugin": [ - "./lib/extensions/copy-webpack-plugin/index.d.ts" - ], - "html-webpack-plugin": [ - "./lib/extensions/html-webpack-plugin/index.d.ts" - ], - "interpolate-html-webpack-plugin": [ - "./lib/extensions/interpolate-html-webpack-plugin/index.d.ts" - ], - "mini-css-extract-plugin": [ - "./lib/extensions/mini-css-extract-plugin/index.d.ts" - ], - "webpack-define-plugin": [ - "./lib/extensions/webpack-define-plugin/index.d.ts" - ], - "webpack-hot-module-replacement-plugin": [ - "./lib/extensions/webpack-hot-module-replacement-plugin/index.d.ts" - ], - "webpack-manifest-plugin": [ - "./lib/extensions/webpack-manifest-plugin/index.d.ts" - ], - "webpack-provide-plugin": [ - "./lib/extensions/webpack-provide-plugin/index.d.ts" - ] + ".": { + "types": "./lib/index.d.ts", + "default": "./lib/index.js" + }, + "./service": { + "types": "./lib/service/index.d.ts", + "default": "./lib/service/index.js" + }, + "./*": { + "types": "./lib/extensions/*/index.d.ts", + "default": "./lib/extensions/*/index.js" } }, + "module": "./lib/index.js", + "types": "./lib/index.d.ts", "devDependencies": { "@skypack/package-check": "0.2.2", "@types/node": "16.18.3", @@ -114,6 +62,7 @@ "@roots/bud-framework": "workspace:sources/@roots/bud-framework", "@roots/bud-support": "workspace:sources/@roots/bud-support", "@roots/bud-terser": "workspace:sources/@roots/bud-terser", + "@roots/container": "workspace:sources/@roots/container", "clean-webpack-plugin": "4.0.0", "copy-webpack-plugin": "11.0.0", "html-webpack-plugin": "5.5.0", diff --git a/sources/@roots/bud-extensions/src/extensions/cdn/index.ts b/sources/@roots/bud-extensions/src/extensions/cdn/index.ts index 5766648c82..05635290f3 100644 --- a/sources/@roots/bud-extensions/src/extensions/cdn/index.ts +++ b/sources/@roots/bud-extensions/src/extensions/cdn/index.ts @@ -28,12 +28,16 @@ export interface Options { } /** + * `@roots/bud-extensions/cdn + * + * @remarks * Include remote modules in compilation * * @public * @decorator `@label` * @decorator `@expose` * @decorator `@options` + * @decorator `@disabled` */ @label(`@roots/bud-extensions/cdn`) @expose(`cdn`) @@ -49,7 +53,7 @@ export interface Options { @disabled export default class Cdn extends Extension { /** - * CDN manifest key to URL mapping + * CDN key to URL mapping * * @public */ @@ -99,13 +103,14 @@ export default class Cdn extends Extension { > { return Array.from( new Set([ - ...(this.app.maybeCall(this.getOption(`allowedUris`)) ?? []), + ...this.getOption(`allowedUris`), ...(this.sources.values() ?? []), ]), ).filter( v => typeof v === `string` || v instanceof RegExp || isFunction(v), ) } + public set allowedUris( value: | Array boolean)> @@ -275,7 +280,8 @@ export default class Cdn extends Extension { bud.hooks.on(`build.experiments`, experiments => ({ ...(experiments ?? {}), buildHttp: { - allowedUris: this.allowedUris, + allowedUris: + this.allowedUris.length > 0 ? this.allowedUris : undefined, cacheLocation: this.cacheEnabled ? this.cacheLocation : false, frozen: this.frozen, lockfileLocation: this.lockfileLocation, diff --git a/sources/@roots/bud-extensions/src/index.test.ts b/sources/@roots/bud-extensions/src/index.test.ts deleted file mode 100644 index 3d9c78b8a0..0000000000 --- a/sources/@roots/bud-extensions/src/index.test.ts +++ /dev/null @@ -1,9 +0,0 @@ -import {describe, expect, it} from 'vitest' - -import * as Extensions from './index.js' - -describe(`@roots/bud-extensions`, () => { - it(`exports extensions service`, async () => { - expect(Extensions.default).toBeInstanceOf(Function) - }) -}) diff --git a/sources/@roots/bud-extensions/src/index.ts b/sources/@roots/bud-extensions/src/index.ts index f2c3332dc2..4d43a9f64e 100644 --- a/sources/@roots/bud-extensions/src/index.ts +++ b/sources/@roots/bud-extensions/src/index.ts @@ -12,6 +12,6 @@ import './types.js' -import Extensions from './service.js' +import Extensions from './service/index.js' export default Extensions diff --git a/sources/@roots/bud-extensions/src/__snapshots__/service.test.ts.snap b/sources/@roots/bud-extensions/src/service/__snapshots__/index.test.ts.snap similarity index 86% rename from sources/@roots/bud-extensions/src/__snapshots__/service.test.ts.snap rename to sources/@roots/bud-extensions/src/service/__snapshots__/index.test.ts.snap index 4f7cb38245..3ac7619ca7 100644 --- a/sources/@roots/bud-extensions/src/__snapshots__/service.test.ts.snap +++ b/sources/@roots/bud-extensions/src/service/__snapshots__/index.test.ts.snap @@ -1,6 +1,6 @@ // Vitest Snapshot v1 -exports[`@roots/bud-extensions > [development] bud.extensions.repository options matches snapshot 1`] = ` +exports[`@roots/bud-extensions > bud.extensions.repository options should match snapshot in development 1`] = ` [ "@roots/bud-extensions/cdn", "@roots/bud-extensions/clean-webpack-plugin", diff --git a/sources/@roots/bud-extensions/src/service.test.ts b/sources/@roots/bud-extensions/src/service/index.test.ts similarity index 62% rename from sources/@roots/bud-extensions/src/service.test.ts rename to sources/@roots/bud-extensions/src/service/index.test.ts index b66647beb5..6befccd2c2 100644 --- a/sources/@roots/bud-extensions/src/service.test.ts +++ b/sources/@roots/bud-extensions/src/service/index.test.ts @@ -1,29 +1,14 @@ import {factory} from '@repo/test-kit/bud' -import type {WebpackPluginInstance} from '@roots/bud-support/webpack' +import type {Bud} from '@roots/bud-framework/bud' +import type {Modules} from '@roots/bud-framework' +import type {ApplyPlugin} from '@roots/bud-framework/extension' import {beforeEach, describe, expect, it, vi} from 'vitest' import Extensions from './index.js' describe(`@roots/bud-extensions`, () => { - let bud - let extensions - - let mockWebpackPlugin: WebpackPluginInstance = { - apply: vi.fn(), - } - - let options = { - test: `foo`, - } - - let mockModule: any = { - label: `mock_extension`, - register: vi.fn(async () => null), - boot: vi.fn(async () => null), - options: options, - make: vi.fn(async () => mockWebpackPlugin), - when: vi.fn(async () => true), - } + let bud: Bud + let extensions: Extensions beforeEach(async () => { bud = await factory() @@ -37,9 +22,22 @@ describe(`@roots/bud-extensions`, () => { it(`add fn registers a module`, async () => { extensions.repository = {} as any + const options = {test: `foo`} + + const mockWebpackPlugin: ApplyPlugin = {apply: vi.fn()} + + const mockModule: any = { + label: `mock_extension`, + register: vi.fn(async () => null), + boot: vi.fn(async () => null), + options: options, + make: vi.fn(async () => mockWebpackPlugin), + when: vi.fn(async () => true), + } + await extensions.add(mockModule) - const instance = extensions.get(`mock_extension`) + const instance = extensions.get(`mock_extension` as keyof Modules) expect(instance.label).toBe(`mock_extension`) expect(extensions.get(mockModule.label)?.options?.test).toEqual( @@ -50,24 +48,34 @@ describe(`@roots/bud-extensions`, () => { it(`should assign a uuid as key for extensions without names`, async () => { extensions.repository = {} as any - await extensions.add( - // @ts-ignore - { - register: () => { - // noop - }, + const mockExtension = { + register: async () => { + /*noop*/ }, - ) + } + + await extensions.add(mockExtension) expect(Object.keys(extensions.repository).sort().pop()).toMatch( /[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/, ) }) + it(`should accept a plugin signifier`, async () => { + extensions.repository = {} as any + + // @ts-ignore + await extensions.add(`palette-webpack-plugin`) + + expect( + Object.values(extensions.repository).sort().pop().constructor.name, + ).toBe(`PaletteWebpackPlugin`) + }) + it(`should accept a plugin definition`, async () => { extensions.repository = {} as any - const plugin = await import('palette-webpack-plugin') + const plugin = await import(`palette-webpack-plugin`) await extensions.add(plugin.default) expect( @@ -78,7 +86,7 @@ describe(`@roots/bud-extensions`, () => { it(`should accept a plugin instance`, async () => { extensions.repository = {} as any - const plugin = await import('palette-webpack-plugin') + const plugin = await import(`palette-webpack-plugin`) // @ts-ignore const instance = new plugin.default() await extensions.add(instance) @@ -94,7 +102,7 @@ describe(`@roots/bud-extensions`, () => { await extensions.add( // @ts-ignore { - register: () => { + register: async () => { // noop }, }, @@ -108,7 +116,7 @@ describe(`@roots/bud-extensions`, () => { ) }) - it(`[development] bud.extensions.repository options matches snapshot`, async () => { + it(`bud.extensions.repository options should match snapshot in development`, async () => { bud = await factory({mode: `development`}) const extensions = new Extensions(() => bud) diff --git a/sources/@roots/bud-extensions/src/service.ts b/sources/@roots/bud-extensions/src/service/index.ts similarity index 76% rename from sources/@roots/bud-extensions/src/service.ts rename to sources/@roots/bud-extensions/src/service/index.ts index 71b168af23..b9db7584de 100644 --- a/sources/@roots/bud-extensions/src/service.ts +++ b/sources/@roots/bud-extensions/src/service/index.ts @@ -10,6 +10,10 @@ import {Service} from '@roots/bud-framework/service' import type {Extensions as Contract} from '@roots/bud-framework/services' import {bind} from '@roots/bud-support/decorators' import {isUndefined} from '@roots/bud-support/lodash-es' +import Container from '@roots/container' + +import {handleManifestSchemaWarning} from './util/handleManifestSchemaWarning.js' +import {isConstructor} from './util/isConstructor.js' /** * Extensions Service @@ -40,11 +44,11 @@ export default class Extensions * * @public */ - public resolvedOptions: Contract.Service['resolvedOptions'] = { - allowlist: [], - denylist: [], - discovery: true, - } + public options: Container<{ + allowlist: Array + denylist: Array + discovery: boolean + }> /** - * Modules on which an import attempt was made and failed @@ -56,40 +60,30 @@ export default class Extensions - * - * @public - */ - public unresolvable: Set<`${keyof Modules & string}`> = new Set() + public unresolvable: Set = new Set() + + public constructor(bud: () => Bud) { + super(bud) + this.options = new Container({ + allowlist: [], + denylist: [], + discovery: true, + }) + } /** * `register` callback * - * @remarks + * @todo * All this is doing is helping transition people to using `bud.extensions` key for - * `allowList` and `denyList`. It can be removed in a future release. - * ⸺ 2022-10-18 + * `allowList` and `denyList`. It can be removed in a future release. (2022-10-18) * * @public * @decorator `@bind` */ @bind public override async register(bud: Bud): Promise { - if (bud.context.manifest?.bud?.allowlist) { - bud.context.manifest.bud.extensions = { - ...(bud.context.manifest.bud.extensions ?? {}), - allowlist: bud.context.manifest.bud.allowlist, - } - this.logger.warn( - `package.json: bud.allowlist is deprecated. Use bud.extensions.allowlist instead.`, - ) - } - - if (bud.context.manifest?.bud?.denylist) { - bud.context.manifest.bud.extensions = { - ...(bud.context.manifest.bud.extensions ?? {}), - denylist: bud.context.manifest.bud.denylist, - } - this.logger.warn( - `package.json: bud.denylist is deprecated. Use bud.extensions.denylist instead.`, - ) - } + handleManifestSchemaWarning.bind(this)(bud) } /** @@ -100,66 +94,61 @@ export default class Extensions */ @bind public override async booted(bud: Bud): Promise { - if (!isUndefined(bud.context.manifest?.bud?.extensions?.discovery)) { - this.resolvedOptions.discovery = - bud.context.manifest.bud.extensions.discovery - } + const {manifest} = bud.context - if (!isUndefined(bud.context.args.discovery)) { - this.resolvedOptions.discovery = bud.context.args.discovery - } + if (manifest?.bud?.extensions) { + const {discovery, allowlist, denylist} = manifest.bud.extensions - if ( - !isUndefined( - bud.context.manifest?.[this.app.label]?.extensions?.discovery, - ) - ) { - this.resolvedOptions.discovery = - bud.context.manifest?.bud?.[this.app.label].extensions.discovery + if (!isUndefined(discovery)) this.options.set(`discovery`, discovery) + if (!isUndefined(allowlist)) + this.options.merge(`allowlist`, allowlist) + if (!isUndefined(denylist)) this.options.merge(`denylist`, denylist) } - if (!isUndefined(bud.context.args.discovery)) { - this.resolvedOptions.discovery = bud.context.args.discovery + if (manifest?.[this.app.label]?.extensions) { + const {discovery, allowlist, denylist} = + manifest[this.app.label].extensions + + if (!isUndefined(discovery)) this.options.set(`discovery`, discovery) + if (!isUndefined(allowlist)) + this.options.merge(`allowlist`, allowlist) + if (!isUndefined(denylist)) this.options.merge(`denylist`, denylist) } - this.resolvedOptions.allowlist.push( - ...(bud.context.manifest?.bud?.extensions?.allowlist ?? []), - ...(bud.context.manifest?.bud?.[this.app.label]?.extensions - ?.allowlist ?? []), - ) - this.resolvedOptions.denylist.push( - ...(bud.context.manifest?.bud?.extensions?.denylist ?? []), - ...(bud.context.manifest?.bud?.[this.app.label]?.extensions - ?.denylist ?? []), + if ( + !isUndefined(bud.context.extensions.builtIn) && + Array.isArray(bud.context.extensions.builtIn) ) - - if (bud.context.extensions.builtIn) await Promise.all( - bud.context.extensions.builtIn - ?.filter(Boolean) - .map(async signifier => await this.import(signifier, true)), + bud.context.extensions.builtIn.filter(Boolean).map(this.import), ) + if (!isUndefined(bud.context.args.discovery)) { + this.options.set(`discovery`, bud.context.args.discovery) + } + if (!isUndefined(bud.context.args.discovery)) { + this.options.set(`discovery`, bud.context.args.discovery) + } + if ( - this.resolvedOptions.discovery && - this.resolvedOptions.allowlist.length === 0 && - bud.context.extensions?.discovered + this.options.is(`discovery`, true) && + this.options.isEmpty(`allowlist`) && + bud.context.extensions?.discovered && + Array.isArray(bud.context.extensions.discovered) ) await Promise.all( bud.context.extensions.discovered .filter(Boolean) .filter(this.isAllowed) - .map(async signifier => await this.import(signifier, true)), + .map(this.import), ) - else if (this.resolvedOptions.allowlist.length > 0) + else if (this.options.isNotEmpty(`allowlist`)) await Promise.all( - this.resolvedOptions.allowlist - ?.filter(Boolean) + this.options + .get(`allowlist`) + .filter(Boolean) .filter(this.isAllowed) - .map( - async (signifier: `${keyof Modules & string}`) => - await this.import(signifier, true), - ), + .map(this.import), ) await this.runAll(`init`) @@ -254,18 +243,7 @@ export default class Extensions | ExtensionLiteral | {apply: (...args: any[]) => any}, ): Promise { - const isConstructor = (f: any): f is new (...args: any[]) => any => { - try { - Reflect.construct(String, [], f) - } catch (e) { - return false - } - return true - } - - if (source instanceof Extension) { - return source - } + if (source instanceof Extension) return source if (typeof source === `function`) { if (isConstructor(source)) { @@ -289,10 +267,10 @@ export default class Extensions @bind public isAllowed(signifier: string): boolean { return ( - (this.resolvedOptions.denylist.length === 0 || - !this.resolvedOptions.denylist.includes(signifier)) && - (this.resolvedOptions.allowlist.length === 0 || - this.resolvedOptions.allowlist.includes(signifier)) + (this.options.isEmpty(`denylist`) || + !this.options.get(`denylist`).includes(signifier)) && + (this.options.isEmpty(`allowlist`) || + this.options.get(`allowlist`).includes(signifier)) ) } @@ -303,9 +281,9 @@ export default class Extensions * @decorator `@bind` */ @bind - public async import( + public async import( signifier: K, - fatalOnError = true, + fatalOnError: boolean | number = true, ): Promise { if (fatalOnError && this.unresolvable.has(signifier)) throw new Error(`Extension ${signifier} is not importable`) diff --git a/sources/@roots/bud-extensions/src/service/util/handleManifestSchemaWarning.ts b/sources/@roots/bud-extensions/src/service/util/handleManifestSchemaWarning.ts new file mode 100644 index 0000000000..bb8c2d24cb --- /dev/null +++ b/sources/@roots/bud-extensions/src/service/util/handleManifestSchemaWarning.ts @@ -0,0 +1,27 @@ +import type {Bud} from '@roots/bud-framework' + +import type Extensions from '../index.js' + +export function handleManifestSchemaWarning(this: Extensions, bud: Bud) { + if (bud.context.manifest?.bud?.allowlist) { + bud.context.manifest.bud.extensions = { + ...(bud.context.manifest.bud.extensions ?? {}), + allowlist: bud.context.manifest.bud.allowlist, + } + + this.logger.warn( + `package.json: bud.allowlist is deprecated. Use bud.extensions.allowlist instead.`, + ) + } + + if (bud.context.manifest?.bud?.denylist) { + bud.context.manifest.bud.extensions = { + ...(bud.context.manifest.bud.extensions ?? {}), + denylist: bud.context.manifest.bud.denylist, + } + + this.logger.warn( + `package.json: bud.denylist is deprecated. Use bud.extensions.denylist instead.`, + ) + } +} diff --git a/sources/@roots/bud-extensions/src/service/util/isConstructor.ts b/sources/@roots/bud-extensions/src/service/util/isConstructor.ts new file mode 100644 index 0000000000..c0b5fc3e0a --- /dev/null +++ b/sources/@roots/bud-extensions/src/service/util/isConstructor.ts @@ -0,0 +1,10 @@ +export const isConstructor = ( + f: any, +): f is new (...args: any[]) => any => { + try { + Reflect.construct(String, [], f) + } catch (e) { + return false + } + return true +} diff --git a/sources/@roots/bud-extensions/tsconfig.json b/sources/@roots/bud-extensions/tsconfig.json index f81e2b057b..a024a81c50 100644 --- a/sources/@roots/bud-extensions/tsconfig.json +++ b/sources/@roots/bud-extensions/tsconfig.json @@ -7,6 +7,9 @@ "include": ["./src"], "exclude": ["./lib", "./node_modules", "**/*.test.ts"], "references": [ - {"path": "./../bud-framework/tsconfig.json"} + {"path": "../bud-framework/tsconfig.json"}, + {"path": "../bud-support/tsconfig.json"}, + {"path": "../bud-terser/tsconfig.json"}, + {"path": "../container/tsconfig.json"}, ] } diff --git a/sources/@roots/bud-framework/src/types/options/context.ts b/sources/@roots/bud-framework/src/types/options/context.ts index 3cbafb1bb0..ed64707162 100644 --- a/sources/@roots/bud-framework/src/types/options/context.ts +++ b/sources/@roots/bud-framework/src/types/options/context.ts @@ -1,7 +1,6 @@ import type {Readable, Writable} from 'node:stream' import type {Bud} from '../../bud.js' -import type {Modules} from '../registry/modules.js' export interface BaseContext { label: string @@ -71,9 +70,9 @@ export interface BaseContext { target: Array }> config: Record - extensions: { - builtIn: Partial> - discovered: Partial> + extensions?: { + builtIn?: Array + discovered?: Array } services: Array env: Record diff --git a/sources/@roots/bud-framework/src/types/services/extensions.ts b/sources/@roots/bud-framework/src/types/services/extensions.ts index 0a0c259f8b..0b1ecf5291 100644 --- a/sources/@roots/bud-framework/src/types/services/extensions.ts +++ b/sources/@roots/bud-framework/src/types/services/extensions.ts @@ -1,3 +1,5 @@ +import type Container from '@roots/container' + import type {Bud} from '../../bud.js' import type { ApplyPlugin, @@ -29,13 +31,13 @@ export type LifecycleMethods = * @public */ export interface Service extends BaseService { - unresolvable: Set<`${keyof Modules & string}`> + unresolvable: Set - resolvedOptions: { + options: Container<{ discovery: boolean allowlist: Array denylist: Array - } + }> repository: Modules diff --git a/yarn.lock b/yarn.lock index da45ac64e0..4af6b06d6f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6077,6 +6077,7 @@ __metadata: "@roots/bud-framework": "workspace:sources/@roots/bud-framework" "@roots/bud-support": "workspace:sources/@roots/bud-support" "@roots/bud-terser": "workspace:sources/@roots/bud-terser" + "@roots/container": "workspace:sources/@roots/container" "@skypack/package-check": 0.2.2 "@types/node": 16.18.3 clean-webpack-plugin: 4.0.0