refactor(i18n): report route namespace dependencies (#42503)

This commit is contained in:
Stephen Zhou
2026-09-20 03:13:49 +00:00
committed by GitHub
parent 61dc68c3a7
commit 969a831b5b
8 changed files with 443 additions and 57 deletions
@@ -2,9 +2,9 @@
import { mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import path from 'node:path'
import { build, createBuilder, createServer } from 'vite'
import { afterEach, beforeEach, describe, expect, it } from 'vite-plus/test'
import { i18nPrunePlugin } from '../i18n-prune'
import { build, createBuilder, createLogger, createServer } from 'vite'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vite-plus/test'
import { i18nAnalysisPlugin } from '../i18n-analysis'
describe('i18n build check', () => {
let root: string
@@ -33,13 +33,96 @@ describe('i18n build check', () => {
root,
configFile: false,
logLevel: 'silent',
plugins: [i18nPrunePlugin()],
plugins: [i18nAnalysisPlugin()],
build: {
write: false,
lib: { entry: path.join(root, 'entry.ts'), formats: ['es'] },
},
})
it('reports each route with inherited, lazy and environment-specific namespaces', async () => {
const files: Record<string, string> = {
'entry.ts': `export { page } from './app/(group)/items/[id]/page'
export { other } from './app/other/page'
export { layout } from './app/layout'
export { group } from './app/(group)/layout'`,
'app/layout.ts': `export function layout(t: (key: string) => string) { return t('common:title') }`,
'app/(group)/layout.ts': `export function group(t: (key: string) => string) { return t('group:title') }`,
'app/(group)/items/[id]/page.ts': `export { page } from '../../../../component'`,
'app/other/page.ts': `export function other(t: (key: string) => string) { return t('other:title') }`,
'component.ts': `export function page(t: (key: string) => string) { return t('app:used') }
export const lazy = () => import('./lazy')`,
'lazy.ts': `export function lazy(t: (key: string) => string) { return t('lazy:title') }`,
'unimported.ts': `export function ignored(t: (key: string) => string) { return t('ignored:title') }`,
}
for (const [file, source] of Object.entries(files)) {
mkdirSync(path.dirname(path.join(root, file)), { recursive: true })
writeFileSync(path.join(root, file), source)
}
for (const namespace of ['common', 'group', 'other', 'lazy', 'client', 'ssr'])
writeFileSync(
path.join(root, `i18n/locales/en-US/${namespace}.json`),
JSON.stringify({ title: 'Title' }),
)
writeFileSync(localeFile, '{}')
const logger = createLogger('silent')
const info = vi.spyOn(logger, 'info')
const builder = await createBuilder({
root,
configFile: false,
customLogger: logger,
plugins: [
{
name: 'environment-translation',
enforce: 'pre',
transform(code, id) {
if (id === path.join(root, 'component.ts'))
return code.replace('app:used', `${this.environment.name}:title`)
},
},
i18nAnalysisPlugin(),
],
environments: { client: {}, ssr: {} },
build: { write: true, lib: { entry: path.join(root, 'entry.ts'), formats: ['es'] } },
})
await builder.buildApp()
const reports = info.mock.calls
.map(([message]) => message)
.filter((message) => message.startsWith('[i18n] Route'))
expect(reports).toHaveLength(1)
expect(reports[0]).toContain(
'/items/[id] (app/(group)/items/[id]/page.ts): client, common, group, lazy, ssr',
)
expect(reports[0]).toContain('/other (app/other/page.ts): common, other')
expect(reports[0]).toContain(' page: client, ssr')
expect(reports[0]).toContain(' shared: common, group')
expect(reports[0]).toContain(' lazy: lazy')
expect(reports[0]).not.toContain('ignored')
const artifact: unknown = JSON.parse(
readFileSync(path.join(root, 'dist/i18n-routes.json'), 'utf8'),
)
expect(artifact).toMatchObject({
version: 1,
routes: expect.arrayContaining([
expect.objectContaining({
route: '/items/[id]',
groups: {
page: [
{ namespace: 'client', sources: ['component.ts'] },
{ namespace: 'ssr', sources: ['component.ts'] },
],
shared: [
{ namespace: 'common', sources: ['app/layout.ts'] },
{ namespace: 'group', sources: ['app/(group)/layout.ts'] },
],
lazy: [{ namespace: 'lazy', sources: ['lazy.ts'] }],
slots: [],
},
}),
]),
})
})
it('fails builds with actionable findings without modifying translations', async () => {
const content = JSON.stringify({ used: 'Used', unused: 'Unused' })
writeFileSync(localeFile, content)
@@ -101,51 +184,55 @@ describe('i18n build check', () => {
await expect(buildFixture()).resolves.toBeDefined()
})
// Two rounds of three real builds need headroom under CI coverage and parallel load.
it('checks the union of client, SSR and RSC graphs only after every environment builds', async () => {
writeFileSync(localeFile, JSON.stringify({ client: 'Client', ssr: 'SSR', rsc: 'RSC' }))
for (const name of ['client', 'ssr', 'rsc']) {
writeFileSync(
path.join(root, `${name}.ts`),
`
// Six real environment builds plus graph analysis need headroom under CI contention.
it(
'checks the union of client, SSR and RSC graphs only after every environment builds',
{ timeout: 15_000 },
async () => {
writeFileSync(localeFile, JSON.stringify({ client: 'Client', ssr: 'SSR', rsc: 'RSC' }))
for (const name of ['client', 'ssr', 'rsc']) {
writeFileSync(
path.join(root, `${name}.ts`),
`
export function translate(t: (key: string) => string) { return t('app:${name}') }
`,
)
}
const completed: string[] = []
const builder = await createBuilder({
root,
configFile: false,
logLevel: 'silent',
plugins: [i18nPrunePlugin()],
environments: Object.fromEntries(
['client', 'ssr', 'rsc'].map((name) => [
name,
{
build: {
write: false,
lib: { entry: path.join(root, `${name}.ts`), formats: ['es'] },
)
}
const completed: string[] = []
const builder = await createBuilder({
root,
configFile: false,
logLevel: 'silent',
plugins: [i18nAnalysisPlugin()],
environments: Object.fromEntries(
['client', 'ssr', 'rsc'].map((name) => [
name,
{
build: {
write: false,
lib: { entry: path.join(root, `${name}.ts`), formats: ['es'] },
},
},
]),
),
builder: {
async buildApp(builder) {
for (const environment of Object.values(builder.environments)) {
await builder.build(environment)
completed.push(environment.name)
}
},
]),
),
builder: {
async buildApp(builder) {
for (const environment of Object.values(builder.environments)) {
await builder.build(environment)
completed.push(environment.name)
}
},
},
})
})
await expect(builder.buildApp()).resolves.toBeUndefined()
expect(completed).toEqual(['client', 'ssr', 'rsc'])
await expect(builder.buildApp()).resolves.toBeUndefined()
expect(completed).toEqual(['client', 'ssr', 'rsc'])
// A second build must not inherit the first build's server usage.
writeFileSync(path.join(root, 'ssr.ts'), 'export const value = 1')
await expect(builder.buildApp()).rejects.toThrow('app:ssr')
}, 15_000)
// A second build must not inherit the first build's server usage.
writeFileSync(path.join(root, 'ssr.ts'), 'export const value = 1')
await expect(builder.buildApp()).rejects.toThrow('app:ssr')
},
)
it('retains usages from environment-specific versions of the same module', async () => {
writeFileSync(localeFile, JSON.stringify({ client: 'Client', ssr: 'SSR' }))
@@ -162,7 +249,7 @@ describe('i18n build check', () => {
return code.replace('app:used', `app:${this.environment.name}`)
},
},
i18nPrunePlugin(),
i18nAnalysisPlugin(),
],
environments: {
client: {},
@@ -210,7 +297,7 @@ describe('i18n build check', () => {
return code.replace('$.placeholder', `$.${this.environment.name}`)
},
},
i18nPrunePlugin(),
i18nAnalysisPlugin(),
],
environments: { client: {}, ssr: {} },
builder: {
@@ -238,7 +325,7 @@ describe('i18n build check', () => {
root,
configFile: false,
logLevel: 'silent',
plugins: [i18nPrunePlugin()],
plugins: [i18nAnalysisPlugin()],
server: { middlewareMode: true, watch: null },
})
try {
@@ -3,7 +3,7 @@ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import path from 'node:path'
import { afterEach, beforeEach, describe, expect, it } from 'vite-plus/test'
import { checkTranslationGraph } from '../i18n-prune/graph'
import { checkTranslationGraph } from '../i18n-analysis/graph'
let webRoot: string
let modules: Map<string, string>
@@ -28,7 +28,7 @@ function sortedUnusedKeysByNamespace(result: ReturnType<typeof checkTranslationG
describe('translation graph analysis', () => {
beforeEach(() => {
modules = new Map()
webRoot = mkdtempSync(path.join(tmpdir(), 'dify-i18n-prune-'))
webRoot = mkdtempSync(path.join(tmpdir(), 'dify-i18n-analysis-'))
writeSource('placeholder.ts', '')
})
@@ -0,0 +1,67 @@
// @vitest-environment node
import { describe, expect, it } from 'vite-plus/test'
import { analyzeRouteNamespaces } from '../i18n-analysis/routes'
describe('route namespace analysis', () => {
it('includes ancestor boundaries and parallel slots without leaking sibling layouts', () => {
const usage = new Map(
Object.entries({
'/web/app/layout.tsx': ['common'],
'/web/app/(console)/layout.tsx': ['console'],
'/web/app/(console)/items/page.tsx': ['items'],
'/web/app/(console)/items/loading.tsx': ['loading'],
'/web/app/(console)/items/error.tsx': ['error'],
'/web/app/(console)/@sidebar/default.tsx': ['sidebar'],
'/web/app/(console)/@sidebar/detail/page.tsx': ['detail'],
'/web/app/(public)/layout.tsx': ['public'],
'/web/app/(public)/page.tsx': [],
'/web/shared.ts': ['shared'],
'/web/lazy.ts': ['items', 'lazy'],
}).map(([id, namespaces]) => [id, new Set(namespaces)]),
)
const dependencies = new Map([
['/web/lazy.ts', { static: new Set(['/web/shared.ts']), dynamic: new Set<string>() }],
[
'/web/app/(console)/items/page.tsx',
{ static: new Set(['\0virtual-component']), dynamic: new Set(['/web/lazy.ts']) },
],
['\0virtual-component', { static: new Set(['/web/shared.ts']), dynamic: new Set<string>() }],
['/web/shared.ts', { static: new Set(['\0virtual-component']), dynamic: new Set<string>() }],
])
const report = analyzeRouteNamespaces('/web', dependencies, usage)
expect(report.find((item) => item.page === 'app/(console)/items/page.tsx')).toMatchObject({
route: '/items',
page: 'app/(console)/items/page.tsx',
namespaces: [
'common',
'console',
'detail',
'error',
'items',
'lazy',
'loading',
'shared',
'sidebar',
],
})
const groups = report.find((item) => item.page === 'app/(console)/items/page.tsx')!.groups
expect(groups.page).toEqual([
{ namespace: 'items', sources: ['app/(console)/items/page.tsx'] },
{ namespace: 'shared', sources: ['shared.ts'] },
])
expect(groups.lazy).toEqual([
{ namespace: 'items', sources: ['lazy.ts'] },
{ namespace: 'lazy', sources: ['lazy.ts'] },
])
expect(groups.slots).toEqual([
{ namespace: 'detail', sources: ['app/(console)/@sidebar/detail/page.tsx'] },
{ namespace: 'sidebar', sources: ['app/(console)/@sidebar/default.tsx'] },
])
expect(report.find((item) => item.page === 'app/(public)/page.tsx')).toMatchObject({
route: '/',
page: 'app/(public)/page.tsx',
namespaces: ['common', 'public'],
groups: { page: [], lazy: [], slots: [] },
})
})
})
@@ -1,18 +1,38 @@
import type { Plugin } from 'vite'
import type { Logger, Plugin } from 'vite'
import type { ModuleDependencies } from './i18n-analysis/routes'
import fs from 'node:fs/promises'
import path from 'node:path'
import { normalizePath } from 'vite'
import { analyzeRouteNamespaces } from './i18n-analysis/routes'
const SOURCE_META = 'dify:i18n-source'
// Vite shares these instances across client, SSR, and RSC environments. Each
// completed graph replaces its preceding scan build; only buildApp finalizes it.
export function i18nPrunePlugin(): Plugin[] {
export function i18nAnalysisPlugin(): Plugin[] {
let root: string
let logger: Logger
let reportPath: string | undefined
let buildingApp = false
const graphs = new Map<string, Map<string, string>>()
const edges = new Map<string, Map<string, ModuleDependencies>>()
const moduleId = (id: string) => normalizePath(id.split('?')[0]!)
const check = async () => {
const { checkTranslationGraph } = await import('./i18n-prune/graph')
const { checkTranslationGraph } = await import('./i18n-analysis/graph')
const usage = new Map<string, Set<string>>()
const dependencies = new Map<string, ModuleDependencies>()
for (const graph of edges.values()) {
for (const [id, imports] of graph) {
const merged = dependencies.get(id) ?? {
static: new Set<string>(),
dynamic: new Set<string>(),
}
for (const dependency of imports.static) merged.static.add(dependency)
for (const dependency of imports.dynamic) merged.dynamic.add(dependency)
dependencies.set(id, merged)
}
}
let unused: Record<string, string[]> | undefined
const protectedNamespaces = new Set<string>()
for (const graph of graphs.values()) {
@@ -21,6 +41,11 @@ export function i18nPrunePlugin(): Plugin[] {
const modules = new Map<string, string>()
for (const [id, code] of graph) modules.set(normalizePath(id.split('?')[0]!), code)
const result = checkTranslationGraph(root, modules)
for (const [id, namespaces] of result.moduleNamespaces) {
const merged = usage.get(id) ?? new Set<string>()
for (const namespace of namespaces) merged.add(namespace)
usage.set(id, merged)
}
for (const namespace of result.protectedNamespaces) protectedNamespaces.add(namespace)
// Intersect unused sets: usage (or protection) in any environment keeps a key.
unused =
@@ -33,6 +58,27 @@ export function i18nPrunePlugin(): Plugin[] {
}),
)
}
const routes = analyzeRouteNamespaces(normalizePath(root), dependencies, usage)
if (routes.length) {
logger.info(
[
'[i18n] Route namespace analysis (static build graph; may overestimate or miss runtime usage):',
'Includes ancestor boundaries, dynamic imports and all built parallel-slot branches. Interception segments remain in route labels.',
...routes.flatMap(({ route, page, namespaces, groups }) => [
` ${route} (${page}): ${namespaces.join(', ') || '(none detected)'}`,
...Object.entries(groups).map(
([name, entries]) =>
` ${name}: ${entries.map((entry) => entry.namespace).join(', ') || '(none detected)'}`,
),
]),
].join('\n'),
)
}
if (routes.length && reportPath) {
await fs.mkdir(path.dirname(reportPath), { recursive: true })
await fs.writeFile(reportPath, `${JSON.stringify({ version: 1, routes }, null, 2)}\n`)
logger.info(`[i18n] Full namespace sources: ${reportPath}`)
}
const keys = Object.entries(unused ?? {}).flatMap(([namespace, unused]) =>
unused.map((key) => `${namespace}:${key}`),
)
@@ -56,19 +102,27 @@ export function i18nPrunePlugin(): Plugin[] {
return [
{
name: 'i18n-prune:collect',
name: 'i18n-analysis:collect',
apply: 'build',
enforce: 'pre',
sharedDuringBuild: true,
configResolved(config) {
root = config.root
logger = config.logger
reportPath = config.build.write
? path.resolve(root, config.build.outDir, 'i18n-routes.json')
: undefined
},
async buildApp() {
graphs.clear()
edges.clear()
buildingApp = true
},
buildStart() {
if (!buildingApp) graphs.clear()
if (!buildingApp) {
graphs.clear()
edges.clear()
}
},
transform(code, id) {
const file = normalizePath(id.split('?')[0]!)
@@ -85,16 +139,29 @@ export function i18nPrunePlugin(): Plugin[] {
},
async generateBundle() {
const graph = new Map<string, string>()
const dependencies = new Map<string, ModuleDependencies>()
for (const id of this.getModuleIds()) {
const source: unknown = this.getModuleInfo(id)?.meta[SOURCE_META]
const info = this.getModuleInfo(id)
if (info) {
const imports = dependencies.get(moduleId(id)) ?? {
static: new Set<string>(),
dynamic: new Set<string>(),
}
for (const dependency of info.importedIds) imports.static.add(moduleId(dependency))
for (const dependency of info.dynamicallyImportedIds)
imports.dynamic.add(moduleId(dependency))
dependencies.set(moduleId(id), imports)
}
const source: unknown = info?.meta[SOURCE_META]
if (typeof source === 'string') graph.set(id, source)
}
graphs.set(this.environment.name, graph)
edges.set(this.environment.name, dependencies)
if (!buildingApp) await check()
},
},
{
name: 'i18n-prune:check',
name: 'i18n-analysis:check',
apply: 'build',
sharedDuringBuild: true,
buildApp: {
@@ -111,6 +178,7 @@ export function i18nPrunePlugin(): Plugin[] {
} finally {
buildingApp = false
graphs.clear()
edges.clear()
}
},
},
+53
View File
@@ -0,0 +1,53 @@
# i18n build analysis
`i18nAnalysisPlugin()` prints a route namespace report during production Vite builds,
before the existing unused-key check. With Vinext it reports once, after combining
the completed client, SSR and RSC graphs. Development does not run the analysis.
Each route contains its pattern, source page, sorted namespaces and four groups:
```text
/items/[id] (app/(console)/items/[id]/page.tsx): app, common, workflow
page: app
shared: common
lazy: workflow
slots: (none detected)
```
The report follows resolved static and dynamic imports, including virtual modules,
and adds ancestor layouts, templates, loading/error/not-found boundaries. Route
groups are omitted from the URL pattern. Parallel slots include all built branches
because their active page can depend on navigation history. Interception segments
remain visible in labels; the source page disambiguates entries with the same URL.
This is a conservative module-level estimate, not a runtime loading manifest.
It shares the unused-key analyzer's translation recognition and dynamic-key
fallbacks. Unresolved namespaces can include the entire catalog. Unused exports,
conditional branches, lazy features and parallel slots can overestimate usage;
runtime-only imports or unrecognized translation APIs can be missed. Only pages
and boundaries present in the build graph are reported. Translation loading is
unchanged, and this report does not introduce additional build failures.
## Groups and source files
- `page`: the page and its transitive static imports.
- `shared`: ancestor layouts, templates and loading/error/not-found boundaries,
including their transitive static imports. This includes same-segment boundaries.
- `lazy`: modules reached through dynamic imports from the page or shared entries,
excluding modules already statically reachable from either. Dynamic import does
not prove that a feature loads only after user interaction.
- `slots`: modules reachable from all built ancestor parallel-slot branches,
excluding modules already reachable from the page/shared entries (including lazy).
Page and shared groups can overlap. A namespace can appear in multiple groups
when different modules use it; group counts must not be added to obtain the total.
Environment graphs are combined conservatively, so a static path in any environment
makes a module static for this report. This is not a client-only bundle analysis.
When build output writing is enabled, `<build.outDir>/i18n-routes.json` contains
`{ version: 1, routes }`. Every route retains `route`, `page` and `namespaces`, with
`groups` mapping each category to `{ namespace, sources }` entries. Sources are
sorted paths relative to the Vite root and identify modules containing recognized
translation usage, not necessarily the entire chain of imports leading to them.
The report is written before the unused-key check, so it remains available if that
check fails. `build.write: false` only prints the summary and writes no report.
@@ -101,6 +101,8 @@ export function checkTranslationGraph(root: string, modules: ReadonlyMap<string,
const checker = program.getTypeChecker()
const used = new Map<string, Set<string>>()
const protectedNamespaces = new Set<string>()
const moduleNamespaces = new Map<string, Set<string>>()
let currentNamespaces = new Set<string>()
function declarations(node: ts.Node): readonly ts.Declaration[] {
const symbol = checker.getSymbolAtLocation(node)
@@ -418,6 +420,7 @@ export function checkTranslationGraph(root: string, modules: ReadonlyMap<string,
const text = separator > 0 && !key.namespace ? key.text.slice(separator + 1) : key.text
for (const rawNamespace of namespaces) {
const namespace = camelCase(rawNamespace)
currentNamespaces.add(namespace)
if (!catalog.has(namespace)) continue
if (key.wildcard && text === '.*' && !info.prefix) {
protectedNamespaces.add(namespace)
@@ -513,6 +516,8 @@ export function checkTranslationGraph(root: string, modules: ReadonlyMap<string,
}
for (const id of modules.keys()) {
const source = program.getSourceFile(id)
currentNamespaces = new Set<string>()
moduleNamespaces.set(id, currentNamespaces)
if (source) visit(source)
}
const unused: Record<string, string[]> = {}
@@ -521,5 +526,10 @@ export function checkTranslationGraph(root: string, modules: ReadonlyMap<string,
const missing = [...keys].filter((key) => !used.get(namespace)?.has(key)).sort()
if (missing.length) unused[namespace] = missing
}
return { unused, protectedNamespaces: [...protectedNamespaces].sort(), moduleCount: modules.size }
return {
unused,
protectedNamespaces: [...protectedNamespaces].sort(),
moduleCount: modules.size,
moduleNamespaces,
}
}
+101
View File
@@ -0,0 +1,101 @@
import path from 'node:path'
export type ModuleDependencies = { static: Set<string>; dynamic: Set<string> }
type NamespaceSources = { namespace: string; sources: string[] }
export type RouteNamespaceReport = {
route: string
page: string
namespaces: string[]
groups: Record<'page' | 'shared' | 'lazy' | 'slots', NamespaceSources[]>
}
// Use resolved build edges (including virtual modules), not an independent
// resolver which could disagree with aliases, exports or environment conditions.
export function analyzeRouteNamespaces(
root: string,
dependencies: ReadonlyMap<string, ModuleDependencies>,
usage: ReadonlyMap<string, ReadonlySet<string>>,
): RouteNamespaceReport[] {
const app = `${root.replaceAll('\\', '/')}/app/`
const files = [...usage.keys()].filter((file) => file.startsWith(app))
const boundaries = files.filter((file) =>
/\/(?:layout|template|loading|error|not-found|global-error)\.[jt]sx?$/.test(file),
)
return files
.filter((file) => /\/page\.[jt]sx?$/.test(file))
.sort()
.map((page) => {
const segments = page.slice(app.length).split('/').slice(0, -1)
const ancestors = new Set([app.slice(0, -1)])
for (let i = 1; i <= segments.length; i++) ancestors.add(app + segments.slice(0, i).join('/'))
const sharedEntries = boundaries.filter((file) => ancestors.has(path.posix.dirname(file)))
const slotEntries: string[] = []
// Parallel slots can retain previously active pages on navigation.
for (const file of files) {
const parts = file.slice(app.length).split('/')
if (
parts.some(
(part, index) =>
part.startsWith('@') &&
ancestors.has(index ? app + parts.slice(0, index).join('/') : app.slice(0, -1)),
)
)
slotEntries.push(file)
}
function reachable(entries: string[], includeDynamic: boolean) {
const visited = new Set<string>()
const pending = [...entries]
while (pending.length) {
const id = pending.pop()!
if (visited.has(id)) continue
visited.add(id)
const imports = dependencies.get(id)
for (const dependency of imports?.static ?? []) pending.push(dependency)
if (includeDynamic)
for (const dependency of imports?.dynamic ?? []) pending.push(dependency)
}
return visited
}
function sources(modules: Set<string>): NamespaceSources[] {
const byNamespace = new Map<string, Set<string>>()
for (const id of modules) {
for (const namespace of usage.get(id) ?? []) {
const files = byNamespace.get(namespace) ?? new Set<string>()
files.add(path.posix.relative(root.replaceAll('\\', '/'), id))
byNamespace.set(namespace, files)
}
}
return [...byNamespace]
.sort(([a], [b]) => a.localeCompare(b))
.map(([namespace, files]) => ({
namespace,
sources: [...files].sort(),
}))
}
const pageModules = reachable([page], false)
const sharedModules = reachable(sharedEntries, false)
const mainModules = reachable([page, ...sharedEntries], true)
const lazyModules = new Set(
[...mainModules].filter((id) => !pageModules.has(id) && !sharedModules.has(id)),
)
const slotModules = new Set(
[...reachable(slotEntries, true)].filter((id) => !mainModules.has(id)),
)
const groups = {
page: sources(pageModules),
shared: sources(sharedModules),
lazy: sources(lazyModules),
slots: sources(slotModules),
}
const namespaces = new Set(
Object.values(groups).flatMap((group) => group.map((item) => item.namespace)),
)
return {
// Keep interception segments visible; their URL depends on navigation.
route: `/${segments.filter((segment) => !/^\([^)]*\)$/.test(segment) && !segment.startsWith('@')).join('/')}`,
page: page.slice(root.replaceAll('\\', '/').length + 1),
namespaces: [...namespaces].sort(),
groups,
}
})
}
+2 -2
View File
@@ -2,7 +2,7 @@ import { fileURLToPath } from 'node:url'
import { configDefaults, defineConfig, lazyPlugins } from 'vite-plus'
import { playwright } from 'vite-plus/test/browser-playwright'
import { customI18nHmrPlugin } from './plugins/vite/custom-i18n-hmr.ts'
import { i18nPrunePlugin } from './plugins/vite/i18n-prune.ts'
import { i18nAnalysisPlugin } from './plugins/vite/i18n-analysis.ts'
import { getRootClientInjectTarget } from './plugins/vite/inject-target.ts'
import { nextStaticImageTestPlugin } from './plugins/vite/next-static-image-test.ts'
@@ -40,7 +40,7 @@ export default defineConfig(({ command, mode, isPreview }) => {
: undefined
return [
i18nPrunePlugin(),
i18nAnalysisPlugin(),
Inspect(),
inspector,
tailwindcss(),