@systemfsoftware/effect-schema-vite 1.2.0 → 1.4.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.
- package/dist/effect-schema-vite.d.ts +53 -0
- package/dist/index.d.ts +26 -1
- package/dist/index.mjs +90 -52
- package/package.json +5 -5
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Plugin as Plugin_2 } from 'vite';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Build the law-suite body injected into a consumer's `schema-laws.test.ts`.
|
|
5
|
+
*
|
|
6
|
+
* Import specifiers are relative to `lawFilePath`, never to the Vite root: the
|
|
7
|
+
* file being rewritten lives in `src/`, so a root-relative specifier resolves
|
|
8
|
+
* to `src/src/…` and yields a suite that silently imports nothing.
|
|
9
|
+
*
|
|
10
|
+
* @since 1.4.0
|
|
11
|
+
*/
|
|
12
|
+
export declare const generateSchemaLaws: (lawFilePath: string, srcDir: string) => string;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Vite plugin that walks the consumer's `src/` directory, finds every
|
|
16
|
+
* exported Effect `Schema`, and auto-injects `ruleOfSchemas` round-trip
|
|
17
|
+
* property tests for each one.
|
|
18
|
+
*
|
|
19
|
+
* The laws are injected by rewriting the consumer's own
|
|
20
|
+
* `src/schema-laws.test.ts` — the one test filename the placement taxonomy
|
|
21
|
+
* whitelists by name. It is deliberately NOT a virtual module: the generated
|
|
22
|
+
* body carries real `import` edges to each schema file, so vitest's
|
|
23
|
+
* related-file walk reaches them. A virtual module breaks that walk (its id
|
|
24
|
+
* has no path on disk), which silently drops every generated law from
|
|
25
|
+
* `stryker --related` runs and reports the survivors as coverage gaps.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```ts
|
|
29
|
+
* // vitest.config.ts
|
|
30
|
+
* import { inlineSchemaTests } from '@systemfsoftware/effect-schema-vite'
|
|
31
|
+
*
|
|
32
|
+
* export default defineConfig({
|
|
33
|
+
* plugins: [inlineSchemaTests()],
|
|
34
|
+
* })
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export declare const inlineSchemaTests: (options?: InlineSchemaTestsOptions) => Plugin_2;
|
|
38
|
+
|
|
39
|
+
/** @since 0.1.0 */
|
|
40
|
+
export declare interface InlineSchemaTestsOptions {
|
|
41
|
+
/** Directory to scan for schema files, relative to Vite root. Default: `"src"`. */
|
|
42
|
+
dir?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The one test filename the placement taxonomy whitelists by name. The plugin
|
|
47
|
+
* rewrites this file in the consumer's `src/`; nothing else is touched.
|
|
48
|
+
*
|
|
49
|
+
* @since 1.4.0
|
|
50
|
+
*/
|
|
51
|
+
export declare const LAW_FILE_BASENAME: 'schema-laws.test.ts';
|
|
52
|
+
|
|
53
|
+
export { }
|
package/dist/index.d.ts
CHANGED
|
@@ -5,11 +5,36 @@ interface InlineSchemaTestsOptions {
|
|
|
5
5
|
/** Directory to scan for schema files, relative to Vite root. Default: `"src"`. */
|
|
6
6
|
dir?: string;
|
|
7
7
|
}
|
|
8
|
+
/**
|
|
9
|
+
* The one test filename the placement taxonomy whitelists by name. The plugin
|
|
10
|
+
* rewrites this file in the consumer's `src/`; nothing else is touched.
|
|
11
|
+
*
|
|
12
|
+
* @since 1.4.0
|
|
13
|
+
*/
|
|
14
|
+
declare const LAW_FILE_BASENAME: 'schema-laws.test.ts';
|
|
15
|
+
/**
|
|
16
|
+
* Build the law-suite body injected into a consumer's `schema-laws.test.ts`.
|
|
17
|
+
*
|
|
18
|
+
* Import specifiers are relative to `lawFilePath`, never to the Vite root: the
|
|
19
|
+
* file being rewritten lives in `src/`, so a root-relative specifier resolves
|
|
20
|
+
* to `src/src/…` and yields a suite that silently imports nothing.
|
|
21
|
+
*
|
|
22
|
+
* @since 1.4.0
|
|
23
|
+
*/
|
|
24
|
+
declare const generateSchemaLaws: (lawFilePath: string, srcDir: string) => string;
|
|
8
25
|
/**
|
|
9
26
|
* Vite plugin that walks the consumer's `src/` directory, finds every
|
|
10
27
|
* exported Effect `Schema`, and auto-injects `ruleOfSchemas` round-trip
|
|
11
28
|
* property tests for each one.
|
|
12
29
|
*
|
|
30
|
+
* The laws are injected by rewriting the consumer's own
|
|
31
|
+
* `src/schema-laws.test.ts` — the one test filename the placement taxonomy
|
|
32
|
+
* whitelists by name. It is deliberately NOT a virtual module: the generated
|
|
33
|
+
* body carries real `import` edges to each schema file, so vitest's
|
|
34
|
+
* related-file walk reaches them. A virtual module breaks that walk (its id
|
|
35
|
+
* has no path on disk), which silently drops every generated law from
|
|
36
|
+
* `stryker --related` runs and reports the survivors as coverage gaps.
|
|
37
|
+
*
|
|
13
38
|
* @example
|
|
14
39
|
* ```ts
|
|
15
40
|
* // vitest.config.ts
|
|
@@ -22,4 +47,4 @@ interface InlineSchemaTestsOptions {
|
|
|
22
47
|
*/
|
|
23
48
|
declare const inlineSchemaTests: (options?: InlineSchemaTestsOptions) => Plugin;
|
|
24
49
|
//#endregion
|
|
25
|
-
export { type InlineSchemaTestsOptions, inlineSchemaTests };
|
|
50
|
+
export { type InlineSchemaTestsOptions, LAW_FILE_BASENAME, generateSchemaLaws, inlineSchemaTests };
|
package/dist/index.mjs
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
2
|
-
import { extname, join, relative, resolve } from "node:path";
|
|
2
|
+
import { dirname, extname, join, relative, resolve } from "node:path";
|
|
3
3
|
import { parseSync } from "oxc-parser";
|
|
4
4
|
//#region src/vite-inline-schema-tests.ts
|
|
5
5
|
/**
|
|
6
|
+
* The one test filename the placement taxonomy whitelists by name. The plugin
|
|
7
|
+
* rewrites this file in the consumer's `src/`; nothing else is touched.
|
|
8
|
+
*
|
|
9
|
+
* @since 1.4.0
|
|
10
|
+
*/
|
|
11
|
+
const LAW_FILE_BASENAME = "schema-laws.test.ts";
|
|
12
|
+
/**
|
|
6
13
|
* Walk a directory and return every exported const whose type annotation
|
|
7
14
|
* or initialiser references Effect Schema APIs.
|
|
8
15
|
*/
|
|
9
|
-
function findExportedSchemas(
|
|
16
|
+
function findExportedSchemas(dir) {
|
|
10
17
|
const schemas = [];
|
|
11
18
|
function walk(current) {
|
|
12
19
|
let entries;
|
|
@@ -30,7 +37,7 @@ function findExportedSchemas(root, dir) {
|
|
|
30
37
|
const names = findExportedSchemaNames(readFileSync(full, "utf-8"));
|
|
31
38
|
for (const name of names) schemas.push({
|
|
32
39
|
name,
|
|
33
|
-
|
|
40
|
+
filePath: full
|
|
34
41
|
});
|
|
35
42
|
}
|
|
36
43
|
}
|
|
@@ -45,7 +52,13 @@ function findExportedSchemaNames(source) {
|
|
|
45
52
|
for (const node of result.program.body) {
|
|
46
53
|
if (node.type !== "ExportNamedDeclaration") continue;
|
|
47
54
|
const decl = node.declaration;
|
|
48
|
-
if (!decl
|
|
55
|
+
if (!decl) continue;
|
|
56
|
+
if (decl.type === "ClassDeclaration") {
|
|
57
|
+
const className = decl.id?.name;
|
|
58
|
+
if (typeof className === "string" && !className.startsWith("_") && extendsSchemaClass(decl.superClass)) names.push(className);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (decl.type !== "VariableDeclaration") continue;
|
|
49
62
|
for (const declarator of decl.declarations) {
|
|
50
63
|
const id = declarator.id;
|
|
51
64
|
if (id.type !== "Identifier") continue;
|
|
@@ -63,55 +76,90 @@ function findExportedSchemaNames(source) {
|
|
|
63
76
|
}
|
|
64
77
|
}
|
|
65
78
|
function typeRefContainsSchema(t) {
|
|
66
|
-
if (!t
|
|
67
|
-
|
|
68
|
-
if (
|
|
69
|
-
const tn = node.typeName;
|
|
70
|
-
if (tn.type === "Identifier" && typeof tn.name === "string" && tn.name.includes("Schema")) return true;
|
|
71
|
-
if (tn.type === "TSQualifiedName" && typeof tn.name === "string" && tn.name.includes("Schema")) return true;
|
|
72
|
-
}
|
|
73
|
-
if (node.typeParameters) {
|
|
74
|
-
const params = node.typeParameters;
|
|
75
|
-
if (Array.isArray(params.params)) {
|
|
76
|
-
for (const p of params.params) if (typeRefContainsSchema(p)) return true;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
if (node.types && Array.isArray(node.types)) {
|
|
80
|
-
for (const m of node.types) if (typeRefContainsSchema(m)) return true;
|
|
81
|
-
}
|
|
79
|
+
if (!t) return false;
|
|
80
|
+
if (t.type === "TSTypeReference" && t.typeName.type === "Identifier") return t.typeName.name.includes("Schema");
|
|
81
|
+
if (t.type === "TSUnionType" || t.type === "TSIntersectionType") return t.types.some((member) => typeRefContainsSchema(member));
|
|
82
82
|
return false;
|
|
83
83
|
}
|
|
84
84
|
function initRefersToSchema(expr) {
|
|
85
|
-
if (!expr
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
if (callee.type === "Identifier" && callee.name === "pipe") {
|
|
90
|
-
const args = node.arguments;
|
|
91
|
-
if (Array.isArray(args)) {
|
|
92
|
-
for (const arg of args) if (initRefersToSchema(arg)) return true;
|
|
93
|
-
}
|
|
94
|
-
return false;
|
|
95
|
-
}
|
|
85
|
+
if (!expr) return false;
|
|
86
|
+
if (expr.type === "CallExpression") {
|
|
87
|
+
const callee = expr.callee;
|
|
88
|
+
if (callee.type === "Identifier" && callee.name === "pipe") return expr.arguments.some((arg) => arg.type !== "SpreadElement" && initRefersToSchema(arg));
|
|
96
89
|
if (callee.type === "MemberExpression") return memberChainStartsWithS(callee);
|
|
97
|
-
if (callee.type === "Identifier"
|
|
90
|
+
if (callee.type === "Identifier") return callee.name.includes("Schema");
|
|
91
|
+
return false;
|
|
98
92
|
}
|
|
99
|
-
if (
|
|
93
|
+
if (expr.type === "MemberExpression") return memberChainStartsWithS(expr);
|
|
100
94
|
return false;
|
|
101
95
|
}
|
|
102
96
|
function memberChainStartsWithS(node) {
|
|
103
97
|
const obj = node.object;
|
|
104
|
-
if (
|
|
105
|
-
if (obj.type === "Identifier" && obj.name === "S") return true;
|
|
106
|
-
if (obj.type === "Identifier" && typeof obj.name === "string" && obj.name.includes("Schema")) return true;
|
|
98
|
+
if (obj.type === "Identifier") return obj.name === "S" || obj.name.includes("Schema");
|
|
107
99
|
if (obj.type === "MemberExpression") return memberChainStartsWithS(obj);
|
|
100
|
+
if (obj.type === "CallExpression") {
|
|
101
|
+
const callee = obj.callee;
|
|
102
|
+
if (callee.type === "MemberExpression") return memberChainStartsWithS(callee);
|
|
103
|
+
if (callee.type === "Identifier") return callee.name === "S" || callee.name.includes("Schema");
|
|
104
|
+
}
|
|
108
105
|
return false;
|
|
109
106
|
}
|
|
110
107
|
/**
|
|
108
|
+
* True when a class extends `Schema.Class(...)` or `Schema.TaggedClass(...)`.
|
|
109
|
+
* The constructor is curried — `Schema.Class<Foo>()({...})` is two nested
|
|
110
|
+
* CallExpressions wrapping one MemberExpression — so unwrap the call chain
|
|
111
|
+
* before inspecting the member.
|
|
112
|
+
*
|
|
113
|
+
* `Schema.TaggedError` is deliberately excluded: an error is a failure value,
|
|
114
|
+
* not a codec, so it is not subject to the round-trip laws (and its `cause`
|
|
115
|
+
* field is routinely `S.Unknown`, which does not round-trip).
|
|
116
|
+
*/
|
|
117
|
+
function extendsSchemaClass(superClass) {
|
|
118
|
+
if (!superClass) return false;
|
|
119
|
+
let callee = superClass;
|
|
120
|
+
while (callee.type === "CallExpression") callee = callee.callee;
|
|
121
|
+
if (callee.type !== "MemberExpression") return false;
|
|
122
|
+
if (callee.property.type !== "Identifier") return false;
|
|
123
|
+
const propName = callee.property.name;
|
|
124
|
+
if (propName !== "Class" && propName !== "TaggedClass") return false;
|
|
125
|
+
return memberChainStartsWithS(callee);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Build the law-suite body injected into a consumer's `schema-laws.test.ts`.
|
|
129
|
+
*
|
|
130
|
+
* Import specifiers are relative to `lawFilePath`, never to the Vite root: the
|
|
131
|
+
* file being rewritten lives in `src/`, so a root-relative specifier resolves
|
|
132
|
+
* to `src/src/…` and yields a suite that silently imports nothing.
|
|
133
|
+
*
|
|
134
|
+
* @since 1.4.0
|
|
135
|
+
*/
|
|
136
|
+
const generateSchemaLaws = (lawFilePath, srcDir) => {
|
|
137
|
+
const schemas = findExportedSchemas(srcDir);
|
|
138
|
+
if (schemas.length === 0) return "// no schemas found\nexport {}\n";
|
|
139
|
+
const specifierOf = (filePath) => {
|
|
140
|
+
const rel = relative(dirname(lawFilePath), filePath).replace(/\.ts$/, "");
|
|
141
|
+
return rel.startsWith(".") ? rel : `./${rel}`;
|
|
142
|
+
};
|
|
143
|
+
return [
|
|
144
|
+
`import { ruleOfSchemas } from '@systemfsoftware/effect-schema-law'`,
|
|
145
|
+
schemas.map((s) => `import { ${s.name} } from '${specifierOf(s.filePath)}'`).join("\n"),
|
|
146
|
+
"",
|
|
147
|
+
schemas.map((s) => `ruleOfSchemas('${s.name}', ${s.name})`).join("\n")
|
|
148
|
+
].join("\n");
|
|
149
|
+
};
|
|
150
|
+
/**
|
|
111
151
|
* Vite plugin that walks the consumer's `src/` directory, finds every
|
|
112
152
|
* exported Effect `Schema`, and auto-injects `ruleOfSchemas` round-trip
|
|
113
153
|
* property tests for each one.
|
|
114
154
|
*
|
|
155
|
+
* The laws are injected by rewriting the consumer's own
|
|
156
|
+
* `src/schema-laws.test.ts` — the one test filename the placement taxonomy
|
|
157
|
+
* whitelists by name. It is deliberately NOT a virtual module: the generated
|
|
158
|
+
* body carries real `import` edges to each schema file, so vitest's
|
|
159
|
+
* related-file walk reaches them. A virtual module breaks that walk (its id
|
|
160
|
+
* has no path on disk), which silently drops every generated law from
|
|
161
|
+
* `stryker --related` runs and reports the survivors as coverage gaps.
|
|
162
|
+
*
|
|
115
163
|
* @example
|
|
116
164
|
* ```ts
|
|
117
165
|
* // vitest.config.ts
|
|
@@ -125,27 +173,17 @@ function memberChainStartsWithS(node) {
|
|
|
125
173
|
const inlineSchemaTests = (options) => {
|
|
126
174
|
let config;
|
|
127
175
|
return {
|
|
128
|
-
name: "@systemfsoftware/
|
|
176
|
+
name: "@systemfsoftware/schema-laws",
|
|
129
177
|
enforce: "pre",
|
|
130
178
|
configResolved(c) {
|
|
131
179
|
config = c;
|
|
132
180
|
},
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
if (id !== "\0virtual:@systemfsoftware/inline-schema-tests") return;
|
|
138
|
-
const srcDir = resolve(config.root, options?.dir ?? "src");
|
|
139
|
-
const schemas = findExportedSchemas(config.root, srcDir);
|
|
140
|
-
if (schemas.length === 0) return "// no schemas found";
|
|
141
|
-
return [
|
|
142
|
-
`import { ruleOfSchemas } from '@systemfsoftware/effect-schema-law'`,
|
|
143
|
-
schemas.map((s) => `import { ${s.name} } from '${s.importPath}'`).join("\n"),
|
|
144
|
-
"",
|
|
145
|
-
schemas.map((s) => `ruleOfSchemas('${s.name}', ${s.name})`).join("\n")
|
|
146
|
-
].join("\n");
|
|
181
|
+
transform(_code, id) {
|
|
182
|
+
const lawFile = id.split("?")[0];
|
|
183
|
+
if (lawFile === void 0 || !lawFile.endsWith(`/schema-laws.test.ts`)) return;
|
|
184
|
+
return generateSchemaLaws(lawFile, resolve(config.root, options?.dir ?? "src"));
|
|
147
185
|
}
|
|
148
186
|
};
|
|
149
187
|
};
|
|
150
188
|
//#endregion
|
|
151
|
-
export { inlineSchemaTests };
|
|
189
|
+
export { LAW_FILE_BASENAME, generateSchemaLaws, inlineSchemaTests };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@systemfsoftware/effect-schema-vite",
|
|
3
3
|
"license": "MIT",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.4.0",
|
|
5
5
|
"author": "Ryan Lee <drdgvhbh@gmail.com>",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -45,11 +45,11 @@
|
|
|
45
45
|
"tsdown": "^0.22.9",
|
|
46
46
|
"vite": "^7",
|
|
47
47
|
"vitest": "^4",
|
|
48
|
+
"@systemfsoftware/arethetypeswrong-cli": "^1.0.7",
|
|
48
49
|
"@systemfsoftware/oxlint-config": "^0.1.0",
|
|
49
|
-
"@systemfsoftware/
|
|
50
|
+
"@systemfsoftware/effect-schema-law": "^0.5.0",
|
|
50
51
|
"@systemfsoftware/vitest-config": "^0.1.0",
|
|
51
|
-
"@systemfsoftware/
|
|
52
|
-
"@systemfsoftware/effect-schema-law": "^0.4.0"
|
|
52
|
+
"@systemfsoftware/tsconfig": "^1.2.6"
|
|
53
53
|
},
|
|
54
54
|
"peerDependencies": {
|
|
55
55
|
"@systemfsoftware/effect-schema-law": "*",
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
},
|
|
63
63
|
"scripts": {
|
|
64
64
|
"clean": "rimraf dist",
|
|
65
|
-
"build": "tsdown",
|
|
65
|
+
"build": "tsdown && pnpm api:check",
|
|
66
66
|
"typecheck": "tsc --noEmit --incremental",
|
|
67
67
|
"test": "vitest run",
|
|
68
68
|
"test:run": "vitest run",
|