diff --git a/config/eslint.config.cjs b/config/eslint.config.cjs index 282a72fcbb..9069e871c4 100644 --- a/config/eslint.config.cjs +++ b/config/eslint.config.cjs @@ -44,7 +44,7 @@ module.exports = { ], parser: `@typescript-eslint/parser`, parserOptions: { - ecmaFeatures: {jsx: true}, + ecmaFeatures: { jsx: true }, ecmaVersion: 2021, sourceType: `module`, }, @@ -58,11 +58,22 @@ module.exports = { `sort-class-members`, ], rules: { + [`@typescript-eslint/ban-types`]: [ + ERROR, + { + types: { + Buffer: { + message: `Use Uint8Array instead.`, + suggest: [`Uint8Array`], + }, + }, + }, + ], [`@typescript-eslint/explicit-member-accessibility`]: ERROR, [`@typescript-eslint/quotes`]: [ ERROR, `backtick`, - {avoidEscape: true}, + { avoidEscape: true }, ], [`arrow-body-style`]: OFF, [`comma-dangle`]: [ @@ -106,11 +117,18 @@ module.exports = { ], [`n/no-unsupported-features/es-syntax`]: [ ERROR, - {ignores: [`modules`], version: `>=16.0.0`}, + { ignores: [`modules`], version: `>=16.0.0` }, ], [`n/shebang`]: OFF, [`no-console`]: ERROR, [`no-extra-semi`]: OFF, + [`no-restricted-globals`]: [ + ERROR, + { + message: `Use Uint8Array instead.`, + name: `Buffer`, + }, + ], [`perfectionist/sort-classes`]: OFF, [`perfectionist/sort-imports`]: [ ERROR, @@ -162,6 +180,6 @@ module.exports = { `.mjs`, ], }, - react: {version: `detect`}, + react: { version: `detect` }, }, } diff --git a/sources/@roots/bud-build/src/config/infrastructureLogging.ts b/sources/@roots/bud-build/src/config/infrastructureLogging.ts index 96ed4dc05c..50d7e17678 100644 --- a/sources/@roots/bud-build/src/config/infrastructureLogging.ts +++ b/sources/@roots/bud-build/src/config/infrastructureLogging.ts @@ -9,16 +9,16 @@ export const infrastructureLogging: Factory< console: bud.hooks.filter(`build.infrastructureLogging.console`, { ...console, error: (...args: any[]) => { - logger.scope(bud.label, `webpack`).error(...args) + logger.scope(bud.label).error(...args) }, info: (...args: any[]) => { - logger.scope(bud.label, `webpack`).info(...args) + logger.scope(bud.label).info(...args) }, log: (...args: any[]) => { - logger.scope(bud.label, `webpack`).log(...args) + logger.scope(bud.label).log(...args) }, warn: (...args: any[]) => { - logger.scope(bud.label, `webpack`).info(...args) + logger.scope(bud.label).info(...args) }, }), level: bud.hooks.filter(`build.infrastructureLogging.level`, `log`), diff --git a/sources/@roots/bud-build/src/rules/svg.inline.ts b/sources/@roots/bud-build/src/rules/svg.inline.ts index 6a589d3a9f..9bded1f876 100644 --- a/sources/@roots/bud-build/src/rules/svg.inline.ts +++ b/sources/@roots/bud-build/src/rules/svg.inline.ts @@ -10,6 +10,6 @@ const inlineSvg: Factory = async ({filter, makeRule, path}) => .setGenerator({dataUrl}) .setType(`asset/inline`) -const dataUrl = (data: Buffer) => dataUri(data.toString()) +const dataUrl = (data: Uint8Array) => dataUri(data.toString()) export {dataUrl, inlineSvg as default} diff --git a/sources/@roots/bud-build/src/service/index.ts b/sources/@roots/bud-build/src/service/index.ts index 09b6a3f6df..64510c856c 100644 --- a/sources/@roots/bud-build/src/service/index.ts +++ b/sources/@roots/bud-build/src/service/index.ts @@ -1,4 +1,3 @@ -import type {Records} from '@roots/bud-build/config' import type {Bud, Build as BudBuild} from '@roots/bud-framework' import type {Items, Loaders, Rules} from '@roots/bud-framework' import type {Configuration} from '@roots/bud-framework/config' @@ -92,28 +91,41 @@ class Build extends Service implements BudBuild { this.logger.log(`bud.build.make called`) await this.app.hooks.fire(`build.before`, this.app) - await import(`@roots/bud-build/config`).then( - async (records: Records) => - await Promise.all( - Object.entries(records).map(async ([prop, factory]) => { - try { - const value = await factory(this.app) - if (isUndefined(value)) return - - this.config[prop] = value - this.logger.log(`built`, prop) - } catch (error) { - throw error - } - }), - ), - ) - - this.logger.log(`configuration successfully built`) + await import(`@roots/bud-build/config`) + .then( + async records => + await Promise.all( + Object.entries(records).map(async ([prop, factory]) => { + const value = await factory(this.app).catch(this.catch) + if (isUndefined(value)) { + this.logger.success(`omitting:`, prop, `(undefined)`) + return + } + + Object.defineProperty(this.config, prop, { + configurable: true, + enumerable: true, + value, + writable: true, + }) + + this.logger + .success(`defined:`, prop, `(${typeof this.config[prop]})`) + .info(prop, `info:`, this.config[prop]) + }), + ), + ) + .catch(this.catch) + + this.logger.success(`configuration built`) this.logger.info(this.config) - await this.app.hooks.fire(`build.after`, this.app) - return this.config + await this.app.hooks.fire(`build.after`, this.app).catch(this.catch) + + return Object.entries(this.config).reduce((a, [k, v]) => { + if (isUndefined(v)) return a + return {...a, [k]: v} + }, {}) } /** diff --git a/sources/@roots/bud-dashboard/src/components/error.tsx b/sources/@roots/bud-dashboard/src/components/error.tsx index 6438886f5f..1a6f30b645 100644 --- a/sources/@roots/bud-dashboard/src/components/error.tsx +++ b/sources/@roots/bud-dashboard/src/components/error.tsx @@ -3,70 +3,78 @@ import {BudError} from '@roots/bud-support/errors' import figures from '@roots/bud-support/figures' import {Box, type ReactNode, Static, Text} from '@roots/bud-support/ink' -const basePath = - global.process.env.PROJECT_CWD ?? - global.process.env.INIT_CWD ?? - global.process.cwd() - -export const Error = ({error}: {error: unknown}): ReactNode => { - let normalError: BudError +type RawError = BudError | Error | string | undefined +const cleanErrorObject = (error: RawError): BudError => { if (!error) { - error = BudError.normalize(`Unknown error`) + error = new BudError(`Unknown error`) + } + + if (typeof error === `string`) { + error = new BudError(error) } - normalError = - error instanceof BudError ? error : BudError.normalize(error) + + return error instanceof BudError ? error : BudError.normalize(error) +} + +export const Error = ({error: input}: {error: RawError}): ReactNode => { + const error = cleanErrorObject(input) return ( {(_, key) => ( - - {` ${normalError.name} `} - - - - {figures.cross} - {normalError.message} - - - {normalError.details && - !normalError.details.startsWith(`resolve`) && ( - - - - {figures.ellipsis} - {` `}Details{` `} - + {error.name && ( + + {figures.cross} + + {error.name} + + + )} - {normalError.details.replace(basePath, `.`)} + {error.message && ( + + {error.message} + + )} + + {error.details && !error.details.startsWith(`resolve`) && ( + + + + {figures.ellipsis} + {` `}Details{` `} - - )} - {normalError.thrownBy && ( + {error.details} + + + )} + + {error.thrownBy && ( {figures.ellipsis} {` `}Thrown by{` `} - {normalError.thrownBy} + {error.thrownBy} )} - {normalError.docs && ( + {error.docs && ( {figures.arrowRight} {` `}Documentation{` `} - {normalError.docs.href} + {error.docs.href} )} - {normalError.issues && ( + {error.issues && ( @@ -75,24 +83,24 @@ export const Error = ({error}: {error: unknown}): ReactNode => { Issues {` `} - {normalError.issues.href} + {error.issues.href} )} - {normalError.file && ( + {error.file && ( {figures.info} {` `}See file{` `} - {normalError.file.path} + {error.file.path} )} - {normalError.origin && - !(normalError.origin instanceof BudError) && - normalError.stack && ( + {error.origin && + !(error.origin instanceof BudError) && + error.stack && ( {figures.home} @@ -108,14 +116,14 @@ export const Error = ({error}: {error: unknown}): ReactNode => { borderTop={false} paddingLeft={1} > - {normalError.stack} + {error.stack} )} - {normalError.origin && - normalError.origin instanceof BudError && - normalError.stack && ( + {error.origin && + error.origin instanceof BudError && + error.stack && ( {figures.home} @@ -133,11 +141,11 @@ export const Error = ({error}: {error: unknown}): ReactNode => { paddingLeft={1} > - {normalError.origin.message} + {error.origin.message} {`\n`} - {normalError.origin.stack && ( - {normalError.origin.stack} + {error.origin.stack && ( + {error.origin.stack} )} diff --git a/sources/@roots/bud-emotion/src/extension.ts b/sources/@roots/bud-emotion/src/extension.ts index 991fff3517..0294ec6e5c 100644 --- a/sources/@roots/bud-emotion/src/extension.ts +++ b/sources/@roots/bud-emotion/src/extension.ts @@ -12,7 +12,7 @@ import { */ @label(`@roots/bud-emotion`) @dependsOnOptional([`@roots/bud-babel`, `@roots/bud-swc`]) -export class BudEmotion extends Extension<{}, null> { +export class BudEmotion extends Extension, null> { /** * {@link Extension.boot} */ diff --git a/sources/@roots/bud-extensions/src/fix-style-only-entrypoints/index.ts b/sources/@roots/bud-extensions/src/fix-style-only-entrypoints/index.ts index a9b22dc76a..36153d3b63 100644 --- a/sources/@roots/bud-extensions/src/fix-style-only-entrypoints/index.ts +++ b/sources/@roots/bud-extensions/src/fix-style-only-entrypoints/index.ts @@ -1,9 +1,7 @@ +import type {Bud} from '@roots/bud-framework' + import {Extension} from '@roots/bud-framework/extension' -import { - label, - plugin, - production, -} from '@roots/bud-framework/extension/decorators' +import {label, plugin} from '@roots/bud-framework/extension/decorators' import FixStyleOnlyEntrypoints from './plugin.js' @@ -12,5 +10,27 @@ import FixStyleOnlyEntrypoints from './plugin.js' */ @label(`@roots/bud-extensions/fix-style-only-entrypoints`) @plugin(FixStyleOnlyEntrypoints) -@production -export default class BudFixStyleOnlyEntrypoints extends Extension {} +export default class BudFixStyleOnlyEntrypoints extends Extension { + /** + * When + */ + public override when(bud: Bud): boolean { + if (this.enabled === true) return true + + if (bud.isDevelopment) return false + + const entrypoints = bud.hooks.filter(`build.entry`, undefined) + + if (!entrypoints) return false + + if ( + !Object.values(entrypoints).every(value => + value.import.every(entry => entry.endsWith(`.css`)), + ) + ) { + return false + } + + return true + } +} diff --git a/sources/@roots/bud-extensions/src/webpack-hot-module-replacement-plugin/index.ts b/sources/@roots/bud-extensions/src/webpack-hot-module-replacement-plugin/index.ts index fbe18793dc..5f6a43ac7a 100644 --- a/sources/@roots/bud-extensions/src/webpack-hot-module-replacement-plugin/index.ts +++ b/sources/@roots/bud-extensions/src/webpack-hot-module-replacement-plugin/index.ts @@ -13,7 +13,7 @@ import { @label(`@roots/bud-extensions/webpack-hot-module-replacement-plugin`) @plugin(Webpack.HotModuleReplacementPlugin) export default class BudHMR extends Extension< - {}, + NonNullable, HotModuleReplacementPlugin > { /** diff --git a/sources/@roots/bud-framework/src/bootstrap.ts b/sources/@roots/bud-framework/src/bootstrap.ts index 1ff0c46223..34c3195a26 100644 --- a/sources/@roots/bud-framework/src/bootstrap.ts +++ b/sources/@roots/bud-framework/src/bootstrap.ts @@ -1,11 +1,8 @@ import type {Bud, BudService, Registry} from '@roots/bud-framework' -import chalk from '@roots/bud-support/chalk' import {BudError} from '@roots/bud-support/errors' -import figures from '@roots/bud-support/figures' import camelCase from '@roots/bud-support/lodash/camelCase' import isString from '@roots/bud-support/lodash/isString' -import logger from '@roots/bud-support/logger' import {FS} from './fs.js' import {Module} from './module.js' @@ -86,7 +83,7 @@ export const services: Array = [] */ const filterServices = (app: Bud) => - (signifier: string): Boolean => { + (signifier: string): boolean => { if (!isString(signifier)) return true return ( @@ -119,13 +116,12 @@ const instantiateServices = Object.defineProperties(app, { [label]: { configurable: true, + enumerable: true, value, writable: true, }, }) - logger.log(chalk.blue(label), figures.arrowLeft, chalk.cyan(signifier)) - services.push(label) } @@ -215,7 +211,7 @@ export const bootstrap = async function (bud: Bud) { }), ) - bud.hooks.action(`compiler.before`, bud.module.compilerBefore) + bud.after(bud.module.after) const initializeEvents: Array<`${keyof Registry.EventsStore}`> = [ `bootstrap`, @@ -235,7 +231,5 @@ export const bootstrap = async function (bud: Bud) { Promise.resolve({}), ) - bud.after(bud.module.after) - return bud } diff --git a/sources/@roots/bud-framework/src/bud.ts b/sources/@roots/bud-framework/src/bud.ts index c196becbc0..e6a1e595d0 100644 --- a/sources/@roots/bud-framework/src/bud.ts +++ b/sources/@roots/bud-framework/src/bud.ts @@ -339,12 +339,10 @@ export class Bud { * Await all promised tasks */ @bind - public async promise(promise?: (bud: Bud) => Promise) { - if (promise) - await this.promised - .then(async () => await promise(this)) - .catch(this.catch) - else await this.promised.catch(this.catch) + public async promise(promise?: (bud: Bud) => Promise) { + await this.promised + .then(async () => promise && (await promise(this))) + .catch(this.catch) return this } @@ -374,15 +372,19 @@ export class Bud { @bind public set( key: K, - value: Bud[K], + unknownValue: Bud[K], bind: boolean = true, ): Bud { - if (bind && isFunction(value) && `bind` in value) { - Object.assign(this, {[key]: value.bind(this)}) - return this - } - - Object.assign(this, {[key]: value}) + const bindable = + bind && isFunction(unknownValue) && `bind` in unknownValue + const value = bindable ? unknownValue.bind(this) : unknownValue + + Object.defineProperty(this, key, { + configurable: true, + enumerable: true, + value, + writable: true, + }) return this } @@ -395,7 +397,6 @@ export class Bud { logger.scope(this.label).log(...messages) return this } - /** * Log success * @deprecated Import logger instance from `@roots/bud-support/logger` @@ -405,7 +406,6 @@ export class Bud { logger.scope(this.label).log(...messages) return this } - /** * Log warning * @deprecated Import logger instance from `@roots/bud-support/logger` diff --git a/sources/@roots/bud-framework/src/config/optimization/runtimeChunk.ts b/sources/@roots/bud-framework/src/config/optimization/runtimeChunk.ts index 5f573e7f4e..0a46bad658 100644 --- a/sources/@roots/bud-framework/src/config/optimization/runtimeChunk.ts +++ b/sources/@roots/bud-framework/src/config/optimization/runtimeChunk.ts @@ -1,5 +1,5 @@ export type RuntimeChunk = | 'multiple' | 'single' - | {name?: Function | string} + | {name?: ((...args: Array) => unknown) | string} | boolean diff --git a/sources/@roots/bud-framework/src/config/optimization/splitChunks.ts b/sources/@roots/bud-framework/src/config/optimization/splitChunks.ts index 5d452e3766..a51d155d76 100644 --- a/sources/@roots/bud-framework/src/config/optimization/splitChunks.ts +++ b/sources/@roots/bud-framework/src/config/optimization/splitChunks.ts @@ -1,5 +1,7 @@ import type {AssetInfo, Chunk, PathData} from '@roots/bud-support/webpack' +type Fn = (...args: Array) => unknown + export interface SplitChunks { /** * Sets the name delimiter for created chunks. @@ -10,12 +12,7 @@ export interface SplitChunks { * Assign modules to a cache group (modules from different cache groups are tried to keep in separate chunks, default categories: 'default', 'defaultVendors'). */ cacheGroups?: { - [index: string]: - | false - | Function - | RegExp - | SplitChunksCacheGroup - | string + [index: string]: false | Fn | RegExp | SplitChunksCacheGroup | string } /** @@ -127,7 +124,7 @@ export interface SplitChunks { /** * Give chunks created a name (chunks with equal name are merged). */ - name?: false | Function | string + name?: false | Fn | string /** * Compare used exports when checking common modules. Modules will only be put in the same chunk when exports are equal. @@ -171,7 +168,7 @@ export interface SplitChunksCacheGroup { /** * Assign modules to a cache group by module layer. */ - layer?: Function | RegExp | string + layer?: Fn | RegExp | string /** * Maximum number of requests which are accepted for on-demand loading. @@ -221,7 +218,7 @@ export interface SplitChunksCacheGroup { /** * Give chunks for this cache group a name (chunks with equal name are merged). */ - name?: false | Function | string + name?: false | Fn | string /** * Priority of this cache group. @@ -236,12 +233,12 @@ export interface SplitChunksCacheGroup { /** * Assign modules to a cache group by module name. */ - test?: Function | RegExp | string + test?: Fn | RegExp | string /** * Assign modules to a cache group by module type. */ - type?: Function | RegExp | string + type?: Fn | RegExp | string /** * Compare used exports when checking common modules. Modules will only be put in the same chunk when exports are equal. diff --git a/sources/@roots/bud-framework/src/fs.ts b/sources/@roots/bud-framework/src/fs.ts index 25975d5f0b..7db4ea5ff2 100644 --- a/sources/@roots/bud-framework/src/fs.ts +++ b/sources/@roots/bud-framework/src/fs.ts @@ -1,6 +1,7 @@ import type {Bud} from '@roots/bud-framework' import type {Contract} from '@roots/bud-framework/service' +import {Buffer} from 'node:buffer' import {join} from 'node:path' import {bind} from '@roots/bud-support/decorators/bind' diff --git a/sources/@roots/bud-framework/src/methods/when.ts b/sources/@roots/bud-framework/src/methods/when.ts index 7ed07f84c9..b9c3710dd1 100644 --- a/sources/@roots/bud-framework/src/methods/when.ts +++ b/sources/@roots/bud-framework/src/methods/when.ts @@ -1,4 +1,3 @@ -import chalk from '@roots/bud-support/chalk' import {InputError} from '@roots/bud-support/errors' import isArray from '@roots/bud-support/lodash/isArray' import isFunction from '@roots/bud-support/lodash/isFunction' @@ -64,11 +63,6 @@ export function when( throw new InputError( `bud.when: all supplied conditionals must be functions`, { - details: `\n This is incorrect: bud.when(() => true, ${chalk.red( - `bud.vendor()`, - )}).\n This is what you wanted: bud.when(() => true, ${chalk.green( - `() => bud.vendor()`, - )})`, docs: new URL(`https://bud.js.org/docs/bud.when`), }, ) diff --git a/sources/@roots/bud-framework/src/module.ts b/sources/@roots/bud-framework/src/module.ts index fbd914f743..ef87bbcf2d 100644 --- a/sources/@roots/bud-framework/src/module.ts +++ b/sources/@roots/bud-framework/src/module.ts @@ -2,8 +2,9 @@ import {join, normalize, relative} from 'node:path' import {fileURLToPath, pathToFileURL} from 'node:url' import {bind} from '@roots/bud-support/decorators/bind' -import {ModuleError} from '@roots/bud-support/errors' +import {BudError, ModuleError} from '@roots/bud-support/errors' import {resolve} from '@roots/bud-support/import-meta-resolve' +import isEqual from '@roots/bud-support/lodash/isEqual' import noop from '@roots/bud-support/lodash/noop' import logger from '@roots/bud-support/logger' import args from '@roots/bud-support/utilities/args' @@ -12,24 +13,56 @@ import {paths} from '@roots/bud-support/utilities/paths' import {type Bud} from './index.js' import {Service} from './service.js' +/** + * Map of module signifiers to absolute paths + */ +type Resolutions = Record + +/** + * Module cache data + */ +interface ModuleCache { + resolutions: Resolutions + version: string +} + /** * Module resolver */ export class Module extends Service { /** - * Resolved module cache + * Cached resolutions data + */ + public cache: ModuleCache = { + resolutions: {}, + version: null, + } + + /** + * Imported modules */ - public resolved: Record = {} + public modules: Record = {} /** - * At end of process write resolutions to cache + * Resolved module paths */ - @bind - public async after() { - await this.app.fs.write(this.resolutionsPath, { - resolutions: this.resolved, - version: this.app.context.bud.version, - }) + public resolutions: Resolutions = {} + + /** + * Cache enabled + */ + public get cacheEnabled(): boolean { + if (args.force === true) return false + if (args.cache === false) return false + + return true + } + + /** + * Cache location + */ + public get cachePath(): string { + return join(paths.storage, `bud.resolutions.yml`) } /** @@ -38,61 +71,61 @@ export class Module extends Service { @bind public override async bootstrap(bud: Bud) { if (args.force) { - logger - .scope(`module`) - .info(`flushing resolutions`, this.resolutionsPath) + return await this.resetCache() } if (!this.cacheEnabled) { - this.resolved = {} - return - } - - const data = await bud.fs.read(this.resolutionsPath).catch(error => { - logger.scope(`module`).info(`cache is enabled but no cache exists`) - this.resolved = {} - }) - - if (!data?.resolutions) { - logger + return this.logger .scope(`module`) - .info(`cache is enabled but resolution data is missing`, data) - - this.resolved = {} - return + .log(`cache disabled. skipping read.`) } - logger - .scope(`module`) - .info(`cache is enabled and cached resolutions exist`, data) + if (await bud.fs.exists(this.cachePath)) { + this.logger.scope(`module`).log(`cache is enabled and exists`) + this.cache = await bud.fs.read(this.cachePath).catch(noop) - this.resolved = data.resolutions - } + if (!this.cache?.resolutions) { + this.logger + .scope(`module`) + .log(`cache is enabled but resolution data is missing`) - /** - * Cache enabled - */ - public get cacheEnabled(): boolean { - if (args.force === true) return false - if (args.cache === false) return false + return await this.resetCache() + } - return true + this.resolutions = {...this.cache.resolutions} + } } /** - * {@link Service.compilerBefore} + * At end of process write resolutions to cache */ @bind - public override async compilerBefore(bud: Bud) { - await bud.fs - .write(this.resolutionsPath, { - resolutions: this.resolved, - version: bud.context.bud.version, - }) - .catch(this.catch) - .then(result => { - bud.fs.read(this.resolutionsPath) - }) + public async after(bud: Bud) { + if (args.cache === false) { + this.logger.scope(`module`).log(`cache disabled. skipping write.`) + return bud + } + + if (isEqual(this.cache.resolutions, this.resolutions)) { + this.logger + .scope(`module`) + .log(`resolutions unchanged. skipping write.`) + .info(`resolutions:`, this.resolutions) + .info(`cache:`, this.cache.resolutions) + return bud + } + + logger + .scope(`module`) + .log(`writing resolutions`) + .info(this.resolutions) + + await bud.fs.write(this.cachePath, { + resolutions: this.resolutions, + version: bud.context.bud.version, + }) + + return bud } /** @@ -100,10 +133,12 @@ export class Module extends Service { */ @bind public async getDirectory(signifier: string, context?: string) { + this.logger.scope(`module`).info(`getDirectory`, signifier, context) + return await this.resolve(signifier, context) .then(path => relative(this.app.context.basedir, path)) .then(path => path.split(signifier).shift()) - .then(path => this.app.path(path as any, signifier)) + .then(path => this.app.path(path, signifier)) .catch(this.catch) } @@ -111,8 +146,9 @@ export class Module extends Service { * Get `package.json` absolute path from a module signifier */ @bind - public async getManifestPath(pkgName: string) { - return await this.getDirectory(pkgName) + public async getManifestPath(signifier: string) { + this.logger.scope(`module`).info(`getManifestPath`, signifier) + return await this.getDirectory(signifier) .then(dir => this.app.path(dir, `package.json`)) .catch(this.catch) } @@ -121,38 +157,51 @@ export class Module extends Service { * Import a module from its signifier */ @bind - public async import( - signifier: T, + public async import( + signifier: string, context?: string, options: {bustCache?: boolean; raw?: boolean} = { bustCache: false, raw: false, }, ) { - if (this.resolved?.[signifier]) { - const path = options.bustCache - ? `${this.resolved[signifier]}?v=${Date.now()}` - : this.resolved[signifier] + if (options.bustCache) { + signifier = `${signifier}?v=${Date.now()}` + } + + if (this.hasModule(signifier)) { + const code = this.getModule(signifier) + logger.scope(`module`).log(`[cache hit]`, `module:`, signifier) + return options.raw ? code : code?.default ?? code + } + + if (!this.hasResolution(signifier)) { + await this.resolve(signifier, context).catch(this.catch) + } - const result = await import(path).catch(error => { + const code = await import(this.getResolution(signifier)).catch( + error => { logger .scope(`module`) - .info( - `Could not import ${signifier} from ${this.resolved[signifier]}. Removing from cached module registry.`, + .log( + `Could not import module:`, + signifier, + `Removing from cached module registry.`, error, ) - delete this.resolved[signifier] - }) + this.removeResolution(signifier) + }, + ) - return options.raw ? result : result?.default ?? result + if (!code) { + throw new BudError(`Could not import ${signifier}`) } - const path = await this.resolve(signifier, context).catch(this.catch) - const result = await import(path).catch(this.catch) + this.setModule(signifier, code) - logger.scope(`module`).info(`[cache miss]`, `imported`, signifier) - return options.raw ? result : result?.default ?? result + logger.scope(`module`).log(`imported module:`, signifier) + return options.raw ? code : code?.default ?? code } /** @@ -160,12 +209,11 @@ export class Module extends Service { */ @bind public makeContextURL(context?: string): string { - return ( - context ?? - (pathToFileURL( - join(this.app.context.basedir, `package.json`), - ) as unknown as string) - ) + if (context) return context + + return pathToFileURL( + join(this.app.context.basedir, `package.json`), + ) as unknown as string } /** @@ -174,16 +222,32 @@ export class Module extends Service { @bind public async readManifest(signifier: string) { return await this.getManifestPath(signifier).then(async path => { - logger.scope(`module`).info(signifier, `manifest resolved to`, path) - return await this.app.fs.read(path) + const value = await this.app.fs.read(path) + logger.scope(`module`).info(signifier, `manifest`, value) + return value }) } /** - * Cache location + * Reset cache */ - public get resolutionsPath(): string { - return join(paths.storage, `bud.resolutions.yml`) + @bind + public async resetCache() { + this.logger.scope(`module`).log(`clearing runtime module cache`) + + this.cache = { + resolutions: {}, + version: this.app.context.bud?.version, + } + this.resolutions = {...this.cache.resolutions} + + if (await this.app.fs.exists(this.cachePath)) { + this.logger + .scope(`module`) + .log(`removing cache file`, this.cachePath) + + await this.app.fs.remove(this.cachePath) + } } /** @@ -194,45 +258,115 @@ export class Module extends Service { signifier: string, context?: string, ): Promise { - if (this.resolved?.[signifier]) { + if (this.hasResolution(signifier)) { logger .scope(`module`) .info( `[cache hit]`, - `resolved ${signifier} to ${this.resolved[signifier]}`, + `path:`, + signifier, + `=>`, + this.getResolution(signifier), ) - return this.resolved[signifier] + return this.resolutions[signifier] } await resolve(signifier, this.makeContextURL()) .then(path => { - this.resolved[signifier] = normalize(fileURLToPath(path)) + this.setResolution(signifier, normalize(fileURLToPath(path))) + logger .scope(`module`) - .info( + .log( `[cache miss]`, - `resolved ${signifier} to ${this.resolved[signifier]}`, + `path:`, + signifier, + `=>`, + this.getResolution(signifier), ) }) .catch(noop) - - if (this.resolved[signifier]) return this.resolved[signifier] + if (this.hasResolution(signifier)) return this.getResolution(signifier) await resolve(signifier, this.makeContextURL(context)) .then(path => { - this.resolved[signifier] = normalize(fileURLToPath(path)) + this.setResolution(signifier, normalize(fileURLToPath(path))) + logger .scope(`module`) - .info( + .log( `[cache miss]`, - `resolved ${signifier} to ${this.resolved[signifier]}`, + `path:`, + signifier, + `=>`, + this.getResolution(signifier), ) }) .catch(noop) - - if (this.resolved[signifier]) return this.resolved[signifier] + if (this.hasResolution(signifier)) return this.getResolution(signifier) throw new ModuleError(`Could not resolve ${signifier}`) } + + /** + * Get a previously imported module + */ + @bind + public getModule(signifier: string) { + return this.modules[signifier] + } + + /** + * Check if a module has been imported + */ + @bind + public hasModule(signifier: string) { + return signifier in this.modules + } + + /** + * Remove a module + */ + @bind + public removeModule(signifier: string) { + return delete this.modules[signifier] + } + + /** + * Set a module + */ + @bind + public setModule(signifier: string, module: any) { + this.modules[signifier] = module + } + + /** + * Get a module resolution + */ + @bind + public getResolution(signifier: string) { + return this.resolutions[signifier] + } + + /** + * Check if a module has been resolved + */ + @bind + public hasResolution(signifier: string) { + return signifier in this.resolutions + } + + @bind + public removeResolution(signifier: string) { + return delete this.resolutions[signifier] + } + + /** + * Resolve a module from a particular URL + */ + @bind + public setResolution(signifier: string, url: string) { + this.resolutions[signifier] = url + } } diff --git a/sources/@roots/bud-framework/src/project.ts b/sources/@roots/bud-framework/src/project.ts index 9645d27154..363da1ab02 100644 --- a/sources/@roots/bud-framework/src/project.ts +++ b/sources/@roots/bud-framework/src/project.ts @@ -16,7 +16,10 @@ export default class Project extends Service { */ @bind public override async buildAfter(bud: Bud) { - if (!bud.context.debug) return + if (!bud.context.debug) { + this.logger.log(`debug mode disabled. skipping write.`) + return bud + } await bud.promise(async bud => { await bud.fs @@ -24,7 +27,7 @@ export default class Project extends Service { ...omit(bud.context, [`env`, `stdout`, `stderr`, `stdin`]), bootstrap: { args: args.raw, - resolutions: bud.module.resolved, + resolutions: bud.module.resolutions ?? {}, }, children: bud.children ? Object.keys(bud.children) : [], env: bud.env.getKeys(), @@ -62,6 +65,8 @@ export default class Project extends Service { this.logger.log(`webpack.output.yml written to disk`) }) }) + + return bud } /** @@ -69,10 +74,13 @@ export default class Project extends Service { */ @bind public override async compilerDone(bud: Bud, stats: Stats) { - this.logger.log(`compiler done`) + if (!bud.context.debug) { + return bud + } - if (!bud.context.debug) return - if (!stats) return + if (!stats) { + return bud + } await bud.fs.write( bud.path(`@storage`, bud.label, `debug`, `stats.yml`), @@ -81,5 +89,7 @@ export default class Project extends Service { message: stats.toString(), }, ) + + return bud } } diff --git a/sources/@roots/bud-hooks/src/base/base.ts b/sources/@roots/bud-hooks/src/base/base.ts index a02719fdb7..20a6f42144 100644 --- a/sources/@roots/bud-hooks/src/base/base.ts +++ b/sources/@roots/bud-hooks/src/base/base.ts @@ -3,6 +3,7 @@ import type {Bud} from '@roots/bud-framework' import {bind} from '@roots/bud-support/decorators/bind' import {BudError} from '@roots/bud-support/errors' import isUndefined from '@roots/bud-support/lodash/isUndefined' +import logger from '@roots/bud-support/logger' /** * Synchronous hooks registry @@ -30,6 +31,13 @@ export abstract class Hooks { return this._app() } + /** + * Get logger + */ + public get logger() { + return logger.scope(`hooks`) + } + @bind public catch(e: Error, id?: string, iteration?: number): void { if (!id) { diff --git a/sources/@roots/bud-hooks/src/event/event.ts b/sources/@roots/bud-hooks/src/event/event.ts index 7e4623eec3..35bdaa7c05 100644 --- a/sources/@roots/bud-hooks/src/event/event.ts +++ b/sources/@roots/bud-hooks/src/event/event.ts @@ -21,7 +21,12 @@ export class EventHooks extends Hooks { await Promise.all( this.store[id].map(async (action: any) => { + if (typeof action === `undefined`) return + + this.logger.info(`running ${id} callback:`, action) + await action(...value).catch((error: Error) => { + this.logger.error(`problem running ${id} callback`) throw error }) }), @@ -38,7 +43,7 @@ export class EventHooks extends Hooks { if (!(id in this.store)) this.store[id] = [] input.map(value => { - this.app.hooks.logger.info(`registered ${id} callback`) + this.logger.info(`registered ${id} callback`) this.store[id].push(value as any) }) diff --git a/sources/@roots/bud-server/src/middleware/proxy/responseInterceptor.ts b/sources/@roots/bud-server/src/middleware/proxy/responseInterceptor.ts index e6ddd2aac1..f6b731646e 100644 --- a/sources/@roots/bud-server/src/middleware/proxy/responseInterceptor.ts +++ b/sources/@roots/bud-server/src/middleware/proxy/responseInterceptor.ts @@ -1,5 +1,6 @@ import type {Bud} from '@roots/bud-framework' +import type {Buffer} from 'node:buffer' import type {IncomingMessage, ServerResponse} from 'node:http' import {responseInterceptor} from '@roots/bud-support/http-proxy-middleware' @@ -52,6 +53,7 @@ const transformResponseBuffer = ( bud: Bud, url: Record, proxy: IncomingMessage, + // eslint-disable-next-line @typescript-eslint/ban-types buffer: Buffer, ) => { if (!isTransformable(proxy)) return buffer @@ -61,7 +63,7 @@ const transformResponseBuffer = ( .filter(`dev.middleware.proxy.replacements`, [ [url.publicProxy.origin, url.dev.origin], ]) - .reduce(transformBody, buffer.toString(`utf8`)) + .reduce(transformBody, buffer.toString()) } const isTransformable = (message?: IncomingMessage) => { diff --git a/sources/@roots/bud-stylelint/package.json b/sources/@roots/bud-stylelint/package.json index 42c14364b4..bae67a42ea 100644 --- a/sources/@roots/bud-stylelint/package.json +++ b/sources/@roots/bud-stylelint/package.json @@ -50,6 +50,7 @@ "type": "module", "exports": { ".": "./lib/index.js", + "./extension/base": "./lib/extension/base/index.js", "./config": "./config/index.cjs", "./extension": "./lib/extension/index.js", "./bud/commands": "./lib/bud/commands/index.js" @@ -61,6 +62,9 @@ ], "extension": [ "./lib/extension/index.d.ts" + ], + "extension/base": [ + "./lib/extension/base/index.d.ts" ] } }, diff --git a/sources/@roots/bud-stylelint/src/extension/base.ts b/sources/@roots/bud-stylelint/src/extension/base/index.ts similarity index 100% rename from sources/@roots/bud-stylelint/src/extension/base.ts rename to sources/@roots/bud-stylelint/src/extension/base/index.ts index 35ec569cf8..1727051017 100644 --- a/sources/@roots/bud-stylelint/src/extension/base.ts +++ b/sources/@roots/bud-stylelint/src/extension/base/index.ts @@ -231,7 +231,6 @@ export class BudStylelintPublicApi extends Extension { * {@link Options.threads} */ public declare setThreads: Api[`setThreads`] - /** * Stylelint plugins * @@ -246,6 +245,7 @@ export class BudStylelintPublicApi extends Extension { public set plugins(plugins: Api[`config`][`plugins`]) { this.setConfig((config = {}) => ({...config, plugins})) } + /** * Get stylelint plugins * diff --git a/sources/@roots/bud-stylelint/src/extension/index.ts b/sources/@roots/bud-stylelint/src/extension/index.ts index 6917cb0f54..0cbb788a3b 100644 --- a/sources/@roots/bud-stylelint/src/extension/index.ts +++ b/sources/@roots/bud-stylelint/src/extension/index.ts @@ -7,11 +7,11 @@ import { label, plugin, } from '@roots/bud-framework/extension/decorators' +import {BudStylelintPublicApi} from '@roots/bud-stylelint/extension/base' import {deprecated} from '@roots/bud-support/decorators/deprecated' +import noop from '@roots/bud-support/lodash/noop' import Plugin from 'stylelint-webpack-plugin' -import {BudStylelintPublicApi} from './base.js' - export type Options = Plugin.Options & { config?: Plugin.Options & { plugins?: Plugin.Options[`config`][`plugins`] & Array @@ -65,7 +65,15 @@ export default class BudStylelintWebpackPlugin extends BudStylelintPublicApi { * {@link Extension.register} */ @bind - public override async register({context}: Bud) { + public override async register({context, module}: Bud) { + const stylelintPath = await module + .resolve(`stylelint`, import.meta.url) + .catch(noop) + if (stylelintPath) { + this.logger.log(`setting stylelint path:`, stylelintPath) + this.setStylelintPath(stylelintPath) + } + const configFile = Object.values(context.files).find(({name}) => name.includes(`stylelint`), ) diff --git a/sources/@roots/bud-stylelint/src/index.ts b/sources/@roots/bud-stylelint/src/index.ts index 1373394026..4d685e699c 100644 --- a/sources/@roots/bud-stylelint/src/index.ts +++ b/sources/@roots/bud-stylelint/src/index.ts @@ -8,9 +8,9 @@ * @see https://github.com/roots/bud/tree/main/sources/@roots/bud-styelint */ -import type {BudStylelintPublicApi} from './extension/base.js' +import type {BudStylelintPublicApi} from '@roots/bud-stylelint/extension/base' -import BudStylelint from './extension/index.js' +import BudStylelint from '@roots/bud-stylelint/extension' declare module '@roots/bud-framework' { interface Bud { diff --git a/sources/@roots/bud-support/src/errors/index.ts b/sources/@roots/bud-support/src/errors/index.ts index e3927ee1eb..5533af4ead 100644 --- a/sources/@roots/bud-support/src/errors/index.ts +++ b/sources/@roots/bud-support/src/errors/index.ts @@ -1,5 +1,20 @@ +import {join} from 'node:path' + +import args from '@roots/bud-support/utilities/args' import cleanStack from 'clean-stack' +const cwd = `${ + global.process.env.PROJECT_CWD ?? + global.process.env.INIT_CWD ?? + global.process.cwd() +}` + +const basePath = args?.basedir + ? join(cwd, args.basedir) + : global.process.env.BUD_BASEDIR + ? join(cwd, `${global.process.env.BUD_BASEDIR}`) + : cwd + /** * Props for Bud errors */ @@ -24,6 +39,9 @@ interface BudErrorProps extends Error { * Error base class */ class BudError extends Error { + /** + * Normalize error + */ public static normalize(error: unknown) { if (error instanceof BudError) return error @@ -97,30 +115,47 @@ class BudError extends Error { super(message) Object.assign(this, options) + Object.assign(this, message) if (!this.instance) this.instance = `default` + if (this.message) { + this.message = this.message + .replaceAll(/file:\/\//g, ``) + .replaceAll(new RegExp(basePath, `g`), ``) + } + if (this.stack) { this.stack = cleanStack(this.stack, { + basePath, pathFilter: path => !path.includes(`react-reconciler`) && !path.includes(`bud-support/lib/errors`), - }) + pretty: true, + }).replaceAll(/file:\/\//g, ``) } if (this.message) { this.message = cleanStack(this.message, { + basePath, pathFilter: path => !path.includes(`react-reconciler`) && !path.includes(`bud-support/lib/errors`), - }) + pretty: true, + }).replaceAll(/file:\/\//g, ``) } if (this.thrownBy) { - this.thrownBy = this.thrownBy.replace(process.cwd(), ``) + this.thrownBy = this.thrownBy + .replace(new RegExp(basePath, `g`), ``) + .replaceAll(/file:\/\//g, ``) } - this.isBudError = true + if (this.file) { + this.file.path = this.file.path + .replaceAll(new RegExp(basePath, `g`), ``) + .replaceAll(/file:\/\//g, ``) + } } } diff --git a/sources/@roots/bud-support/src/import-meta-resolve/index.ts b/sources/@roots/bud-support/src/import-meta-resolve/index.ts index e07037c0ce..bf3c2fdc4f 100644 --- a/sources/@roots/bud-support/src/import-meta-resolve/index.ts +++ b/sources/@roots/bud-support/src/import-meta-resolve/index.ts @@ -1,2 +1,5 @@ -import {resolve} from 'import-meta-resolve' -export {resolve} +import * as importMeta from 'import-meta-resolve' + +export const resolve = async (id: string, from: string) => { + return importMeta.resolve(id, from) +} diff --git a/sources/@roots/bud-support/src/value/index.ts b/sources/@roots/bud-support/src/value/index.ts index fba5a520a7..9b9d34292d 100644 --- a/sources/@roots/bud-support/src/value/index.ts +++ b/sources/@roots/bud-support/src/value/index.ts @@ -8,14 +8,17 @@ class Value { /** * Get {@link Value.identity} */ - public static call( - value: (T & CallableFunction) | Value, + public static async call( + value: ((...args: A) => Promise) | Value, ...args: A - ): T { - return Value.isCallable(value) - ? Value.get(value)(...args) - : Value.get(value) + ): Promise { + if (typeof value === `function`) { + const fn = Value.get(value) + return await fn(...args) + } + return Value.get(value) } + /** * Get {@link Value.identity} */ diff --git a/sources/@roots/bud/src/cli/commands/index.tsx b/sources/@roots/bud/src/cli/commands/index.tsx index 157ddfdd46..5d8355ce13 100644 --- a/sources/@roots/bud/src/cli/commands/index.tsx +++ b/sources/@roots/bud/src/cli/commands/index.tsx @@ -13,6 +13,7 @@ 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 force from '@roots/bud/cli/flags/force' +import ignoreErrors from '@roots/bud/cli/flags/ignoreErrors' import log from '@roots/bud/cli/flags/log' import mode from '@roots/bud/cli/flags/mode' import notify from '@roots/bud/cli/flags/notify' @@ -22,18 +23,18 @@ import use from '@roots/bud/cli/flags/use' import verbose from '@roots/bud/cli/flags/verbose' import {isset} from '@roots/bud/cli/helpers/isset' import * as instance from '@roots/bud/instance' +import * as Dash from '@roots/bud-dashboard/components/error' import {Bud} from '@roots/bud-framework' -import chalk from '@roots/bud-support/chalk' import {Command, Option} from '@roots/bud-support/clipanion' import {bind} from '@roots/bud-support/decorators/bind' import {BudError} from '@roots/bud-support/errors' import figures from '@roots/bud-support/figures' import * as Ink from '@roots/bud-support/ink' import isNumber from '@roots/bud-support/lodash/isNumber' +import noop from '@roots/bud-support/lodash/noop' import logger from '@roots/bud-support/logger' import args from '@roots/bud-support/utilities/args' -import * as Fallback from '../components/Error.js' import {Menu} from '../components/Menu.js' export type {BaseContext, Context} @@ -97,6 +98,8 @@ export default class BudCommand extends Command { public force = force + public ignoreErrors = ignoreErrors + public log = log public mode = mode @@ -132,12 +135,8 @@ export default class BudCommand extends Command { /** * Render static cli output */ - public renderStatic(...children: Array) { - return this.render( - - {(child, id) => {child}} - , - ) + public renderStatic(el: React.ReactElement) { + return this.render({el}) } /** @@ -151,7 +150,8 @@ export default class BudCommand extends Command { bail: () => any = () => setTimeout(exit, 100), ): Promise> { const {execa} = await import(`@roots/bud-support/execa`) - const process = execa(bin, args.filter(Boolean), { + + return execa(bin, args.filter(Boolean), { cwd: this.bud.path(), encoding: `utf8`, env: {NODE_ENV: `development`}, @@ -166,8 +166,6 @@ export default class BudCommand extends Command { .on(`exit`, bail) .on(`disconnect`, bail) .on(`close`, bail) - - return await process } /** @@ -299,14 +297,10 @@ export default class BudCommand extends Command { } } - this.renderStatic( - - - , - ) + this.renderStatic() - // fallthrough - if (!this.bud || this.bud?.isProduction) exit(1) + if (!this.bud || this.bud?.isProduction || this.ignoreErrors === true) + exit(1) } /** @@ -392,9 +386,14 @@ export default class BudCommand extends Command { if (!pathParts) { process.exitCode = 2 throw new Error( - `Could not find ${signifier} binary\n\nChecked:\n - ${binary}\n - ${checkedPaths - .map(path => join(binaryPath, path)) - .join(`\n - `)}`, + [ + `Could not find ${signifier} binary\n`, + `Checked:`, + `- ${binary}`, + ...checkedPaths + .map(path => join(binaryPath, path)) + .map(path => `- ${path}`), + ].join(`\n`), ) } @@ -404,26 +403,22 @@ export default class BudCommand extends Command { const binaryArguments = userArgs?.length ? userArgs : defaultArgs this.context.stdout.write( - chalk.dim( - `${figures.pointerSmall} ${signifier} ${binaryArguments.join( - ` `, - )}\n` - .replace(this.bud.path(`@src`), `@src`) - .replace(this.bud.path(), ``), - ), + `${figures.pointerSmall} ${signifier} ${binaryArguments.join(` `)}\n` + .replace(this.bud.path(`@src`), `@src`) + .replace(this.bud.path(), ``), ) - const result = await this.$(binary, binaryArguments).catch(() => {}) - const code = result && isNumber(result?.exitCode) ? result.exitCode : 1 + const result = await this.$(binary, binaryArguments).catch(noop) - if (code) { - this.context.stderr.write( - chalk.red(`${figures.cross} exiting with code ${code}\n`), - ) - return code + const exitCode = + result && isNumber(result?.exitCode) ? result.exitCode : 1 + + if (exitCode) { + this.context.stderr.write(`${figures.cross} exit code ${exitCode}\n`) + return exitCode } - this.context.stdout.write(chalk.green(`${figures.tick} success\n`)) - return code + this.context.stdout.write(`${figures.tick} success\n`) + return exitCode } } diff --git a/sources/@roots/bud/src/cli/flags/ignoreErrors.ts b/sources/@roots/bud/src/cli/flags/ignoreErrors.ts new file mode 100644 index 0000000000..bb3595de26 --- /dev/null +++ b/sources/@roots/bud/src/cli/flags/ignoreErrors.ts @@ -0,0 +1,9 @@ +import {Option} from '@roots/bud-support/clipanion' + +export default Option.Boolean( + `--ignore-errors,--ignoreErrors`, + undefined, + { + description: `Ignore errors`, + }, +) diff --git a/sources/@roots/entrypoints-webpack-plugin/src/html.emitter.ts b/sources/@roots/entrypoints-webpack-plugin/src/html.emitter.ts index 3ace1d975a..7f4a3e7259 100644 --- a/sources/@roots/entrypoints-webpack-plugin/src/html.emitter.ts +++ b/sources/@roots/entrypoints-webpack-plugin/src/html.emitter.ts @@ -1,5 +1,7 @@ import type {Entrypoints} from '@roots/entrypoints-webpack-plugin' +import {Buffer} from 'node:buffer' + import {bind} from 'helpful-decorators' import Webpack from 'webpack' diff --git a/sources/@roots/filesystem/src/s3/index.ts b/sources/@roots/filesystem/src/s3/index.ts index e80d4e0849..33e09bcffd 100644 --- a/sources/@roots/filesystem/src/s3/index.ts +++ b/sources/@roots/filesystem/src/s3/index.ts @@ -4,6 +4,7 @@ import type { S3Client, } from '@aws-sdk/client-s3' +import {Buffer} from 'node:buffer' import type {Readable} from 'node:stream' import SDK from '@roots/filesystem/vendor/sdk' diff --git a/sources/@roots/wordpress-theme-json-webpack-plugin/scripts/build-types.js b/sources/@roots/wordpress-theme-json-webpack-plugin/scripts/build-types.js index 1a86c49524..978fff7243 100644 --- a/sources/@roots/wordpress-theme-json-webpack-plugin/scripts/build-types.js +++ b/sources/@roots/wordpress-theme-json-webpack-plugin/scripts/build-types.js @@ -54,6 +54,7 @@ try { */ // @ts-nocheck +// eslint-disable `, }) } catch (err) { diff --git a/sources/@roots/wordpress-theme-json-webpack-plugin/src/theme.ts b/sources/@roots/wordpress-theme-json-webpack-plugin/src/theme.ts index 101ebb9d71..f9383b40e8 100644 --- a/sources/@roots/wordpress-theme-json-webpack-plugin/src/theme.ts +++ b/sources/@roots/wordpress-theme-json-webpack-plugin/src/theme.ts @@ -629,7 +629,7 @@ export interface SettingsBlocksPropertiesComplete { /** * Archive block. Display a monthly archive of your posts. This block has no block-level settings */ - 'core/archives'?: {} + 'core/archives'?: NonNullable 'core/audio'?: SettingsPropertiesComplete 'core/avatar'?: SettingsPropertiesComplete 'core/block'?: SettingsPropertiesComplete