diff --git a/package.json b/package.json index e6a52fa401..b14f6cfff1 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,7 @@ "eslint-plugin-perfectionist": "2.11.0", "eslint-typegen": "0.2.4", "execa": "9.2.0", - "globby": "14.0.1", + "fdir": "6.1.1", "h3": "1.12.0", "happy-dom": "14.12.3", "jiti": "1.21.6", @@ -79,6 +79,7 @@ "nuxt-content-twoslash": "0.0.10", "ofetch": "1.3.4", "pathe": "1.1.2", + "picomatch": "^4.0.2", "playwright-core": "1.44.1", "rimraf": "5.0.7", "semver": "7.6.2", diff --git a/packages/kit/package.json b/packages/kit/package.json index 9fd8c6fea7..a1d69ca070 100644 --- a/packages/kit/package.json +++ b/packages/kit/package.json @@ -31,13 +31,14 @@ "consola": "^3.2.3", "defu": "^6.1.4", "destr": "^2.0.3", - "globby": "^14.0.1", + "fdir": "^6.1.1", "hash-sum": "^2.0.0", "ignore": "^5.3.1", "jiti": "^1.21.6", "klona": "^2.0.6", "mlly": "^1.7.1", "pathe": "^1.1.2", + "picomatch": "^4.0.2", "pkg-types": "^1.1.1", "scule": "^1.3.0", "semver": "^7.6.2", @@ -48,6 +49,7 @@ }, "devDependencies": { "@types/hash-sum": "1.0.2", + "@types/picomatch": "^2.3.3", "@types/semver": "7.5.8", "nitropack": "2.9.6", "unbuild": "latest", diff --git a/packages/kit/src/resolve.ts b/packages/kit/src/resolve.ts index b170210efe..86a6553201 100644 --- a/packages/kit/src/resolve.ts +++ b/packages/kit/src/resolve.ts @@ -1,7 +1,7 @@ import { existsSync, promises as fsp } from 'node:fs' import { fileURLToPath } from 'node:url' import { basename, dirname, isAbsolute, join, normalize, resolve } from 'pathe' -import { globby } from 'globby' +import { fdir } from 'fdir' import { resolvePath as _resolvePath } from 'mlly' import { resolveAlias as _resolveAlias } from 'pathe/utils' 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 } = {}) { 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) if (!isIgnored(p)) { files.push(p) diff --git a/packages/nuxt/package.json b/packages/nuxt/package.json index dcf6d24318..f40a78ae5c 100644 --- a/packages/nuxt/package.json +++ b/packages/nuxt/package.json @@ -79,7 +79,7 @@ "esbuild": "^0.21.5", "escape-string-regexp": "^5.0.0", "estree-walker": "^3.0.3", - "globby": "^14.0.1", + "fdir": "^6.1.1", "h3": "^1.12.0", "hookable": "^5.5.3", "ignore": "^5.3.1", @@ -95,6 +95,7 @@ "ohash": "^1.1.3", "pathe": "^1.1.2", "perfect-debounce": "^1.0.0", + "picomatch": "^4.0.2", "pkg-types": "^1.1.1", "radix3": "^1.1.2", "scule": "^1.3.0", @@ -121,6 +122,7 @@ "@nuxt/ui-templates": "1.3.4", "@parcel/watcher": "2.4.1", "@types/estree": "1.0.5", + "@types/picomatch": "^2.3.3", "@vitejs/plugin-vue": "5.0.4", "@vue/compiler-sfc": "3.4.29", "unbuild": "latest", diff --git a/packages/nuxt/src/components/scan.ts b/packages/nuxt/src/components/scan.ts index a09413991e..16ce7d97da 100644 --- a/packages/nuxt/src/components/scan.ts +++ b/packages/nuxt/src/components/scan.ts @@ -1,12 +1,13 @@ import { readdir } from 'node:fs/promises' import { basename, dirname, extname, join, relative } from 'pathe' -import { globby } from 'globby' +import { fdir } from 'fdir' import { kebabCase, pascalCase, splitByCase } from 'scule' import { isIgnored, logger, useNuxt } from '@nuxt/kit' import { withTrailingSlash } from 'ufo' import type { Component, ComponentsDir } from 'nuxt/schema' import { resolveComponentNameSegments } from '../core/utils' +import { toArray } from '../utils' /** * 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) const resolvedNames = new Map() - 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) if (files.length) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4244e351ff..5292b249f2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -86,9 +86,9 @@ importers: execa: specifier: 9.2.0 version: 9.2.0 - globby: - specifier: 14.0.1 - version: 14.0.1 + fdir: + specifier: 6.1.1 + version: 6.1.1(picomatch@4.0.2) h3: specifier: 1.12.0 version: 1.12.0 @@ -119,6 +119,9 @@ importers: pathe: specifier: 1.1.2 version: 1.1.2 + picomatch: + specifier: ^4.0.2 + version: 4.0.2 playwright-core: specifier: 1.44.1 version: 1.44.1 @@ -170,9 +173,9 @@ importers: destr: specifier: ^2.0.3 version: 2.0.3 - globby: - specifier: ^14.0.1 - version: 14.0.1 + fdir: + specifier: ^6.1.1 + version: 6.1.1(picomatch@4.0.2) hash-sum: specifier: ^2.0.0 version: 2.0.0 @@ -191,6 +194,9 @@ importers: pathe: specifier: ^1.1.2 version: 1.1.2 + picomatch: + specifier: ^4.0.2 + version: 4.0.2 pkg-types: specifier: ^1.1.1 version: 1.1.1 @@ -216,6 +222,9 @@ importers: '@types/hash-sum': specifier: 1.0.2 version: 1.0.2 + '@types/picomatch': + specifier: ^2.3.3 + version: 2.3.3 '@types/semver': specifier: 7.5.8 version: 7.5.8 @@ -300,9 +309,9 @@ importers: estree-walker: specifier: ^3.0.3 version: 3.0.3 - globby: - specifier: ^14.0.1 - version: 14.0.1 + fdir: + specifier: ^6.1.1 + version: 6.1.1(picomatch@4.0.2) h3: specifier: ^1.12.0 version: 1.12.0 @@ -348,6 +357,9 @@ importers: perfect-debounce: specifier: ^1.0.0 version: 1.0.0 + picomatch: + specifier: ^4.0.2 + version: 4.0.2 pkg-types: specifier: ^1.1.1 version: 1.1.1 @@ -421,6 +433,9 @@ importers: '@types/estree': specifier: 1.0.5 version: 1.0.5 + '@types/picomatch': + specifier: ^2.3.3 + version: 2.3.3 '@vitejs/plugin-vue': 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)) @@ -572,9 +587,9 @@ importers: execa: specifier: 9.2.0 version: 9.2.0 - globby: - specifier: 14.0.1 - version: 14.0.1 + fdir: + specifier: 6.1.1 + version: 6.1.1(picomatch@4.0.2) html-minifier: specifier: 4.0.0 version: 4.0.0 @@ -587,6 +602,9 @@ importers: pathe: specifier: 1.1.2 version: 1.1.2 + picomatch: + specifier: 4.0.2 + version: 4.0.2 prettier: specifier: 3.3.2 version: 3.3.2 @@ -2520,6 +2538,9 @@ packages: '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} + '@types/picomatch@2.3.3': + resolution: {integrity: sha512-Yll76ZHikRFCyz/pffKGjrCwe/le2CDwOP5F210KQo27kpRE46U2rDnzikNlVn6/ezH3Mhn46bJMTfeVTtcYMg==} + '@types/pify@5.0.4': resolution: {integrity: sha512-gxKJ1Aw8LbyCsCQWIsip9bYKJCNsKHMoZoQMAe2IWH7U7hgp/l6TvJpbFvu8ZlGBimjZZNvEx2S1ZQlj02ayNQ==} @@ -4264,6 +4285,14 @@ packages: fastq@1.15.0: 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: resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} engines: {node: '>=8'} @@ -9186,6 +9215,8 @@ snapshots: '@types/normalize-package-data@2.4.4': {} + '@types/picomatch@2.3.3': {} + '@types/pify@5.0.4': {} '@types/pug@2.0.10': {} @@ -11518,6 +11549,10 @@ snapshots: dependencies: reusify: 1.0.4 + fdir@6.1.1(picomatch@4.0.2): + optionalDependencies: + picomatch: 4.0.2 + figures@3.2.0: dependencies: escape-string-regexp: 1.0.5 diff --git a/test/bundle.test.ts b/test/bundle.test.ts index 14878051b8..7a33c4df48 100644 --- a/test/bundle.test.ts +++ b/test/bundle.test.ts @@ -2,8 +2,8 @@ import { fileURLToPath } from 'node:url' import fsp from 'node:fs/promises' import { beforeAll, describe, expect, it } from 'vitest' import { execaCommand } from 'execa' -import { globby } from 'globby' -import { join } from 'pathe' +import { fdir } from 'fdir' +import { join, resolve } from 'pathe' 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)) @@ -31,7 +31,7 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM it('default server bundle size', async () => { 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"`) 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 () => { 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"`) 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) { - const files: string[] = await globby(pattern, { cwd: rootDir }) +async function analyzeSizes (pattern: string | string[], rootDir: string, opts: { excludeNodeModules?: boolean } = {}) { + 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 for (const file of files) { - const path = join(rootDir, file) + const path = resolve(rootDir, file) const isSymlink = (await fsp.lstat(path).catch(() => null))?.isSymbolicLink() if (!isSymlink) {