@venizia/dev-configs 0.4.7-5 → 0.4.8-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 (166) hide show
  1. package/README.md +3 -3
  2. package/dist/checks/catalog.js.map +1 -1
  3. package/dist/checks/module-cycles.js.map +1 -1
  4. package/dist/checks/purity/manifest.js.map +1 -1
  5. package/dist/checks/purity/probe.js.map +1 -1
  6. package/dist/checks/purity/run.js.map +1 -1
  7. package/dist/checks/split-report.js.map +1 -1
  8. package/dist/checks/test-controls.js.map +1 -1
  9. package/dist/checks/test-dead-code.js.map +1 -1
  10. package/dist/checks/test-mutate.js.map +1 -1
  11. package/dist/checks/test-weight.js.map +1 -1
  12. package/dist/cli/check.js.map +1 -1
  13. package/dist/cli/clean.js.map +1 -1
  14. package/dist/cli/codemod/async-style.d.ts +1 -1
  15. package/dist/cli/codemod/async-style.js +3 -3
  16. package/dist/cli/codemod/async-style.js.map +1 -1
  17. package/dist/cli/codemod/connector-imports.js.map +1 -1
  18. package/dist/cli/codemod/const-classes.js.map +1 -1
  19. package/dist/cli/codemod/drop-redundant-casts.js.map +1 -1
  20. package/dist/cli/codemod/files.js.map +1 -1
  21. package/dist/cli/codemod/index.js.map +1 -1
  22. package/dist/cli/codemod/namespace-functions/index.js.map +1 -1
  23. package/dist/cli/codemod/namespace-functions/logger-scope.js.map +1 -1
  24. package/dist/cli/codemod/namespace-functions/mapping.js.map +1 -1
  25. package/dist/cli/codemod/namespace-functions/rewriter.js.map +1 -1
  26. package/dist/cli/index.js.map +1 -1
  27. package/dist/cli/lint.js.map +1 -1
  28. package/dist/linters/eslint.js +7 -5
  29. package/dist/linters/eslint.js.map +1 -1
  30. package/dist/linters/house/async-rules.d.ts +10 -0
  31. package/dist/linters/house/async-rules.js +29 -0
  32. package/dist/linters/house/async-rules.js.map +1 -0
  33. package/dist/linters/house/await-support.d.ts +2 -0
  34. package/dist/linters/house/await-support.js +15 -10
  35. package/dist/linters/house/await-support.js.map +1 -1
  36. package/dist/linters/house/common.d.ts +2 -0
  37. package/dist/linters/house/common.js +2 -0
  38. package/dist/linters/house/common.js.map +1 -1
  39. package/dist/linters/house/configs.d.ts +3 -29
  40. package/dist/linters/house/configs.js +24 -51
  41. package/dist/linters/house/configs.js.map +1 -1
  42. package/dist/linters/house/const-class-values.rule.js.map +1 -1
  43. package/dist/linters/house/effect-cleanup-consistency.rule.js +2 -1
  44. package/dist/linters/house/effect-cleanup-consistency.rule.js.map +1 -1
  45. package/dist/linters/house/hook-support.js.map +1 -1
  46. package/dist/linters/house/index.d.ts +2 -1
  47. package/dist/linters/house/index.js +4 -1
  48. package/dist/linters/house/index.js.map +1 -1
  49. package/dist/linters/house/max-params-own.rule.js.map +1 -1
  50. package/dist/linters/house/no-async-hook-callback.rule.js.map +1 -1
  51. package/dist/linters/house/no-await-promise-chain.rule.js +2 -3
  52. package/dist/linters/house/no-await-promise-chain.rule.js.map +1 -1
  53. package/dist/linters/house/no-leaked-render.rule.js.map +1 -1
  54. package/dist/linters/house/no-return-await.rule.js +2 -8
  55. package/dist/linters/house/no-return-await.rule.js.map +1 -1
  56. package/dist/linters/house/no-silent-catch.rule.js +1 -1
  57. package/dist/linters/house/no-silent-catch.rule.js.map +1 -1
  58. package/dist/linters/house/no-unbraced-case.rule.d.ts +15 -0
  59. package/dist/linters/house/no-unbraced-case.rule.js +45 -0
  60. package/dist/linters/house/no-unbraced-case.rule.js.map +1 -0
  61. package/dist/linters/house/plugin.js +2 -0
  62. package/dist/linters/house/plugin.js.map +1 -1
  63. package/dist/linters/house/promise-chain-support.js.map +1 -1
  64. package/dist/linters/ignore.js.map +1 -1
  65. package/dist/linters/index.d.ts +1 -0
  66. package/dist/linters/index.js +1 -0
  67. package/dist/linters/index.js.map +1 -1
  68. package/dist/linters/padding.d.ts +8 -0
  69. package/dist/linters/padding.js +25 -0
  70. package/dist/linters/padding.js.map +1 -0
  71. package/dist/linters/rules/eqeqeq.rule.js.map +1 -1
  72. package/dist/linters/rules/index.d.ts +2 -0
  73. package/dist/linters/rules/index.js +2 -0
  74. package/dist/linters/rules/index.js.map +1 -1
  75. package/dist/linters/rules/lines-between-class-members.rule.d.ts +14 -0
  76. package/dist/linters/rules/lines-between-class-members.rule.js +17 -0
  77. package/dist/linters/rules/lines-between-class-members.rule.js.map +1 -0
  78. package/dist/linters/rules/naming-convention.rule.js.map +1 -1
  79. package/dist/linters/rules/no-restricted-imports.rule.js.map +1 -1
  80. package/dist/linters/rules/no-restricted-properties.rule.js.map +1 -1
  81. package/dist/linters/rules/no-unused-vars.rule.js.map +1 -1
  82. package/dist/linters/rules/padding-line-between-statements.rule.d.ts +16 -0
  83. package/dist/linters/rules/padding-line-between-statements.rule.js +25 -0
  84. package/dist/linters/rules/padding-line-between-statements.rule.js.map +1 -0
  85. package/dist/linters/rules/prefer-nullish-coalescing.rule.js.map +1 -1
  86. package/dist/linters/rules/switch-case-braces.rule.js.map +1 -1
  87. package/dist/testing/repo-tmp.js.map +1 -1
  88. package/package.json +3 -5
  89. package/dist/checks/catalog.d.ts.map +0 -1
  90. package/dist/checks/index.d.ts.map +0 -1
  91. package/dist/checks/module-cycles.d.ts.map +0 -1
  92. package/dist/checks/purity/manifest.d.ts.map +0 -1
  93. package/dist/checks/purity/probe.d.ts.map +0 -1
  94. package/dist/checks/purity/run.d.ts.map +0 -1
  95. package/dist/checks/split-report.d.ts.map +0 -1
  96. package/dist/checks/test-controls.d.ts.map +0 -1
  97. package/dist/checks/test-dead-code.d.ts.map +0 -1
  98. package/dist/checks/test-mutate.d.ts.map +0 -1
  99. package/dist/checks/test-weight.d.ts.map +0 -1
  100. package/dist/cli/check.d.ts.map +0 -1
  101. package/dist/cli/clean.d.ts.map +0 -1
  102. package/dist/cli/codemod/async-style.d.ts.map +0 -1
  103. package/dist/cli/codemod/connector-imports.d.ts.map +0 -1
  104. package/dist/cli/codemod/const-classes.d.ts.map +0 -1
  105. package/dist/cli/codemod/drop-redundant-casts.d.ts.map +0 -1
  106. package/dist/cli/codemod/files.d.ts.map +0 -1
  107. package/dist/cli/codemod/index.d.ts.map +0 -1
  108. package/dist/cli/codemod/namespace-functions/index.d.ts.map +0 -1
  109. package/dist/cli/codemod/namespace-functions/logger-scope.d.ts.map +0 -1
  110. package/dist/cli/codemod/namespace-functions/mapping.d.ts.map +0 -1
  111. package/dist/cli/codemod/namespace-functions/mappings/boot.d.ts.map +0 -1
  112. package/dist/cli/codemod/namespace-functions/mappings/connectors.d.ts.map +0 -1
  113. package/dist/cli/codemod/namespace-functions/mappings/core-server.d.ts.map +0 -1
  114. package/dist/cli/codemod/namespace-functions/mappings/core-worker.d.ts.map +0 -1
  115. package/dist/cli/codemod/namespace-functions/mappings/dev-configs.d.ts.map +0 -1
  116. package/dist/cli/codemod/namespace-functions/mappings/filter.d.ts.map +0 -1
  117. package/dist/cli/codemod/namespace-functions/mappings/helpers.d.ts.map +0 -1
  118. package/dist/cli/codemod/namespace-functions/mappings/index.d.ts.map +0 -1
  119. package/dist/cli/codemod/namespace-functions/mappings/inversion.d.ts.map +0 -1
  120. package/dist/cli/codemod/namespace-functions/mappings/kernel.d.ts.map +0 -1
  121. package/dist/cli/codemod/namespace-functions/rewriter.d.ts.map +0 -1
  122. package/dist/cli/index.d.ts.map +0 -1
  123. package/dist/cli/lint.d.ts.map +0 -1
  124. package/dist/compilers/bun.d.ts.map +0 -1
  125. package/dist/compilers/index.d.ts.map +0 -1
  126. package/dist/index.d.ts.map +0 -1
  127. package/dist/linters/browser.d.ts.map +0 -1
  128. package/dist/linters/eslint.d.ts.map +0 -1
  129. package/dist/linters/house/await-support.d.ts.map +0 -1
  130. package/dist/linters/house/common.d.ts.map +0 -1
  131. package/dist/linters/house/configs.d.ts.map +0 -1
  132. package/dist/linters/house/const-class-values.rule.d.ts.map +0 -1
  133. package/dist/linters/house/effect-cleanup-consistency.rule.d.ts.map +0 -1
  134. package/dist/linters/house/hook-support.d.ts.map +0 -1
  135. package/dist/linters/house/index.d.ts.map +0 -1
  136. package/dist/linters/house/max-params-own.rule.d.ts.map +0 -1
  137. package/dist/linters/house/no-async-hook-callback.rule.d.ts.map +0 -1
  138. package/dist/linters/house/no-await-promise-chain.rule.d.ts.map +0 -1
  139. package/dist/linters/house/no-inline-await.rule.d.ts.map +0 -1
  140. package/dist/linters/house/no-leaked-render.rule.d.ts.map +0 -1
  141. package/dist/linters/house/no-new-error.rule.d.ts.map +0 -1
  142. package/dist/linters/house/no-return-await.rule.d.ts.map +0 -1
  143. package/dist/linters/house/no-silent-catch.rule.d.ts.map +0 -1
  144. package/dist/linters/house/plugin.d.ts.map +0 -1
  145. package/dist/linters/house/promise-chain-support.d.ts.map +0 -1
  146. package/dist/linters/ignore.d.ts.map +0 -1
  147. package/dist/linters/index.d.ts.map +0 -1
  148. package/dist/linters/jsx-files.d.ts.map +0 -1
  149. package/dist/linters/oxlint-presets.d.ts.map +0 -1
  150. package/dist/linters/oxlint.d.ts.map +0 -1
  151. package/dist/linters/react-eslint.d.ts.map +0 -1
  152. package/dist/linters/react-oxlint.d.ts.map +0 -1
  153. package/dist/linters/rules/common.d.ts.map +0 -1
  154. package/dist/linters/rules/eqeqeq.rule.d.ts.map +0 -1
  155. package/dist/linters/rules/index.d.ts.map +0 -1
  156. package/dist/linters/rules/naming-convention.rule.d.ts.map +0 -1
  157. package/dist/linters/rules/no-restricted-imports.rule.d.ts.map +0 -1
  158. package/dist/linters/rules/no-restricted-properties.rule.d.ts.map +0 -1
  159. package/dist/linters/rules/no-unused-vars.rule.d.ts.map +0 -1
  160. package/dist/linters/rules/prefer-nullish-coalescing.rule.d.ts.map +0 -1
  161. package/dist/linters/rules/switch-case-braces.rule.d.ts.map +0 -1
  162. package/dist/testing/index.d.ts.map +0 -1
  163. package/dist/testing/rejection.helper.d.ts.map +0 -1
  164. package/dist/testing/repo-tmp.d.ts.map +0 -1
  165. package/dist/testing/spawn.helper.d.ts.map +0 -1
  166. package/dist/testing/tmpdir-preload.d.ts.map +0 -1
package/README.md CHANGED
@@ -160,7 +160,7 @@ booleans. `BrowserOxlintConfigs.create()` and `ReactOxlintConfigs.create()` take
160
160
 
161
161
  ### houseConfigs and the house rules
162
162
 
163
- `houseConfigs` is `namingConventionConfigs` plus the house rules oxlint cannot express;
163
+ `houseConfigs` is `namingConventionConfigs` plus the house rules oxlint cannot express and the four async style rules;
164
164
  `reactHouseConfigs` is the same over `reactNamingConventionConfigs`. The rules ship as the ESLint plugin
165
165
  `houseEslintPlugin`, registered as `venizia`:
166
166
 
@@ -241,14 +241,14 @@ flavor = "oxfmt"
241
241
 
242
242
  `eslintConfigs` keeps its shape and resolves to the same rules as before, but is now owned here instead
243
243
  of wrapping `@minimaltech/eslint-node`: ESLint recommended, typescript-eslint, naming
244
- conventions, `curly`, `eslint-plugin-n`, plus these IGNIS rules:
244
+ conventions, `curly`, plus these IGNIS rules (the node-protocol check runs in `oxlintConfigs` only):
245
245
 
246
246
  | Rule | Setting |
247
247
  | :--- | :--- |
248
248
  | `@typescript-eslint/no-explicit-any` | `off` |
249
249
  | `@typescript-eslint/ban-ts-comment` | `error` - `@ts-expect-error` with a description is still allowed |
250
250
  | `@typescript-eslint/no-namespace` | `error` |
251
- | `unicorn/switch-case-braces` | `['error', 'always']` |
251
+ | `venizia/no-unbraced-case` | `error` - the ESLint twin of `unicorn/switch-case-braces` (`always`); the fix wraps the statements in `{ }` |
252
252
 
253
253
  ### BrowserOxlintConfigs
254
254
 
@@ -1 +1 @@
1
- {"version":3,"file":"catalog.js","sourceRoot":"","sources":["../../src/checks/catalog.ts"],"names":[],"mappings":"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","sourcesContent":["import { Glob } from 'bun';\nimport { existsSync } from 'node:fs';\nimport { join } from 'node:path';\n\n/**\n * Guards dependency versions against the root `workspaces.catalog`.\n *\n * A catalogued dep is referenced as `catalog:` and resolved at publish time by `bun publish`, which\n * substitutes the real range while packing. `npm publish` does NOT - it ships `catalog:` verbatim\n * and the manifest is unresolvable for every consumer, which is how 0.1.1-9 / -6 / -4 broke.\n */\nexport interface ICatalogProblem {\n file: string;\n message: string;\n}\n\nexport interface ICatalogOptions {\n /** The repository root: where the root `package.json` and the globs resolve. Default: the current folder. */\n root?: string;\n\n /** Manifests to check, beside `packages/*\\/package.json`. A glob relative to the root. */\n globs?: string[];\n\n /**\n * Files that are not published manifests but carry `dependencies` and `devDependencies` the\n * release resolves (a VULCAN preset): `catalog:` is allowed in their `peerDependencies`, because a\n * library preset gives one name one catalog range. A file belongs here when its path starts with\n * this prefix, for example `presets/`.\n */\n presetPrefix?: string;\n\n /**\n * Manifests outside the workspace (a wiki with its own lockfile): `catalog:` does not resolve\n * there, so a catalogued dependency must be pinned to exactly the catalog's version instead.\n */\n pinned?: string[];\n}\n\nexport interface ICatalogReport {\n problems: ICatalogProblem[];\n entries: number;\n referenced: number;\n\n /** Set when there is no `workspaces.catalog` to check against. */\n noCatalog: boolean;\n}\n\n/** `peerDependencies` are compatibility statements for consumers and stay deliberately looser than the install range. */\nconst CATALOGUED_BLOCKS = ['dependencies', 'devDependencies'] as const;\n\nconst DEFAULT_GLOB = 'packages/*/package.json';\n\ninterface IManifest {\n workspaces?: { catalog?: Record<string, string> };\n dependencies?: Record<string, string>;\n devDependencies?: Record<string, string>;\n peerDependencies?: Record<string, string>;\n}\n\nexport class CatalogCheck {\n /** Reads the root catalog and every manifest the options name, and reports everything wrong with their ranges. */\n static async find(opts: ICatalogOptions = {}): Promise<ICatalogReport> {\n const { root = process.cwd(), globs = [], presetPrefix, pinned = [] } = opts;\n const result = await Bun.file(join(root, 'package.json')).text();\n const rootJson: IManifest = JSON.parse(result);\n const catalog: Record<string, string> = rootJson.workspaces?.catalog ?? {};\n\n if (Object.keys(catalog).length === 0) {\n return { problems: [], entries: 0, referenced: 0, noCatalog: true };\n }\n\n const problems: ICatalogProblem[] = [];\n const referenced = new Set<string>();\n const presentPinned = pinned.filter((file) => existsSync(join(root, file)));\n\n const files = new Set<string>();\n for (const pattern of [DEFAULT_GLOB, ...globs]) {\n for await (const file of new Glob(pattern).scan(root)) {\n files.add(file);\n }\n }\n\n for (const file of [...files, ...presentPinned]) {\n let json: IManifest;\n\n try {\n const result2 = await Bun.file(join(root, file)).text();\n json = JSON.parse(result2);\n } catch (error) {\n problems.push({\n file,\n message: `invalid JSON - ${error instanceof Error ? error.message : error}`,\n });\n continue;\n }\n\n const isPinned = presentPinned.includes(file);\n\n for (const block of CATALOGUED_BLOCKS) {\n const entriesList = Object.entries(json[block] ?? {});\n for (const [dep, range] of entriesList) {\n if (!(dep in catalog)) {\n continue;\n }\n\n referenced.add(dep);\n\n if (isPinned) {\n if (range !== catalog[dep]) {\n problems.push({\n file,\n message: `${block}.${dep} is \"${range}\" but the catalog pins \"${catalog[dep]}\" - this project is outside the workspace, so pin exactly that`,\n });\n }\n\n continue;\n }\n\n if (range === 'catalog:') {\n continue;\n }\n\n problems.push({\n file,\n message: `${block}.${dep} is \"${range}\" but the catalog owns it - use \"catalog:\" (catalog pins ${catalog[dep]})`,\n });\n }\n }\n\n // A preset is not a published manifest: its markers are resolved by the release, and the library kind gives a\n // name one catalog range, so a lib's peer takes the same marker as its dev dependency.\n const isPreset = presetPrefix !== undefined && file.startsWith(presetPrefix);\n\n // A peer resolved from the catalog would collapse to the exact install range, losing the looseness that makes it a peer.\n const peerEntries = Object.entries(json.peerDependencies ?? {});\n for (const [dep, range] of peerEntries) {\n if (range !== 'catalog:' || isPreset) {\n continue;\n }\n\n problems.push({\n file,\n message: `peerDependencies.${dep} uses \"catalog:\" - a peer range must stay hand-authored and looser than the install range`,\n });\n }\n }\n\n // A catalog entry nobody references is dead weight that will drift out of date unnoticed.\n const catalogKeys = Object.keys(catalog);\n for (const dep of catalogKeys) {\n if (referenced.has(dep)) {\n continue;\n }\n\n problems.push({\n file: 'package.json',\n message: `catalog entry \"${dep}\" is referenced by no workspace - drop it`,\n });\n }\n\n return { problems, entries: catalogKeys.length, referenced: referenced.size, noCatalog: false };\n }\n}\n"]}
1
+ {"version":3,"file":"catalog.js","sourceRoot":"","sources":["../../src/checks/catalog.ts"],"names":[],"mappings":"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","sourcesContent":["import { Glob } from 'bun';\nimport { existsSync } from 'node:fs';\nimport { join } from 'node:path';\n\n/**\n * Guards dependency versions against the root `workspaces.catalog`.\n *\n * A catalogued dep is referenced as `catalog:` and resolved at publish time by `bun publish`, which\n * substitutes the real range while packing. `npm publish` does NOT - it ships `catalog:` verbatim\n * and the manifest is unresolvable for every consumer, which is how 0.1.1-9 / -6 / -4 broke.\n */\nexport interface ICatalogProblem {\n file: string;\n message: string;\n}\n\nexport interface ICatalogOptions {\n /** The repository root: where the root `package.json` and the globs resolve. Default: the current folder. */\n root?: string;\n\n /** Manifests to check, beside `packages/*\\/package.json`. A glob relative to the root. */\n globs?: string[];\n\n /**\n * Files that are not published manifests but carry `dependencies` and `devDependencies` the\n * release resolves (a VULCAN preset): `catalog:` is allowed in their `peerDependencies`, because a\n * library preset gives one name one catalog range. A file belongs here when its path starts with\n * this prefix, for example `presets/`.\n */\n presetPrefix?: string;\n\n /**\n * Manifests outside the workspace (a wiki with its own lockfile): `catalog:` does not resolve\n * there, so a catalogued dependency must be pinned to exactly the catalog's version instead.\n */\n pinned?: string[];\n}\n\nexport interface ICatalogReport {\n problems: ICatalogProblem[];\n entries: number;\n referenced: number;\n\n /** Set when there is no `workspaces.catalog` to check against. */\n noCatalog: boolean;\n}\n\n/** `peerDependencies` are compatibility statements for consumers and stay deliberately looser than the install range. */\nconst CATALOGUED_BLOCKS = ['dependencies', 'devDependencies'] as const;\n\nconst DEFAULT_GLOB = 'packages/*/package.json';\n\ninterface IManifest {\n workspaces?: { catalog?: Record<string, string> };\n dependencies?: Record<string, string>;\n devDependencies?: Record<string, string>;\n peerDependencies?: Record<string, string>;\n}\n\nexport class CatalogCheck {\n /** Reads the root catalog and every manifest the options name, and reports everything wrong with their ranges. */\n static async find(opts: ICatalogOptions = {}): Promise<ICatalogReport> {\n const { root = process.cwd(), globs = [], presetPrefix, pinned = [] } = opts;\n const result = await Bun.file(join(root, 'package.json')).text();\n const rootJson: IManifest = JSON.parse(result);\n const catalog: Record<string, string> = rootJson.workspaces?.catalog ?? {};\n\n if (Object.keys(catalog).length === 0) {\n return { problems: [], entries: 0, referenced: 0, noCatalog: true };\n }\n\n const problems: ICatalogProblem[] = [];\n const referenced = new Set<string>();\n const presentPinned = pinned.filter((file) => existsSync(join(root, file)));\n\n const files = new Set<string>();\n\n for (const pattern of [DEFAULT_GLOB, ...globs]) {\n for await (const file of new Glob(pattern).scan(root)) {\n files.add(file);\n }\n }\n\n for (const file of [...files, ...presentPinned]) {\n let json: IManifest;\n\n try {\n const result2 = await Bun.file(join(root, file)).text();\n json = JSON.parse(result2);\n } catch (error) {\n problems.push({\n file,\n message: `invalid JSON - ${error instanceof Error ? error.message : error}`,\n });\n continue;\n }\n\n const isPinned = presentPinned.includes(file);\n\n for (const block of CATALOGUED_BLOCKS) {\n const entriesList = Object.entries(json[block] ?? {});\n\n for (const [dep, range] of entriesList) {\n if (!(dep in catalog)) {\n continue;\n }\n\n referenced.add(dep);\n\n if (isPinned) {\n if (range !== catalog[dep]) {\n problems.push({\n file,\n message: `${block}.${dep} is \"${range}\" but the catalog pins \"${catalog[dep]}\" - this project is outside the workspace, so pin exactly that`,\n });\n }\n\n continue;\n }\n\n if (range === 'catalog:') {\n continue;\n }\n\n problems.push({\n file,\n message: `${block}.${dep} is \"${range}\" but the catalog owns it - use \"catalog:\" (catalog pins ${catalog[dep]})`,\n });\n }\n }\n\n // A preset is not a published manifest: its markers are resolved by the release, and the library kind gives a\n // name one catalog range, so a lib's peer takes the same marker as its dev dependency.\n const isPreset = presetPrefix !== undefined && file.startsWith(presetPrefix);\n\n // A peer resolved from the catalog would collapse to the exact install range, losing the looseness that makes it a peer.\n const peerEntries = Object.entries(json.peerDependencies ?? {});\n\n for (const [dep, range] of peerEntries) {\n if (range !== 'catalog:' || isPreset) {\n continue;\n }\n\n problems.push({\n file,\n message: `peerDependencies.${dep} uses \"catalog:\" - a peer range must stay hand-authored and looser than the install range`,\n });\n }\n }\n\n // A catalog entry nobody references is dead weight that will drift out of date unnoticed.\n const catalogKeys = Object.keys(catalog);\n\n for (const dep of catalogKeys) {\n if (referenced.has(dep)) {\n continue;\n }\n\n problems.push({\n file: 'package.json',\n message: `catalog entry \"${dep}\" is referenced by no workspace - drop it`,\n });\n }\n\n return { problems, entries: catalogKeys.length, referenced: referenced.size, noCatalog: false };\n }\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"module-cycles.js","sourceRoot":"","sources":["../../src/checks/module-cycles.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Lists import cycles in a built ESM directory. bun turns every member of a\n * cycle into a lazy initializer, and a barrel `export *` over such a member\n * can leave its exports undefined. Usage: devz check module-cycles packages/helpers/dist/esm [--max 0]\n */\nimport { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';\nimport { dirname, join, relative, resolve } from 'node:path';\nimport { parseArgs } from 'node:util';\n\n// Only match import/export statements at the start of a line (after optional\n// whitespace), ignoring matches inside comments or string literals.\nconst IMPORT_PATTERN = /^\\s*(?:import|export)\\s[^;]*?from\\s+'([^']+)'|^\\s*import\\s+'([^']+)'/gm;\n\nexport class ModuleGraph {\n private constructor(\n private readonly root: string,\n private readonly edges: Map<string, string[]>,\n ) {}\n\n static fromDirectory(opts: { dir: string }): ModuleGraph {\n const root = resolve(opts.dir);\n const files = ModuleGraph.walk({ dir: root });\n const edges = new Map<string, string[]>();\n\n for (const file of files) {\n const text = readFileSync(file, 'utf8');\n const targets = new Set<string>();\n const matches = text.matchAll(IMPORT_PATTERN);\n for (const match of matches) {\n const target = ModuleGraph.resolveRelative({\n from: file,\n specifier: match[1] ?? match[2],\n });\n if (target) {\n targets.add(target);\n }\n }\n edges.set(file, [...targets]);\n }\n\n return new ModuleGraph(root, edges);\n }\n\n /** Strongly connected components with more than one member, as repo-relative paths. */\n cycles(): string[][] {\n let index = 0;\n const stack: string[] = [];\n const onStack = new Set<string>();\n const indices = new Map<string, number>();\n const lowLinks = new Map<string, number>();\n const found: string[][] = [];\n\n const visit = (node: string): void => {\n indices.set(node, index);\n lowLinks.set(node, index);\n index++;\n stack.push(node);\n onStack.add(node);\n\n const nexts = this.edges.get(node) ?? [];\n for (const next of nexts) {\n if (!indices.has(next)) {\n visit(next);\n lowLinks.set(node, Math.min(lowLinks.get(node)!, lowLinks.get(next)!));\n } else if (onStack.has(next)) {\n lowLinks.set(node, Math.min(lowLinks.get(node)!, indices.get(next)!));\n }\n }\n\n if (lowLinks.get(node) !== indices.get(node)) {\n return;\n }\n const component: string[] = [];\n let member: string;\n do {\n member = stack.pop()!;\n onStack.delete(member);\n component.push(relative(this.root, member));\n } while (member !== node);\n if (component.length > 1) {\n found.push(component.sort());\n }\n };\n\n const edgesKeys = this.edges.keys();\n for (const node of edgesKeys) {\n if (!indices.has(node)) {\n visit(node);\n }\n }\n\n return found;\n }\n\n private static walk(opts: { dir: string }): string[] {\n const out: string[] = [];\n const dirEntries = readdirSync(opts.dir);\n for (const name of dirEntries) {\n const full = join(opts.dir, name);\n if (statSync(full).isDirectory()) {\n out.push(...ModuleGraph.walk({ dir: full }));\n } else if (full.endsWith('.js')) {\n out.push(full);\n }\n }\n return out;\n }\n\n private static resolveRelative(opts: { from: string; specifier: string }): string | undefined {\n if (!opts.specifier.startsWith('.')) {\n return undefined;\n }\n const base = resolve(dirname(opts.from), opts.specifier);\n const candidates = [base, `${base}.js`, join(base, 'index.js')];\n for (const candidate of candidates) {\n if (existsSync(candidate) && statSync(candidate).isFile()) {\n return candidate;\n }\n }\n return undefined;\n }\n}\n\nexport class ModuleCyclesCheck {\n /**\n * `devz check module-cycles <dir> [--max <n>]`: prints the cycles of a built ESM directory and\n * returns 1 when there are more than `max` (no bound by default), 2 on a usage error.\n */\n static run(opts: {\n args: string[];\n print: (line: string) => void;\n printError: (line: string) => void;\n }): number {\n const { args, print, printError } = opts;\n const { positionals, values } = parseArgs({\n args,\n allowPositionals: true,\n options: { max: { type: 'string' }, dist: { type: 'string' } },\n });\n const dir = values.dist ?? positionals[0];\n const usage = 'usage: devz check module-cycles <built ESM dir> [--max <n>]';\n if (!dir) {\n printError(usage);\n return 2;\n }\n\n let max = Number.POSITIVE_INFINITY;\n if (values.max !== undefined) {\n if (!/^\\d+$/.test(values.max)) {\n printError(usage);\n return 2;\n }\n max = Number(values.max);\n }\n\n const cycles = ModuleGraph.fromDirectory({ dir }).cycles();\n print(`${dir}: ${cycles.length} cycle(s)`);\n for (const cycle of cycles) {\n print(` - ${cycle.join(' <-> ')}`);\n }\n\n return cycles.length > max ? 1 : 0;\n }\n}\n"]}
1
+ {"version":3,"file":"module-cycles.js","sourceRoot":"","sources":["../../src/checks/module-cycles.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Lists import cycles in a built ESM directory. bun turns every member of a\n * cycle into a lazy initializer, and a barrel `export *` over such a member\n * can leave its exports undefined. Usage: devz check module-cycles packages/helpers/dist/esm [--max 0]\n */\nimport { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';\nimport { dirname, join, relative, resolve } from 'node:path';\nimport { parseArgs } from 'node:util';\n\n// Only match import/export statements at the start of a line (after optional\n// whitespace), ignoring matches inside comments or string literals.\nconst IMPORT_PATTERN = /^\\s*(?:import|export)\\s[^;]*?from\\s+'([^']+)'|^\\s*import\\s+'([^']+)'/gm;\n\nexport class ModuleGraph {\n private constructor(\n private readonly root: string,\n private readonly edges: Map<string, string[]>,\n ) {}\n\n static fromDirectory(opts: { dir: string }): ModuleGraph {\n const root = resolve(opts.dir);\n const files = ModuleGraph.walk({ dir: root });\n const edges = new Map<string, string[]>();\n\n for (const file of files) {\n const text = readFileSync(file, 'utf8');\n const targets = new Set<string>();\n const matches = text.matchAll(IMPORT_PATTERN);\n\n for (const match of matches) {\n const target = ModuleGraph.resolveRelative({\n from: file,\n specifier: match[1] ?? match[2],\n });\n\n if (target) {\n targets.add(target);\n }\n }\n\n edges.set(file, [...targets]);\n }\n\n return new ModuleGraph(root, edges);\n }\n\n /** Strongly connected components with more than one member, as repo-relative paths. */\n cycles(): string[][] {\n let index = 0;\n const stack: string[] = [];\n const onStack = new Set<string>();\n const indices = new Map<string, number>();\n const lowLinks = new Map<string, number>();\n const found: string[][] = [];\n\n const visit = (node: string): void => {\n indices.set(node, index);\n lowLinks.set(node, index);\n index++;\n stack.push(node);\n onStack.add(node);\n\n const nexts = this.edges.get(node) ?? [];\n\n for (const next of nexts) {\n if (!indices.has(next)) {\n visit(next);\n lowLinks.set(node, Math.min(lowLinks.get(node)!, lowLinks.get(next)!));\n } else if (onStack.has(next)) {\n lowLinks.set(node, Math.min(lowLinks.get(node)!, indices.get(next)!));\n }\n }\n\n if (lowLinks.get(node) !== indices.get(node)) {\n return;\n }\n\n const component: string[] = [];\n let member: string;\n\n do {\n member = stack.pop()!;\n onStack.delete(member);\n component.push(relative(this.root, member));\n } while (member !== node);\n\n if (component.length > 1) {\n found.push(component.sort());\n }\n };\n\n const edgesKeys = this.edges.keys();\n\n for (const node of edgesKeys) {\n if (!indices.has(node)) {\n visit(node);\n }\n }\n\n return found;\n }\n\n private static walk(opts: { dir: string }): string[] {\n const out: string[] = [];\n const dirEntries = readdirSync(opts.dir);\n\n for (const name of dirEntries) {\n const full = join(opts.dir, name);\n\n if (statSync(full).isDirectory()) {\n out.push(...ModuleGraph.walk({ dir: full }));\n } else if (full.endsWith('.js')) {\n out.push(full);\n }\n }\n\n return out;\n }\n\n private static resolveRelative(opts: { from: string; specifier: string }): string | undefined {\n if (!opts.specifier.startsWith('.')) {\n return undefined;\n }\n\n const base = resolve(dirname(opts.from), opts.specifier);\n const candidates = [base, `${base}.js`, join(base, 'index.js')];\n\n for (const candidate of candidates) {\n if (existsSync(candidate) && statSync(candidate).isFile()) {\n return candidate;\n }\n }\n\n return undefined;\n }\n}\n\nexport class ModuleCyclesCheck {\n /**\n * `devz check module-cycles <dir> [--max <n>]`: prints the cycles of a built ESM directory and\n * returns 1 when there are more than `max` (no bound by default), 2 on a usage error.\n */\n static run(opts: {\n args: string[];\n print: (line: string) => void;\n printError: (line: string) => void;\n }): number {\n const { args, print, printError } = opts;\n const { positionals, values } = parseArgs({\n args,\n allowPositionals: true,\n options: { max: { type: 'string' }, dist: { type: 'string' } },\n });\n const dir = values.dist ?? positionals[0];\n const usage = 'usage: devz check module-cycles <built ESM dir> [--max <n>]';\n\n if (!dir) {\n printError(usage);\n\n return 2;\n }\n\n let max = Number.POSITIVE_INFINITY;\n\n if (values.max !== undefined) {\n if (!/^\\d+$/.test(values.max)) {\n printError(usage);\n\n return 2;\n }\n\n max = Number(values.max);\n }\n\n const cycles = ModuleGraph.fromDirectory({ dir }).cycles();\n print(`${dir}: ${cycles.length} cycle(s)`);\n\n for (const cycle of cycles) {\n print(` - ${cycle.join(' <-> ')}`);\n }\n\n return cycles.length > max ? 1 : 0;\n }\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"manifest.js","sourceRoot":"","sources":["../../../src/checks/purity/manifest.ts"],"names":[],"mappings":"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","sourcesContent":["import { readFileSync } from 'node:fs';\nimport { join } from 'node:path';\n\nexport interface IPurityEntry {\n label: string;\n package: string;\n /** Repository-relative path to the built file the `exports` map points at. */\n entry: string;\n /**\n * Specifiers to pass through as `bun build --external`. Only for a third-party dependency whose\n * OWN packaging is out of this gate's remit - e.g. one that relies on its `package.json` `browser`\n * field remap, which the downstream app's bundler (Vite, ...) honours and `bun build` does not.\n * Never for the repository's own source: an external hides that file from the module graph\n * entirely, so using it on our own code would let a real leak through unseen rather than prove\n * anything pure. Enforced by {@link PurityManifest.assertNoWorkspaceExternal}, not left to this comment alone.\n */\n external?: string[];\n /**\n * This row is EXPECTED to be impure, so failing is its passing state. Derived from\n * {@link IPurityClaim.impure} - never authored on a row directly.\n */\n expectImpure?: boolean;\n}\n\n/**\n * A package whose published entry points carry a browser-purity claim.\n *\n * The ROW SET IS NEVER AUTHORED. It is read from the package's own `exports` map, so a sub-path\n * added to a manifest is probed the moment it is published.\n */\nexport interface IPurityClaim {\n /** Directory under `packages/`, and the token `devz check purity <token>` filters on. */\n package: string;\n /**\n * Sub-paths (as spelled in `exports`) when the claim covers only PART of the published surface.\n * Omit it and the whole surface is claimed. Every sub-path named here must still exist in\n * `exports`; a rename fails the derivation loudly rather than quietly dropping a row.\n */\n subpaths?: string[];\n /** `external` for one sub-path - see {@link IPurityEntry.external}. Keyed as spelled in `exports`. */\n external?: Record<string, string[]>;\n /**\n * Entries that CANNOT be browser-pure, keyed as spelled in `exports`, valued by the conditions\n * that are impure. The waiver is exact in BOTH directions: a listed entry that turns out to be\n * pure fails as loudly as an unlisted one that is not. Deriving still owns the ROW SET, so a\n * sub-path added later is claimed pure by default and has to earn its line here.\n */\n impure?: Record<string, string[]>;\n}\n\n/** What a repository may tune about the engine; everything else is the engine's contract. */\nexport interface IPurityOptions {\n /**\n * When set, every claimed sub-path must publish its runtime entries below this prefix (for\n * example `./dist/esm/`): a browser bundler needs an ES module, so a CommonJS-only entry makes\n * the claim unconsumable. Omit it for a repository that publishes a single build.\n */\n esmEntryPrefix?: string;\n\n /** Package-name prefix of this repository's own packages; an `external` may never start with it. Default `@venizia/`. */\n workspaceScope?: string;\n}\n\nconst DEFAULT_WORKSPACE_SCOPE = '@venizia/';\n\n/** The one place this engine builds an `Error`: `getError` would make dev-configs depend on inversion, which depends on dev-configs. */\n// eslint-disable-next-line venizia/no-new-error -- getError would be a dev-configs -> inversion cycle\nconst purityError = (message: string): Error => new Error(message);\n\n/** `types` describes the entry, it is not a runtime target; `./package.json` is not code. */\nconst NON_RUNTIME_CONDITION = 'types';\nconst NON_CODE_SUBPATH = './package.json';\n\ntype TExportsTarget = string | Record<string, string>;\n\nconst readExportsMap = (opts: {\n root: string;\n package: string;\n}): Record<string, TExportsTarget> => {\n const manifestPath = join(opts.root, 'packages', opts.package, 'package.json');\n const manifest = JSON.parse(readFileSync(manifestPath, 'utf8')) as {\n exports?: Record<string, TExportsTarget>;\n };\n\n if (!manifest.exports) {\n throw purityError(\n `[purity][manifest] '${opts.package}' claims browser purity but publishes no \\`exports\\` map ` +\n `| There is nothing to derive rows from - remove the claim or add the map`,\n );\n }\n\n return manifest.exports;\n};\n\n/**\n * `exports` may spell one sub-path as several conditions pointing at different builds - the dual\n * CJS/ESM packages do exactly that - and each is a separately resolvable file that a consumer can\n * end up importing, so each is probed. `default` normally repeats `require`, so files are deduped\n * and the FIRST condition naming a file is the one that labels it.\n */\nconst listConditionTargets = (\n target: TExportsTarget,\n): Array<{ condition: string; file: string }> => {\n if (typeof target === 'string') {\n return [{ condition: 'default', file: target }];\n }\n\n const seen = new Set<string>();\n const targets: Array<{ condition: string; file: string }> = [];\n\n const targetEntries = Object.entries(target);\n for (const [condition, file] of targetEntries) {\n if (condition === NON_RUNTIME_CONDITION || seen.has(file)) {\n continue;\n }\n\n seen.add(file);\n targets.push({ condition, file });\n }\n\n return targets;\n};\n\nconst buildLabel = (opts: {\n package: string;\n subpath: string;\n condition: string;\n isSoleTarget: boolean;\n}): string => {\n const { package: packageName, subpath, condition, isSoleTarget } = opts;\n const suffix = subpath === '.' ? '' : subpath.slice(1);\n const base = `${packageName}${suffix}`;\n\n return isSoleTarget ? base : `${base} [${condition}]`;\n};\n\n/** Derives the rows of the purity gate from the `exports` map of each claimed package, then runs the guards on them. */\nexport class PurityManifest {\n /**\n * A code-level guard, not just the JSDoc on `external`: it is an escape hatch from this gate, and a\n * hatch with no lock is one bad edit away from hiding a real leak - `external: ['@venizia/ignis-helpers']`\n * on a red entry would silently turn it green by removing the exact root-barrel import this gate\n * exists to catch from the module graph, rather than proving it absent.\n *\n * A plain `Error`, not `getError`: the engine runs from the repository root through `devz`, outside\n * every workspace package, and has no logger to resolve.\n */\n static assertNoWorkspaceExternal(opts: {\n manifest: IPurityEntry[];\n workspaceScope?: string;\n }): IPurityEntry[] {\n const { manifest, workspaceScope = DEFAULT_WORKSPACE_SCOPE } = opts;\n\n for (const row of manifest) {\n const specifiers = row.external ?? [];\n for (const specifier of specifiers) {\n if (!specifier.startsWith(workspaceScope)) {\n continue;\n }\n\n throw purityError(\n `[purity][manifest] Entry '${row.label}' externalises '${specifier}', a workspace package ` +\n `| \\`external\\` may only exempt THIRD-PARTY packaging, never the repository's own code - doing so ` +\n `would hide the exact leak this gate exists to catch`,\n );\n }\n }\n\n return manifest;\n }\n\n /**\n * Public for the tests, which have to drive a claim that no longer matches its package's\n * `exports` map - the staleness guard is the whole point of deriving, and it cannot be asserted\n * through a built manifest, which by construction is never stale.\n */\n static deriveEntries(opts: { claim: IPurityClaim; root: string }): IPurityEntry[] {\n const { claim, root } = opts;\n const exportsMap = readExportsMap({ root, package: claim.package });\n const published = Object.keys(exportsMap).filter((subpath) => subpath !== NON_CODE_SUBPATH);\n\n const subpathsList = claim.subpaths ?? [];\n for (const subpath of subpathsList) {\n if (published.includes(subpath)) {\n continue;\n }\n\n throw purityError(\n `[purity][manifest] '${claim.package}' claims '${subpath}', which its \\`exports\\` map no longer ` +\n `publishes | Published: ${published.join(', ')} - a renamed sub-path must not silently drop its row`,\n );\n }\n\n const claimExternalKeys = Object.keys(claim.external ?? {});\n for (const subpath of claimExternalKeys) {\n if (published.includes(subpath)) {\n continue;\n }\n\n throw purityError(\n `[purity][manifest] '${claim.package}' declares \\`external\\` for '${subpath}', which its ` +\n `\\`exports\\` map no longer publishes | An external that outlives its entry is a waiver ` +\n `nobody is reading`,\n );\n }\n\n const claimed = claim.subpaths ?? published;\n\n const claimImpureKeys = Object.keys(claim.impure ?? {});\n for (const subpath of claimImpureKeys) {\n if (claimed.includes(subpath)) {\n continue;\n }\n\n throw purityError(\n `[purity][manifest] '${claim.package}' waives '${subpath}' as impure, but that sub-path is not ` +\n `claimed | Claimed: ${claimed.join(', ')} - a waiver against nothing is one nobody is reading`,\n );\n }\n\n const entries: IPurityEntry[] = [];\n\n for (const subpath of claimed) {\n const targets = listConditionTargets(exportsMap[subpath]!);\n\n const conditions = claim.impure?.[subpath] ?? [];\n for (const condition of conditions) {\n if (targets.some((target) => target.condition === condition)) {\n continue;\n }\n\n throw purityError(\n `[purity][manifest] '${claim.package}' waives '${subpath}' under condition '${condition}', ` +\n `which its \\`exports\\` map does not publish | Published: ${targets.map((t) => t.condition).join(', ')}`,\n );\n }\n\n for (const { condition, file } of targets) {\n entries.push({\n label: buildLabel({\n package: claim.package,\n subpath,\n condition,\n isSoleTarget: targets.length === 1,\n }),\n package: claim.package,\n entry: join('packages', claim.package, file),\n ...(claim.external?.[subpath] ? { external: claim.external[subpath] } : {}),\n ...(claim.impure?.[subpath]?.includes(condition) ? { expectImpure: true } : {}),\n });\n }\n }\n\n return entries;\n }\n\n /**\n * A browser claim is only honest if a browser bundler can actually consume the entry. A CommonJS\n * entry forces every consumer into a workaround, so the claim carries the obligation: every claimed\n * sub-path must publish its runtime entries below `esmEntryPrefix`. Checked here rather than trusted\n * to a comment, for the same reason as {@link PurityManifest.assertNoWorkspaceExternal} - the failure is otherwise\n * invisible until someone tries to bundle the package downstream.\n */\n static assertBrowserEsmEntry(opts: {\n claims: IPurityClaim[];\n root: string;\n esmEntryPrefix: string;\n }): IPurityClaim[] {\n const { claims, root, esmEntryPrefix } = opts;\n\n for (const claim of claims) {\n const exportsMap = readExportsMap({ root, package: claim.package });\n const claimed =\n claim.subpaths ?? Object.keys(exportsMap).filter((s) => s !== NON_CODE_SUBPATH);\n\n for (const subpath of claimed) {\n const target = exportsMap[subpath];\n\n const runtimeTargets =\n typeof target === 'object' && target !== null\n ? Object.entries(target)\n .filter(([condition]) => condition !== NON_RUNTIME_CONDITION)\n .map(([, file]) => file)\n : [];\n\n if (\n runtimeTargets.length > 0 &&\n runtimeTargets.every((file) => file.startsWith(esmEntryPrefix))\n ) {\n continue;\n }\n\n throw purityError(\n `[purity][manifest] '${claim.package}' claims browser purity for '${subpath}' but its runtime ` +\n `entry is not in ${esmEntryPrefix.replace(/^\\.\\//, '')} | A browser bundler needs an ES module, so the claim ` +\n `cannot be consumed - build the package as ES modules`,\n );\n }\n }\n\n return claims;\n }\n\n /** The rows of a whole repository: every claim derived from its package's `exports`, then the guards. */\n static build(opts: {\n claims: IPurityClaim[];\n root: string;\n options?: IPurityOptions;\n }): IPurityEntry[] {\n const { claims, root, options = {} } = opts;\n const { esmEntryPrefix, workspaceScope = DEFAULT_WORKSPACE_SCOPE } = options;\n\n const checked =\n esmEntryPrefix === undefined\n ? claims\n : PurityManifest.assertBrowserEsmEntry({ claims, root, esmEntryPrefix });\n\n return PurityManifest.assertNoWorkspaceExternal({\n manifest: checked.flatMap((claim) => PurityManifest.deriveEntries({ claim, root })),\n workspaceScope,\n });\n }\n}\n"]}
1
+ {"version":3,"file":"manifest.js","sourceRoot":"","sources":["../../../src/checks/purity/manifest.ts"],"names":[],"mappings":"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","sourcesContent":["import { readFileSync } from 'node:fs';\nimport { join } from 'node:path';\n\nexport interface IPurityEntry {\n label: string;\n package: string;\n /** Repository-relative path to the built file the `exports` map points at. */\n entry: string;\n /**\n * Specifiers to pass through as `bun build --external`. Only for a third-party dependency whose\n * OWN packaging is out of this gate's remit - e.g. one that relies on its `package.json` `browser`\n * field remap, which the downstream app's bundler (Vite, ...) honours and `bun build` does not.\n * Never for the repository's own source: an external hides that file from the module graph\n * entirely, so using it on our own code would let a real leak through unseen rather than prove\n * anything pure. Enforced by {@link PurityManifest.assertNoWorkspaceExternal}, not left to this comment alone.\n */\n external?: string[];\n /**\n * This row is EXPECTED to be impure, so failing is its passing state. Derived from\n * {@link IPurityClaim.impure} - never authored on a row directly.\n */\n expectImpure?: boolean;\n}\n\n/**\n * A package whose published entry points carry a browser-purity claim.\n *\n * The ROW SET IS NEVER AUTHORED. It is read from the package's own `exports` map, so a sub-path\n * added to a manifest is probed the moment it is published.\n */\nexport interface IPurityClaim {\n /** Directory under `packages/`, and the token `devz check purity <token>` filters on. */\n package: string;\n /**\n * Sub-paths (as spelled in `exports`) when the claim covers only PART of the published surface.\n * Omit it and the whole surface is claimed. Every sub-path named here must still exist in\n * `exports`; a rename fails the derivation loudly rather than quietly dropping a row.\n */\n subpaths?: string[];\n /** `external` for one sub-path - see {@link IPurityEntry.external}. Keyed as spelled in `exports`. */\n external?: Record<string, string[]>;\n /**\n * Entries that CANNOT be browser-pure, keyed as spelled in `exports`, valued by the conditions\n * that are impure. The waiver is exact in BOTH directions: a listed entry that turns out to be\n * pure fails as loudly as an unlisted one that is not. Deriving still owns the ROW SET, so a\n * sub-path added later is claimed pure by default and has to earn its line here.\n */\n impure?: Record<string, string[]>;\n}\n\n/** What a repository may tune about the engine; everything else is the engine's contract. */\nexport interface IPurityOptions {\n /**\n * When set, every claimed sub-path must publish its runtime entries below this prefix (for\n * example `./dist/esm/`): a browser bundler needs an ES module, so a CommonJS-only entry makes\n * the claim unconsumable. Omit it for a repository that publishes a single build.\n */\n esmEntryPrefix?: string;\n\n /** Package-name prefix of this repository's own packages; an `external` may never start with it. Default `@venizia/`. */\n workspaceScope?: string;\n}\n\nconst DEFAULT_WORKSPACE_SCOPE = '@venizia/';\n\n/** The one place this engine builds an `Error`: `getError` would make dev-configs depend on inversion, which depends on dev-configs. */\n// eslint-disable-next-line venizia/no-new-error -- getError would be a dev-configs -> inversion cycle\nconst purityError = (message: string): Error => new Error(message);\n\n/** `types` describes the entry, it is not a runtime target; `./package.json` is not code. */\nconst NON_RUNTIME_CONDITION = 'types';\nconst NON_CODE_SUBPATH = './package.json';\n\ntype TExportsTarget = string | Record<string, string>;\n\nconst readExportsMap = (opts: {\n root: string;\n package: string;\n}): Record<string, TExportsTarget> => {\n const manifestPath = join(opts.root, 'packages', opts.package, 'package.json');\n const manifest = JSON.parse(readFileSync(manifestPath, 'utf8')) as {\n exports?: Record<string, TExportsTarget>;\n };\n\n if (!manifest.exports) {\n throw purityError(\n `[purity][manifest] '${opts.package}' claims browser purity but publishes no \\`exports\\` map ` +\n `| There is nothing to derive rows from - remove the claim or add the map`,\n );\n }\n\n return manifest.exports;\n};\n\n/**\n * `exports` may spell one sub-path as several conditions pointing at different builds - the dual\n * CJS/ESM packages do exactly that - and each is a separately resolvable file that a consumer can\n * end up importing, so each is probed. `default` normally repeats `require`, so files are deduped\n * and the FIRST condition naming a file is the one that labels it.\n */\nconst listConditionTargets = (\n target: TExportsTarget,\n): Array<{ condition: string; file: string }> => {\n if (typeof target === 'string') {\n return [{ condition: 'default', file: target }];\n }\n\n const seen = new Set<string>();\n const targets: Array<{ condition: string; file: string }> = [];\n\n const targetEntries = Object.entries(target);\n\n for (const [condition, file] of targetEntries) {\n if (condition === NON_RUNTIME_CONDITION || seen.has(file)) {\n continue;\n }\n\n seen.add(file);\n targets.push({ condition, file });\n }\n\n return targets;\n};\n\nconst buildLabel = (opts: {\n package: string;\n subpath: string;\n condition: string;\n isSoleTarget: boolean;\n}): string => {\n const { package: packageName, subpath, condition, isSoleTarget } = opts;\n const suffix = subpath === '.' ? '' : subpath.slice(1);\n const base = `${packageName}${suffix}`;\n\n return isSoleTarget ? base : `${base} [${condition}]`;\n};\n\n/** Derives the rows of the purity gate from the `exports` map of each claimed package, then runs the guards on them. */\nexport class PurityManifest {\n /**\n * A code-level guard, not just the JSDoc on `external`: it is an escape hatch from this gate, and a\n * hatch with no lock is one bad edit away from hiding a real leak - `external: ['@venizia/ignis-helpers']`\n * on a red entry would silently turn it green by removing the exact root-barrel import this gate\n * exists to catch from the module graph, rather than proving it absent.\n *\n * A plain `Error`, not `getError`: the engine runs from the repository root through `devz`, outside\n * every workspace package, and has no logger to resolve.\n */\n static assertNoWorkspaceExternal(opts: {\n manifest: IPurityEntry[];\n workspaceScope?: string;\n }): IPurityEntry[] {\n const { manifest, workspaceScope = DEFAULT_WORKSPACE_SCOPE } = opts;\n\n for (const row of manifest) {\n const specifiers = row.external ?? [];\n\n for (const specifier of specifiers) {\n if (!specifier.startsWith(workspaceScope)) {\n continue;\n }\n\n throw purityError(\n `[purity][manifest] Entry '${row.label}' externalises '${specifier}', a workspace package ` +\n `| \\`external\\` may only exempt THIRD-PARTY packaging, never the repository's own code - doing so ` +\n `would hide the exact leak this gate exists to catch`,\n );\n }\n }\n\n return manifest;\n }\n\n /**\n * Public for the tests, which have to drive a claim that no longer matches its package's\n * `exports` map - the staleness guard is the whole point of deriving, and it cannot be asserted\n * through a built manifest, which by construction is never stale.\n */\n static deriveEntries(opts: { claim: IPurityClaim; root: string }): IPurityEntry[] {\n const { claim, root } = opts;\n const exportsMap = readExportsMap({ root, package: claim.package });\n const published = Object.keys(exportsMap).filter((subpath) => subpath !== NON_CODE_SUBPATH);\n\n const subpathsList = claim.subpaths ?? [];\n\n for (const subpath of subpathsList) {\n if (published.includes(subpath)) {\n continue;\n }\n\n throw purityError(\n `[purity][manifest] '${claim.package}' claims '${subpath}', which its \\`exports\\` map no longer ` +\n `publishes | Published: ${published.join(', ')} - a renamed sub-path must not silently drop its row`,\n );\n }\n\n const claimExternalKeys = Object.keys(claim.external ?? {});\n\n for (const subpath of claimExternalKeys) {\n if (published.includes(subpath)) {\n continue;\n }\n\n throw purityError(\n `[purity][manifest] '${claim.package}' declares \\`external\\` for '${subpath}', which its ` +\n `\\`exports\\` map no longer publishes | An external that outlives its entry is a waiver ` +\n `nobody is reading`,\n );\n }\n\n const claimed = claim.subpaths ?? published;\n\n const claimImpureKeys = Object.keys(claim.impure ?? {});\n\n for (const subpath of claimImpureKeys) {\n if (claimed.includes(subpath)) {\n continue;\n }\n\n throw purityError(\n `[purity][manifest] '${claim.package}' waives '${subpath}' as impure, but that sub-path is not ` +\n `claimed | Claimed: ${claimed.join(', ')} - a waiver against nothing is one nobody is reading`,\n );\n }\n\n const entries: IPurityEntry[] = [];\n\n for (const subpath of claimed) {\n const targets = listConditionTargets(exportsMap[subpath]!);\n\n const conditions = claim.impure?.[subpath] ?? [];\n\n for (const condition of conditions) {\n if (targets.some((target) => target.condition === condition)) {\n continue;\n }\n\n throw purityError(\n `[purity][manifest] '${claim.package}' waives '${subpath}' under condition '${condition}', ` +\n `which its \\`exports\\` map does not publish | Published: ${targets.map((t) => t.condition).join(', ')}`,\n );\n }\n\n for (const { condition, file } of targets) {\n entries.push({\n label: buildLabel({\n package: claim.package,\n subpath,\n condition,\n isSoleTarget: targets.length === 1,\n }),\n package: claim.package,\n entry: join('packages', claim.package, file),\n ...(claim.external?.[subpath] ? { external: claim.external[subpath] } : {}),\n ...(claim.impure?.[subpath]?.includes(condition) ? { expectImpure: true } : {}),\n });\n }\n }\n\n return entries;\n }\n\n /**\n * A browser claim is only honest if a browser bundler can actually consume the entry. A CommonJS\n * entry forces every consumer into a workaround, so the claim carries the obligation: every claimed\n * sub-path must publish its runtime entries below `esmEntryPrefix`. Checked here rather than trusted\n * to a comment, for the same reason as {@link PurityManifest.assertNoWorkspaceExternal} - the failure is otherwise\n * invisible until someone tries to bundle the package downstream.\n */\n static assertBrowserEsmEntry(opts: {\n claims: IPurityClaim[];\n root: string;\n esmEntryPrefix: string;\n }): IPurityClaim[] {\n const { claims, root, esmEntryPrefix } = opts;\n\n for (const claim of claims) {\n const exportsMap = readExportsMap({ root, package: claim.package });\n const claimed =\n claim.subpaths ?? Object.keys(exportsMap).filter((s) => s !== NON_CODE_SUBPATH);\n\n for (const subpath of claimed) {\n const target = exportsMap[subpath];\n\n const runtimeTargets =\n typeof target === 'object' && target !== null\n ? Object.entries(target)\n .filter(([condition]) => condition !== NON_RUNTIME_CONDITION)\n .map(([, file]) => file)\n : [];\n\n if (\n runtimeTargets.length > 0 &&\n runtimeTargets.every((file) => file.startsWith(esmEntryPrefix))\n ) {\n continue;\n }\n\n throw purityError(\n `[purity][manifest] '${claim.package}' claims browser purity for '${subpath}' but its runtime ` +\n `entry is not in ${esmEntryPrefix.replace(/^\\.\\//, '')} | A browser bundler needs an ES module, so the claim ` +\n `cannot be consumed - build the package as ES modules`,\n );\n }\n }\n\n return claims;\n }\n\n /** The rows of a whole repository: every claim derived from its package's `exports`, then the guards. */\n static build(opts: {\n claims: IPurityClaim[];\n root: string;\n options?: IPurityOptions;\n }): IPurityEntry[] {\n const { claims, root, options = {} } = opts;\n const { esmEntryPrefix, workspaceScope = DEFAULT_WORKSPACE_SCOPE } = options;\n\n const checked =\n esmEntryPrefix === undefined\n ? claims\n : PurityManifest.assertBrowserEsmEntry({ claims, root, esmEntryPrefix });\n\n return PurityManifest.assertNoWorkspaceExternal({\n manifest: checked.flatMap((claim) => PurityManifest.deriveEntries({ claim, root })),\n workspaceScope,\n });\n }\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"probe.js","sourceRoot":"","sources":["../../../src/checks/purity/probe.ts"],"names":[],"mappings":"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","sourcesContent":["import { mkdir, rm } from 'node:fs/promises';\nimport { builtinModules } from 'node:module';\nimport { join } from 'node:path';\n\nexport interface IPurityResult {\n entry: string;\n ok: boolean;\n sizeBytes: number;\n builtins: string[];\n globals: string[];\n /** Node-global reads that cannot throw in a browser. Reported, never fatal. */\n guardedGlobals: string[];\n /**\n * Specifiers this probe run excluded from its own module graph. Always echoed back, on both the\n * passing and failing path - an entry that externalises something is measuring less than an entry\n * that does not, and that must stay visible next to the result, not be a silent input.\n */\n external: string[];\n buildError?: string;\n}\n\n/** Where the probe keeps its metafile, relative to the `cwd` it is given: the repository's own scratch, never the system temp. */\nexport const PURITY_SCRATCH_DIR = '.agents/local/tmp';\n\n/**\n * A browser entry must never reach `@venizia/dev-configs`: it is the Bun-only tooling package (its\n * `./testing` entry imports `bun:test`). Matches the workspace folder (not its own test fixtures) and the installed copy.\n */\nconst DEV_CONFIGS_PATH =\n /(^|\\/)(packages\\/dev-configs\\/(?!src\\/__tests__\\/)|node_modules\\/@venizia\\/dev-configs\\/)/;\n\n/** What a matched global means: ERROR is fatal, GUARDED is reported and never fatal. */\nexport class ProbeSeverities {\n static readonly ERROR = 'error';\n static readonly GUARDED = 'guarded';\n\n static readonly SCHEME_SET = new Set<string>([this.ERROR, this.GUARDED]);\n\n static isValid(value: string): value is TProbeSeverity {\n return this.SCHEME_SET.has(value);\n }\n}\n\nexport type TProbeSeverity = Extract<\n (typeof ProbeSeverities)[keyof typeof ProbeSeverities],\n string\n>;\n\n/**\n * Node globals survive verbatim into a `target: 'browser'` bundle, so a gate reading only module\n * specifiers reports clean on `process.env.NODE_ENV`. Each pattern must be anchored: a bundle\n * legitimately contains identifiers like `processQueue`. A lookbehind that rejects a preceding dot\n * also rejects `globalThis.process`, so every access path needs its own pattern.\n *\n * `severity` separates a read that throws from one that cannot. Optional chaining is not itself the\n * difference: `process?.env` throws a ReferenceError when `process` is a free identifier, while\n * `globalThis.process?.env` is an ordinary property read on an object that always exists.\n *\n * `guarded` means \"reported, never fatal\". Bare `process?.` sits there because this probe matches\n * bundled text and has no scope information: hono's `getColorEnabled` writes\n * `const { process, Deno } = globalThis;` and then `process?.env`, which is a local binding and\n * perfectly safe, and no regex can tell that apart from a free global. Failing on it would make the\n * gate cry wolf on a dependency; the unguarded `process.` and `globalThis.process.` forms stay\n * fatal, and those are the ones that are unambiguously browser-breaking.\n *\n * Exported for `probe.test.ts` to assert against directly, alongside {@link PurityProbe.stripBundlerPathComments}.\n */\nexport const GLOBAL_PATTERNS: Array<{\n name: string;\n pattern: RegExp;\n severity: TProbeSeverity;\n}> = [\n { name: 'process', pattern: /(?<![A-Za-z0-9_$.])process\\s*\\./, severity: 'error' },\n { name: 'process?.', pattern: /(?<![A-Za-z0-9_$.])process\\s*\\?\\./, severity: 'guarded' },\n {\n name: 'globalThis.process',\n pattern: /(?<![A-Za-z0-9_$.])globalThis\\s*\\.\\s*process\\s*\\./,\n severity: 'error',\n },\n {\n name: 'globalThis.process?.',\n pattern: /(?<![A-Za-z0-9_$.])globalThis\\s*\\.\\s*process\\s*\\?\\./,\n severity: 'guarded',\n },\n {\n name: '__dirname',\n pattern: /(?<![A-Za-z0-9_$.])__dirname(?![A-Za-z0-9_$])/,\n severity: 'error',\n },\n {\n name: '__filename',\n pattern: /(?<![A-Za-z0-9_$.])__filename(?![A-Za-z0-9_$])/,\n severity: 'error',\n },\n {\n name: 'createRequire',\n pattern: /(?<![A-Za-z0-9_$.])createRequire\\s*\\(/,\n severity: 'error',\n },\n { name: 'require(', pattern: /(?<![A-Za-z0-9_$.])require\\s*\\(/, severity: 'error' },\n // Member access only. `typeof Bun !== 'undefined'` is the legitimate runtime probe and never\n // throws; `Bun.serve(...)` in a browser does.\n { name: 'Bun', pattern: /(?<![A-Za-z0-9_$.])Bun\\s*\\./, severity: 'error' },\n];\n\n/**\n * Bun's bundler prefixes every CommonJS-wrapped module with a full-line `// <path>.js` header (this\n * whole workspace emits CJS `dist/*.js` - no package declares `\"type\": \"module\"`). A source path is\n * free-form and can legitimately contain `process` as a substring with no identifier boundary on\n * its left, e.g. this gate's own `transport/in-process.js`: the header reads\n * `// packages/core-worker/dist/transport/in-process.js`, and `process.` there is a filename\n * fragment, not a read of the global. `GLOBAL_PATTERNS` matches bundled TEXT with no scope\n * information, so it cannot tell a path comment from code - stripped before matching.\n */\nconst BUNDLER_PATH_COMMENT = /^\\/\\/ [\\w@./-]+\\.(?:c|m)?js$/gm;\n\ninterface IMetafileImport {\n path: string;\n original?: string;\n external?: boolean;\n}\n\ninterface IMetafileInput {\n imports: IMetafileImport[];\n}\n\ninterface IMetafile {\n inputs: Record<string, IMetafileInput>;\n}\n\n/**\n * `node:module`'s own list, not a hand-maintained one: a bare `import ... from 'fs'` bundles\n * identically to `'node:fs'` (Bun stubs both to an empty object), and the metafile records\n * whichever spelling the source used. Matching by prefix alone misses the bare form entirely.\n */\nconst BUILTIN_MODULE_NAMES = new Set(builtinModules);\n\nconst isBuiltinSpecifier = (specifier: string): boolean => {\n const bareName = specifier.startsWith('node:') ? specifier.slice('node:'.length) : specifier;\n return BUILTIN_MODULE_NAMES.has(bareName);\n};\n\n/**\n * Measured against Bun 1.3.14: `--target=browser` never fails on a `node:` import and never leaves\n * the specifier in the bundled text. An unpolyfillable builtin (`fs`, `child_process`, ...) is\n * stubbed to an empty object; a polyfillable one (`crypto`, `path`, ...) is inlined in full under a\n * `/bun-vfs$$/` virtual path. Either way `exitCode` stays 0 and the specifier never survives into\n * stdout. The `--metafile` module graph is the only place it is preserved.\n */\nconst findBuiltinSpecifiers = (metafile: IMetafile): string[] => {\n const builtins = new Set<string>();\n\n const metafileInputsEntries = Object.entries(metafile.inputs);\n for (const [inputPath, input] of metafileInputsEntries) {\n if (isBuiltinSpecifier(inputPath)) {\n builtins.add(inputPath);\n }\n\n for (const dependency of input.imports) {\n const specifier = dependency.original ?? dependency.path;\n if (isBuiltinSpecifier(specifier)) {\n builtins.add(specifier);\n }\n }\n }\n\n return [...builtins].sort();\n};\n\n/**\n * A real npm dependency bundles in full under this target (measured: `zod` produces zero\n * `external` entries) and a genuinely missing package fails the build outright (nonzero exit,\n * caught before this runs). But a package's own `browser` field can remap a dependency to `false` -\n * the standard convention for disabling a Node-only dependency in browser builds - and Bun honours\n * that remap by leaving the import external with exit 0, whether or not the remapped name is a\n * builtin. An unresolved import in a browser bundle is not safe to wave through either way -\n * UNLESS the manifest entry asked for exactly this specifier via `external`, in which case its\n * presence here is the requested exclusion working, not a leak.\n */\n/**\n * A relative specifier is the package's own file, and a truly unreachable one fails the build with a\n * nonzero exit, which is caught before this runs. Bun's METAFILE nonetheless marks a re-export of the\n * form `export { name } from './x.js'` as `external` while inlining it - measured on\n * `helpers/dist/esm/core.js`: six such specifiers reported external, `Bundled 192 modules`, and the\n * output contains `class BaseHelper` with no bare relative import left in it. Reading the metafile\n * literally there reports a leak the bundle does not have.\n */\nconst isRelativeSpecifier = (specifier: string): boolean => {\n return specifier.startsWith('./') || specifier.startsWith('../');\n};\n\nconst findUnresolvedExternalSpecifiers = (opts: {\n metafile: IMetafile;\n builtins: Set<string>;\n requestedExternal: Set<string>;\n}): string[] => {\n const { metafile, builtins, requestedExternal } = opts;\n const unresolved = new Set<string>();\n\n const isRequestedExternal = (specifier: string): boolean => {\n for (const requested of requestedExternal) {\n if (specifier === requested || specifier.startsWith(`${requested}/`)) {\n return true;\n }\n }\n return false;\n };\n\n const metafileInputsValues = Object.values(metafile.inputs);\n for (const input of metafileInputsValues) {\n for (const dependency of input.imports) {\n if (!dependency.external) {\n continue;\n }\n\n const specifier = dependency.original ?? dependency.path;\n if (\n isRelativeSpecifier(specifier) ||\n builtins.has(specifier) ||\n isRequestedExternal(specifier)\n ) {\n continue;\n }\n\n unresolved.add(specifier);\n }\n }\n\n return [...unresolved].sort();\n};\n\n/**\n * Reads and removes the metafile unconditionally. Returns `undefined` rather than throwing -\n * `PurityProbe.probeEntry` reports on every entry it is given rather than aborting the run of the other five.\n */\nconst readMetafile = async (path: string): Promise<IMetafile | undefined> => {\n try {\n const result = await Bun.file(path).text();\n return JSON.parse(result) as IMetafile;\n } catch (error) {\n const reason = error instanceof Error ? error.message : String(error);\n console.error(`[purity] Failed to read the build metafile at ${path}: ${reason}`);\n return undefined;\n } finally {\n await rm(path, { force: true });\n }\n};\n\nexport class PurityProbe {\n /**\n * Public so `probe.test.ts` can assert against the literal collision string directly, without\n * spawning `bun build` - a fixture whose OWN entry-path comment happens to reproduce the collision\n * is not something a filename choice can be relied on to keep reproducing.\n */\n static stripBundlerPathComments(opts: { text: string }): string {\n return opts.text.replace(BUNDLER_PATH_COMMENT, '');\n }\n\n /**\n * The CLI, not `Bun.build()`: the in-process bundler resolves this workspace's symlinked\n * dependencies to directories and dies on them.\n */\n static async probeEntry(opts: {\n entry: string;\n cwd: string;\n external?: string[];\n /** Defaults to {@link PURITY_SCRATCH_DIR} under `cwd`. */\n scratchDir?: string;\n }): Promise<IPurityResult> {\n const { entry, cwd, external = [], scratchDir = join(cwd, PURITY_SCRATCH_DIR) } = opts;\n await mkdir(scratchDir, { recursive: true });\n const metafilePath = join(scratchDir, `purity-${crypto.randomUUID()}.json`);\n\n // `--env=disable`: Bun inlines `process.env.NODE_ENV` as a compile-time string constant by\n // default, regardless of whether the read is reachable. That erases the very read this probe\n // exists to catch, and the erasure depends on the ambient `NODE_ENV` the probe happens to run\n // under - the same failure shape as the undetected leak this gate was built to prevent.\n const externalArgs = external.flatMap((specifier) => ['--external', specifier]);\n const proc = Bun.spawn(\n [\n 'bun',\n 'build',\n entry,\n '--target=browser',\n '--format=esm',\n '--env=disable',\n `--metafile=${metafilePath}`,\n ...externalArgs,\n ],\n { cwd, stdout: 'pipe', stderr: 'pipe' },\n );\n\n const [stdout, stderr, exitCode] = await Promise.all([\n new Response(proc.stdout).text(),\n new Response(proc.stderr).text(),\n proc.exited,\n ]);\n\n if (exitCode !== 0) {\n await rm(metafilePath, { force: true });\n return {\n entry,\n ok: false,\n sizeBytes: 0,\n builtins: [],\n globals: [],\n guardedGlobals: [],\n external,\n buildError: stderr.trim(),\n };\n }\n\n const metafile = await readMetafile(metafilePath);\n if (!metafile) {\n return {\n entry,\n ok: false,\n sizeBytes: stdout.length,\n builtins: [],\n globals: [],\n guardedGlobals: [],\n external,\n buildError: 'Could not read the bun build metafile - the probe result is not trustworthy',\n };\n }\n\n const builtins = findBuiltinSpecifiers(metafile);\n const reachesDevConfigs = Object.keys(metafile.inputs).some((inputPath) =>\n DEV_CONFIGS_PATH.test(inputPath),\n );\n const unresolvedExternals = findUnresolvedExternalSpecifiers({\n metafile,\n builtins: new Set(builtins),\n requestedExternal: new Set(external),\n });\n const scannableText = PurityProbe.stripBundlerPathComments({ text: stdout });\n const matched = GLOBAL_PATTERNS.filter((g) => g.pattern.test(scannableText));\n const globals = matched.filter((g) => g.severity === ProbeSeverities.ERROR).map((g) => g.name);\n const guardedGlobals = matched\n .filter((g) => g.severity === ProbeSeverities.GUARDED)\n .map((g) => g.name);\n\n if (builtins.length > 0 || unresolvedExternals.length > 0 || reachesDevConfigs) {\n const reasons: string[] = [];\n if (reachesDevConfigs) {\n reasons.push(\n 'reaches @venizia/dev-configs, which is Bun-only tooling and never a browser dependency',\n );\n }\n if (builtins.length > 0) {\n reasons.push(`node builtin import(s): ${builtins.join(', ')}`);\n }\n if (unresolvedExternals.length > 0) {\n reasons.push(`unresolved external import(s): ${unresolvedExternals.join(', ')}`);\n }\n\n return {\n entry,\n ok: false,\n sizeBytes: stdout.length,\n builtins,\n globals,\n guardedGlobals,\n external,\n buildError: reasons.join(' | '),\n };\n }\n\n return {\n entry,\n ok: globals.length === 0,\n sizeBytes: stdout.length,\n builtins,\n globals,\n guardedGlobals,\n external,\n };\n }\n}\n"]}
1
+ {"version":3,"file":"probe.js","sourceRoot":"","sources":["../../../src/checks/purity/probe.ts"],"names":[],"mappings":"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","sourcesContent":["import { mkdir, rm } from 'node:fs/promises';\nimport { builtinModules } from 'node:module';\nimport { join } from 'node:path';\n\nexport interface IPurityResult {\n entry: string;\n ok: boolean;\n sizeBytes: number;\n builtins: string[];\n globals: string[];\n /** Node-global reads that cannot throw in a browser. Reported, never fatal. */\n guardedGlobals: string[];\n /**\n * Specifiers this probe run excluded from its own module graph. Always echoed back, on both the\n * passing and failing path - an entry that externalises something is measuring less than an entry\n * that does not, and that must stay visible next to the result, not be a silent input.\n */\n external: string[];\n buildError?: string;\n}\n\n/** Where the probe keeps its metafile, relative to the `cwd` it is given: the repository's own scratch, never the system temp. */\nexport const PURITY_SCRATCH_DIR = '.agents/local/tmp';\n\n/**\n * A browser entry must never reach `@venizia/dev-configs`: it is the Bun-only tooling package (its\n * `./testing` entry imports `bun:test`). Matches the workspace folder (not its own test fixtures) and the installed copy.\n */\nconst DEV_CONFIGS_PATH =\n /(^|\\/)(packages\\/dev-configs\\/(?!src\\/__tests__\\/)|node_modules\\/@venizia\\/dev-configs\\/)/;\n\n/** What a matched global means: ERROR is fatal, GUARDED is reported and never fatal. */\nexport class ProbeSeverities {\n static readonly ERROR = 'error';\n static readonly GUARDED = 'guarded';\n\n static readonly SCHEME_SET = new Set<string>([this.ERROR, this.GUARDED]);\n\n static isValid(value: string): value is TProbeSeverity {\n return this.SCHEME_SET.has(value);\n }\n}\n\nexport type TProbeSeverity = Extract<\n (typeof ProbeSeverities)[keyof typeof ProbeSeverities],\n string\n>;\n\n/**\n * Node globals survive verbatim into a `target: 'browser'` bundle, so a gate reading only module\n * specifiers reports clean on `process.env.NODE_ENV`. Each pattern must be anchored: a bundle\n * legitimately contains identifiers like `processQueue`. A lookbehind that rejects a preceding dot\n * also rejects `globalThis.process`, so every access path needs its own pattern.\n *\n * `severity` separates a read that throws from one that cannot. Optional chaining is not itself the\n * difference: `process?.env` throws a ReferenceError when `process` is a free identifier, while\n * `globalThis.process?.env` is an ordinary property read on an object that always exists.\n *\n * `guarded` means \"reported, never fatal\". Bare `process?.` sits there because this probe matches\n * bundled text and has no scope information: hono's `getColorEnabled` writes\n * `const { process, Deno } = globalThis;` and then `process?.env`, which is a local binding and\n * perfectly safe, and no regex can tell that apart from a free global. Failing on it would make the\n * gate cry wolf on a dependency; the unguarded `process.` and `globalThis.process.` forms stay\n * fatal, and those are the ones that are unambiguously browser-breaking.\n *\n * Exported for `probe.test.ts` to assert against directly, alongside {@link PurityProbe.stripBundlerPathComments}.\n */\nexport const GLOBAL_PATTERNS: Array<{\n name: string;\n pattern: RegExp;\n severity: TProbeSeverity;\n}> = [\n { name: 'process', pattern: /(?<![A-Za-z0-9_$.])process\\s*\\./, severity: 'error' },\n { name: 'process?.', pattern: /(?<![A-Za-z0-9_$.])process\\s*\\?\\./, severity: 'guarded' },\n {\n name: 'globalThis.process',\n pattern: /(?<![A-Za-z0-9_$.])globalThis\\s*\\.\\s*process\\s*\\./,\n severity: 'error',\n },\n {\n name: 'globalThis.process?.',\n pattern: /(?<![A-Za-z0-9_$.])globalThis\\s*\\.\\s*process\\s*\\?\\./,\n severity: 'guarded',\n },\n {\n name: '__dirname',\n pattern: /(?<![A-Za-z0-9_$.])__dirname(?![A-Za-z0-9_$])/,\n severity: 'error',\n },\n {\n name: '__filename',\n pattern: /(?<![A-Za-z0-9_$.])__filename(?![A-Za-z0-9_$])/,\n severity: 'error',\n },\n {\n name: 'createRequire',\n pattern: /(?<![A-Za-z0-9_$.])createRequire\\s*\\(/,\n severity: 'error',\n },\n { name: 'require(', pattern: /(?<![A-Za-z0-9_$.])require\\s*\\(/, severity: 'error' },\n // Member access only. `typeof Bun !== 'undefined'` is the legitimate runtime probe and never\n // throws; `Bun.serve(...)` in a browser does.\n { name: 'Bun', pattern: /(?<![A-Za-z0-9_$.])Bun\\s*\\./, severity: 'error' },\n];\n\n/**\n * Bun's bundler prefixes every CommonJS-wrapped module with a full-line `// <path>.js` header (this\n * whole workspace emits CJS `dist/*.js` - no package declares `\"type\": \"module\"`). A source path is\n * free-form and can legitimately contain `process` as a substring with no identifier boundary on\n * its left, e.g. this gate's own `transport/in-process.js`: the header reads\n * `// packages/core-worker/dist/transport/in-process.js`, and `process.` there is a filename\n * fragment, not a read of the global. `GLOBAL_PATTERNS` matches bundled TEXT with no scope\n * information, so it cannot tell a path comment from code - stripped before matching.\n */\nconst BUNDLER_PATH_COMMENT = /^\\/\\/ [\\w@./-]+\\.(?:c|m)?js$/gm;\n\ninterface IMetafileImport {\n path: string;\n original?: string;\n external?: boolean;\n}\n\ninterface IMetafileInput {\n imports: IMetafileImport[];\n}\n\ninterface IMetafile {\n inputs: Record<string, IMetafileInput>;\n}\n\n/**\n * `node:module`'s own list, not a hand-maintained one: a bare `import ... from 'fs'` bundles\n * identically to `'node:fs'` (Bun stubs both to an empty object), and the metafile records\n * whichever spelling the source used. Matching by prefix alone misses the bare form entirely.\n */\nconst BUILTIN_MODULE_NAMES = new Set(builtinModules);\n\nconst isBuiltinSpecifier = (specifier: string): boolean => {\n const bareName = specifier.startsWith('node:') ? specifier.slice('node:'.length) : specifier;\n\n return BUILTIN_MODULE_NAMES.has(bareName);\n};\n\n/**\n * Measured against Bun 1.3.14: `--target=browser` never fails on a `node:` import and never leaves\n * the specifier in the bundled text. An unpolyfillable builtin (`fs`, `child_process`, ...) is\n * stubbed to an empty object; a polyfillable one (`crypto`, `path`, ...) is inlined in full under a\n * `/bun-vfs$$/` virtual path. Either way `exitCode` stays 0 and the specifier never survives into\n * stdout. The `--metafile` module graph is the only place it is preserved.\n */\nconst findBuiltinSpecifiers = (metafile: IMetafile): string[] => {\n const builtins = new Set<string>();\n\n const metafileInputsEntries = Object.entries(metafile.inputs);\n\n for (const [inputPath, input] of metafileInputsEntries) {\n if (isBuiltinSpecifier(inputPath)) {\n builtins.add(inputPath);\n }\n\n for (const dependency of input.imports) {\n const specifier = dependency.original ?? dependency.path;\n\n if (isBuiltinSpecifier(specifier)) {\n builtins.add(specifier);\n }\n }\n }\n\n return [...builtins].sort();\n};\n\n/**\n * A real npm dependency bundles in full under this target (measured: `zod` produces zero\n * `external` entries) and a genuinely missing package fails the build outright (nonzero exit,\n * caught before this runs). But a package's own `browser` field can remap a dependency to `false` -\n * the standard convention for disabling a Node-only dependency in browser builds - and Bun honours\n * that remap by leaving the import external with exit 0, whether or not the remapped name is a\n * builtin. An unresolved import in a browser bundle is not safe to wave through either way -\n * UNLESS the manifest entry asked for exactly this specifier via `external`, in which case its\n * presence here is the requested exclusion working, not a leak.\n */\n/**\n * A relative specifier is the package's own file, and a truly unreachable one fails the build with a\n * nonzero exit, which is caught before this runs. Bun's METAFILE nonetheless marks a re-export of the\n * form `export { name } from './x.js'` as `external` while inlining it - measured on\n * `helpers/dist/esm/core.js`: six such specifiers reported external, `Bundled 192 modules`, and the\n * output contains `class BaseHelper` with no bare relative import left in it. Reading the metafile\n * literally there reports a leak the bundle does not have.\n */\nconst isRelativeSpecifier = (specifier: string): boolean => {\n return specifier.startsWith('./') || specifier.startsWith('../');\n};\n\nconst findUnresolvedExternalSpecifiers = (opts: {\n metafile: IMetafile;\n builtins: Set<string>;\n requestedExternal: Set<string>;\n}): string[] => {\n const { metafile, builtins, requestedExternal } = opts;\n const unresolved = new Set<string>();\n\n const isRequestedExternal = (specifier: string): boolean => {\n for (const requested of requestedExternal) {\n if (specifier === requested || specifier.startsWith(`${requested}/`)) {\n return true;\n }\n }\n\n return false;\n };\n\n const metafileInputsValues = Object.values(metafile.inputs);\n\n for (const input of metafileInputsValues) {\n for (const dependency of input.imports) {\n if (!dependency.external) {\n continue;\n }\n\n const specifier = dependency.original ?? dependency.path;\n\n if (\n isRelativeSpecifier(specifier) ||\n builtins.has(specifier) ||\n isRequestedExternal(specifier)\n ) {\n continue;\n }\n\n unresolved.add(specifier);\n }\n }\n\n return [...unresolved].sort();\n};\n\n/**\n * Reads and removes the metafile unconditionally. Returns `undefined` rather than throwing -\n * `PurityProbe.probeEntry` reports on every entry it is given rather than aborting the run of the other five.\n */\nconst readMetafile = async (path: string): Promise<IMetafile | undefined> => {\n try {\n const result = await Bun.file(path).text();\n\n return JSON.parse(result) as IMetafile;\n } catch (error) {\n const reason = error instanceof Error ? error.message : String(error);\n console.error(`[purity] Failed to read the build metafile at ${path}: ${reason}`);\n\n return undefined;\n } finally {\n await rm(path, { force: true });\n }\n};\n\nexport class PurityProbe {\n /**\n * Public so `probe.test.ts` can assert against the literal collision string directly, without\n * spawning `bun build` - a fixture whose OWN entry-path comment happens to reproduce the collision\n * is not something a filename choice can be relied on to keep reproducing.\n */\n static stripBundlerPathComments(opts: { text: string }): string {\n return opts.text.replace(BUNDLER_PATH_COMMENT, '');\n }\n\n /**\n * The CLI, not `Bun.build()`: the in-process bundler resolves this workspace's symlinked\n * dependencies to directories and dies on them.\n */\n static async probeEntry(opts: {\n entry: string;\n cwd: string;\n external?: string[];\n /** Defaults to {@link PURITY_SCRATCH_DIR} under `cwd`. */\n scratchDir?: string;\n }): Promise<IPurityResult> {\n const { entry, cwd, external = [], scratchDir = join(cwd, PURITY_SCRATCH_DIR) } = opts;\n await mkdir(scratchDir, { recursive: true });\n const metafilePath = join(scratchDir, `purity-${crypto.randomUUID()}.json`);\n\n // `--env=disable`: Bun inlines `process.env.NODE_ENV` as a compile-time string constant by\n // default, regardless of whether the read is reachable. That erases the very read this probe\n // exists to catch, and the erasure depends on the ambient `NODE_ENV` the probe happens to run\n // under - the same failure shape as the undetected leak this gate was built to prevent.\n const externalArgs = external.flatMap((specifier) => ['--external', specifier]);\n const proc = Bun.spawn(\n [\n 'bun',\n 'build',\n entry,\n '--target=browser',\n '--format=esm',\n '--env=disable',\n `--metafile=${metafilePath}`,\n ...externalArgs,\n ],\n { cwd, stdout: 'pipe', stderr: 'pipe' },\n );\n\n const [stdout, stderr, exitCode] = await Promise.all([\n new Response(proc.stdout).text(),\n new Response(proc.stderr).text(),\n proc.exited,\n ]);\n\n if (exitCode !== 0) {\n await rm(metafilePath, { force: true });\n\n return {\n entry,\n ok: false,\n sizeBytes: 0,\n builtins: [],\n globals: [],\n guardedGlobals: [],\n external,\n buildError: stderr.trim(),\n };\n }\n\n const metafile = await readMetafile(metafilePath);\n\n if (!metafile) {\n return {\n entry,\n ok: false,\n sizeBytes: stdout.length,\n builtins: [],\n globals: [],\n guardedGlobals: [],\n external,\n buildError: 'Could not read the bun build metafile - the probe result is not trustworthy',\n };\n }\n\n const builtins = findBuiltinSpecifiers(metafile);\n const reachesDevConfigs = Object.keys(metafile.inputs).some((inputPath) =>\n DEV_CONFIGS_PATH.test(inputPath),\n );\n const unresolvedExternals = findUnresolvedExternalSpecifiers({\n metafile,\n builtins: new Set(builtins),\n requestedExternal: new Set(external),\n });\n const scannableText = PurityProbe.stripBundlerPathComments({ text: stdout });\n const matched = GLOBAL_PATTERNS.filter((g) => g.pattern.test(scannableText));\n const globals = matched.filter((g) => g.severity === ProbeSeverities.ERROR).map((g) => g.name);\n const guardedGlobals = matched\n .filter((g) => g.severity === ProbeSeverities.GUARDED)\n .map((g) => g.name);\n\n if (builtins.length > 0 || unresolvedExternals.length > 0 || reachesDevConfigs) {\n const reasons: string[] = [];\n\n if (reachesDevConfigs) {\n reasons.push(\n 'reaches @venizia/dev-configs, which is Bun-only tooling and never a browser dependency',\n );\n }\n\n if (builtins.length > 0) {\n reasons.push(`node builtin import(s): ${builtins.join(', ')}`);\n }\n\n if (unresolvedExternals.length > 0) {\n reasons.push(`unresolved external import(s): ${unresolvedExternals.join(', ')}`);\n }\n\n return {\n entry,\n ok: false,\n sizeBytes: stdout.length,\n builtins,\n globals,\n guardedGlobals,\n external,\n buildError: reasons.join(' | '),\n };\n }\n\n return {\n entry,\n ok: globals.length === 0,\n sizeBytes: stdout.length,\n builtins,\n globals,\n guardedGlobals,\n external,\n };\n }\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"run.js","sourceRoot":"","sources":["../../../src/checks/purity/run.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { PurityManifest } from './manifest';\nimport type { IPurityClaim, IPurityEntry, IPurityOptions } from './manifest';\nimport { PurityProbe } from './probe';\nimport type { IPurityResult } from './probe';\n\n/** The outcome of one row, as the text report and the JSON verdict both name it. */\nexport class PurityVerdicts {\n static readonly PASS = 'pass';\n static readonly FAIL = 'fail';\n static readonly WAIVED = 'waived';\n static readonly MISSING = 'missing';\n static readonly STALE_WAIVER = 'stale-waiver';\n\n static readonly SCHEME_SET = new Set<string>([\n this.PASS,\n this.FAIL,\n this.WAIVED,\n this.MISSING,\n this.STALE_WAIVER,\n ]);\n\n static isValid(value: string): value is TPurityVerdict {\n return this.SCHEME_SET.has(value);\n }\n}\n\nexport type TPurityVerdict = Extract<(typeof PurityVerdicts)[keyof typeof PurityVerdicts], string>;\n\n/** One row of the `--json` verdict: nothing machine- or time-specific, so two runs on one commit diff empty. */\nexport interface IPurityRowVerdict {\n label: string;\n entry: string;\n verdict: TPurityVerdict;\n sizeBytes?: number;\n builtins?: string[];\n globals?: string[];\n guardedGlobals?: string[];\n external?: string[];\n buildError?: string;\n}\n\nexport interface IPurityReport {\n rows: IPurityRowVerdict[];\n failed: number;\n waived: number;\n checked: number;\n}\n\nconst formatList = (values: string[]): string => values.join(', ') || 'none';\n\nexport class PurityCheck {\n /**\n * Probes every row of the claims and returns the report. `print` receives the human report line by\n * line, in the exact shape the repository scripts printed before the engine moved here.\n */\n static async run(opts: {\n root: string;\n claims: IPurityClaim[];\n options?: IPurityOptions;\n /** Only the rows of this package; a token that matches nothing is a broken gate and returns `undefined`. */\n filter?: string;\n /** The claims file, repository-relative, named in the hint printed for a stale waiver. */\n claimsFile: string;\n print: (line: string) => void;\n /** Where the no-match message goes (stderr for the command): it is an error, not a report line. */\n printError: (line: string) => void;\n }): Promise<IPurityReport | undefined> {\n const { root, claims, options, filter, claimsFile, print, printError } = opts;\n const manifest = PurityManifest.build({ claims, root, options });\n const rows: IPurityEntry[] = filter\n ? manifest.filter((row) => row.package === filter)\n : manifest;\n\n // Non-zero, not a friendly note: a mistyped `package` or a renamed directory would otherwise turn\n // a release-blocking gate into a silent no-op that still reports success.\n if (rows.length === 0) {\n const known = [...new Set(manifest.map((row) => row.package))].sort().join(', ');\n printError(\n `[purity] No manifest entry matches \"${filter}\" | Known packages: ${known} - a filter that ` +\n `matches nothing is a broken gate, not an empty one.`,\n );\n return undefined;\n }\n\n const labelWidth = Math.max(...rows.map((row) => row.label.length));\n const verdicts: IPurityRowVerdict[] = [];\n let failed = 0;\n let waived = 0;\n\n for (const row of rows) {\n const entry = join(root, row.entry);\n\n // The `exports` map is the source of these rows, so a missing file is either an unbuilt package\n // or a manifest pointing at something the build never emits - both fatal, and neither is worth\n // reporting as a bundler error twenty lines long.\n if (!existsSync(entry)) {\n failed += 1;\n print(` ✗ ${row.label.padEnd(labelWidth)} MISSING ${row.entry}`);\n print(\n ` The package's \\`exports\\` map points here and nothing is built - run \\`make ${row.package}\\``,\n );\n verdicts.push({ label: row.label, entry: row.entry, verdict: PurityVerdicts.MISSING });\n continue;\n }\n\n const result: IPurityResult = await PurityProbe.probeEntry({\n entry,\n cwd: root,\n external: row.external,\n });\n\n const detail = {\n label: row.label,\n entry: row.entry,\n sizeBytes: result.sizeBytes,\n builtins: result.builtins,\n globals: result.globals,\n guardedGlobals: result.guardedGlobals,\n external: result.external,\n // The bundler's message names absolute paths; the verdict must read the same on any machine.\n ...(result.buildError ? { buildError: result.buildError.replaceAll(root, '<root>') } : {}),\n };\n\n // Guarded reads are printed on the passing path too: they are safe, but a reviewer needs to\n // see that the entry is clean by construction and not merely by the shape of one expression.\n const guarded =\n result.guardedGlobals.length > 0 ? ` | guarded: ${result.guardedGlobals.join(', ')}` : '';\n\n // Same reasoning as `guarded`, printed on both paths: an entry that externalises a dependency\n // measures less than one that does not, and that must stay visible next to the verdict.\n const external =\n result.external.length > 0 ? ` | external: ${result.external.join(', ')}` : '';\n\n // Checked before the normal verdict, because for a waived row the two outcomes swap meaning.\n if (row.expectImpure) {\n // A waived entry that comes back pure fails: the waiver now describes something untrue, and\n // the reader who trusts it next will be trusting it about a different entry.\n if (result.ok) {\n failed += 1;\n print(\n ` ✗ ${row.label.padEnd(labelWidth)} STALE WAIVER - this entry is browser-pure now`,\n );\n print(` Drop it from \\`impure\\` in ${claimsFile} so the gate measures it again`);\n verdicts.push({ ...detail, verdict: PurityVerdicts.STALE_WAIVER });\n continue;\n }\n\n waived += 1;\n print(\n ` ~ ${row.label.padEnd(labelWidth)} waived | builtins: ${formatList(result.builtins)} | globals: ${formatList(result.globals)}${external}`,\n );\n verdicts.push({ ...detail, verdict: PurityVerdicts.WAIVED });\n continue;\n }\n\n if (result.ok) {\n print(\n ` ✓ ${row.label.padEnd(labelWidth)} ${(result.sizeBytes / 1024).toFixed(1)} KB${guarded}${external}`,\n );\n verdicts.push({ ...detail, verdict: PurityVerdicts.PASS });\n continue;\n }\n\n failed += 1;\n print(\n ` ✗ ${row.label.padEnd(labelWidth)} builtins: ${formatList(result.builtins)} | globals: ${formatList(result.globals)}${guarded}${external}`,\n );\n if (result.buildError) {\n print(` ${result.buildError.split('\\n')[0]}`);\n }\n verdicts.push({ ...detail, verdict: PurityVerdicts.FAIL });\n }\n\n const note = waived > 0 ? ` (${waived} waived as known-impure)` : '';\n\n if (failed > 0) {\n print(`\\n[purity] ${failed}/${rows.length} entries are not browser-pure${note}.`);\n } else {\n const checked = rows.length - waived;\n print(`\\n[purity] ${checked}/${checked} entries are browser-pure${note}.`);\n }\n\n return { rows: verdicts, failed, waived, checked: rows.length - waived };\n }\n}\n"]}
1
+ {"version":3,"file":"run.js","sourceRoot":"","sources":["../../../src/checks/purity/run.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { PurityManifest } from './manifest';\nimport type { IPurityClaim, IPurityEntry, IPurityOptions } from './manifest';\nimport { PurityProbe } from './probe';\nimport type { IPurityResult } from './probe';\n\n/** The outcome of one row, as the text report and the JSON verdict both name it. */\nexport class PurityVerdicts {\n static readonly PASS = 'pass';\n static readonly FAIL = 'fail';\n static readonly WAIVED = 'waived';\n static readonly MISSING = 'missing';\n static readonly STALE_WAIVER = 'stale-waiver';\n\n static readonly SCHEME_SET = new Set<string>([\n this.PASS,\n this.FAIL,\n this.WAIVED,\n this.MISSING,\n this.STALE_WAIVER,\n ]);\n\n static isValid(value: string): value is TPurityVerdict {\n return this.SCHEME_SET.has(value);\n }\n}\n\nexport type TPurityVerdict = Extract<(typeof PurityVerdicts)[keyof typeof PurityVerdicts], string>;\n\n/** One row of the `--json` verdict: nothing machine- or time-specific, so two runs on one commit diff empty. */\nexport interface IPurityRowVerdict {\n label: string;\n entry: string;\n verdict: TPurityVerdict;\n sizeBytes?: number;\n builtins?: string[];\n globals?: string[];\n guardedGlobals?: string[];\n external?: string[];\n buildError?: string;\n}\n\nexport interface IPurityReport {\n rows: IPurityRowVerdict[];\n failed: number;\n waived: number;\n checked: number;\n}\n\nconst formatList = (values: string[]): string => values.join(', ') || 'none';\n\nexport class PurityCheck {\n /**\n * Probes every row of the claims and returns the report. `print` receives the human report line by\n * line, in the exact shape the repository scripts printed before the engine moved here.\n */\n static async run(opts: {\n root: string;\n claims: IPurityClaim[];\n options?: IPurityOptions;\n /** Only the rows of this package; a token that matches nothing is a broken gate and returns `undefined`. */\n filter?: string;\n /** The claims file, repository-relative, named in the hint printed for a stale waiver. */\n claimsFile: string;\n print: (line: string) => void;\n /** Where the no-match message goes (stderr for the command): it is an error, not a report line. */\n printError: (line: string) => void;\n }): Promise<IPurityReport | undefined> {\n const { root, claims, options, filter, claimsFile, print, printError } = opts;\n const manifest = PurityManifest.build({ claims, root, options });\n const rows: IPurityEntry[] = filter\n ? manifest.filter((row) => row.package === filter)\n : manifest;\n\n // Non-zero, not a friendly note: a mistyped `package` or a renamed directory would otherwise turn\n // a release-blocking gate into a silent no-op that still reports success.\n if (rows.length === 0) {\n const known = [...new Set(manifest.map((row) => row.package))].sort().join(', ');\n printError(\n `[purity] No manifest entry matches \"${filter}\" | Known packages: ${known} - a filter that ` +\n `matches nothing is a broken gate, not an empty one.`,\n );\n\n return undefined;\n }\n\n const labelWidth = Math.max(...rows.map((row) => row.label.length));\n const verdicts: IPurityRowVerdict[] = [];\n let failed = 0;\n let waived = 0;\n\n for (const row of rows) {\n const entry = join(root, row.entry);\n\n // The `exports` map is the source of these rows, so a missing file is either an unbuilt package\n // or a manifest pointing at something the build never emits - both fatal, and neither is worth\n // reporting as a bundler error twenty lines long.\n if (!existsSync(entry)) {\n failed += 1;\n print(` ✗ ${row.label.padEnd(labelWidth)} MISSING ${row.entry}`);\n print(\n ` The package's \\`exports\\` map points here and nothing is built - run \\`make ${row.package}\\``,\n );\n verdicts.push({ label: row.label, entry: row.entry, verdict: PurityVerdicts.MISSING });\n continue;\n }\n\n const result: IPurityResult = await PurityProbe.probeEntry({\n entry,\n cwd: root,\n external: row.external,\n });\n\n const detail = {\n label: row.label,\n entry: row.entry,\n sizeBytes: result.sizeBytes,\n builtins: result.builtins,\n globals: result.globals,\n guardedGlobals: result.guardedGlobals,\n external: result.external,\n // The bundler's message names absolute paths; the verdict must read the same on any machine.\n ...(result.buildError ? { buildError: result.buildError.replaceAll(root, '<root>') } : {}),\n };\n\n // Guarded reads are printed on the passing path too: they are safe, but a reviewer needs to\n // see that the entry is clean by construction and not merely by the shape of one expression.\n const guarded =\n result.guardedGlobals.length > 0 ? ` | guarded: ${result.guardedGlobals.join(', ')}` : '';\n\n // Same reasoning as `guarded`, printed on both paths: an entry that externalises a dependency\n // measures less than one that does not, and that must stay visible next to the verdict.\n const external =\n result.external.length > 0 ? ` | external: ${result.external.join(', ')}` : '';\n\n // Checked before the normal verdict, because for a waived row the two outcomes swap meaning.\n if (row.expectImpure) {\n // A waived entry that comes back pure fails: the waiver now describes something untrue, and\n // the reader who trusts it next will be trusting it about a different entry.\n if (result.ok) {\n failed += 1;\n print(\n ` ✗ ${row.label.padEnd(labelWidth)} STALE WAIVER - this entry is browser-pure now`,\n );\n print(` Drop it from \\`impure\\` in ${claimsFile} so the gate measures it again`);\n verdicts.push({ ...detail, verdict: PurityVerdicts.STALE_WAIVER });\n continue;\n }\n\n waived += 1;\n print(\n ` ~ ${row.label.padEnd(labelWidth)} waived | builtins: ${formatList(result.builtins)} | globals: ${formatList(result.globals)}${external}`,\n );\n verdicts.push({ ...detail, verdict: PurityVerdicts.WAIVED });\n continue;\n }\n\n if (result.ok) {\n print(\n ` ✓ ${row.label.padEnd(labelWidth)} ${(result.sizeBytes / 1024).toFixed(1)} KB${guarded}${external}`,\n );\n verdicts.push({ ...detail, verdict: PurityVerdicts.PASS });\n continue;\n }\n\n failed += 1;\n print(\n ` ✗ ${row.label.padEnd(labelWidth)} builtins: ${formatList(result.builtins)} | globals: ${formatList(result.globals)}${guarded}${external}`,\n );\n\n if (result.buildError) {\n print(` ${result.buildError.split('\\n')[0]}`);\n }\n\n verdicts.push({ ...detail, verdict: PurityVerdicts.FAIL });\n }\n\n const note = waived > 0 ? ` (${waived} waived as known-impure)` : '';\n\n if (failed > 0) {\n print(`\\n[purity] ${failed}/${rows.length} entries are not browser-pure${note}.`);\n } else {\n const checked = rows.length - waived;\n print(`\\n[purity] ${checked}/${checked} entries are browser-pure${note}.`);\n }\n\n return { rows: verdicts, failed, waived, checked: rows.length - waived };\n }\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"split-report.js","sourceRoot":"","sources":["../../src/checks/split-report.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Informational report behind the file-splitting convention (conventions/file-splitting.md):\n * hub candidates, stray types files, scope folders without a barrel, files over the soft mark,\n * and import cycles per package. Never a gate. Usage: devz check split-report [--dist <dir>] [packages/kernel]\n * - with no argument, scans every packages/* and examples/* directory that has a src/.\n */\nimport { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';\nimport { basename, join, relative, resolve } from 'node:path';\nimport { ModuleGraph } from './module-cycles';\n\nconst SKIP = new Set(['node_modules', 'dist', '__tests__', 'generated', '.turbo']);\nconst SOFT_LINE_MARK = 500;\nconst HUB_EXPORT_MARK = 10;\nconst DECLARATION_KEYWORDS = ['class', 'const', 'type', 'interface', 'function', 'enum', 'let'];\nconst DECLARATION = new RegExp(\n `^export\\\\s+(?:default\\\\s+)?(?:abstract\\\\s+)?(${DECLARATION_KEYWORDS.join('|')})\\\\s+(\\\\w+)`,\n 'gm',\n);\nconst EXPORTED_CLASS = /^export\\s+(?:abstract\\s+)?class\\s+(\\w+)/gm;\n\ninterface IFileFacts {\n file: string;\n lines: number;\n exports: number;\n classes: string[];\n}\n\nexport class SplitReport {\n /** `distDir` is the built ESM folder inside a package (`dist/esm`, or `dist` where one build is published); `print` takes the report line by line. */\n static run(opts: { packageDir: string; distDir?: string; print?: (line: string) => void }): void {\n const { distDir = 'dist', print = console.log } = opts;\n const src = join(opts.packageDir, 'src');\n if (!existsSync(src)) {\n return;\n }\n const facts = SplitReport.walk({ dir: src }).map((file) => SplitReport.facts({ file }));\n const rel = (file: string) => relative(opts.packageDir, file);\n\n const hubs = facts.filter(\n (f) => ['types.ts', 'constants.ts'].includes(basename(f.file)) && f.exports > HUB_EXPORT_MARK,\n );\n const multiClass = facts.filter(\n (f) => f.classes.length >= 2 && !SplitReport.isAbstractBasePair({ facts: f }),\n );\n const strayTypes = facts.filter(\n (f) =>\n ['types.ts', 'constants.ts'].includes(basename(f.file)) && !f.file.includes('/common/'),\n );\n const long = facts.filter((f) => f.lines > SOFT_LINE_MARK);\n const noBarrel = SplitReport.foldersWithoutIndex({ dir: src });\n const esm = join(opts.packageDir, distDir);\n const cycles = existsSync(esm) ? ModuleGraph.fromDirectory({ dir: esm }).cycles().length : -1;\n\n print(`\\n## ${basename(opts.packageDir)} - ${facts.length} files`);\n const hubTitle = `hub candidates (> ${HUB_EXPORT_MARK} exports)`;\n const hubRows = hubs.map((f) => `${rel(f.file)} (${f.exports})`);\n SplitReport.section({ title: hubTitle, rows: hubRows, print });\n const classTitle = 'files with 2+ exported classes';\n const classRows = multiClass.map((f) => `${rel(f.file)} (${f.classes.join(', ')})`);\n SplitReport.section({ title: classTitle, rows: classRows, print });\n const strayTitle = 'types/constants outside common/';\n const strayRows = strayTypes.map((f) => rel(f.file));\n SplitReport.section({ title: strayTitle, rows: strayRows, print });\n const barrelTitle = 'scope folders without index.ts';\n const barrelRows = noBarrel.map((d) => relative(opts.packageDir, d));\n SplitReport.section({ title: barrelTitle, rows: barrelRows, print });\n const longTitle = `over ${SOFT_LINE_MARK} lines (a prompt to explain, not a defect)`;\n const longRows = long.map((f) => `${rel(f.file)} (${f.lines})`);\n SplitReport.section({ title: longTitle, rows: longRows, print });\n const cycleMsg = cycles < 0 ? 'no dist' : cycles;\n print(`- import cycles in ${distDir}: ${cycleMsg}`);\n }\n\n /** Every packages/* and examples/* directory that has a src/ folder. */\n static defaultTargets(opts: { root?: string } = {}): string[] {\n const root = opts.root ?? process.cwd();\n const withSource = (folder: string): string[] =>\n existsSync(resolve(root, folder))\n ? readdirSync(resolve(root, folder))\n .map((name) => resolve(root, folder, name))\n .filter((dir) => existsSync(join(dir, 'src')))\n : [];\n\n return [...withSource('packages'), ...withSource('examples')];\n }\n\n private static section(opts: {\n title: string;\n rows: string[];\n print: (line: string) => void;\n }): void {\n opts.print(`- ${opts.title}: ${opts.rows.length}`);\n for (const row of opts.rows) {\n opts.print(` ${row}`);\n }\n }\n\n private static facts(opts: { file: string }): IFileFacts {\n const text = readFileSync(opts.file, 'utf8');\n return {\n file: opts.file,\n lines: text.split('\\n').length,\n exports: [...text.matchAll(DECLARATION)].length,\n classes: [...text.matchAll(EXPORTED_CLASS)].map((match) => match[1]),\n };\n }\n\n /** abstract.ts + base.ts pairs are the format; exactly one of each is one concept. */\n private static isAbstractBasePair(opts: { facts: IFileFacts }): boolean {\n const [first, second] = opts.facts.classes;\n return opts.facts.classes.length === 2 && /Abstract/.test(first) && /Base/.test(second);\n }\n\n private static walk(opts: { dir: string }): string[] {\n const out: string[] = [];\n const dirEntries = readdirSync(opts.dir);\n for (const name of dirEntries) {\n if (SKIP.has(name)) {\n continue;\n }\n const full = join(opts.dir, name);\n if (statSync(full).isDirectory()) {\n out.push(...SplitReport.walk({ dir: full }));\n } else if (full.endsWith('.ts') && !full.endsWith('.d.ts')) {\n out.push(full);\n }\n }\n return out;\n }\n\n private static foldersWithoutIndex(opts: { dir: string }): string[] {\n const out: string[] = [];\n const dirEntries = readdirSync(opts.dir);\n for (const name of dirEntries) {\n if (SKIP.has(name)) {\n continue;\n }\n const full = join(opts.dir, name);\n if (!statSync(full).isDirectory()) {\n continue;\n }\n const hasTs = readdirSync(full).some(\n (entry) => entry.endsWith('.ts') && entry !== 'index.ts',\n );\n if (hasTs && !existsSync(join(full, 'index.ts'))) {\n out.push(full);\n }\n out.push(...SplitReport.foldersWithoutIndex({ dir: full }));\n }\n return out;\n }\n}\n"]}
1
+ {"version":3,"file":"split-report.js","sourceRoot":"","sources":["../../src/checks/split-report.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Informational report behind the file-splitting convention (conventions/file-splitting.md):\n * hub candidates, stray types files, scope folders without a barrel, files over the soft mark,\n * and import cycles per package. Never a gate. Usage: devz check split-report [--dist <dir>] [packages/kernel]\n * - with no argument, scans every packages/* and examples/* directory that has a src/.\n */\nimport { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';\nimport { basename, join, relative, resolve } from 'node:path';\nimport { ModuleGraph } from './module-cycles';\n\nconst SKIP = new Set(['node_modules', 'dist', '__tests__', 'generated', '.turbo']);\nconst SOFT_LINE_MARK = 500;\nconst HUB_EXPORT_MARK = 10;\nconst DECLARATION_KEYWORDS = ['class', 'const', 'type', 'interface', 'function', 'enum', 'let'];\nconst DECLARATION = new RegExp(\n `^export\\\\s+(?:default\\\\s+)?(?:abstract\\\\s+)?(${DECLARATION_KEYWORDS.join('|')})\\\\s+(\\\\w+)`,\n 'gm',\n);\nconst EXPORTED_CLASS = /^export\\s+(?:abstract\\s+)?class\\s+(\\w+)/gm;\n\ninterface IFileFacts {\n file: string;\n lines: number;\n exports: number;\n classes: string[];\n}\n\nexport class SplitReport {\n /** `distDir` is the built ESM folder inside a package (`dist/esm`, or `dist` where one build is published); `print` takes the report line by line. */\n static run(opts: { packageDir: string; distDir?: string; print?: (line: string) => void }): void {\n const { distDir = 'dist', print = console.log } = opts;\n const src = join(opts.packageDir, 'src');\n\n if (!existsSync(src)) {\n return;\n }\n\n const facts = SplitReport.walk({ dir: src }).map((file) => SplitReport.facts({ file }));\n const rel = (file: string) => relative(opts.packageDir, file);\n\n const hubs = facts.filter(\n (f) => ['types.ts', 'constants.ts'].includes(basename(f.file)) && f.exports > HUB_EXPORT_MARK,\n );\n const multiClass = facts.filter(\n (f) => f.classes.length >= 2 && !SplitReport.isAbstractBasePair({ facts: f }),\n );\n const strayTypes = facts.filter(\n (f) =>\n ['types.ts', 'constants.ts'].includes(basename(f.file)) && !f.file.includes('/common/'),\n );\n const long = facts.filter((f) => f.lines > SOFT_LINE_MARK);\n const noBarrel = SplitReport.foldersWithoutIndex({ dir: src });\n const esm = join(opts.packageDir, distDir);\n const cycles = existsSync(esm) ? ModuleGraph.fromDirectory({ dir: esm }).cycles().length : -1;\n\n print(`\\n## ${basename(opts.packageDir)} - ${facts.length} files`);\n const hubTitle = `hub candidates (> ${HUB_EXPORT_MARK} exports)`;\n const hubRows = hubs.map((f) => `${rel(f.file)} (${f.exports})`);\n SplitReport.section({ title: hubTitle, rows: hubRows, print });\n const classTitle = 'files with 2+ exported classes';\n const classRows = multiClass.map((f) => `${rel(f.file)} (${f.classes.join(', ')})`);\n SplitReport.section({ title: classTitle, rows: classRows, print });\n const strayTitle = 'types/constants outside common/';\n const strayRows = strayTypes.map((f) => rel(f.file));\n SplitReport.section({ title: strayTitle, rows: strayRows, print });\n const barrelTitle = 'scope folders without index.ts';\n const barrelRows = noBarrel.map((d) => relative(opts.packageDir, d));\n SplitReport.section({ title: barrelTitle, rows: barrelRows, print });\n const longTitle = `over ${SOFT_LINE_MARK} lines (a prompt to explain, not a defect)`;\n const longRows = long.map((f) => `${rel(f.file)} (${f.lines})`);\n SplitReport.section({ title: longTitle, rows: longRows, print });\n const cycleMsg = cycles < 0 ? 'no dist' : cycles;\n print(`- import cycles in ${distDir}: ${cycleMsg}`);\n }\n\n /** Every packages/* and examples/* directory that has a src/ folder. */\n static defaultTargets(opts: { root?: string } = {}): string[] {\n const root = opts.root ?? process.cwd();\n const withSource = (folder: string): string[] =>\n existsSync(resolve(root, folder))\n ? readdirSync(resolve(root, folder))\n .map((name) => resolve(root, folder, name))\n .filter((dir) => existsSync(join(dir, 'src')))\n : [];\n\n return [...withSource('packages'), ...withSource('examples')];\n }\n\n private static section(opts: {\n title: string;\n rows: string[];\n print: (line: string) => void;\n }): void {\n opts.print(`- ${opts.title}: ${opts.rows.length}`);\n\n for (const row of opts.rows) {\n opts.print(` ${row}`);\n }\n }\n\n private static facts(opts: { file: string }): IFileFacts {\n const text = readFileSync(opts.file, 'utf8');\n\n return {\n file: opts.file,\n lines: text.split('\\n').length,\n exports: [...text.matchAll(DECLARATION)].length,\n classes: [...text.matchAll(EXPORTED_CLASS)].map((match) => match[1]),\n };\n }\n\n /** abstract.ts + base.ts pairs are the format; exactly one of each is one concept. */\n private static isAbstractBasePair(opts: { facts: IFileFacts }): boolean {\n const [first, second] = opts.facts.classes;\n\n return opts.facts.classes.length === 2 && /Abstract/.test(first) && /Base/.test(second);\n }\n\n private static walk(opts: { dir: string }): string[] {\n const out: string[] = [];\n const dirEntries = readdirSync(opts.dir);\n\n for (const name of dirEntries) {\n if (SKIP.has(name)) {\n continue;\n }\n\n const full = join(opts.dir, name);\n\n if (statSync(full).isDirectory()) {\n out.push(...SplitReport.walk({ dir: full }));\n } else if (full.endsWith('.ts') && !full.endsWith('.d.ts')) {\n out.push(full);\n }\n }\n\n return out;\n }\n\n private static foldersWithoutIndex(opts: { dir: string }): string[] {\n const out: string[] = [];\n const dirEntries = readdirSync(opts.dir);\n\n for (const name of dirEntries) {\n if (SKIP.has(name)) {\n continue;\n }\n\n const full = join(opts.dir, name);\n\n if (!statSync(full).isDirectory()) {\n continue;\n }\n\n const hasTs = readdirSync(full).some(\n (entry) => entry.endsWith('.ts') && entry !== 'index.ts',\n );\n\n if (hasTs && !existsSync(join(full, 'index.ts'))) {\n out.push(full);\n }\n\n out.push(...SplitReport.foldersWithoutIndex({ dir: full }));\n }\n\n return out;\n }\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"test-controls.js","sourceRoot":"","sources":["../../src/checks/test-controls.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync, readFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport type ts from 'typescript';\n\n/**\n * A positive control is a test that proves a gate can fail: a fixture that must be refused, a broken\n * subject the check must catch. Deleting one makes the gate pass for the wrong reason, so the trim-tests\n * method keeps a committed registry of them and fails when one disappears.\n */\nexport interface IControlEntry {\n /** The gate the control belongs to; reported with a failure. */\n gate: string;\n /** Repository-relative path of the test file. */\n file: string;\n /**\n * `test` (default): `file` is a test file and `titlePattern` is matched against its titles.\n * `file`: `file` is a NEGATIVE FIXTURE, a file a test needs to be absent from the result (a class the\n * scanner must ignore). It only has to exist: without it the test's \"not found\" assertion passes for the\n * wrong reason.\n */\n kind?: 'test' | 'file';\n /** A regular expression matched against the TITLES (suite and case) in that file. Not used by `file` entries. */\n titlePattern?: string;\n /** At least this many titles must match. Default 1. */\n min?: number;\n}\n\nexport interface IControlProblem {\n entry: IControlEntry;\n message: string;\n}\n\nexport interface IControlsReport {\n checked: number;\n problems: IControlProblem[];\n}\n\nexport class TestControlsCheck {\n /**\n * The titles in a test file: the first string argument of `test`, `it`, `describe` and of the\n * second call of `test.each(rows)('title', ...)`, with their `.only`/`.skip`/`.if(...)` forms.\n * A `test.each` title is returned as written (`%s` and all): the registry patterns are regular\n * expressions, so the expansion does not matter.\n */\n static async listTitles(opts: { source: string; fileName?: string }): Promise<string[]> {\n const result = await import('typescript');\n const typescript: typeof ts = result.default;\n const file = typescript.createSourceFile(\n opts.fileName ?? 'file.test.ts',\n opts.source,\n typescript.ScriptTarget.Latest,\n true,\n );\n const titles: string[] = [];\n const RUNNERS = new Set(['test', 'it', 'describe']);\n\n const rootName = (expression: ts.Expression): string | undefined => {\n if (typescript.isIdentifier(expression)) {\n return expression.text;\n }\n\n if (typescript.isPropertyAccessExpression(expression)) {\n return rootName(expression.expression);\n }\n\n if (typescript.isCallExpression(expression)) {\n return rootName(expression.expression);\n }\n\n return undefined;\n };\n\n const visit = (node: ts.Node): void => {\n if (typescript.isCallExpression(node)) {\n const first = node.arguments[0];\n const root = rootName(node.expression);\n\n if (\n root !== undefined &&\n RUNNERS.has(root) &&\n first &&\n (typescript.isStringLiteral(first) || typescript.isNoSubstitutionTemplateLiteral(first))\n ) {\n titles.push(first.text);\n }\n }\n\n typescript.forEachChild(node, visit);\n };\n\n visit(file);\n\n return titles;\n }\n\n /** Checks every entry of the registry against the test files it names. */\n static async run(opts: { root: string; controls: IControlEntry[] }): Promise<IControlsReport> {\n const problems: IControlProblem[] = [];\n\n for (const entry of opts.controls) {\n const path = join(opts.root, entry.file);\n if (!existsSync(path)) {\n problems.push({\n entry,\n message: `the ${entry.kind === 'file' ? 'negative fixture' : 'test file'} ${entry.file} does not exist`,\n });\n continue;\n }\n\n if (entry.kind === 'file') {\n continue;\n }\n\n const titles = await TestControlsCheck.listTitles({\n source: readFileSync(path, 'utf8'),\n fileName: entry.file,\n });\n const pattern = new RegExp(entry.titlePattern ?? '');\n const matched = titles.filter((title) => pattern.test(title)).length;\n const min = entry.min ?? 1;\n\n if (matched < min) {\n problems.push({\n entry,\n message: `${matched} title(s) match /${entry.titlePattern ?? ''}/ in ${entry.file}, ${min} required | the control of gate '${entry.gate}' is gone or renamed`,\n });\n }\n }\n\n return { checked: opts.controls.length, problems };\n }\n}\n"]}
1
+ {"version":3,"file":"test-controls.js","sourceRoot":"","sources":["../../src/checks/test-controls.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync, readFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport type ts from 'typescript';\n\n/**\n * A positive control is a test that proves a gate can fail: a fixture that must be refused, a broken\n * subject the check must catch. Deleting one makes the gate pass for the wrong reason, so the trim-tests\n * method keeps a committed registry of them and fails when one disappears.\n */\nexport interface IControlEntry {\n /** The gate the control belongs to; reported with a failure. */\n gate: string;\n /** Repository-relative path of the test file. */\n file: string;\n /**\n * `test` (default): `file` is a test file and `titlePattern` is matched against its titles.\n * `file`: `file` is a NEGATIVE FIXTURE, a file a test needs to be absent from the result (a class the\n * scanner must ignore). It only has to exist: without it the test's \"not found\" assertion passes for the\n * wrong reason.\n */\n kind?: 'test' | 'file';\n /** A regular expression matched against the TITLES (suite and case) in that file. Not used by `file` entries. */\n titlePattern?: string;\n /** At least this many titles must match. Default 1. */\n min?: number;\n}\n\nexport interface IControlProblem {\n entry: IControlEntry;\n message: string;\n}\n\nexport interface IControlsReport {\n checked: number;\n problems: IControlProblem[];\n}\n\nexport class TestControlsCheck {\n /**\n * The titles in a test file: the first string argument of `test`, `it`, `describe` and of the\n * second call of `test.each(rows)('title', ...)`, with their `.only`/`.skip`/`.if(...)` forms.\n * A `test.each` title is returned as written (`%s` and all): the registry patterns are regular\n * expressions, so the expansion does not matter.\n */\n static async listTitles(opts: { source: string; fileName?: string }): Promise<string[]> {\n const result = await import('typescript');\n const typescript: typeof ts = result.default;\n const file = typescript.createSourceFile(\n opts.fileName ?? 'file.test.ts',\n opts.source,\n typescript.ScriptTarget.Latest,\n true,\n );\n const titles: string[] = [];\n const RUNNERS = new Set(['test', 'it', 'describe']);\n\n const rootName = (expression: ts.Expression): string | undefined => {\n if (typescript.isIdentifier(expression)) {\n return expression.text;\n }\n\n if (typescript.isPropertyAccessExpression(expression)) {\n return rootName(expression.expression);\n }\n\n if (typescript.isCallExpression(expression)) {\n return rootName(expression.expression);\n }\n\n return undefined;\n };\n\n const visit = (node: ts.Node): void => {\n if (typescript.isCallExpression(node)) {\n const first = node.arguments[0];\n const root = rootName(node.expression);\n\n if (\n root !== undefined &&\n RUNNERS.has(root) &&\n first &&\n (typescript.isStringLiteral(first) || typescript.isNoSubstitutionTemplateLiteral(first))\n ) {\n titles.push(first.text);\n }\n }\n\n typescript.forEachChild(node, visit);\n };\n\n visit(file);\n\n return titles;\n }\n\n /** Checks every entry of the registry against the test files it names. */\n static async run(opts: { root: string; controls: IControlEntry[] }): Promise<IControlsReport> {\n const problems: IControlProblem[] = [];\n\n for (const entry of opts.controls) {\n const path = join(opts.root, entry.file);\n\n if (!existsSync(path)) {\n problems.push({\n entry,\n message: `the ${entry.kind === 'file' ? 'negative fixture' : 'test file'} ${entry.file} does not exist`,\n });\n continue;\n }\n\n if (entry.kind === 'file') {\n continue;\n }\n\n const titles = await TestControlsCheck.listTitles({\n source: readFileSync(path, 'utf8'),\n fileName: entry.file,\n });\n const pattern = new RegExp(entry.titlePattern ?? '');\n const matched = titles.filter((title) => pattern.test(title)).length;\n const min = entry.min ?? 1;\n\n if (matched < min) {\n problems.push({\n entry,\n message: `${matched} title(s) match /${entry.titlePattern ?? ''}/ in ${entry.file}, ${min} required | the control of gate '${entry.gate}' is gone or renamed`,\n });\n }\n }\n\n return { checked: opts.controls.length, problems };\n }\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"test-dead-code.js","sourceRoot":"","sources":["../../src/checks/test-dead-code.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawnSync } from 'node:child_process';\nimport { dirname } from 'node:path';\n\n/**\n * Support files of the tests (fixtures, helpers, probes) that no other file refers to. The second\n * step of the trim-tests method. The output is CANDIDATES: a fixture can be loaded by a path built at\n * run time or by scanning a folder, so a candidate is deleted only after the package suite is green\n * without it.\n */\nexport interface IDeadTestCodeCandidate {\n file: string;\n /**\n * `unreferenced`: neither its name nor its path under `__tests__` appears in another file, nor does\n * its folder. `folder-referenced`: only its folder path does, so a scan of that folder may load it.\n */\n kind: 'unreferenced' | 'folder-referenced';\n}\n\nconst SUPPORT_EXTENSION = /\\.(?:[cm]?[jt]sx?|json|sql|txt)$/;\nconst TEST_FILE = /\\.(?:test|spec)\\.[cm]?[jt]sx?$/;\n\nconst git = (opts: { root: string; args: string[] }): { status: number; stdout: string } => {\n const result = spawnSync('git', opts.args, {\n cwd: opts.root,\n encoding: 'utf8',\n maxBuffer: 256 * 1024 * 1024,\n });\n return { status: result.status ?? 1, stdout: result.stdout ?? '' };\n};\n\n/** Files other than `self` whose text contains `needle` (a fixed string). */\nconst referencedElsewhere = (opts: { root: string; needle: string; self: string }): boolean => {\n const result = git({ root: opts.root, args: ['grep', '-l', '-I', '-F', '-e', opts.needle] });\n return result.stdout.split('\\n').some((line) => line !== '' && line !== opts.self);\n};\n\nconst escapeForRegex = (text: string): string => text.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n\n/** Files other than `self` that mention the name of the file as a path segment or an import. */\nconst nameReferencedElsewhere = (opts: { root: string; stem: string; self: string }): boolean => {\n const pattern = `[/'\"\\`]${escapeForRegex(opts.stem)}(\\\\.|'|\"|\\`|$)`;\n const result = git({ root: opts.root, args: ['grep', '-l', '-I', '-E', '-e', pattern] });\n return result.stdout.split('\\n').some((line) => line !== '' && line !== opts.self);\n};\n\nexport class DeadTestCodeCheck {\n static find(opts: { root: string }): IDeadTestCodeCandidate[] {\n const tracked = git({ root: opts.root, args: ['ls-files'] }).stdout.split('\\n');\n const support = tracked.filter(\n (file) =>\n file.includes('/__tests__/') &&\n SUPPORT_EXTENSION.test(file) &&\n !TEST_FILE.test(file) &&\n !file.endsWith('.d.ts'),\n );\n const candidates: IDeadTestCodeCandidate[] = [];\n\n for (const file of support) {\n const underTests = file.slice(file.indexOf('/__tests__/') + '/__tests__/'.length);\n const stem = underTests.split('/').pop()!.replace(SUPPORT_EXTENSION, '');\n const withoutExtension = underTests.replace(SUPPORT_EXTENSION, '');\n\n if (\n nameReferencedElsewhere({ root: opts.root, stem, self: file }) ||\n referencedElsewhere({ root: opts.root, needle: withoutExtension, self: file })\n ) {\n continue;\n }\n\n const folder = dirname(underTests);\n const folderReferenced =\n folder !== '.' && referencedElsewhere({ root: opts.root, needle: folder, self: file });\n\n candidates.push({ file, kind: folderReferenced ? 'folder-referenced' : 'unreferenced' });\n }\n\n return candidates;\n }\n}\n"]}
1
+ {"version":3,"file":"test-dead-code.js","sourceRoot":"","sources":["../../src/checks/test-dead-code.ts"],"names":[],"mappings":";;;AAAA,2DAA+C;AAC/C,yCAAoC;AAiBpC,MAAM,iBAAiB,GAAG,kCAAkC,CAAC;AAC7D,MAAM,SAAS,GAAG,gCAAgC,CAAC;AAEnD,MAAM,GAAG,GAAG,CAAC,IAAsC,EAAsC,EAAE;IACzF,MAAM,MAAM,GAAG,IAAA,8BAAS,EAAC,KAAK,EAAE,IAAI,CAAC,IAAI,EAAE;QACzC,GAAG,EAAE,IAAI,CAAC,IAAI;QACd,QAAQ,EAAE,MAAM;QAChB,SAAS,EAAE,GAAG,GAAG,IAAI,GAAG,IAAI;KAC7B,CAAC,CAAC;IAEH,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,IAAI,EAAE,EAAE,CAAC;AACrE,CAAC,CAAC;AAEF,6EAA6E;AAC7E,MAAM,mBAAmB,GAAG,CAAC,IAAoD,EAAW,EAAE;IAC5F,MAAM,MAAM,GAAG,GAAG,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IAE7F,OAAO,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,KAAK,EAAE,IAAI,IAAI,KAAK,IAAI,CAAC,IAAI,CAAC,CAAC;AACrF,CAAC,CAAC;AAEF,MAAM,cAAc,GAAG,CAAC,IAAY,EAAU,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,qBAAqB,EAAE,MAAM,CAAC,CAAC;AAE7F,gGAAgG;AAChG,MAAM,uBAAuB,GAAG,CAAC,IAAkD,EAAW,EAAE;IAC9F,MAAM,OAAO,GAAG,UAAU,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,gBAAgB,CAAC;IACpE,MAAM,MAAM,GAAG,GAAG,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,CAAC,EAAE,CAAC,CAAC;IAEzF,OAAO,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,KAAK,EAAE,IAAI,IAAI,KAAK,IAAI,CAAC,IAAI,CAAC,CAAC;AACrF,CAAC,CAAC;AAEF,MAAa,iBAAiB;IAC5B,MAAM,CAAC,IAAI,CAAC,IAAsB;QAChC,MAAM,OAAO,GAAG,GAAG,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAChF,MAAM,OAAO,GAAG,OAAO,CAAC,MAAM,CAC5B,CAAC,IAAI,EAAE,EAAE,CACP,IAAI,CAAC,QAAQ,CAAC,aAAa,CAAC;YAC5B,iBAAiB,CAAC,IAAI,CAAC,IAAI,CAAC;YAC5B,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC;YACrB,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,CAC1B,CAAC;QACF,MAAM,UAAU,GAA6B,EAAE,CAAC;QAEhD,KAAK,MAAM,IAAI,IAAI,OAAO,EAAE,CAAC;YAC3B,MAAM,UAAU,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC;YAClF,MAAM,IAAI,GAAG,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAG,CAAC,OAAO,CAAC,iBAAiB,EAAE,EAAE,CAAC,CAAC;YACzE,MAAM,gBAAgB,GAAG,UAAU,CAAC,OAAO,CAAC,iBAAiB,EAAE,EAAE,CAAC,CAAC;YAEnE,IACE,uBAAuB,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;gBAC9D,mBAAmB,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,gBAAgB,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,EAC9E,CAAC;gBACD,SAAS;YACX,CAAC;YAED,MAAM,MAAM,GAAG,IAAA,mBAAO,EAAC,UAAU,CAAC,CAAC;YACnC,MAAM,gBAAgB,GACpB,MAAM,KAAK,GAAG,IAAI,mBAAmB,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;YAEzF,UAAU,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,gBAAgB,CAAC,CAAC,CAAC,mBAAmB,CAAC,CAAC,CAAC,cAAc,EAAE,CAAC,CAAC;QAC3F,CAAC;QAED,OAAO,UAAU,CAAC;IACpB,CAAC;CACF;AAjCD,8CAiCC","sourcesContent":["import { spawnSync } from 'node:child_process';\nimport { dirname } from 'node:path';\n\n/**\n * Support files of the tests (fixtures, helpers, probes) that no other file refers to. The second\n * step of the trim-tests method. The output is CANDIDATES: a fixture can be loaded by a path built at\n * run time or by scanning a folder, so a candidate is deleted only after the package suite is green\n * without it.\n */\nexport interface IDeadTestCodeCandidate {\n file: string;\n /**\n * `unreferenced`: neither its name nor its path under `__tests__` appears in another file, nor does\n * its folder. `folder-referenced`: only its folder path does, so a scan of that folder may load it.\n */\n kind: 'unreferenced' | 'folder-referenced';\n}\n\nconst SUPPORT_EXTENSION = /\\.(?:[cm]?[jt]sx?|json|sql|txt)$/;\nconst TEST_FILE = /\\.(?:test|spec)\\.[cm]?[jt]sx?$/;\n\nconst git = (opts: { root: string; args: string[] }): { status: number; stdout: string } => {\n const result = spawnSync('git', opts.args, {\n cwd: opts.root,\n encoding: 'utf8',\n maxBuffer: 256 * 1024 * 1024,\n });\n\n return { status: result.status ?? 1, stdout: result.stdout ?? '' };\n};\n\n/** Files other than `self` whose text contains `needle` (a fixed string). */\nconst referencedElsewhere = (opts: { root: string; needle: string; self: string }): boolean => {\n const result = git({ root: opts.root, args: ['grep', '-l', '-I', '-F', '-e', opts.needle] });\n\n return result.stdout.split('\\n').some((line) => line !== '' && line !== opts.self);\n};\n\nconst escapeForRegex = (text: string): string => text.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n\n/** Files other than `self` that mention the name of the file as a path segment or an import. */\nconst nameReferencedElsewhere = (opts: { root: string; stem: string; self: string }): boolean => {\n const pattern = `[/'\"\\`]${escapeForRegex(opts.stem)}(\\\\.|'|\"|\\`|$)`;\n const result = git({ root: opts.root, args: ['grep', '-l', '-I', '-E', '-e', pattern] });\n\n return result.stdout.split('\\n').some((line) => line !== '' && line !== opts.self);\n};\n\nexport class DeadTestCodeCheck {\n static find(opts: { root: string }): IDeadTestCodeCandidate[] {\n const tracked = git({ root: opts.root, args: ['ls-files'] }).stdout.split('\\n');\n const support = tracked.filter(\n (file) =>\n file.includes('/__tests__/') &&\n SUPPORT_EXTENSION.test(file) &&\n !TEST_FILE.test(file) &&\n !file.endsWith('.d.ts'),\n );\n const candidates: IDeadTestCodeCandidate[] = [];\n\n for (const file of support) {\n const underTests = file.slice(file.indexOf('/__tests__/') + '/__tests__/'.length);\n const stem = underTests.split('/').pop()!.replace(SUPPORT_EXTENSION, '');\n const withoutExtension = underTests.replace(SUPPORT_EXTENSION, '');\n\n if (\n nameReferencedElsewhere({ root: opts.root, stem, self: file }) ||\n referencedElsewhere({ root: opts.root, needle: withoutExtension, self: file })\n ) {\n continue;\n }\n\n const folder = dirname(underTests);\n const folderReferenced =\n folder !== '.' && referencedElsewhere({ root: opts.root, needle: folder, self: file });\n\n candidates.push({ file, kind: folderReferenced ? 'folder-referenced' : 'unreferenced' });\n }\n\n return candidates;\n }\n}\n"]}