Merge branch 'main' into docs/kit

This commit is contained in:
Andrey Yolkin 2023-07-30 19:36:40 +03:00 committed by GitHub
commit 066390972c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 660 additions and 534 deletions

View File

@ -86,7 +86,7 @@ jobs:
run: pnpm install
- name: Initialize CodeQL
uses: github/codeql-action/init@1813ca74c3faaa3a2da2070b9b8a0b3e7373a0d8 # v2.21.0
uses: github/codeql-action/init@0ba4244466797eb048eb91a6cd43d5c03ca8bd05 # v2.21.2
with:
languages: javascript
queries: +security-and-quality
@ -98,7 +98,7 @@ jobs:
path: packages
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@1813ca74c3faaa3a2da2070b9b8a0b3e7373a0d8 # v2.21.0
uses: github/codeql-action/analyze@0ba4244466797eb048eb91a6cd43d5c03ca8bd05 # v2.21.2
with:
category: "/language:javascript"

View File

@ -66,6 +66,6 @@ jobs:
# Upload the results to GitHub's code scanning dashboard.
- name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@1813ca74c3faaa3a2da2070b9b8a0b3e7373a0d8 # v2.21.0
uses: github/codeql-action/upload-sarif@0ba4244466797eb048eb91a6cd43d5c03ca8bd05 # v2.21.2
with:
sarif_file: results.sarif

View File

@ -7,6 +7,8 @@ head.title: ".env"
# .env File
## At Build, Dev, and Generate Time
Nuxt CLI has built-in [dotenv](https://github.com/motdotla/dotenv) support in development mode and when running `nuxi build` and `nuxi generate`.
In addition to any process environment variables, if you have a `.env` file in your project root directory, it will be automatically loaded **at build, dev, and generate time**, and any environment variables set there will be accessible within your `nuxt.config` file and modules.
@ -27,7 +29,17 @@ When updating `.env` in development mode, the Nuxt instance is automatically res
Note that removing a variable from `.env` or removing the `.env` file entirely will not unset values that have already been set.
::
However, **after your server is built**, you are responsible for setting environment variables when you run the server. Your `.env` file will not be read at this point. How you do this is different for every environment. On a Linux server, you could pass the environment variables as arguments using the terminal `DATABASE_HOST=mydatabaseconnectionstring node .output/server/index.mjs`. Or you could source your env file using `source .env && node .output/server/index.mjs`.
## Production Preview
**After your server is built**, you are responsible for setting environment variables when you run the server. Your `.env` file will not be read at this point. How you do this is different for every environment.
For local production preview purpose, we recommend using [`nuxi preview`](https://nuxt.com/docs/api/commands/preview) since using this command, the `.env` file will be loaded into `process.env` for convenience. Note that this command requires dependencies to be installed in the package directory.
Or you could pass the environment variables as arguments using the terminal. For example, on Linux or macOS:
```bash
DATABASE_HOST=mydatabaseconnectionstring node .output/server/index.mjs
```
Note that for a purely static site, it is not possible to set runtime configuration config after your project is prerendered.

View File

@ -32,6 +32,7 @@ interface PageMeta {
keepalive?: boolean | KeepAliveProps
layout?: false | LayoutKey | Ref<LayoutKey> | ComputedRef<LayoutKey>
middleware?: MiddlewareKey | NavigationGuard | Array<MiddlewareKey | NavigationGuard>
scrollToTop?: boolean | ((to: RouteLocationNormalizedLoaded, from: RouteLocationNormalizedLoaded) => boolean)
[key: string]: unknown
}
```
@ -98,6 +99,12 @@ interface PageMeta {
Validate whether a given route can validly be rendered with this page. Return true if it is valid, or false if not. If another match can't be found, this will mean a 404. You can also directly return an object with `statusCode`/`statusMessage` to respond immediately with an error (other matches will not be checked).
**`scrollTopTop`**
- **Type**: `boolean | (to: RouteLocationNormalized, from: RouteLocationNormalized) => boolean`
Tell Nuxt to scroll to the top before rendering the page or not. If you want to overwrite the default scroll behavior of Nuxt, you can do so in `~/app/router.options.ts` (see [docs](/docs/guide/directory-structure/pages/#router-options)) for more info.
**`[key: string]`**
- **Type**: `any`

View File

@ -267,3 +267,42 @@ Options to pass in [`c12`](https://github.com/unjs/c12#options) `loadConfig` cal
- `extendViteConfig(callback, options?)`
- `addWebpackPlugin(webpackPlugin, options?)`
- `addVitePlugin(vitePlugin, options?)`
## Examples
### Accessing Nuxt Vite Config
If you are building an integration that needs access to the runtime Vite or webpack config that Nuxt uses, it is possible to extract this using Kit utilities.
Some examples of projects doing this already:
- [histoire](https://github.com/histoire-dev/histoire/blob/main/packages/histoire-plugin-nuxt/src/index.ts)
- [nuxt-vitest](https://github.com/danielroe/nuxt-vitest/blob/main/packages/nuxt-vitest/src/config.ts)
- [@storybook-vue/nuxt](https://github.com/storybook-vue/nuxt/blob/main/src/preset.ts)
Here is a brief example of how you might access the Vite config from a project; you could implement a similar approach to get the webpack configuration.
```js
import { loadNuxt, buildNuxt } from '@nuxt/kit'
// https://github.com/nuxt/nuxt/issues/14534
async function getViteConfig() {
const nuxt = await loadNuxt({ cwd: process.cwd(), dev: false, overrides: { ssr: false } })
return new Promise((resolve, reject) => {
nuxt.hook('vite:extendConfig', (config, { isClient }) => {
if (isClient) {
resolve(config)
throw new Error('_stop_')
}
})
buildNuxt(nuxt).catch((err) => {
if (!err.toString().includes('_stop_')) {
reject(err)
}
})
}).finally(() => nuxt.close())
}
const viteConfig = await getViteConfig()
console.log(viteConfig)
```

View File

@ -37,7 +37,7 @@
"nuxt": "workspace:*",
"vite": "4.4.7",
"vue": "3.3.4",
"magic-string": "^0.30.1"
"magic-string": "^0.30.2"
},
"devDependencies": {
"@actions/core": "1.10.0",
@ -45,7 +45,7 @@
"@nuxt/webpack-builder": "workspace:*",
"@nuxtjs/eslint-config-typescript": "12.0.0",
"@types/fs-extra": "11.0.1",
"@types/node": "18.17.0",
"@types/node": "18.17.1",
"@types/semver": "7.5.0",
"case-police": "0.6.1",
"chalk": "5.3.0",
@ -53,11 +53,11 @@
"cheerio": "1.0.0-rc.12",
"consola": "3.2.3",
"devalue": "4.3.2",
"eslint": "8.45.0",
"eslint-plugin-import": "2.27.5",
"eslint": "8.46.0",
"eslint-plugin-import": "2.28.0",
"eslint-plugin-jsdoc": "41.1.2",
"eslint-plugin-no-only-tests": "3.1.0",
"execa": "7.1.1",
"execa": "7.2.0",
"fs-extra": "11.1.1",
"globby": "13.2.2",
"h3": "1.7.1",
@ -70,19 +70,19 @@
"nuxt-vitest": "0.10.2",
"ofetch": "1.1.1",
"pathe": "1.1.1",
"playwright-core": "1.36.1",
"playwright-core": "1.36.2",
"rimraf": "5.0.1",
"semver": "7.5.4",
"std-env": "3.3.3",
"typescript": "5.0.4",
"ufo": "1.1.2",
"typescript": "5.1.6",
"ufo": "1.2.0",
"vite": "4.4.7",
"vitest": "0.33.0",
"vitest-environment-nuxt": "0.10.2",
"vue": "3.3.4",
"vue-eslint-parser": "9.3.1",
"vue-router": "4.2.4",
"vue-tsc": "1.8.6"
"vue-tsc": "1.8.8"
},
"packageManager": "pnpm@8.6.10",
"engines": {

View File

@ -36,7 +36,7 @@
"semver": "^7.5.4",
"unctx": "^2.3.1",
"unimport": "^3.1.0",
"untyped": "^1.3.2"
"untyped": "^1.4.0"
},
"devDependencies": {
"@types/hash-sum": "1.0.0",

View File

@ -6,6 +6,7 @@ import { genDynamicImport, genImport, genSafeVariableName } from 'knitwork'
import type { NuxtTemplate } from '@nuxt/schema'
/** @deprecated */
// TODO: Remove support for compiling ejs templates in v4
export async function compileTemplate (template: NuxtTemplate, ctx: any) {
const data = { ...ctx, options: template.options }
if (template.src) {

View File

@ -1,4 +1,5 @@
import { resolve } from 'pathe'
import type { JSValue } from 'untyped'
import { applyDefaults } from 'untyped'
import type { LoadConfigOptions } from 'c12'
import { loadConfig } from 'c12'
@ -50,5 +51,5 @@ export async function loadNuxtConfig (opts: LoadNuxtConfigOptions): Promise<Nuxt
}
// Resolve and apply defaults
return await applyDefaults(NuxtConfigSchema, nuxtConfig) as NuxtOptions
return await applyDefaults(NuxtConfigSchema, nuxtConfig as NuxtConfig & Record<string, JSValue>) as unknown as NuxtOptions
}

View File

@ -33,7 +33,7 @@
"deep-object-diff": "1.1.9",
"defu": "6.1.2",
"destr": "2.0.0",
"execa": "7.1.1",
"execa": "7.2.0",
"flat": "5.0.2",
"giget": "1.1.2",
"h3": "1.7.1",
@ -47,7 +47,7 @@
"pkg-types": "1.0.3",
"scule": "1.0.0",
"semver": "7.5.4",
"ufo": "1.1.2",
"ufo": "1.2.0",
"unbuild": "latest"
},
"optionalDependencies": {

View File

@ -73,10 +73,14 @@ export const writeTypes = async (nuxt: Nuxt) => {
tsConfig.include.push(`${absolutePath}/*`)
}
} else {
tsConfig.compilerOptions.paths[alias] = [absolutePath.replace(/(?<=\w)\.\w+$/g, '')] /* remove extension */
const path = stats?.isFile()
? absolutePath.replace(/(?<=\w)\.\w+$/g, '') /* remove extension */
: absolutePath
tsConfig.compilerOptions.paths[alias] = [path]
if (!absolutePath.startsWith(rootDirWithSlash)) {
tsConfig.include.push(absolutePath.replace(/(?<=\w)\.\w+$/g, ''))
tsConfig.include.push(path)
}
}
}

View File

@ -56,10 +56,10 @@
"@nuxt/kit": "workspace:../kit",
"@nuxt/schema": "workspace:../schema",
"@nuxt/telemetry": "^2.3.2",
"@nuxt/ui-templates": "^1.2.1",
"@nuxt/ui-templates": "^1.3.1",
"@nuxt/vite-builder": "workspace:../vite",
"@unhead/ssr": "^1.1.32",
"@unhead/vue": "^1.1.32",
"@unhead/ssr": "^1.1.33",
"@unhead/vue": "^1.1.33",
"@vue/shared": "^3.3.4",
"acorn": "8.10.0",
"c12": "^1.4.2",
@ -68,7 +68,7 @@
"defu": "^6.1.2",
"destr": "^2.0.0",
"devalue": "^4.3.2",
"esbuild": "^0.18.16",
"esbuild": "^0.18.17",
"escape-string-regexp": "^5.0.0",
"estree-walker": "^3.0.3",
"fs-extra": "^11.1.1",
@ -79,7 +79,7 @@
"klona": "^2.0.6",
"knitwork": "^1.0.0",
"local-pkg": "^0.4.3",
"magic-string": "^0.30.1",
"magic-string": "^0.30.2",
"mlly": "^1.4.0",
"nitropack": "^2.5.2",
"nuxi": "workspace:../nuxi",
@ -91,7 +91,7 @@
"prompts": "^2.4.2",
"scule": "^1.0.0",
"strip-literal": "^1.0.1",
"ufo": "^1.1.2",
"ufo": "^1.2.0",
"ultrahtml": "^1.3.0",
"uncrypto": "^0.1.3",
"unctx": "^2.3.1",
@ -99,7 +99,7 @@
"unimport": "^3.1.0",
"unplugin": "^1.4.0",
"unplugin-vue-router": "^0.6.4",
"untyped": "^1.3.2",
"untyped": "^1.4.0",
"vue": "^3.3.4",
"vue-bundle-renderer": "^1.0.3",
"vue-devtools-stub": "^0.1.0",

View File

@ -2,7 +2,7 @@ import { getCurrentInstance, hasInjectionContext, inject, onUnmounted } from 'vu
import type { Ref } from 'vue'
import type { NavigationFailure, NavigationGuard, RouteLocationNormalized, RouteLocationPathRaw, RouteLocationRaw, Router, useRoute as _useRoute, useRouter as _useRouter } from '#vue-router'
import { sanitizeStatusCode } from 'h3'
import { hasProtocol, joinURL, parseURL, withQuery } from 'ufo'
import { hasProtocol, isScriptProtocol, joinURL, parseURL, withQuery } from 'ufo'
import { useNuxtApp, useRuntimeConfig } from '../nuxt'
import type { NuxtError } from './error'
@ -133,11 +133,14 @@ export const navigateTo = (to: RouteLocationRaw | undefined | null, options?: Na
}
const isExternal = options?.external || hasProtocol(toPath, { acceptRelative: true })
if (isExternal && !options?.external) {
throw new Error('Navigating to external URL is not allowed by default. Use `navigateTo (url, { external: true })`.')
if (isExternal) {
if (!options?.external) {
throw new Error('Navigating to an external URL is not allowed by default. Use `navigateTo(url, { external: true })`.')
}
const protocol = parseURL(toPath).protocol
if (protocol && isScriptProtocol(protocol)) {
throw new Error(`Cannot navigate to a URL with '${protocol}' protocol.`)
}
if (isExternal && parseURL(toPath).protocol === 'script:') {
throw new Error('Cannot navigate to an URL with script protocol.')
}
const inMiddleware = isProcessingMiddleware()

View File

@ -1,4 +1,4 @@
import { promises as fsp } from 'node:fs'
import { promises as fsp, mkdirSync, writeFileSync } from 'node:fs'
import { dirname, join, resolve } from 'pathe'
import { defu } from 'defu'
import { compileTemplate, findPath, normalizePlugin, normalizeTemplate, resolveAlias, resolveFiles, resolvePath, templateUtils, tryResolveModule } from '@nuxt/kit'
@ -32,13 +32,21 @@ export async function generateApp (nuxt: Nuxt, app: NuxtApp, options: { filter?:
app.templates = app.templates.map(tmpl => normalizeTemplate(tmpl))
// Compile templates into vfs
// TODO: remove utils in v4
const templateContext = { utils: templateUtils, nuxt, app }
await Promise.all((app.templates as Array<ReturnType<typeof normalizeTemplate>>)
const filteredTemplates = (app.templates as Array<ReturnType<typeof normalizeTemplate>>)
.filter(template => !options.filter || options.filter(template))
.map(async (template) => {
const contents = await compileTemplate(template, templateContext)
const writes: Array<() => void> = []
await Promise.allSettled(filteredTemplates
.map(async (template) => {
const fullPath = template.dst || resolve(nuxt.options.buildDir, template.filename!)
const mark = performance.mark(fullPath)
const contents = await compileTemplate(template, templateContext).catch((e) => {
console.error(`[nuxt] Could not compile template \`${template.filename}\`.`)
throw e
})
nuxt.vfs[fullPath] = contents
const aliasPath = '#build/' + template.filename!.replace(/\.\w+$/, '')
@ -49,13 +57,26 @@ export async function generateApp (nuxt: Nuxt, app: NuxtApp, options: { filter?:
nuxt.vfs[fullPath.replace(/\//g, '\\')] = contents
}
const perf = performance.measure(fullPath, mark?.name) // TODO: remove when Node 14 reaches EOL
const setupTime = perf ? Math.round((perf.duration * 100)) / 100 : 0 // TODO: remove when Node 14 reaches EOL
if (nuxt.options.debug || setupTime > 500) {
console.info(`[nuxt] compiled \`${template.filename}\` in ${setupTime}ms`)
}
if (template.write) {
await fsp.mkdir(dirname(fullPath), { recursive: true })
await fsp.writeFile(fullPath, contents, 'utf8')
writes.push(() => {
mkdirSync(dirname(fullPath), { recursive: true })
writeFileSync(fullPath, contents, 'utf8')
})
}
}))
await nuxt.callHook('app:templatesGenerated', app)
// Write template files in single synchronous step to avoid (possible) additional
// runtime overhead of cascading HMRs from vite/webpack
for (const write of writes) { write() }
await nuxt.callHook('app:templatesGenerated', app, filteredTemplates, options)
}
async function resolveApp (nuxt: Nuxt, app: NuxtApp) {

View File

@ -1,6 +1,7 @@
import { existsSync } from 'node:fs'
import { genArrayFromRaw, genDynamicImport, genExport, genImport, genObjectFromRawEntries, genSafeVariableName, genString } from 'knitwork'
import { isAbsolute, join, relative, resolve } from 'pathe'
import type { JSValue } from 'untyped'
import { generateTypes, resolveSchema } from 'untyped'
import escapeRE from 'escape-string-regexp'
import { hash } from 'ohash'
@ -144,7 +145,7 @@ export const schemaTemplate: NuxtTemplate<TemplateContext> = {
),
modules.length > 0 ? ` modules?: (undefined | null | false | NuxtModule | string | [NuxtModule | string, Record<string, any>] | ${modules.map(([configKey, importName]) => `[${genString(importName)}, Exclude<NuxtConfig[${configKey}], boolean>]`).join(' | ')})[],` : '',
' }',
generateTypes(await resolveSchema(Object.fromEntries(Object.entries(nuxt.options.runtimeConfig).filter(([key]) => key !== 'public'))),
generateTypes(await resolveSchema(Object.fromEntries(Object.entries(nuxt.options.runtimeConfig).filter(([key]) => key !== 'public')) as Record<string, JSValue>),
{
interfaceName: 'RuntimeConfig',
addExport: false,
@ -152,7 +153,7 @@ export const schemaTemplate: NuxtTemplate<TemplateContext> = {
allowExtraKeys: false,
indentation: 2
}),
generateTypes(await resolveSchema(nuxt.options.runtimeConfig.public),
generateTypes(await resolveSchema(nuxt.options.runtimeConfig.public as Record<string, JSValue>),
{
interfaceName: 'PublicRuntimeConfig',
addExport: false,

View File

@ -36,7 +36,7 @@ export interface PageMeta {
/** You may define a path matcher, if you have a more complex pattern than can be expressed with the file name. */
path?: string
/** Set to `false` to avoid scrolling to top on page navigations */
scrollToTop?: boolean
scrollToTop?: boolean | ((to: RouteLocationNormalizedLoaded, from: RouteLocationNormalizedLoaded) => boolean)
}
declare module 'vue-router' {

View File

@ -20,8 +20,10 @@ export default <RouterConfig> {
// savedPosition is only available for popstate navigations (back button)
let position: ScrollPosition = savedPosition || undefined
const routeAllowsScrollToTop = typeof to.meta.scrollToTop === 'function' ? to.meta.scrollToTop(to, from) : to.meta.scrollToTop
// Scroll to top if route is changed by default
if (!position && from && to && to.meta.scrollToTop !== false && _isDifferentRoute(from, to)) {
if (!position && from && to && routeAllowsScrollToTop !== false && _isDifferentRoute(from, to)) {
position = { left: 0, top: 0 }
}

View File

@ -30,7 +30,7 @@
"@types/file-loader": "5.0.1",
"@types/pug": "2.0.6",
"@types/sass-loader": "8.0.5",
"@unhead/schema": "1.1.32",
"@unhead/schema": "1.1.33",
"@vitejs/plugin-vue": "4.2.3",
"@vitejs/plugin-vue-jsx": "3.0.1",
"@vue/compiler-core": "3.3.4",
@ -49,16 +49,16 @@
"webpack-dev-middleware": "6.1.1"
},
"dependencies": {
"@nuxt/ui-templates": "^1.2.1",
"@nuxt/ui-templates": "^1.3.1",
"defu": "^6.1.2",
"hookable": "^5.5.3",
"pathe": "^1.1.1",
"pkg-types": "^1.0.3",
"postcss-import-resolver": "^2.0.0",
"std-env": "^3.3.3",
"ufo": "^1.1.2",
"ufo": "^1.2.0",
"unimport": "^3.1.0",
"untyped": "^1.3.2"
"untyped": "^1.4.0"
},
"engines": {
"node": "^14.18.0 || >=16.10.0"

View File

@ -123,7 +123,7 @@ export interface NuxtHooks {
* @param app The configured `NuxtApp` object
* @returns Promise
*/
'app:templatesGenerated': (app: NuxtApp) => HookResult
'app:templatesGenerated': (app: NuxtApp, templates: ResolvedNuxtTemplate[], options?: GenerateAppOptions) => HookResult
/**
* Called before Nuxt bundle builder.

View File

@ -26,15 +26,15 @@
"@nuxt/schema": "workspace:../schema",
"consola": "^3.2.3",
"defu": "^6.1.2",
"execa": "^7.1.1",
"execa": "^7.2.0",
"get-port-please": "^3.0.1",
"ofetch": "^1.1.1",
"pathe": "^1.1.1",
"ufo": "^1.1.2"
"ufo": "^1.2.0"
},
"devDependencies": {
"@jest/globals": "29.6.1",
"playwright-core": "1.36.1",
"@jest/globals": "29.6.2",
"playwright-core": "1.36.2",
"unbuild": "latest",
"vitest": "0.33.0"
},

View File

@ -35,7 +35,7 @@
"consola": "^3.2.3",
"cssnano": "^6.0.1",
"defu": "^6.1.2",
"esbuild": "^0.18.16",
"esbuild": "^0.18.17",
"escape-string-regexp": "^5.0.0",
"estree-walker": "^3.0.3",
"externality": "^1.0.2",
@ -43,7 +43,7 @@
"get-port-please": "^3.0.1",
"h3": "^1.7.1",
"knitwork": "^1.0.0",
"magic-string": "^0.30.1",
"magic-string": "^0.30.2",
"mlly": "^1.4.0",
"ohash": "^1.1.2",
"pathe": "^1.1.1",
@ -55,7 +55,7 @@
"rollup-plugin-visualizer": "^5.9.2",
"std-env": "^3.3.3",
"strip-literal": "^1.0.1",
"ufo": "^1.1.2",
"ufo": "^1.2.0",
"unplugin": "^1.4.0",
"vite": "^4.4.7",
"vite-node": "^0.33.0",

View File

@ -35,7 +35,7 @@
"h3": "^1.7.1",
"hash-sum": "^2.0.0",
"lodash-es": "^4.17.21",
"magic-string": "^0.30.1",
"magic-string": "^0.30.2",
"memfs": "^4.2.0",
"mini-css-extract-plugin": "^2.7.6",
"mlly": "^1.4.0",
@ -49,7 +49,7 @@
"pug-plain-loader": "^1.1.0",
"std-env": "^3.3.3",
"time-fix-plugin": "^2.0.7",
"ufo": "^1.1.2",
"ufo": "^1.2.0",
"unplugin": "^1.4.0",
"url-loader": "^4.1.1",
"vue-bundle-renderer": "^1.0.3",

File diff suppressed because it is too large Load Diff

View File

@ -25,7 +25,6 @@
"main"
],
"ignoreDeps": [
"typescript",
"markdownlint-cli",
"nuxt",
"nuxt3",

View File

@ -19,7 +19,7 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
for (const outputDir of ['.output', '.output-inline']) {
it('default client bundle size', async () => {
const clientStats = await analyzeSizes('**/*.js', join(rootDir, outputDir, 'public'))
expect.soft(roundToKilobytes(clientStats.totalBytes)).toMatchInlineSnapshot('"97.3k"')
expect.soft(roundToKilobytes(clientStats.totalBytes)).toMatchInlineSnapshot('"97.5k"')
expect(clientStats.files.map(f => f.replace(/\..*\.js/, '.js'))).toMatchInlineSnapshot(`
[
"_nuxt/entry.js",
@ -32,7 +32,7 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
const serverDir = join(rootDir, '.output/server')
const serverStats = await analyzeSizes(['**/*.mjs', '!node_modules'], serverDir)
expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot('"64.4k"')
expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot('"64.5k"')
const modules = await analyzeSizes('node_modules/**/*', serverDir)
expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot('"2330k"')