diff --git a/examples/css-module/package.json b/examples/css-module/package.json index d6363576be..1ed11efe99 100644 --- a/examples/css-module/package.json +++ b/examples/css-module/package.json @@ -5,6 +5,7 @@ "type": "module", "devDependencies": { "@roots/bud": "workspace:*", + "@roots/bud-postcss": "workspace:*", "@roots/bud-tailwindcss": "workspace:*" } } diff --git a/examples/vue-typescript/package.json b/examples/vue-typescript/package.json index 566490f74b..6b20edcf52 100644 --- a/examples/vue-typescript/package.json +++ b/examples/vue-typescript/package.json @@ -6,6 +6,10 @@ "browserslist": [ "extends @roots/browserslist-config" ], + "dependencies": { + "typescript": "5.5.3", + "vue": "3.4.33" + }, "devDependencies": { "@roots/bud": "workspace:*", "@roots/bud-sass": "workspace:*", diff --git a/examples/vue-typescript/src/index.ts b/examples/vue-typescript/src/index.ts index 5406e62606..18051022e4 100644 --- a/examples/vue-typescript/src/index.ts +++ b/examples/vue-typescript/src/index.ts @@ -2,4 +2,4 @@ import {createApp} from 'vue' import App from './App.vue' const app = createApp(App) -app.mount('#app') +app.mount('#root') diff --git a/sources/@roots/bud-build/src/config/module.ts b/sources/@roots/bud-build/src/config/module.ts index 444d3d5416..4c780c7a2d 100644 --- a/sources/@roots/bud-build/src/config/module.ts +++ b/sources/@roots/bud-build/src/config/module.ts @@ -13,9 +13,9 @@ export const module: Factory<`module`> = async ({ path, }) => filter(`build.module`, { - noParse: getNoParse(filter), + noParse: filter(`build.module.noParse`, undefined), rules: getRules({filter, path, rules}), - unsafeCache: getUnsafeCache(filter), + unsafeCache: filter(`build.module.unsafeCache`, undefined), }) /** @@ -33,7 +33,7 @@ const getRules = ({filter, path, rules}: Props): Array => { ? { oneOf: [ rules[`inline-image`]?.toWebpack?.(), - rules.image.toWebpack?.(), + rules.image?.toWebpack?.(), ].filter(Boolean), test: filter(`pattern.image`), } @@ -43,7 +43,7 @@ const getRules = ({filter, path, rules}: Props): Array => { ? { oneOf: [ rules[`inline-font`]?.toWebpack?.(), - rules.font.toWebpack(), + rules.font?.toWebpack?.(), ].filter(Boolean), test: filter(`pattern.font`), } @@ -53,65 +53,45 @@ const getRules = ({filter, path, rules}: Props): Array => { ? { oneOf: [ rules[`inline-svg`]?.toWebpack?.(), - rules.svg.toWebpack(), + rules.svg?.toWebpack?.(), ].filter(Boolean), test: filter(`pattern.svg`), } : undefined, ]), - { - oneOf: [ - ...filter(`build.module.rules.oneOf`, [ - ...getDefinedRules({rules}), - ...makeIssuerRuleSet({filter, path, rules}), - ]), - ].filter(Boolean), - }, + + ...filter(`build.module.rules.oneOf`, [ + ...makeDefinedRuleSet({rules}), + ...makeIssuerRuleSet({filter, path, rules}), + ]).filter(Boolean), + ...filter(`build.module.rules.after`, []), ].filter(Boolean) } /** - * Get the standard rules defined in the bud config, extensions, etc. + * Make defined rule set */ -const getDefinedRules = ({rules}: Partial) => { - return [ - ...Object.entries(rules) - .filter(([key, _]) => { - return !DEFINED.includes(key) && !RESOURCES.includes(key) - }) - .map(([_, value]) => value), - ...DEFINED.map(key => rules[key]), - ] +const makeDefinedRuleSet = ({rules}: {rules: Props['rules']}) => { + return Object.entries(rules) + .filter( + ([key]) => + ![ + `font`, + `image`, + `inline-font`, + `inline-image`, + `inline-svg`, + `svg`, + ].includes(key), + ) + .map(([, rule]) => rule) .filter(Boolean) - .map(rule => (`toWebpack` in rule ? rule.toWebpack() : rule)) + .map(rule => { + return `toWebpack` in rule ? rule.toWebpack() : rule + }) } -const RESOURCES = [ - `image`, - `font`, - `svg`, - `inline-font`, - `inline-image`, - `inline-svg`, -] - -const DEFINED = [ - `csv`, - `toml`, - `yml`, - `json`, - `html`, - `webp`, - `css-module`, - `css`, - `sass-module`, - `sass`, - `vue`, - `js`, - `ts`, -] - /** * Get rules for css and css-module imports issued by non-css files. */ @@ -152,13 +132,3 @@ const makeIssuerRuleSet = ({filter, path, rules}: Props) => { return results } - -const getNoParse = (filter: Props[`filter`]) => - filter(`build.module.noParse`, undefined) - -/** - * By leaving undefined, webpack will strongly cache parsed modules from node_modules - * but leave the rest. This is the default behavior. - */ -const getUnsafeCache = (filter: Props[`filter`]) => - filter(`build.module.unsafeCache`, undefined) diff --git a/sources/@roots/bud-build/src/items/index.ts b/sources/@roots/bud-build/src/items/index.ts index 8f2f5c6c6e..fa08b5e553 100644 --- a/sources/@roots/bud-build/src/items/index.ts +++ b/sources/@roots/bud-build/src/items/index.ts @@ -10,8 +10,8 @@ export const css: Factory = async ({makeItem}) => makeItem({ ident: `css`, loader: `css`, - options: {modules: false}, }).setOptions(({hooks: {filter}}) => ({ + modules: false, sourceMap: isBoolean(filter(`build.devtool`)) ? filter(`build.devtool`) : true, diff --git a/sources/@roots/bud-build/src/rules/css.module.ts b/sources/@roots/bud-build/src/rules/css.module.ts index 6c6066e243..9c952ffb48 100644 --- a/sources/@roots/bud-build/src/rules/css.module.ts +++ b/sources/@roots/bud-build/src/rules/css.module.ts @@ -3,7 +3,7 @@ import type {Factory} from '@roots/bud-build/registry' const cssModule: Factory = async ({filter, makeRule, path}) => makeRule() .setTest(filter(`pattern.cssModule`)) - .setInclude([() => path(`@src`)]) + .setInclude([({path}) => path(`@src`)]) .setUse([`precss`, `css-module`]) export {cssModule as default} diff --git a/sources/@roots/bud-framework/src/bootstrap/index.ts b/sources/@roots/bud-framework/src/bootstrap/index.ts index fb1ff84826..980b092ad4 100644 --- a/sources/@roots/bud-framework/src/bootstrap/index.ts +++ b/sources/@roots/bud-framework/src/bootstrap/index.ts @@ -89,7 +89,7 @@ export const bootstrap = async (bud: Bud) => { 'location.@modules': bud.context.paths.modules, 'location.@src': bud.context.paths.input, 'location.@storage': bud.context.paths.storage, - 'pattern.css': /(?!.*\.module)\.css$/, + 'pattern.css': /^(?!.*\.module\.css$).*\.css$/, 'pattern.cssModule': /\.module\.css$/, 'pattern.csv': /\.(csv|tsv)$/, 'pattern.font': /\.(ttf|otf|eot|woff2?|ico)$/, @@ -100,8 +100,8 @@ export const bootstrap = async (bud: Bud) => { 'pattern.json5': /\.json5$/, 'pattern.md': /\.md$/, 'pattern.modules': /(node_modules|bower_components|vendor)/, - 'pattern.sass': /(?!.*\.module)\.(scss|sass)$/, - 'pattern.sassModule': /\.module\.(scss|sass)$/, + 'pattern.sass': /^(?!.*\.module\.s[ac]ss$).*\.s[ac]ss$/, + 'pattern.sassModule': /\.module\.s[ac]ss$/, 'pattern.svg': /\.svg$/, 'pattern.toml': /\.toml$/, 'pattern.ts': /\.(m?tsx?)$/, diff --git a/sources/@roots/bud-postcss/src/extension/index.ts b/sources/@roots/bud-postcss/src/extension/index.ts index 87056410be..87c69f22b3 100644 --- a/sources/@roots/bud-postcss/src/extension/index.ts +++ b/sources/@roots/bud-postcss/src/extension/index.ts @@ -120,16 +120,16 @@ class BudPostCss extends BudPostCssOptionsApi { }), }) - build.rules.css.setUse((items = []) => [...items, `postcss`]) - build.rules[`css-module`]?.setUse((items = []) => [ - ...items, - `postcss`, - ]) + build.rules.css.setUse((items = []) => + items.includes(`postcss`) ? items : [...items, `postcss`], + ) + build.rules[`css-module`]?.setUse((items = []) => + items.includes(`postcss`) ? items : [...items, `postcss`], + ) const config = Object.values(context.files).find( file => file?.name?.includes(`postcss`) && file?.module, ) - if (config) { this.logger.log( `PostCSS configuration is being overridden by project configuration file.`, @@ -192,7 +192,7 @@ class BudPostCss extends BudPostCssOptionsApi { ...omit(this.options, [`plugins`, `order`, `postcssOptions`]), plugins: this.get(`order`).map(this.getPlugin).filter(Boolean), }) - .filter(([k, v]) => !isUndefined(v)) + .filter(([, v]) => !isUndefined(v)) .reduce((a, [k, v]) => ({...a, [k]: v}), {}) this.logger.info(`postcss options`, options) diff --git a/sources/@roots/bud-support/src/json5/index.ts b/sources/@roots/bud-support/src/json5/index.ts index 8b7cbffb60..4bbb7c338f 100644 --- a/sources/@roots/bud-support/src/json5/index.ts +++ b/sources/@roots/bud-support/src/json5/index.ts @@ -1,3 +1 @@ -import * as json5 from 'json5' - -export default json5 +export {default} from 'json5' diff --git a/sources/@roots/bud-vue/src/extension.ts b/sources/@roots/bud-vue/src/extension.ts index 400cdace57..2424a5720e 100644 --- a/sources/@roots/bud-vue/src/extension.ts +++ b/sources/@roots/bud-vue/src/extension.ts @@ -6,7 +6,6 @@ import {join} from 'node:path' import {Extension} from '@roots/bud-framework/extension' import { bind, - dependsOnOptional, expose, label, options, @@ -32,7 +31,6 @@ interface Options { version: `^3`, }) @expose(`vue`) -@dependsOnOptional([`@roots/bud-postcss`, `@roots/bud-sass`]) export default class BudVue extends Extension< Options, WebpackPluginInstance @@ -192,11 +190,15 @@ export default class BudVue extends Extension< const style = await this.resolve(`vue-style-loader`, import.meta.url) if (!style) return this.logger.error(`vue-style-loader not found`) - bud.hooks.on(`build.resolveLoader.alias`, (aliases = {}) => ({ - ...aliases, - [`vue-loader`]: loader, - [`vue-style-loader`]: style, - })) + bud.hooks + .on(`build.resolveLoader.alias`, (aliases = {}) => ({ + ...aliases, + [`vue-loader`]: loader, + [`vue-style-loader`]: style, + })) + .hooks.on(`build.resolve.extensions`, (extensions = new Set()) => + extensions.add(`.vue`), + ) bud.build .setLoader(`vue`, loader) @@ -204,29 +206,26 @@ export default class BudVue extends Extension< .setItem(`vue`, {ident: `vue`, loader: `vue`}) .setItem(`vue-style`, {ident: `vue-style`, loader: `vue-style`}) - await bud.hooks - .on(`build.resolve.extensions`, (extensions = new Set()) => - extensions.add(`.vue`), - ) - .hooks.on(`build.module.rules.before`, (rules = []) => [ - ...rules, - { - include: [bud.path(`@src`)], - test: bud.hooks.filter(`pattern.vue`), - use: [bud.build.items.vue.toWebpack()], - }, - ]) - .extensions.add({ - label: `vue-loader`, - make: async () => { - const {VueLoaderPlugin} = await this.import( - `vue-loader`, - import.meta.url, - {raw: true}, - ) - return new VueLoaderPlugin() - }, - }) + bud.hooks.on(`build.module.rules.before`, (rules = []) => [ + ...rules, + { + include: [bud.path(`@src`)], + test: bud.hooks.filter(`pattern.vue`), + use: [bud.build.items.vue.toWebpack()], + }, + ]) + + await bud.extensions.add({ + label: `vue-loader`, + make: async () => { + const {VueLoaderPlugin} = await this.import( + `vue-loader`, + import.meta.url, + {raw: true}, + ) + return new VueLoaderPlugin() + }, + }) } /** diff --git a/tests/e2e/__snapshots__/vue-typescript.test.ts.snap b/tests/e2e/__snapshots__/vue-typescript.test.ts.snap new file mode 100644 index 0000000000..9f033441b7 --- /dev/null +++ b/tests/e2e/__snapshots__/vue-typescript.test.ts.snap @@ -0,0 +1,99 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`html output of examples/vue-typescript > should have expected default state 1`] = ` +" + + + + +" +`; diff --git a/tests/e2e/__snapshots__/vue.test.ts.snap b/tests/e2e/__snapshots__/vue.test.ts.snap new file mode 100644 index 0000000000..f8b9c3a754 --- /dev/null +++ b/tests/e2e/__snapshots__/vue.test.ts.snap @@ -0,0 +1,99 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`html output of examples/vue-3 > should have expected default state 1`] = ` +" + + + + +" +`; diff --git a/tests/e2e/vue-typescript.test.ts b/tests/e2e/vue-typescript.test.ts new file mode 100644 index 0000000000..66d84906e1 --- /dev/null +++ b/tests/e2e/vue-typescript.test.ts @@ -0,0 +1,124 @@ +import {afterAll, beforeAll, describe, expect, it} from 'vitest' + +import {close, page, path, read, setup, update} from './runner/index.js' + +describe(`html output of examples/vue-typescript`, () => { + let original: string | undefined + + beforeAll(async () => { + await setup(`vue-typescript`) + original = await read(`src`, `components`, `TodoList.vue`) + }) + + afterAll(close) + + it(`should have expected default state`, async () => { + expect(original).toMatchSnapshot() + }) + + it(`should rebuild on change`, async () => { + await update( + path(`src`, `components`, `TodoList.vue`), + ` + + + + + +`, + ) + + expect(await page.$(`.hmr-target`)).toBeTruthy() + }) +}) diff --git a/tests/e2e/vue.test.ts b/tests/e2e/vue.test.ts new file mode 100644 index 0000000000..7db126b9ee --- /dev/null +++ b/tests/e2e/vue.test.ts @@ -0,0 +1,124 @@ +import {afterAll, beforeAll, describe, expect, it} from 'vitest' + +import {close, page, path, read, setup, update} from './runner/index.js' + +describe(`html output of examples/vue-3`, () => { + let original: string | undefined + + beforeAll(async () => { + await setup(`vue-3`) + original = await read(`src`, `components`, `TodoList.vue`) + }) + + afterAll(close) + + it(`should have expected default state`, async () => { + expect(original).toMatchSnapshot() + }) + + it(`should rebuild on change`, async () => { + await update( + path(`src`, `components`, `TodoList.vue`), + ` + + + + + +`, + ) + + expect(await page.$(`.hmr-target`)).toBeTruthy() + }) +}) diff --git a/yarn.lock b/yarn.lock index 75ea114534..7f1cf54ece 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8791,6 +8791,7 @@ __metadata: resolution: "@examples/css-module@workspace:examples/css-module" dependencies: "@roots/bud": "workspace:*" + "@roots/bud-postcss": "workspace:*" "@roots/bud-tailwindcss": "workspace:*" languageName: unknown linkType: soft @@ -9157,6 +9158,8 @@ __metadata: "@roots/bud-sass": "workspace:*" "@roots/bud-typescript": "workspace:*" "@roots/bud-vue": "workspace:*" + typescript: "npm:5.5.3" + vue: "npm:3.4.33" languageName: unknown linkType: soft @@ -15681,6 +15684,19 @@ __metadata: languageName: node linkType: hard +"@vue/compiler-core@npm:3.4.33": + version: 3.4.33 + resolution: "@vue/compiler-core@npm:3.4.33" + dependencies: + "@babel/parser": "npm:^7.24.7" + "@vue/shared": "npm:3.4.33" + entities: "npm:^4.5.0" + estree-walker: "npm:^2.0.2" + source-map-js: "npm:^1.2.0" + checksum: 10c0/5f3e97b9bf6c72d20844f4e4e67a96a8a29872ab6d096c27dcb74a94c70944f2899a265951e7f8dd7f18b4c38552560523f66cadf641451efadd277bcd5ed84a + languageName: node + linkType: hard + "@vue/compiler-dom@npm:3.4.30": version: 3.4.30 resolution: "@vue/compiler-dom@npm:3.4.30" @@ -15701,6 +15717,16 @@ __metadata: languageName: node linkType: hard +"@vue/compiler-dom@npm:3.4.33": + version: 3.4.33 + resolution: "@vue/compiler-dom@npm:3.4.33" + dependencies: + "@vue/compiler-core": "npm:3.4.33" + "@vue/shared": "npm:3.4.33" + checksum: 10c0/78b92901c153cc383b2f295a47c9dd383f04065db3c85738dcf5d58de17bfccb27ea47c506f3bd5fa95c50f12d2ab2961ce492db0a4381dbc9d6b88a80600ce3 + languageName: node + linkType: hard + "@vue/compiler-sfc@npm:2.7.16": version: 2.7.16 resolution: "@vue/compiler-sfc@npm:2.7.16" @@ -15750,6 +15776,23 @@ __metadata: languageName: node linkType: hard +"@vue/compiler-sfc@npm:3.4.33": + version: 3.4.33 + resolution: "@vue/compiler-sfc@npm:3.4.33" + dependencies: + "@babel/parser": "npm:^7.24.7" + "@vue/compiler-core": "npm:3.4.33" + "@vue/compiler-dom": "npm:3.4.33" + "@vue/compiler-ssr": "npm:3.4.33" + "@vue/shared": "npm:3.4.33" + estree-walker: "npm:^2.0.2" + magic-string: "npm:^0.30.10" + postcss: "npm:^8.4.39" + source-map-js: "npm:^1.2.0" + checksum: 10c0/a952ff5472e1f5dac101607a2799a59ef3e63ac8f8e7dc6668b0803ba98bab8e5b3e0f7b40130bd53ec18f340ec342d215dc0888d07376601d49c8d7392f4600 + languageName: node + linkType: hard + "@vue/compiler-ssr@npm:3.4.30": version: 3.4.30 resolution: "@vue/compiler-ssr@npm:3.4.30" @@ -15770,6 +15813,16 @@ __metadata: languageName: node linkType: hard +"@vue/compiler-ssr@npm:3.4.33": + version: 3.4.33 + resolution: "@vue/compiler-ssr@npm:3.4.33" + dependencies: + "@vue/compiler-dom": "npm:3.4.33" + "@vue/shared": "npm:3.4.33" + checksum: 10c0/cbab397abceece09e29ff50cf940c009b1472b29e62ea214d65405ec2655864d3e3bc37070df2b9c6cbf183df57710ebbed786ff2c129ef0b8e7378bad28836e + languageName: node + linkType: hard + "@vue/component-compiler-utils@npm:^3.1.0": version: 3.3.0 resolution: "@vue/component-compiler-utils@npm:3.3.0" @@ -15799,6 +15852,15 @@ __metadata: languageName: node linkType: hard +"@vue/reactivity@npm:3.4.33": + version: 3.4.33 + resolution: "@vue/reactivity@npm:3.4.33" + dependencies: + "@vue/shared": "npm:3.4.33" + checksum: 10c0/d866d8fd0fca63beb8a6492aa514b76d7007b93503b85c4ab5e8e0ffae05b185344a0eb918355568dad4f06e74a216825a441a8c2660cb803d776d74f5add67e + languageName: node + linkType: hard + "@vue/runtime-core@npm:3.4.30": version: 3.4.30 resolution: "@vue/runtime-core@npm:3.4.30" @@ -15809,6 +15871,16 @@ __metadata: languageName: node linkType: hard +"@vue/runtime-core@npm:3.4.33": + version: 3.4.33 + resolution: "@vue/runtime-core@npm:3.4.33" + dependencies: + "@vue/reactivity": "npm:3.4.33" + "@vue/shared": "npm:3.4.33" + checksum: 10c0/3c4adcb500a10ba5286491513327aa6abb375582814fd791e71c1d70396ebff6cd88927beb6d0ce893e5dec0eb8ae5c0b1c4123dfa820f407ebbd975e539ef16 + languageName: node + linkType: hard + "@vue/runtime-dom@npm:3.4.30": version: 3.4.30 resolution: "@vue/runtime-dom@npm:3.4.30" @@ -15821,6 +15893,18 @@ __metadata: languageName: node linkType: hard +"@vue/runtime-dom@npm:3.4.33": + version: 3.4.33 + resolution: "@vue/runtime-dom@npm:3.4.33" + dependencies: + "@vue/reactivity": "npm:3.4.33" + "@vue/runtime-core": "npm:3.4.33" + "@vue/shared": "npm:3.4.33" + csstype: "npm:^3.1.3" + checksum: 10c0/f491d2c535f063b4e5bf297f0c4709ba77240c19087a284f1d997cba614e2dc43b77e7e19703a111d5d185864a78eff93b6a2e3ceb8062d1e7663a47a67e7523 + languageName: node + linkType: hard + "@vue/server-renderer@npm:3.4.30": version: 3.4.30 resolution: "@vue/server-renderer@npm:3.4.30" @@ -15833,6 +15917,18 @@ __metadata: languageName: node linkType: hard +"@vue/server-renderer@npm:3.4.33": + version: 3.4.33 + resolution: "@vue/server-renderer@npm:3.4.33" + dependencies: + "@vue/compiler-ssr": "npm:3.4.33" + "@vue/shared": "npm:3.4.33" + peerDependencies: + vue: 3.4.33 + checksum: 10c0/24b3b57bdbb3105ae784748d1c4c0151b93cfa60cf930aba10b95deaae0aefccdb7895363ae6ec5c15470576bfcff2e63c00a65d9d0b1cc57d0b4d8ee990442b + languageName: node + linkType: hard + "@vue/shared@npm:3.4.30": version: 3.4.30 resolution: "@vue/shared@npm:3.4.30" @@ -15847,6 +15943,13 @@ __metadata: languageName: node linkType: hard +"@vue/shared@npm:3.4.33": + version: 3.4.33 + resolution: "@vue/shared@npm:3.4.33" + checksum: 10c0/010603f80bca4951535e9804aed228d40e1c9261d36b66ec06401609029a97cb7ac3b04b252812eeba86036206597c8d83b26d1f4a7071818bb166a999c17819 + languageName: node + linkType: hard + "@webassemblyjs/ast@npm:1.11.6, @webassemblyjs/ast@npm:^1.11.5": version: 1.11.6 resolution: "@webassemblyjs/ast@npm:1.11.6" @@ -36734,6 +36837,17 @@ __metadata: languageName: node linkType: hard +"postcss@npm:^8.4.39": + version: 8.4.39 + resolution: "postcss@npm:8.4.39" + dependencies: + nanoid: "npm:^3.3.7" + picocolors: "npm:^1.0.1" + source-map-js: "npm:^1.2.0" + checksum: 10c0/16f5ac3c4e32ee76d1582b3c0dcf1a1fdb91334a45ad755eeb881ccc50318fb8d64047de4f1601ac96e30061df203f0f2e2edbdc0bfc49b9c57bc9fb9bedaea3 + languageName: node + linkType: hard + "prebuild-install@npm:^7.1.1": version: 7.1.2 resolution: "prebuild-install@npm:7.1.2" @@ -43730,6 +43844,24 @@ __metadata: languageName: node linkType: hard +"vue@npm:3.4.33": + version: 3.4.33 + resolution: "vue@npm:3.4.33" + dependencies: + "@vue/compiler-dom": "npm:3.4.33" + "@vue/compiler-sfc": "npm:3.4.33" + "@vue/runtime-dom": "npm:3.4.33" + "@vue/server-renderer": "npm:3.4.33" + "@vue/shared": "npm:3.4.33" + peerDependencies: + typescript: "*" + peerDependenciesMeta: + typescript: + optional: true + checksum: 10c0/b03990a5a106dde7e67c50304a336293730eb59b52c6157b9c1718e6da32ec550530ec3fc0093ee39ec734e23061570067e3b5c3979ea0eec521dac1bd2dcc40 + languageName: node + linkType: hard + "w3c-xmlserializer@npm:^4.0.0": version: 4.0.0 resolution: "w3c-xmlserializer@npm:4.0.0"