@venizia/dev-configs 0.4.2 → 0.4.4

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 (106) hide show
  1. package/LICENSE.md +2 -5
  2. package/README.md +20 -5
  3. package/dist/checks/catalog.d.ts +39 -0
  4. package/dist/checks/catalog.d.ts.map +1 -0
  5. package/dist/checks/catalog.js +94 -0
  6. package/dist/checks/catalog.js.map +1 -0
  7. package/dist/checks/index.d.ts +11 -0
  8. package/dist/checks/index.d.ts.map +1 -0
  9. package/dist/checks/index.js +27 -0
  10. package/dist/checks/index.js.map +1 -0
  11. package/dist/checks/module-cycles.d.ts +22 -0
  12. package/dist/checks/module-cycles.d.ts.map +1 -0
  13. package/dist/checks/module-cycles.js +149 -0
  14. package/dist/checks/module-cycles.js.map +1 -0
  15. package/dist/checks/purity/manifest.d.ts +94 -0
  16. package/dist/checks/purity/manifest.d.ts.map +1 -0
  17. package/dist/checks/purity/manifest.js +177 -0
  18. package/dist/checks/purity/manifest.js.map +1 -0
  19. package/dist/checks/purity/probe.d.ts +68 -0
  20. package/dist/checks/purity/probe.d.ts.map +1 -0
  21. package/dist/checks/purity/probe.js +295 -0
  22. package/dist/checks/purity/probe.js.map +1 -0
  23. package/dist/checks/purity/run.d.ts +47 -0
  24. package/dist/checks/purity/run.d.ts.map +1 -0
  25. package/dist/checks/purity/run.js +121 -0
  26. package/dist/checks/purity/run.js.map +1 -0
  27. package/dist/checks/split-report.d.ts +19 -0
  28. package/dist/checks/split-report.d.ts.map +1 -0
  29. package/dist/checks/split-report.js +123 -0
  30. package/dist/checks/split-report.js.map +1 -0
  31. package/dist/cli/check.d.ts +10 -0
  32. package/dist/cli/check.d.ts.map +1 -0
  33. package/dist/cli/check.js +206 -0
  34. package/dist/cli/check.js.map +1 -0
  35. package/dist/cli/clean.d.ts +2 -1
  36. package/dist/cli/clean.d.ts.map +1 -1
  37. package/dist/cli/clean.js +18 -1
  38. package/dist/cli/clean.js.map +1 -1
  39. package/dist/cli/codemod/const-classes.d.ts +80 -0
  40. package/dist/cli/codemod/const-classes.d.ts.map +1 -0
  41. package/dist/cli/codemod/const-classes.js +603 -0
  42. package/dist/cli/codemod/const-classes.js.map +1 -0
  43. package/dist/cli/codemod/drop-redundant-casts.d.ts +112 -0
  44. package/dist/cli/codemod/drop-redundant-casts.d.ts.map +1 -0
  45. package/dist/cli/codemod/drop-redundant-casts.js +780 -0
  46. package/dist/cli/codemod/drop-redundant-casts.js.map +1 -0
  47. package/dist/cli/codemod/files.d.ts +22 -0
  48. package/dist/cli/codemod/files.d.ts.map +1 -0
  49. package/dist/cli/codemod/files.js +68 -0
  50. package/dist/cli/codemod/files.js.map +1 -0
  51. package/dist/cli/codemod/index.d.ts +5 -4
  52. package/dist/cli/codemod/index.d.ts.map +1 -1
  53. package/dist/cli/codemod/index.js +77 -36
  54. package/dist/cli/codemod/index.js.map +1 -1
  55. package/dist/cli/codemod/model-settings.d.ts +2 -1
  56. package/dist/cli/codemod/model-settings.d.ts.map +1 -1
  57. package/dist/cli/codemod/model-settings.js +77 -3
  58. package/dist/cli/codemod/model-settings.js.map +1 -1
  59. package/dist/cli/index.js +21 -2
  60. package/dist/cli/index.js.map +1 -1
  61. package/dist/linters/house/common.d.ts +1 -0
  62. package/dist/linters/house/common.d.ts.map +1 -1
  63. package/dist/linters/house/common.js +1 -0
  64. package/dist/linters/house/common.js.map +1 -1
  65. package/dist/linters/house/configs.d.ts.map +1 -1
  66. package/dist/linters/house/configs.js +20 -1
  67. package/dist/linters/house/configs.js.map +1 -1
  68. package/dist/linters/house/const-class-values.rule.d.ts +7 -4
  69. package/dist/linters/house/const-class-values.rule.d.ts.map +1 -1
  70. package/dist/linters/house/const-class-values.rule.js +73 -21
  71. package/dist/linters/house/const-class-values.rule.js.map +1 -1
  72. package/dist/linters/rules/common.d.ts +48 -1
  73. package/dist/linters/rules/common.d.ts.map +1 -1
  74. package/dist/linters/rules/common.js +53 -0
  75. package/dist/linters/rules/common.js.map +1 -1
  76. package/dist/linters/rules/eqeqeq.rule.d.ts +2 -2
  77. package/dist/linters/rules/eqeqeq.rule.d.ts.map +1 -1
  78. package/dist/linters/rules/eqeqeq.rule.js +2 -1
  79. package/dist/linters/rules/eqeqeq.rule.js.map +1 -1
  80. package/dist/linters/rules/no-unused-vars.rule.d.ts +3 -3
  81. package/dist/linters/rules/no-unused-vars.rule.d.ts.map +1 -1
  82. package/dist/linters/rules/no-unused-vars.rule.js +2 -1
  83. package/dist/linters/rules/no-unused-vars.rule.js.map +1 -1
  84. package/dist/linters/rules/switch-case-braces.rule.d.ts +2 -2
  85. package/dist/linters/rules/switch-case-braces.rule.d.ts.map +1 -1
  86. package/dist/linters/rules/switch-case-braces.rule.js +2 -1
  87. package/dist/linters/rules/switch-case-braces.rule.js.map +1 -1
  88. package/dist/testing/index.d.ts +2 -0
  89. package/dist/testing/index.d.ts.map +1 -1
  90. package/dist/testing/index.js +2 -0
  91. package/dist/testing/index.js.map +1 -1
  92. package/dist/testing/repo-tmp.d.ts +12 -0
  93. package/dist/testing/repo-tmp.d.ts.map +1 -0
  94. package/dist/testing/repo-tmp.js +67 -0
  95. package/dist/testing/repo-tmp.js.map +1 -0
  96. package/dist/testing/spawn.helper.d.ts +13 -0
  97. package/dist/testing/spawn.helper.d.ts.map +1 -0
  98. package/dist/testing/spawn.helper.js +22 -0
  99. package/dist/testing/spawn.helper.js.map +1 -0
  100. package/dist/testing/tmpdir-preload.d.ts +2 -0
  101. package/dist/testing/tmpdir-preload.d.ts.map +1 -0
  102. package/dist/testing/tmpdir-preload.js +9 -0
  103. package/dist/testing/tmpdir-preload.js.map +1 -0
  104. package/package.json +9 -1
  105. package/src/tsconfig/tsconfig.base.json +5 -2
  106. package/src/tsconfig/tsconfig.common.json +2 -2
package/LICENSE.md CHANGED
@@ -4,7 +4,7 @@ Ignis is a TypeScript Server Infrastructure framework built on [Hono](https://ho
4
4
 
5
5
  ## MIT License
6
6
 
7
- Copyright (c) 2025 VENIZIA Ltd. Co.
7
+ Copyright (c) 2025 Venizia Co., Ltd.
8
8
 
9
9
  Permission is hereby granted, free of charge, to any person obtaining a copy
10
10
  of this software and associated documentation files (the "Software"), to deal
@@ -86,12 +86,9 @@ Ignis uses the following open-source libraries. We are grateful to the authors a
86
86
 
87
87
  ## Acknowledgments
88
88
 
89
- Ignis Framework is inspired by and built upon the work of many open-source projects:
89
+ Ignis Framework is built on the work of open-source projects:
90
90
 
91
91
  - **[Hono](https://hono.dev/)** by Yusuke Wada - The ultrafast web framework that powers Ignis
92
- - **[LoopBack 4](https://loopback.io/)** by IBM/StrongLoop - Inspiration for the enterprise architecture patterns
93
- - **[NestJS](https://nestjs.com/)** by Kamil Mysliwiec - Inspiration for decorator-based controllers
94
- - **[InversifyJS](https://inversify.io/)** - Inspiration for the IoC container design
95
92
 
96
93
  We thank all the maintainers and contributors of these projects for their excellent work.
97
94
 
package/README.md CHANGED
@@ -61,7 +61,7 @@ The two options in `compilerOptions` repeat the base on purpose - see
61
61
 
62
62
  ### Browser and React projects
63
63
 
64
- The presets above target a Bun or Node backend. Code that runs in a browser swaps each one for its
64
+ The presets above target a Bun backend. Code that runs in a browser swaps each one for its
65
65
  browser version. A React project swaps it for the React version, which builds on the browser one.
66
66
 
67
67
  | Tool | Backend | Browser | React |
@@ -169,15 +169,17 @@ booleans. `BrowserOxlintConfigs.create()` and `ReactOxlintConfigs.create()` take
169
169
  | `MaxParamsOwnRule` | `venizia/max-params-own` | `build({ max: 5 })` | A function we write takes at most `max` positional parameters. Not counted: an inline callback passed to a call or a JSX prop, a signature a declared type fixes, a constructor whose parameters are all `@inject`. A hook callback counts. |
170
170
  | `NoNewErrorRule` | `venizia/no-new-error` | `error` | Throw through `getError` (or an `ApplicationError`), never `new Error(...)` / `Error(...)`. Off in tests. |
171
171
  | `NoLeakedRenderRule` | `venizia/no-leaked-render` | `error`, in `reactHouseConfigs` only | A JSX child is drawn with `cond ? <X /> : null`, never `cond && <X />`, which draws `0` when `cond` is `0`. `--fix` rewrites it. `disabled={a && b}` is a value, not a child, and passes. |
172
- | `ConstClassValuesRule` | `venizia/const-class-values` | off; `build({ namePattern })` | An exported type named like a logic value (`State`, `Status`, `Stage`, `Mode`, `Kind`, `Type` by default) is a const class plus `TConstValue<typeof X>`, not a union of string literals. |
172
+ | `ConstClassValuesRule` | `venizia/const-class-values` | `warn` in both presets; `build({ severity, namePattern })` | V-C-08. A union of string literals in an exported declaration (a type alias, an interface member, a generic constraint, a parameter or return type) is a const class plus `TConstValue<typeof X>`. Not counted: `Pick`, `Omit`, `Extract`, `Exclude`, `Record` arguments and mapped-type keys, and a union with a non-string member. `namePattern` narrows it to exported type aliases whose name matches. Off in tests. |
173
+ | `@typescript-eslint/consistent-type-assertions` | `assertionStyle: never` | `warn` in both presets | V-C-10. `as X`, `as any` and `as unknown as X` warn; `as const` and `satisfies` do not. Off in tests. |
173
174
 
174
- `const-class-values` is registered in the plugin but no preset turns it on: a few IGNIS types still
175
- break it. Enable it with `[ConstClassValuesRule.NAME]: ConstClassValuesRule.build()`.
175
+ `const-class-values` and the cast rule only warn, so adopting them never fails a lint. Raise one to an error
176
+ with `[ConstClassValuesRule.NAME]: ConstClassValuesRule.build({ severity: 'error' })`. The 5-parameter
177
+ limit (V-C-02) is `venizia/max-params-own`, an error.
176
178
 
177
179
  ### namingConventionConfigs
178
180
 
179
181
  `namingConventionConfigs` (type `Linter.Config[]`) sets only `@typescript-eslint/naming-convention`, with
180
- the 15 LoopBack 4 selectors. It targets `.ts`, `.tsx`, `.mts` and `.cts`, and ignores `dist`, `build`,
182
+ its 15 selectors. It targets `.ts`, `.tsx`, `.mts` and `.cts`, and ignores `dist`, `build`,
181
183
  `coverage`, `*.d.ts` and plain JavaScript. ESLint does not report unused disable comments in this
182
184
  config, because oxlint owns the other rules.
183
185
 
@@ -410,3 +412,16 @@ The root of the chain, with no IGNIS dependency: **dev-configs** -> inversion ->
410
412
  - [Source](https://github.com/VENIZIA-AI/ignis/blob/main/packages/dev-configs)
411
413
 
412
414
  MIT licensed - see [LICENSE.md](./LICENSE.md).
415
+
416
+ ---
417
+
418
+ <p align="center">
419
+ <a href="https://venizia.ai">
420
+ <picture>
421
+ <source media="(prefers-color-scheme: dark)" srcset="https://venizia.ai/assets/logo-white.svg">
422
+ <img src="https://venizia.ai/assets/logo.svg" alt="VENIZIA" height="28">
423
+ </picture>
424
+ </a>
425
+ <br>
426
+ <sub>@venizia/dev-configs is part of IGNIS, built and maintained by <a href="https://venizia.ai">VENIZIA</a>.</sub>
427
+ </p>
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Guards dependency versions against the root `workspaces.catalog`.
3
+ *
4
+ * A catalogued dep is referenced as `catalog:` and resolved at publish time by `bun publish`, which
5
+ * substitutes the real range while packing. `npm publish` does NOT - it ships `catalog:` verbatim
6
+ * and the manifest is unresolvable for every consumer, which is how 0.1.1-9 / -6 / -4 broke.
7
+ */
8
+ export interface ICatalogProblem {
9
+ file: string;
10
+ message: string;
11
+ }
12
+ export interface ICatalogOptions {
13
+ /** The repository root: where the root `package.json` and the globs resolve. Default: the current folder. */
14
+ root?: string;
15
+ /** Manifests to check, beside `packages/*\/package.json`. A glob relative to the root. */
16
+ globs?: string[];
17
+ /**
18
+ * Files that are not published manifests but carry `dependencies` and `devDependencies` the
19
+ * release resolves (a VULCAN preset): `catalog:` is allowed in their `peerDependencies`, because a
20
+ * library preset gives one name one catalog range. A file belongs here when its path starts with
21
+ * this prefix, for example `presets/`.
22
+ */
23
+ presetPrefix?: string;
24
+ /**
25
+ * Manifests outside the workspace (a wiki with its own lockfile): `catalog:` does not resolve
26
+ * there, so a catalogued dependency must be pinned to exactly the catalog's version instead.
27
+ */
28
+ pinned?: string[];
29
+ }
30
+ export interface ICatalogReport {
31
+ problems: ICatalogProblem[];
32
+ entries: number;
33
+ referenced: number;
34
+ /** Set when there is no `workspaces.catalog` to check against. */
35
+ noCatalog: boolean;
36
+ }
37
+ /** Reads the root catalog and every manifest the options name, and reports everything wrong with their ranges. */
38
+ export declare const findCatalogProblems: (opts?: ICatalogOptions) => Promise<ICatalogReport>;
39
+ //# sourceMappingURL=catalog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"catalog.d.ts","sourceRoot":"","sources":["../../src/checks/catalog.ts"],"names":[],"mappings":"AAIA;;;;;;GAMG;AACH,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,eAAe;IAC9B,6GAA6G;IAC7G,IAAI,CAAC,EAAE,MAAM,CAAC;IAEd,0FAA0F;IAC1F,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IAEjB;;;;;OAKG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,eAAe,EAAE,CAAC;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IAEnB,kEAAkE;IAClE,SAAS,EAAE,OAAO,CAAC;CACpB;AAcD,kHAAkH;AAClH,eAAO,MAAM,mBAAmB,GAAU,OAAM,eAAoB,KAAG,OAAO,CAAC,cAAc,CAkG5F,CAAC"}
@@ -0,0 +1,94 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.findCatalogProblems = void 0;
4
+ const bun_1 = require("bun");
5
+ const node_fs_1 = require("node:fs");
6
+ const node_path_1 = require("node:path");
7
+ /** `peerDependencies` are compatibility statements for consumers and stay deliberately looser than the install range. */
8
+ const CATALOGUED_BLOCKS = ['dependencies', 'devDependencies'];
9
+ const DEFAULT_GLOB = 'packages/*/package.json';
10
+ /** Reads the root catalog and every manifest the options name, and reports everything wrong with their ranges. */
11
+ const findCatalogProblems = async (opts = {}) => {
12
+ const { root = process.cwd(), globs = [], presetPrefix, pinned = [] } = opts;
13
+ const rootJson = JSON.parse(await Bun.file((0, node_path_1.join)(root, 'package.json')).text());
14
+ const catalog = rootJson.workspaces?.catalog ?? {};
15
+ if (Object.keys(catalog).length === 0) {
16
+ return { problems: [], entries: 0, referenced: 0, noCatalog: true };
17
+ }
18
+ const problems = [];
19
+ const referenced = new Set();
20
+ const presentPinned = pinned.filter((file) => (0, node_fs_1.existsSync)((0, node_path_1.join)(root, file)));
21
+ const files = new Set();
22
+ for (const pattern of [DEFAULT_GLOB, ...globs]) {
23
+ for await (const file of new bun_1.Glob(pattern).scan(root)) {
24
+ files.add(file);
25
+ }
26
+ }
27
+ for (const file of [...files, ...presentPinned]) {
28
+ let json;
29
+ try {
30
+ json = JSON.parse(await Bun.file((0, node_path_1.join)(root, file)).text());
31
+ }
32
+ catch (error) {
33
+ problems.push({
34
+ file,
35
+ message: `invalid JSON - ${error instanceof Error ? error.message : error}`,
36
+ });
37
+ continue;
38
+ }
39
+ const isPinned = presentPinned.includes(file);
40
+ for (const block of CATALOGUED_BLOCKS) {
41
+ const entriesList = Object.entries(json[block] ?? {});
42
+ for (const [dep, range] of entriesList) {
43
+ if (!(dep in catalog)) {
44
+ continue;
45
+ }
46
+ referenced.add(dep);
47
+ if (isPinned) {
48
+ if (range !== catalog[dep]) {
49
+ problems.push({
50
+ file,
51
+ message: `${block}.${dep} is "${range}" but the catalog pins "${catalog[dep]}" - this project is outside the workspace, so pin exactly that`,
52
+ });
53
+ }
54
+ continue;
55
+ }
56
+ if (range === 'catalog:') {
57
+ continue;
58
+ }
59
+ problems.push({
60
+ file,
61
+ message: `${block}.${dep} is "${range}" but the catalog owns it - use "catalog:" (catalog pins ${catalog[dep]})`,
62
+ });
63
+ }
64
+ }
65
+ // A preset is not a published manifest: its markers are resolved by the release, and the library kind gives a
66
+ // name one catalog range, so a lib's peer takes the same marker as its dev dependency.
67
+ const isPreset = presetPrefix !== undefined && file.startsWith(presetPrefix);
68
+ // A peer resolved from the catalog would collapse to the exact install range, losing the looseness that makes it a peer.
69
+ const peerEntries = Object.entries(json.peerDependencies ?? {});
70
+ for (const [dep, range] of peerEntries) {
71
+ if (range !== 'catalog:' || isPreset) {
72
+ continue;
73
+ }
74
+ problems.push({
75
+ file,
76
+ message: `peerDependencies.${dep} uses "catalog:" - a peer range must stay hand-authored and looser than the install range`,
77
+ });
78
+ }
79
+ }
80
+ // A catalog entry nobody references is dead weight that will drift out of date unnoticed.
81
+ const catalogKeys = Object.keys(catalog);
82
+ for (const dep of catalogKeys) {
83
+ if (referenced.has(dep)) {
84
+ continue;
85
+ }
86
+ problems.push({
87
+ file: 'package.json',
88
+ message: `catalog entry "${dep}" is referenced by no workspace - drop it`,
89
+ });
90
+ }
91
+ return { problems, entries: catalogKeys.length, referenced: referenced.size, noCatalog: false };
92
+ };
93
+ exports.findCatalogProblems = findCatalogProblems;
94
+ //# sourceMappingURL=catalog.js.map
@@ -0,0 +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\n/** Reads the root catalog and every manifest the options name, and reports everything wrong with their ranges. */\nexport const findCatalogProblems = async (opts: ICatalogOptions = {}): Promise<ICatalogReport> => {\n const { root = process.cwd(), globs = [], presetPrefix, pinned = [] } = opts;\n const rootJson: IManifest = JSON.parse(await Bun.file(join(root, 'package.json')).text());\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 json = JSON.parse(await Bun.file(join(root, file)).text());\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"]}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * `@venizia/dev-configs/checks`: the engines behind `devz check <name>`. Bun-only tooling - never
3
+ * import it from code a browser runs; the purity gate asserts that no browser-claimed entry reaches it.
4
+ */
5
+ export * from './catalog';
6
+ export * from './module-cycles';
7
+ export * from './purity/manifest';
8
+ export * from './purity/probe';
9
+ export * from './purity/run';
10
+ export * from './split-report';
11
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/checks/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,cAAc,WAAW,CAAC;AAC1B,cAAc,iBAAiB,CAAC;AAChC,cAAc,mBAAmB,CAAC;AAClC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,cAAc,CAAC;AAC7B,cAAc,gBAAgB,CAAC"}
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ /**
18
+ * `@venizia/dev-configs/checks`: the engines behind `devz check <name>`. Bun-only tooling - never
19
+ * import it from code a browser runs; the purity gate asserts that no browser-claimed entry reaches it.
20
+ */
21
+ __exportStar(require("./catalog"), exports);
22
+ __exportStar(require("./module-cycles"), exports);
23
+ __exportStar(require("./purity/manifest"), exports);
24
+ __exportStar(require("./purity/probe"), exports);
25
+ __exportStar(require("./purity/run"), exports);
26
+ __exportStar(require("./split-report"), exports);
27
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/checks/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA;;;GAGG;AACH,4CAA0B;AAC1B,kDAAgC;AAChC,oDAAkC;AAClC,iDAA+B;AAC/B,+CAA6B;AAC7B,iDAA+B","sourcesContent":["/**\n * `@venizia/dev-configs/checks`: the engines behind `devz check <name>`. Bun-only tooling - never\n * import it from code a browser runs; the purity gate asserts that no browser-claimed entry reaches it.\n */\nexport * from './catalog';\nexport * from './module-cycles';\nexport * from './purity/manifest';\nexport * from './purity/probe';\nexport * from './purity/run';\nexport * from './split-report';\n"]}
@@ -0,0 +1,22 @@
1
+ export declare class ModuleGraph {
2
+ private readonly root;
3
+ private readonly edges;
4
+ private constructor();
5
+ static fromDirectory(opts: {
6
+ dir: string;
7
+ }): ModuleGraph;
8
+ /** Strongly connected components with more than one member, as repo-relative paths. */
9
+ cycles(): string[][];
10
+ private static walk;
11
+ private static resolveRelative;
12
+ }
13
+ /**
14
+ * `devz check module-cycles <dir> [--max <n>]`: prints the cycles of a built ESM directory and
15
+ * returns 1 when there are more than `max` (no bound by default), 2 on a usage error.
16
+ */
17
+ export declare const runModuleCycles: (opts: {
18
+ args: string[];
19
+ print: (line: string) => void;
20
+ printError: (line: string) => void;
21
+ }) => number;
22
+ //# sourceMappingURL=module-cycles.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"module-cycles.d.ts","sourceRoot":"","sources":["../../src/checks/module-cycles.ts"],"names":[],"mappings":"AAaA,qBAAa,WAAW;IAEpB,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,KAAK;IAFxB,OAAO;IAKP,MAAM,CAAC,aAAa,CAAC,IAAI,EAAE;QAAE,GAAG,EAAE,MAAM,CAAA;KAAE,GAAG,WAAW;IAwBxD,uFAAuF;IACvF,MAAM,IAAI,MAAM,EAAE,EAAE;IAkDpB,OAAO,CAAC,MAAM,CAAC,IAAI;IAcnB,OAAO,CAAC,MAAM,CAAC,eAAe;CAa/B;AAED;;;GAGG;AACH,eAAO,MAAM,eAAe,GAAI,MAAM;IACpC,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC9B,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CACpC,KAAG,MA8BH,CAAC"}
@@ -0,0 +1,149 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runModuleCycles = exports.ModuleGraph = void 0;
4
+ /**
5
+ * Lists import cycles in a built ESM directory. bun turns every member of a
6
+ * cycle into a lazy initializer, and a barrel `export *` over such a member
7
+ * can leave its exports undefined. Usage: devz check module-cycles packages/helpers/dist/esm [--max 0]
8
+ */
9
+ const node_fs_1 = require("node:fs");
10
+ const node_path_1 = require("node:path");
11
+ const node_util_1 = require("node:util");
12
+ // Only match import/export statements at the start of a line (after optional
13
+ // whitespace), ignoring matches inside comments or string literals.
14
+ const IMPORT_PATTERN = /^\s*(?:import|export)\s[^;]*?from\s+'([^']+)'|^\s*import\s+'([^']+)'/gm;
15
+ class ModuleGraph {
16
+ constructor(root, edges) {
17
+ this.root = root;
18
+ this.edges = edges;
19
+ }
20
+ static fromDirectory(opts) {
21
+ const root = (0, node_path_1.resolve)(opts.dir);
22
+ const files = ModuleGraph.walk({ dir: root });
23
+ const edges = new Map();
24
+ for (const file of files) {
25
+ const text = (0, node_fs_1.readFileSync)(file, 'utf8');
26
+ const targets = new Set();
27
+ const matches = text.matchAll(IMPORT_PATTERN);
28
+ for (const match of matches) {
29
+ const target = ModuleGraph.resolveRelative({
30
+ from: file,
31
+ specifier: match[1] ?? match[2],
32
+ });
33
+ if (target) {
34
+ targets.add(target);
35
+ }
36
+ }
37
+ edges.set(file, [...targets]);
38
+ }
39
+ return new ModuleGraph(root, edges);
40
+ }
41
+ /** Strongly connected components with more than one member, as repo-relative paths. */
42
+ cycles() {
43
+ let index = 0;
44
+ const stack = [];
45
+ const onStack = new Set();
46
+ const indices = new Map();
47
+ const lowLinks = new Map();
48
+ const found = [];
49
+ const visit = (node) => {
50
+ indices.set(node, index);
51
+ lowLinks.set(node, index);
52
+ index++;
53
+ stack.push(node);
54
+ onStack.add(node);
55
+ const nexts = this.edges.get(node) ?? [];
56
+ for (const next of nexts) {
57
+ if (!indices.has(next)) {
58
+ visit(next);
59
+ lowLinks.set(node, Math.min(lowLinks.get(node), lowLinks.get(next)));
60
+ }
61
+ else if (onStack.has(next)) {
62
+ lowLinks.set(node, Math.min(lowLinks.get(node), indices.get(next)));
63
+ }
64
+ }
65
+ if (lowLinks.get(node) !== indices.get(node)) {
66
+ return;
67
+ }
68
+ const component = [];
69
+ let member;
70
+ do {
71
+ member = stack.pop();
72
+ onStack.delete(member);
73
+ component.push((0, node_path_1.relative)(this.root, member));
74
+ } while (member !== node);
75
+ if (component.length > 1) {
76
+ found.push(component.sort());
77
+ }
78
+ };
79
+ const edgesKeys = this.edges.keys();
80
+ for (const node of edgesKeys) {
81
+ if (!indices.has(node)) {
82
+ visit(node);
83
+ }
84
+ }
85
+ return found;
86
+ }
87
+ static walk(opts) {
88
+ const out = [];
89
+ const dirEntries = (0, node_fs_1.readdirSync)(opts.dir);
90
+ for (const name of dirEntries) {
91
+ const full = (0, node_path_1.join)(opts.dir, name);
92
+ if ((0, node_fs_1.statSync)(full).isDirectory()) {
93
+ out.push(...ModuleGraph.walk({ dir: full }));
94
+ }
95
+ else if (full.endsWith('.js')) {
96
+ out.push(full);
97
+ }
98
+ }
99
+ return out;
100
+ }
101
+ static resolveRelative(opts) {
102
+ if (!opts.specifier.startsWith('.')) {
103
+ return undefined;
104
+ }
105
+ const base = (0, node_path_1.resolve)((0, node_path_1.dirname)(opts.from), opts.specifier);
106
+ const candidates = [base, `${base}.js`, (0, node_path_1.join)(base, 'index.js')];
107
+ for (const candidate of candidates) {
108
+ if ((0, node_fs_1.existsSync)(candidate) && (0, node_fs_1.statSync)(candidate).isFile()) {
109
+ return candidate;
110
+ }
111
+ }
112
+ return undefined;
113
+ }
114
+ }
115
+ exports.ModuleGraph = ModuleGraph;
116
+ /**
117
+ * `devz check module-cycles <dir> [--max <n>]`: prints the cycles of a built ESM directory and
118
+ * returns 1 when there are more than `max` (no bound by default), 2 on a usage error.
119
+ */
120
+ const runModuleCycles = (opts) => {
121
+ const { args, print, printError } = opts;
122
+ const { positionals, values } = (0, node_util_1.parseArgs)({
123
+ args,
124
+ allowPositionals: true,
125
+ options: { max: { type: 'string' }, dist: { type: 'string' } },
126
+ });
127
+ const dir = values.dist ?? positionals[0];
128
+ const usage = 'usage: devz check module-cycles <built ESM dir> [--max <n>]';
129
+ if (!dir) {
130
+ printError(usage);
131
+ return 2;
132
+ }
133
+ let max = Number.POSITIVE_INFINITY;
134
+ if (values.max !== undefined) {
135
+ if (!/^\d+$/.test(values.max)) {
136
+ printError(usage);
137
+ return 2;
138
+ }
139
+ max = Number(values.max);
140
+ }
141
+ const cycles = ModuleGraph.fromDirectory({ dir }).cycles();
142
+ print(`${dir}: ${cycles.length} cycle(s)`);
143
+ for (const cycle of cycles) {
144
+ print(` - ${cycle.join(' <-> ')}`);
145
+ }
146
+ return cycles.length > max ? 1 : 0;
147
+ };
148
+ exports.runModuleCycles = runModuleCycles;
149
+ //# sourceMappingURL=module-cycles.js.map
@@ -0,0 +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\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 */\nexport const runModuleCycles = (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"]}
@@ -0,0 +1,94 @@
1
+ export interface IPurityEntry {
2
+ label: string;
3
+ package: string;
4
+ /** Repository-relative path to the built file the `exports` map points at. */
5
+ entry: string;
6
+ /**
7
+ * Specifiers to pass through as `bun build --external`. Only for a third-party dependency whose
8
+ * OWN packaging is out of this gate's remit - e.g. one that relies on its `package.json` `browser`
9
+ * field remap, which the downstream app's bundler (Vite, ...) honours and `bun build` does not.
10
+ * Never for the repository's own source: an external hides that file from the module graph
11
+ * entirely, so using it on our own code would let a real leak through unseen rather than prove
12
+ * anything pure. Enforced by {@link assertNoWorkspaceExternal}, not left to this comment alone.
13
+ */
14
+ external?: string[];
15
+ /**
16
+ * This row is EXPECTED to be impure, so failing is its passing state. Derived from
17
+ * {@link IPurityClaim.impure} - never authored on a row directly.
18
+ */
19
+ expectImpure?: boolean;
20
+ }
21
+ /**
22
+ * A package whose published entry points carry a browser-purity claim.
23
+ *
24
+ * The ROW SET IS NEVER AUTHORED. It is read from the package's own `exports` map, so a sub-path
25
+ * added to a manifest is probed the moment it is published.
26
+ */
27
+ export interface IPurityClaim {
28
+ /** Directory under `packages/`, and the token `devz check purity <token>` filters on. */
29
+ package: string;
30
+ /**
31
+ * Sub-paths (as spelled in `exports`) when the claim covers only PART of the published surface.
32
+ * Omit it and the whole surface is claimed. Every sub-path named here must still exist in
33
+ * `exports`; a rename fails the derivation loudly rather than quietly dropping a row.
34
+ */
35
+ subpaths?: string[];
36
+ /** `external` for one sub-path - see {@link IPurityEntry.external}. Keyed as spelled in `exports`. */
37
+ external?: Record<string, string[]>;
38
+ /**
39
+ * Entries that CANNOT be browser-pure, keyed as spelled in `exports`, valued by the conditions
40
+ * that are impure. The waiver is exact in BOTH directions: a listed entry that turns out to be
41
+ * pure fails as loudly as an unlisted one that is not. Deriving still owns the ROW SET, so a
42
+ * sub-path added later is claimed pure by default and has to earn its line here.
43
+ */
44
+ impure?: Record<string, string[]>;
45
+ }
46
+ /** What a repository may tune about the engine; everything else is the engine's contract. */
47
+ export interface IPurityOptions {
48
+ /**
49
+ * When set, every claimed sub-path must publish its runtime entries below this prefix (for
50
+ * example `./dist/esm/`): a browser bundler needs an ES module, so a CommonJS-only entry makes
51
+ * the claim unconsumable. Omit it for a repository that publishes a single build.
52
+ */
53
+ esmEntryPrefix?: string;
54
+ /** Package-name prefix of this repository's own packages; an `external` may never start with it. Default `@venizia/`. */
55
+ workspaceScope?: string;
56
+ }
57
+ /**
58
+ * A code-level guard, not just the JSDoc on `external`: it is an escape hatch from this gate, and a
59
+ * hatch with no lock is one bad edit away from hiding a real leak - `external: ['@venizia/ignis-helpers']`
60
+ * on a red entry would silently turn it green by removing the exact root-barrel import this gate
61
+ * exists to catch from the module graph, rather than proving it absent.
62
+ *
63
+ * A plain `Error`, not `getError`: the engine runs from the repository root through `devz`, outside
64
+ * every workspace package, and has no logger to resolve.
65
+ */
66
+ export declare const assertNoWorkspaceExternal: (manifest: IPurityEntry[], workspaceScope?: string) => IPurityEntry[];
67
+ /**
68
+ * Exported for the tests, which have to drive a claim that no longer matches its package's
69
+ * `exports` map - the staleness guard is the whole point of deriving, and it cannot be asserted
70
+ * through a built manifest, which by construction is never stale.
71
+ */
72
+ export declare const deriveEntries: (opts: {
73
+ claim: IPurityClaim;
74
+ root: string;
75
+ }) => IPurityEntry[];
76
+ /**
77
+ * A browser claim is only honest if a browser bundler can actually consume the entry. A CommonJS
78
+ * entry forces every consumer into a workaround, so the claim carries the obligation: every claimed
79
+ * sub-path must publish its runtime entries below `esmEntryPrefix`. Checked here rather than trusted
80
+ * to a comment, for the same reason as {@link assertNoWorkspaceExternal} - the failure is otherwise
81
+ * invisible until someone tries to bundle the package downstream.
82
+ */
83
+ export declare const assertBrowserEsmEntry: (opts: {
84
+ claims: IPurityClaim[];
85
+ root: string;
86
+ esmEntryPrefix: string;
87
+ }) => IPurityClaim[];
88
+ /** The rows of a whole repository: every claim derived from its package's `exports`, then the guards. */
89
+ export declare const buildManifest: (opts: {
90
+ claims: IPurityClaim[];
91
+ root: string;
92
+ options?: IPurityOptions;
93
+ }) => IPurityEntry[];
94
+ //# sourceMappingURL=manifest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../../src/checks/purity/manifest.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,8EAA8E;IAC9E,KAAK,EAAE,MAAM,CAAC;IACd;;;;;;;OAOG;IACH,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB;;;OAGG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAED;;;;;GAKG;AACH,MAAM,WAAW,YAAY;IAC3B,yFAAyF;IACzF,OAAO,EAAE,MAAM,CAAC;IAChB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,sGAAsG;IACtG,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IACpC;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;CACnC;AAED,6FAA6F;AAC7F,MAAM,WAAW,cAAc;IAC7B;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB,yHAAyH;IACzH,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAYD;;;;;;;;GAQG;AACH,eAAO,MAAM,yBAAyB,GACpC,UAAU,YAAY,EAAE,EACxB,iBAAgB,MAAgC,KAC/C,YAAY,EAiBd,CAAC;AAiEF;;;;GAIG;AACH,eAAO,MAAM,aAAa,GAAI,MAAM;IAAE,KAAK,EAAE,YAAY,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,KAAG,YAAY,EA8EvF,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,qBAAqB,GAAI,MAAM;IAC1C,MAAM,EAAE,YAAY,EAAE,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,cAAc,EAAE,MAAM,CAAC;CACxB,KAAG,YAAY,EAiCf,CAAC;AAEF,yGAAyG;AACzG,eAAO,MAAM,aAAa,GAAI,MAAM;IAClC,MAAM,EAAE,YAAY,EAAE,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,cAAc,CAAC;CAC1B,KAAG,YAAY,EAWf,CAAC"}