pipework 0.288.0 → 0.290.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +2 -0
  3. package/REFERENCE.md +6 -0
  4. package/dist/cli/commands/test.d.ts.map +1 -1
  5. package/dist/cli/commands/test.js +13 -0
  6. package/dist/cli/commands/test.js.map +1 -1
  7. package/dist/core/config/discover.d.ts +2 -1
  8. package/dist/core/config/discover.d.ts.map +1 -1
  9. package/dist/core/config/discover.js +2 -2
  10. package/dist/core/config/discover.js.map +1 -1
  11. package/dist/core/config/import-config.d.ts +2 -1
  12. package/dist/core/config/import-config.d.ts.map +1 -1
  13. package/dist/core/config/import-config.js +3 -7
  14. package/dist/core/config/import-config.js.map +1 -1
  15. package/dist/core/config/schema.d.ts.map +1 -1
  16. package/dist/core/config/schema.js +3 -1
  17. package/dist/core/config/schema.js.map +1 -1
  18. package/dist/data/db/expressions.d.ts +23 -0
  19. package/dist/data/db/expressions.d.ts.map +1 -1
  20. package/dist/data/db/expressions.js +27 -1
  21. package/dist/data/db/expressions.js.map +1 -1
  22. package/dist/data/db/filter.d.ts +3 -1
  23. package/dist/data/db/filter.d.ts.map +1 -1
  24. package/dist/data/db/filter.js +3 -1
  25. package/dist/data/db/filter.js.map +1 -1
  26. package/dist/data/db/namespace.d.ts +2 -0
  27. package/dist/data/db/namespace.d.ts.map +1 -1
  28. package/dist/data/query/sql/expressions/conditions.d.ts +2 -0
  29. package/dist/data/query/sql/expressions/conditions.d.ts.map +1 -1
  30. package/dist/data/query/sql/expressions/conditions.js +13 -0
  31. package/dist/data/query/sql/expressions/conditions.js.map +1 -1
  32. package/dist/data/query/sql/expressions/index.d.ts +1 -0
  33. package/dist/data/query/sql/expressions/index.d.ts.map +1 -1
  34. package/dist/data/query/sql/expressions/index.js +1 -0
  35. package/dist/data/query/sql/expressions/index.js.map +1 -1
  36. package/dist/data/query/sql/expressions/row.d.ts +19 -0
  37. package/dist/data/query/sql/expressions/row.d.ts.map +1 -0
  38. package/dist/data/query/sql/expressions/row.js +50 -0
  39. package/dist/data/query/sql/expressions/row.js.map +1 -0
  40. package/dist/test/auto-setup.js +4 -1
  41. package/dist/test/auto-setup.js.map +1 -1
  42. package/dist/test/discover-fork.d.ts +6 -1
  43. package/dist/test/discover-fork.d.ts.map +1 -1
  44. package/dist/test/discover-fork.js +6 -2
  45. package/dist/test/discover-fork.js.map +1 -1
  46. package/dist/test/shared-registry-guard.d.ts +2 -0
  47. package/dist/test/shared-registry-guard.d.ts.map +1 -0
  48. package/dist/test/shared-registry-guard.js +35 -0
  49. package/dist/test/shared-registry-guard.js.map +1 -0
  50. package/dist/test/shared-registry.d.ts +16 -0
  51. package/dist/test/shared-registry.d.ts.map +1 -0
  52. package/dist/test/shared-registry.js +63 -0
  53. package/dist/test/shared-registry.js.map +1 -0
  54. package/dist/test/vitest-workspace.d.ts.map +1 -1
  55. package/dist/test/vitest-workspace.js +63 -13
  56. package/dist/test/vitest-workspace.js.map +1 -1
  57. package/package.json +6 -2
  58. package/src/cli/commands/test.ts +15 -0
  59. package/src/core/config/discover.ts +3 -3
  60. package/src/core/config/import-config.ts +9 -2
  61. package/src/core/config/schema.ts +3 -1
  62. package/src/data/db/expressions.ts +29 -1
  63. package/src/data/db/filter.ts +3 -1
  64. package/src/data/query/sql/expressions/conditions.ts +8 -0
  65. package/src/data/query/sql/expressions/index.ts +1 -0
  66. package/src/data/query/sql/expressions/row.ts +63 -0
  67. package/src/test/auto-setup.ts +4 -1
  68. package/src/test/discover-fork.ts +7 -2
  69. package/src/test/shared-registry-guard.ts +41 -0
  70. package/src/test/shared-registry.ts +71 -0
  71. package/src/test/vitest-workspace.ts +66 -13
@@ -0,0 +1,71 @@
1
+ import { readFileSync, globSync } from 'node:fs'
2
+ import { matchesGlob, resolve, sep } from 'node:path'
3
+
4
+ // A project with `isolate: false` (and a non-vm pool) keeps one module registry per
5
+ // worker. A test file that resets or mocks modules changes that registry for itself
6
+ // and for every file after it: `vi.resetModules()` re-evaluates the app's modules but
7
+ // not pipework (Node's own cache), so each `pipe.define(...)` runs twice against the
8
+ // one registry and the file after it fails at load (#1001). Such files are routed to an
9
+ // isolated sibling project from their own text; the guard setup file refuses the same
10
+ // calls in shared-registry projects for anything the text scan misses.
11
+
12
+ /** The `vi` calls that change a worker's module registry or the next file's importers. */
13
+ export const REGISTRY_CALLS = ['resetModules', 'mock', 'doMock', 'unmock', 'doUnmock'] as const
14
+
15
+ // `vi.mock(`, `vi.mock<T>(`, `vitest.doMock (` and `vi?.resetModules(`. Decided from the
16
+ // file's own text, comments and strings included: a miss is caught by the guard, a
17
+ // false hit only costs one isolated worker.
18
+ const REGISTRY_CALL_PATTERN = new RegExp(
19
+ `\\b(?:vi|vitest)\\s*\\??\\.\\s*(?:${REGISTRY_CALLS.join('|')})\\s*(?:<[^>]*>)?\\s*\\(`,
20
+ )
21
+
22
+ export function callsRegistryApi(text: string): boolean {
23
+ return REGISTRY_CALL_PATTERN.test(text)
24
+ }
25
+
26
+ /** Name of the isolated sibling that takes the registry-changing files of `project`. */
27
+ export function isolatedSiblingName(project: string): string {
28
+ return `${project}:registry-isolated`
29
+ }
30
+
31
+ /** Glob-escape a literal path, so `[id].test.ts` matches only itself. */
32
+ export function escapeGlob(path: string): string {
33
+ return path.replace(/[\\*?[\]{}()!|+@]/g, '\\$&')
34
+ }
35
+
36
+ /**
37
+ * The files `include` selects under `base`, minus `exclude`, as sorted `/`-separated
38
+ * paths relative to `base`. Vitest globs the same way, so this is the file set the
39
+ * project would run. A base that does not exist selects nothing.
40
+ */
41
+ export function listProjectFiles(base: string, include: readonly string[], exclude: readonly string[]): string[] {
42
+ let found: string[]
43
+ try {
44
+ found = globSync([...include], {
45
+ cwd: base,
46
+ // Prune the two trees nothing is ever tested in, without walking them.
47
+ exclude: (path) => {
48
+ const name = path.split(/[\\/]/).pop()
49
+ return name === 'node_modules' || name === '.git'
50
+ },
51
+ })
52
+ } catch {
53
+ return []
54
+ }
55
+ const files = found
56
+ .map(file => file.split(sep).join('/'))
57
+ .filter(file => !exclude.some(pattern => matchesGlob(file, pattern)))
58
+ return [...new Set(files)].sort()
59
+ }
60
+
61
+ /** The files of the project whose own text calls a registry-changing `vi` API. */
62
+ export function findRegistryFiles(base: string, include: readonly string[], exclude: readonly string[]): string[] {
63
+ return listProjectFiles(base, include, exclude).filter(file => {
64
+ try {
65
+ return callsRegistryApi(readFileSync(resolve(base, file), 'utf8'))
66
+ } catch {
67
+ return false
68
+ }
69
+ })
70
+ }
71
+
@@ -1,4 +1,4 @@
1
- import { resolve, dirname } from 'node:path'
1
+ import { resolve, dirname, isAbsolute } from 'node:path'
2
2
  import { configDefaults } from 'vitest/config'
3
3
  import type { ViteUserConfig, UserWorkspaceConfig } from 'vitest/config'
4
4
  import type { VitestConfig, VitestModuleConfig, VitestProfileConfig, CoverageConfig } from '../core/config/schema.js'
@@ -10,6 +10,7 @@ import { loadEnvFiles } from '../core/config/env.js'
10
10
  import { resolveModules } from '../workspace/discover.js'
11
11
  import { workspaceSourcePaths } from '../workspace/source-paths.js'
12
12
  import { pipework as pipeworkPlugin } from './vitest-plugin.js'
13
+ import { escapeGlob, findRegistryFiles, isolatedSiblingName } from './shared-registry.js'
13
14
 
14
15
  const DEFAULT_TEST_TIMEOUT = 30_000
15
16
  export const DEFAULT_HOOK_TIMEOUT = 60_000
@@ -22,6 +23,9 @@ export const DEFAULT_HOOK_TIMEOUT = 60_000
22
23
  const DEFAULT_POOL = 'forks'
23
24
  const DEFAULT_ISOLATE = false
24
25
 
26
+ // Setup file that refuses registry-changing `vi` calls in a shared-registry project.
27
+ const SHARED_REGISTRY_GUARD = 'pipework/test/shared-registry'
28
+
25
29
  type PoolName = NonNullable<VitestConfig['pool']>
26
30
 
27
31
  /**
@@ -199,14 +203,12 @@ export function resolveVitestModules(
199
203
 
200
204
  for (const [modulePath, moduleConfig] of [...included.entries()].sort(([a], [b]) => a.localeCompare(b))) {
201
205
  refuseProjectCoverage(`vitest.modules["${modulePath}"].coverage`, moduleConfig)
202
- const project = buildProjectConfig(modulePath, moduleConfig, vitestConfig, configDir, workspacePackages)
203
- projects.push(project)
206
+ pushRouted(projects, buildProjectConfig(modulePath, moduleConfig, vitestConfig, configDir, workspacePackages))
204
207
 
205
208
  if (moduleConfig.profiles !== undefined) {
206
209
  for (const [profileName, profileConfig] of Object.entries(moduleConfig.profiles).sort(([a], [b]) => a.localeCompare(b))) {
207
210
  refuseProjectCoverage(`vitest.modules["${modulePath}"].profiles["${profileName}"].coverage`, profileConfig)
208
- const profileProject = buildProfileConfig(modulePath, profileName, moduleConfig, profileConfig, vitestConfig, configDir, workspacePackages)
209
- projects.push(profileProject)
211
+ pushRouted(projects, buildProfileConfig(modulePath, profileName, moduleConfig, profileConfig, vitestConfig, configDir, workspacePackages))
210
212
  }
211
213
  }
212
214
  }
@@ -411,6 +413,7 @@ interface ProjectBuildInput {
411
413
 
412
414
  function buildProject(input: ProjectBuildInput): UserWorkspaceConfig {
413
415
  const inlineDeps = buildInlineDeps(input)
416
+ const shared = sharesRegistry(input)
414
417
  return {
415
418
  ...input.extra,
416
419
  plugins: [
@@ -434,7 +437,7 @@ function buildProject(input: ProjectBuildInput): UserWorkspaceConfig {
434
437
  inline: inlineDeps,
435
438
  },
436
439
  },
437
- setupFiles: input.setupFiles,
440
+ setupFiles: shared ? [...input.setupFiles, SHARED_REGISTRY_GUARD] : input.setupFiles,
438
441
  // `vitest.pool` / `vitest.isolate` are dedicated fields, so an explicitly
439
442
  // configured one wins over `extra.test`; unset, the default above stands and
440
443
  // `extra.test` still overrides it as it always did.
@@ -458,19 +461,25 @@ function buildSetupFiles(database: boolean, userSetupFiles: string[] | undefined
458
461
  return files
459
462
  }
460
463
 
461
- // Pipework is inlined only where the effective isolate is true: a fresh module
462
- // registry per file needs it. A shared-registry fork already matches Node's single
463
- // module instance, so inlining buys nothing there. The vm pools isolate per file
464
- // whatever `isolate` says, so they always inline. Precedence mirrors buildProject's
465
- // emitted config: dedicated field, then `extra.test`, then the default.
466
- function buildInlineDeps(input: ProjectBuildInput): string[] {
464
+ // Whether the project's workers share one module registry: the effective isolate is
465
+ // false and the pool is not a vm pool (those isolate per file whatever `isolate` says).
466
+ // Precedence mirrors buildProject's emitted config: dedicated field, then `extra.test`,
467
+ // then the default.
468
+ function sharesRegistry(input: ProjectBuildInput): boolean {
467
469
  const extraTest = 'test' in input.extra ? input.extra['test'] as Record<string, unknown> : {}
468
470
  const extraIsolate = typeof extraTest['isolate'] === 'boolean' ? extraTest['isolate'] : undefined
469
471
  const extraPool = extraTest['pool']
470
472
  const isolate = input.isolate ?? extraIsolate ?? DEFAULT_ISOLATE
471
473
  const pool = input.pool ?? extraPool ?? DEFAULT_POOL
474
+ return !isolate && pool !== 'vmThreads' && pool !== 'vmForks'
475
+ }
476
+
477
+ // Pipework is inlined only where the registry is not shared: a fresh module registry
478
+ // per file needs it. A shared-registry fork already matches Node's single module
479
+ // instance, so inlining buys nothing there.
480
+ function buildInlineDeps(input: ProjectBuildInput): string[] {
472
481
  const deps = new Set<string>()
473
- if (isolate || pool === 'vmThreads' || pool === 'vmForks') {
482
+ if (!sharesRegistry(input)) {
474
483
  deps.add('pipework')
475
484
  }
476
485
  for (const dep of input.inlineDeps) {
@@ -479,6 +488,50 @@ function buildInlineDeps(input: ProjectBuildInput): string[] {
479
488
  return [...deps]
480
489
  }
481
490
 
491
+ // Routing for #1001. `isolate: false` projects keep one module registry per worker, and a
492
+ // file that calls vi.resetModules / vi.mock / vi.doMock / vi.unmock / vi.doUnmock breaks
493
+ // the files after it. Per project, the files its `include` selects (under `dir` or the
494
+ // root, minus its merged `exclude`) are read; those whose own text calls one move to a
495
+ // sibling project `<name>:registry-isolated` that is the same project with `isolate: true`
496
+ // and an `include` of exactly those files, and the project itself excludes them. Each
497
+ // project decides for itself from the file's text, so a file two projects select (a
498
+ // module and its profile, or nested module roots) is routed in each, and still runs once
499
+ // per project as before. Files are visited in sorted order, projects in config order.
500
+ function pushRouted(projects: UserWorkspaceConfig[], project: UserWorkspaceConfig): void {
501
+ const test = project.test
502
+ const setup = test?.setupFiles
503
+ if (test === undefined || typeof test.root !== 'string' || !Array.isArray(setup) || !setup.includes(SHARED_REGISTRY_GUARD)) {
504
+ projects.push(project)
505
+ return
506
+ }
507
+ const root = test.root
508
+ const name = String(test.name)
509
+ const dir = typeof test.dir === 'string' ? (isAbsolute(test.dir) ? test.dir : resolve(root, test.dir)) : root
510
+ const include = test.include ?? [...configDefaults.include]
511
+ const exclude = test.exclude ?? [...configDefaults.exclude]
512
+ const routed = findRegistryFiles(dir, include, exclude)
513
+ if (routed.length === 0) {
514
+ projects.push(project)
515
+ return
516
+ }
517
+ const escaped = routed.map(escapeGlob)
518
+ projects.push({
519
+ ...project,
520
+ test: { ...test, exclude: [...exclude, ...escaped] },
521
+ })
522
+ projects.push({
523
+ ...project,
524
+ test: {
525
+ ...test,
526
+ name: isolatedSiblingName(name),
527
+ isolate: true,
528
+ include: escaped,
529
+ setupFiles: setup.filter(file => file !== SHARED_REGISTRY_GUARD),
530
+ server: { deps: { inline: [...new Set(['pipework', ...((test.server?.deps?.inline as string[] | undefined) ?? [])])] } },
531
+ },
532
+ })
533
+ }
534
+
482
535
  function closestMatch(input: string, candidates: string[]): string | null {
483
536
  if (candidates.length === 0) return null
484
537