perf(kit,nuxt): use fdir to speed up fs scanning

This commit is contained in:
Daniel Roe 2024-06-24 16:39:34 +02:00
parent d9e55546d9
commit d7718f4474
No known key found for this signature in database
GPG Key ID: CBC814C393D93268
7 changed files with 73 additions and 26 deletions

View File

@ -68,7 +68,7 @@
"eslint-plugin-perfectionist": "2.11.0", "eslint-plugin-perfectionist": "2.11.0",
"eslint-typegen": "0.2.4", "eslint-typegen": "0.2.4",
"execa": "9.2.0", "execa": "9.2.0",
"globby": "14.0.1", "fdir": "6.1.1",
"h3": "1.12.0", "h3": "1.12.0",
"happy-dom": "14.12.3", "happy-dom": "14.12.3",
"jiti": "1.21.6", "jiti": "1.21.6",
@ -79,6 +79,7 @@
"nuxt-content-twoslash": "0.0.10", "nuxt-content-twoslash": "0.0.10",
"ofetch": "1.3.4", "ofetch": "1.3.4",
"pathe": "1.1.2", "pathe": "1.1.2",
"picomatch": "^4.0.2",
"playwright-core": "1.44.1", "playwright-core": "1.44.1",
"rimraf": "5.0.7", "rimraf": "5.0.7",
"semver": "7.6.2", "semver": "7.6.2",

View File

@ -31,13 +31,14 @@
"consola": "^3.2.3", "consola": "^3.2.3",
"defu": "^6.1.4", "defu": "^6.1.4",
"destr": "^2.0.3", "destr": "^2.0.3",
"globby": "^14.0.1", "fdir": "^6.1.1",
"hash-sum": "^2.0.0", "hash-sum": "^2.0.0",
"ignore": "^5.3.1", "ignore": "^5.3.1",
"jiti": "^1.21.6", "jiti": "^1.21.6",
"klona": "^2.0.6", "klona": "^2.0.6",
"mlly": "^1.7.1", "mlly": "^1.7.1",
"pathe": "^1.1.2", "pathe": "^1.1.2",
"picomatch": "^4.0.2",
"pkg-types": "^1.1.1", "pkg-types": "^1.1.1",
"scule": "^1.3.0", "scule": "^1.3.0",
"semver": "^7.6.2", "semver": "^7.6.2",
@ -48,6 +49,7 @@
}, },
"devDependencies": { "devDependencies": {
"@types/hash-sum": "1.0.2", "@types/hash-sum": "1.0.2",
"@types/picomatch": "^2.3.3",
"@types/semver": "7.5.8", "@types/semver": "7.5.8",
"nitropack": "2.9.6", "nitropack": "2.9.6",
"unbuild": "latest", "unbuild": "latest",

View File

@ -1,7 +1,7 @@
import { existsSync, promises as fsp } from 'node:fs' import { existsSync, promises as fsp } from 'node:fs'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import { basename, dirname, isAbsolute, join, normalize, resolve } from 'pathe' import { basename, dirname, isAbsolute, join, normalize, resolve } from 'pathe'
import { globby } from 'globby' import { fdir } from 'fdir'
import { resolvePath as _resolvePath } from 'mlly' import { resolvePath as _resolvePath } from 'mlly'
import { resolveAlias as _resolveAlias } from 'pathe/utils' import { resolveAlias as _resolveAlias } from 'pathe/utils'
import { tryUseNuxt } from './context' import { tryUseNuxt } from './context'
@ -210,7 +210,7 @@ function existsInVFS (path: string, nuxt = tryUseNuxt()) {
export async function resolveFiles (path: string, pattern: string | string[], opts: { followSymbolicLinks?: boolean } = {}) { export async function resolveFiles (path: string, pattern: string | string[], opts: { followSymbolicLinks?: boolean } = {}) {
const files: string[] = [] const files: string[] = []
for (const file of await globby(pattern, { cwd: path, followSymbolicLinks: opts.followSymbolicLinks ?? true })) { for (const file of await new fdir().withRelativePaths().globWithOptions(toArray(pattern), { dot: true }).crawl(path).withPromise()) {
const p = resolve(path, file) const p = resolve(path, file)
if (!isIgnored(p)) { if (!isIgnored(p)) {
files.push(p) files.push(p)

View File

@ -79,7 +79,7 @@
"esbuild": "^0.21.5", "esbuild": "^0.21.5",
"escape-string-regexp": "^5.0.0", "escape-string-regexp": "^5.0.0",
"estree-walker": "^3.0.3", "estree-walker": "^3.0.3",
"globby": "^14.0.1", "fdir": "^6.1.1",
"h3": "^1.12.0", "h3": "^1.12.0",
"hookable": "^5.5.3", "hookable": "^5.5.3",
"ignore": "^5.3.1", "ignore": "^5.3.1",
@ -95,6 +95,7 @@
"ohash": "^1.1.3", "ohash": "^1.1.3",
"pathe": "^1.1.2", "pathe": "^1.1.2",
"perfect-debounce": "^1.0.0", "perfect-debounce": "^1.0.0",
"picomatch": "^4.0.2",
"pkg-types": "^1.1.1", "pkg-types": "^1.1.1",
"radix3": "^1.1.2", "radix3": "^1.1.2",
"scule": "^1.3.0", "scule": "^1.3.0",
@ -121,6 +122,7 @@
"@nuxt/ui-templates": "1.3.4", "@nuxt/ui-templates": "1.3.4",
"@parcel/watcher": "2.4.1", "@parcel/watcher": "2.4.1",
"@types/estree": "1.0.5", "@types/estree": "1.0.5",
"@types/picomatch": "^2.3.3",
"@vitejs/plugin-vue": "5.0.4", "@vitejs/plugin-vue": "5.0.4",
"@vue/compiler-sfc": "3.4.29", "@vue/compiler-sfc": "3.4.29",
"unbuild": "latest", "unbuild": "latest",

View File

@ -1,12 +1,13 @@
import { readdir } from 'node:fs/promises' import { readdir } from 'node:fs/promises'
import { basename, dirname, extname, join, relative } from 'pathe' import { basename, dirname, extname, join, relative } from 'pathe'
import { globby } from 'globby' import { fdir } from 'fdir'
import { kebabCase, pascalCase, splitByCase } from 'scule' import { kebabCase, pascalCase, splitByCase } from 'scule'
import { isIgnored, logger, useNuxt } from '@nuxt/kit' import { isIgnored, logger, useNuxt } from '@nuxt/kit'
import { withTrailingSlash } from 'ufo' import { withTrailingSlash } from 'ufo'
import type { Component, ComponentsDir } from 'nuxt/schema' import type { Component, ComponentsDir } from 'nuxt/schema'
import { resolveComponentNameSegments } from '../core/utils' import { resolveComponentNameSegments } from '../core/utils'
import { toArray } from '../utils'
/** /**
* Scan the components inside different components folders * Scan the components inside different components folders
@ -32,7 +33,8 @@ export async function scanComponents (dirs: ComponentsDir[], srcDir: string): Pr
// A map from resolved path to component name (used for making duplicate warning message) // A map from resolved path to component name (used for making duplicate warning message)
const resolvedNames = new Map<string, string>() const resolvedNames = new Map<string, string>()
const files = (await globby(dir.pattern!, { cwd: dir.path, ignore: dir.ignore })).sort() const patterns = toArray(dir.pattern!)
const files = (await new fdir().withRelativePaths().globWithOptions(patterns, { ignore: dir.ignore, dot: true }).crawl(dir.path).withPromise()).sort()
// Check if the directory exists (globby will otherwise read it case insensitively on MacOS) // Check if the directory exists (globby will otherwise read it case insensitively on MacOS)
if (files.length) { if (files.length) {

View File

@ -86,9 +86,9 @@ importers:
execa: execa:
specifier: 9.2.0 specifier: 9.2.0
version: 9.2.0 version: 9.2.0
globby: fdir:
specifier: 14.0.1 specifier: 6.1.1
version: 14.0.1 version: 6.1.1(picomatch@4.0.2)
h3: h3:
specifier: 1.12.0 specifier: 1.12.0
version: 1.12.0 version: 1.12.0
@ -119,6 +119,9 @@ importers:
pathe: pathe:
specifier: 1.1.2 specifier: 1.1.2
version: 1.1.2 version: 1.1.2
picomatch:
specifier: ^4.0.2
version: 4.0.2
playwright-core: playwright-core:
specifier: 1.44.1 specifier: 1.44.1
version: 1.44.1 version: 1.44.1
@ -170,9 +173,9 @@ importers:
destr: destr:
specifier: ^2.0.3 specifier: ^2.0.3
version: 2.0.3 version: 2.0.3
globby: fdir:
specifier: ^14.0.1 specifier: ^6.1.1
version: 14.0.1 version: 6.1.1(picomatch@4.0.2)
hash-sum: hash-sum:
specifier: ^2.0.0 specifier: ^2.0.0
version: 2.0.0 version: 2.0.0
@ -191,6 +194,9 @@ importers:
pathe: pathe:
specifier: ^1.1.2 specifier: ^1.1.2
version: 1.1.2 version: 1.1.2
picomatch:
specifier: ^4.0.2
version: 4.0.2
pkg-types: pkg-types:
specifier: ^1.1.1 specifier: ^1.1.1
version: 1.1.1 version: 1.1.1
@ -216,6 +222,9 @@ importers:
'@types/hash-sum': '@types/hash-sum':
specifier: 1.0.2 specifier: 1.0.2
version: 1.0.2 version: 1.0.2
'@types/picomatch':
specifier: ^2.3.3
version: 2.3.3
'@types/semver': '@types/semver':
specifier: 7.5.8 specifier: 7.5.8
version: 7.5.8 version: 7.5.8
@ -300,9 +309,9 @@ importers:
estree-walker: estree-walker:
specifier: ^3.0.3 specifier: ^3.0.3
version: 3.0.3 version: 3.0.3
globby: fdir:
specifier: ^14.0.1 specifier: ^6.1.1
version: 14.0.1 version: 6.1.1(picomatch@4.0.2)
h3: h3:
specifier: ^1.12.0 specifier: ^1.12.0
version: 1.12.0 version: 1.12.0
@ -348,6 +357,9 @@ importers:
perfect-debounce: perfect-debounce:
specifier: ^1.0.0 specifier: ^1.0.0
version: 1.0.0 version: 1.0.0
picomatch:
specifier: ^4.0.2
version: 4.0.2
pkg-types: pkg-types:
specifier: ^1.1.1 specifier: ^1.1.1
version: 1.1.1 version: 1.1.1
@ -421,6 +433,9 @@ importers:
'@types/estree': '@types/estree':
specifier: 1.0.5 specifier: 1.0.5
version: 1.0.5 version: 1.0.5
'@types/picomatch':
specifier: ^2.3.3
version: 2.3.3
'@vitejs/plugin-vue': '@vitejs/plugin-vue':
specifier: 5.0.4 specifier: 5.0.4
version: 5.0.4(vite@5.3.1(@types/node@20.14.7)(sass@1.69.4)(terser@5.27.0))(vue@3.4.29(typescript@5.5.2)) version: 5.0.4(vite@5.3.1(@types/node@20.14.7)(sass@1.69.4)(terser@5.27.0))(vue@3.4.29(typescript@5.5.2))
@ -572,9 +587,9 @@ importers:
execa: execa:
specifier: 9.2.0 specifier: 9.2.0
version: 9.2.0 version: 9.2.0
globby: fdir:
specifier: 14.0.1 specifier: 6.1.1
version: 14.0.1 version: 6.1.1(picomatch@4.0.2)
html-minifier: html-minifier:
specifier: 4.0.0 specifier: 4.0.0
version: 4.0.0 version: 4.0.0
@ -587,6 +602,9 @@ importers:
pathe: pathe:
specifier: 1.1.2 specifier: 1.1.2
version: 1.1.2 version: 1.1.2
picomatch:
specifier: 4.0.2
version: 4.0.2
prettier: prettier:
specifier: 3.3.2 specifier: 3.3.2
version: 3.3.2 version: 3.3.2
@ -2520,6 +2538,9 @@ packages:
'@types/normalize-package-data@2.4.4': '@types/normalize-package-data@2.4.4':
resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==}
'@types/picomatch@2.3.3':
resolution: {integrity: sha512-Yll76ZHikRFCyz/pffKGjrCwe/le2CDwOP5F210KQo27kpRE46U2rDnzikNlVn6/ezH3Mhn46bJMTfeVTtcYMg==}
'@types/pify@5.0.4': '@types/pify@5.0.4':
resolution: {integrity: sha512-gxKJ1Aw8LbyCsCQWIsip9bYKJCNsKHMoZoQMAe2IWH7U7hgp/l6TvJpbFvu8ZlGBimjZZNvEx2S1ZQlj02ayNQ==} resolution: {integrity: sha512-gxKJ1Aw8LbyCsCQWIsip9bYKJCNsKHMoZoQMAe2IWH7U7hgp/l6TvJpbFvu8ZlGBimjZZNvEx2S1ZQlj02ayNQ==}
@ -4264,6 +4285,14 @@ packages:
fastq@1.15.0: fastq@1.15.0:
resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==} resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==}
fdir@6.1.1:
resolution: {integrity: sha512-QfKBVg453Dyn3mr0Q0O+Tkr1r79lOTAKSi9f/Ot4+qVEwxWhav2Z+SudrG9vQjM2aYRMQQZ2/Q1zdA8ACM1pDg==}
peerDependencies:
picomatch: 3.x
peerDependenciesMeta:
picomatch:
optional: true
figures@3.2.0: figures@3.2.0:
resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==}
engines: {node: '>=8'} engines: {node: '>=8'}
@ -9186,6 +9215,8 @@ snapshots:
'@types/normalize-package-data@2.4.4': {} '@types/normalize-package-data@2.4.4': {}
'@types/picomatch@2.3.3': {}
'@types/pify@5.0.4': {} '@types/pify@5.0.4': {}
'@types/pug@2.0.10': {} '@types/pug@2.0.10': {}
@ -11518,6 +11549,10 @@ snapshots:
dependencies: dependencies:
reusify: 1.0.4 reusify: 1.0.4
fdir@6.1.1(picomatch@4.0.2):
optionalDependencies:
picomatch: 4.0.2
figures@3.2.0: figures@3.2.0:
dependencies: dependencies:
escape-string-regexp: 1.0.5 escape-string-regexp: 1.0.5

View File

@ -2,8 +2,8 @@ import { fileURLToPath } from 'node:url'
import fsp from 'node:fs/promises' import fsp from 'node:fs/promises'
import { beforeAll, describe, expect, it } from 'vitest' import { beforeAll, describe, expect, it } from 'vitest'
import { execaCommand } from 'execa' import { execaCommand } from 'execa'
import { globby } from 'globby' import { fdir } from 'fdir'
import { join } from 'pathe' import { join, resolve } from 'pathe'
describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM_CI)('minimal nuxt application', () => { describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM_CI)('minimal nuxt application', () => {
const rootDir = fileURLToPath(new URL('./fixtures/minimal', import.meta.url)) const rootDir = fileURLToPath(new URL('./fixtures/minimal', import.meta.url))
@ -31,7 +31,7 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
it('default server bundle size', async () => { it('default server bundle size', async () => {
const serverDir = join(rootDir, '.output/server') const serverDir = join(rootDir, '.output/server')
const serverStats = await analyzeSizes(['**/*.mjs', '!node_modules'], serverDir) const serverStats = await analyzeSizes('**/*.mjs', serverDir, { excludeNodeModules: true })
expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot(`"210k"`) expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot(`"210k"`)
const modules = await analyzeSizes('node_modules/**/*', serverDir) const modules = await analyzeSizes('node_modules/**/*', serverDir)
@ -71,7 +71,7 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
it('default server bundle size (inlined vue modules)', async () => { it('default server bundle size (inlined vue modules)', async () => {
const serverDir = join(rootDir, '.output-inline/server') const serverDir = join(rootDir, '.output-inline/server')
const serverStats = await analyzeSizes(['**/*.mjs', '!node_modules'], serverDir) const serverStats = await analyzeSizes('**/*.mjs', serverDir, { excludeNodeModules: true })
expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot(`"531k"`) expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot(`"531k"`)
const modules = await analyzeSizes('node_modules/**/*', serverDir) const modules = await analyzeSizes('node_modules/**/*', serverDir)
@ -94,11 +94,16 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
}) })
}) })
async function analyzeSizes (pattern: string | string[], rootDir: string) { async function analyzeSizes (pattern: string | string[], rootDir: string, opts: { excludeNodeModules?: boolean } = {}) {
const files: string[] = await globby(pattern, { cwd: rootDir }) const patterns = Array.isArray(pattern) ? pattern : [pattern]
const files: string[] = new fdir({
resolveSymlinks: true,
exclude: p => opts.excludeNodeModules ? p.includes('node_modules') : false
})
.withRelativePaths().globWithOptions(patterns, { dot: true }).crawl(rootDir).sync()
let totalBytes = 0 let totalBytes = 0
for (const file of files) { for (const file of files) {
const path = join(rootDir, file) const path = resolve(rootDir, file)
const isSymlink = (await fsp.lstat(path).catch(() => null))?.isSymbolicLink() const isSymlink = (await fsp.lstat(path).catch(() => null))?.isSymbolicLink()
if (!isSymlink) { if (!isSymlink) {