@shivaedev/build 0.0.0 → 0.1.1
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/CHANGELOG.md +19 -0
- package/LICENSE +21 -0
- package/README.md +215 -1
- package/dist/BuildFailure.d.ts +9 -0
- package/dist/BuildFailure.d.ts.map +1 -0
- package/dist/BuildFailure.js +4 -0
- package/dist/BuildFailure.js.map +1 -0
- package/dist/adapters/appTestConfig.d.ts +4 -0
- package/dist/adapters/appTestConfig.d.ts.map +1 -0
- package/dist/adapters/appTestConfig.js +17 -0
- package/dist/adapters/appTestConfig.js.map +1 -0
- package/dist/adapters/appViteConfig.d.ts +26 -0
- package/dist/adapters/appViteConfig.d.ts.map +1 -0
- package/dist/adapters/appViteConfig.js +18 -0
- package/dist/adapters/appViteConfig.js.map +1 -0
- package/dist/adapters/bundleCheck.d.ts +3 -0
- package/dist/adapters/bundleCheck.d.ts.map +1 -0
- package/dist/adapters/bundleCheck.js +53 -0
- package/dist/adapters/bundleCheck.js.map +1 -0
- package/dist/adapters/copiedCode.d.ts +9 -0
- package/dist/adapters/copiedCode.d.ts.map +1 -0
- package/dist/adapters/copiedCode.js +84 -0
- package/dist/adapters/copiedCode.js.map +1 -0
- package/dist/adapters/failuresOf.d.ts +5 -0
- package/dist/adapters/failuresOf.d.ts.map +1 -0
- package/dist/adapters/failuresOf.js +29 -0
- package/dist/adapters/failuresOf.js.map +1 -0
- package/dist/adapters/isReadonlyProperty.d.ts +10 -0
- package/dist/adapters/isReadonlyProperty.d.ts.map +1 -0
- package/dist/adapters/isReadonlyProperty.js +7 -0
- package/dist/adapters/isReadonlyProperty.js.map +1 -0
- package/dist/adapters/moduleCalls.d.ts +10 -0
- package/dist/adapters/moduleCalls.d.ts.map +1 -0
- package/dist/adapters/moduleCalls.js +47 -0
- package/dist/adapters/moduleCalls.js.map +1 -0
- package/dist/adapters/moduleFacts.d.ts +4 -0
- package/dist/adapters/moduleFacts.d.ts.map +1 -0
- package/dist/adapters/moduleFacts.js +63 -0
- package/dist/adapters/moduleFacts.js.map +1 -0
- package/dist/adapters/objectForm.d.ts +8 -0
- package/dist/adapters/objectForm.d.ts.map +1 -0
- package/dist/adapters/objectForm.js +66 -0
- package/dist/adapters/objectForm.js.map +1 -0
- package/dist/adapters/operationFacts.d.ts +10 -0
- package/dist/adapters/operationFacts.d.ts.map +1 -0
- package/dist/adapters/operationFacts.js +72 -0
- package/dist/adapters/operationFacts.js.map +1 -0
- package/dist/adapters/runNode.d.ts +6 -0
- package/dist/adapters/runNode.d.ts.map +1 -0
- package/dist/adapters/runNode.js +14 -0
- package/dist/adapters/runNode.js.map +1 -0
- package/dist/adapters/typeReading.d.ts +15 -0
- package/dist/adapters/typeReading.d.ts.map +1 -0
- package/dist/adapters/typeReading.js +46 -0
- package/dist/adapters/typeReading.js.map +1 -0
- package/dist/adapters/typeScript.d.ts +15 -0
- package/dist/adapters/typeScript.d.ts.map +1 -0
- package/dist/adapters/typeScript.js +81 -0
- package/dist/adapters/typeScript.js.map +1 -0
- package/dist/adapters/unionForm.d.ts +7 -0
- package/dist/adapters/unionForm.d.ts.map +1 -0
- package/dist/adapters/unionForm.js +78 -0
- package/dist/adapters/unionForm.js.map +1 -0
- package/dist/adapters/vitePlugin.d.ts +7 -0
- package/dist/adapters/vitePlugin.d.ts.map +1 -0
- package/dist/adapters/vitePlugin.js +88 -0
- package/dist/adapters/vitePlugin.js.map +1 -0
- package/dist/adapters/vitePort.d.ts +2 -0
- package/dist/adapters/vitePort.d.ts.map +1 -0
- package/dist/adapters/vitePort.js +5 -0
- package/dist/adapters/vitePort.js.map +1 -0
- package/dist/adapters/vitest/setup.d.ts +3 -0
- package/dist/adapters/vitest/setup.d.ts.map +1 -0
- package/dist/adapters/vitest/setup.js +11 -0
- package/dist/adapters/vitest/setup.js.map +1 -0
- package/dist/adapters/wireForm.d.ts +5 -0
- package/dist/adapters/wireForm.d.ts.map +1 -0
- package/dist/adapters/wireForm.js +91 -0
- package/dist/adapters/wireForm.js.map +1 -0
- package/dist/byText.d.ts +2 -0
- package/dist/byText.d.ts.map +1 -0
- package/dist/byText.js +8 -0
- package/dist/byText.js.map +1 -0
- package/dist/cache.d.ts +4 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +19 -0
- package/dist/cache.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +11 -0
- package/dist/cli.js.map +1 -0
- package/dist/code.d.ts +11 -0
- package/dist/code.d.ts.map +1 -0
- package/dist/code.js +11 -0
- package/dist/code.js.map +1 -0
- package/dist/command.d.ts +6 -0
- package/dist/command.d.ts.map +1 -0
- package/dist/command.js +28 -0
- package/dist/command.js.map +1 -0
- package/dist/emit.d.ts +23 -0
- package/dist/emit.d.ts.map +1 -0
- package/dist/emit.js +69 -0
- package/dist/emit.js.map +1 -0
- package/dist/facts.d.ts +92 -0
- package/dist/facts.d.ts.map +1 -0
- package/dist/facts.js +3 -0
- package/dist/facts.js.map +1 -0
- package/dist/files.d.ts +4 -0
- package/dist/files.d.ts.map +1 -0
- package/dist/files.js +18 -0
- package/dist/files.js.map +1 -0
- package/dist/formCode.d.ts +6 -0
- package/dist/formCode.d.ts.map +1 -0
- package/dist/formCode.js +46 -0
- package/dist/formCode.js.map +1 -0
- package/dist/generate.d.ts +7 -0
- package/dist/generate.d.ts.map +1 -0
- package/dist/generate.js +69 -0
- package/dist/generate.js.map +1 -0
- package/dist/generateRound.d.ts +14 -0
- package/dist/generateRound.d.ts.map +1 -0
- package/dist/generateRound.js +44 -0
- package/dist/generateRound.js.map +1 -0
- package/dist/importNames.d.ts +7 -0
- package/dist/importNames.d.ts.map +1 -0
- package/dist/importNames.js +57 -0
- package/dist/importNames.js.map +1 -0
- package/dist/inputs.d.ts +15 -0
- package/dist/inputs.d.ts.map +1 -0
- package/dist/inputs.js +64 -0
- package/dist/inputs.js.map +1 -0
- package/dist/ownerOf.d.ts +7 -0
- package/dist/ownerOf.d.ts.map +1 -0
- package/dist/ownerOf.js +30 -0
- package/dist/ownerOf.js.map +1 -0
- package/dist/packageScope.d.ts +13 -0
- package/dist/packageScope.d.ts.map +1 -0
- package/dist/packageScope.js +28 -0
- package/dist/packageScope.js.map +1 -0
- package/dist/plan.d.ts +12 -0
- package/dist/plan.d.ts.map +1 -0
- package/dist/plan.js +115 -0
- package/dist/plan.js.map +1 -0
- package/dist/project.d.ts +11 -0
- package/dist/project.d.ts.map +1 -0
- package/dist/project.js +24 -0
- package/dist/project.js.map +1 -0
- package/dist/rejections.d.ts +11 -0
- package/dist/rejections.d.ts.map +1 -0
- package/dist/rejections.js +46 -0
- package/dist/rejections.js.map +1 -0
- package/dist/report.d.ts +22 -0
- package/dist/report.d.ts.map +1 -0
- package/dist/report.js +20 -0
- package/dist/report.js.map +1 -0
- package/dist/serverCode.d.ts +20 -0
- package/dist/serverCode.d.ts.map +1 -0
- package/dist/serverCode.js +90 -0
- package/dist/serverCode.js.map +1 -0
- package/dist/settleOrphans.d.ts +5 -0
- package/dist/settleOrphans.d.ts.map +1 -0
- package/dist/settleOrphans.js +43 -0
- package/dist/settleOrphans.js.map +1 -0
- package/dist/specifiers.d.ts +4 -0
- package/dist/specifiers.d.ts.map +1 -0
- package/dist/specifiers.js +52 -0
- package/dist/specifiers.js.map +1 -0
- package/package.json +82 -5
- package/src/BuildFailure.ts +5 -0
- package/src/adapters/appTestConfig.ts +18 -0
- package/src/adapters/appViteConfig.ts +31 -0
- package/src/adapters/bundleCheck.ts +57 -0
- package/src/adapters/copiedCode.ts +103 -0
- package/src/adapters/failuresOf.ts +31 -0
- package/src/adapters/isReadonlyProperty.ts +21 -0
- package/src/adapters/moduleCalls.ts +58 -0
- package/src/adapters/moduleFacts.ts +66 -0
- package/src/adapters/objectForm.ts +74 -0
- package/src/adapters/operationFacts.ts +85 -0
- package/src/adapters/runNode.ts +15 -0
- package/src/adapters/typeReading.ts +63 -0
- package/src/adapters/typeScript.ts +101 -0
- package/src/adapters/unionForm.ts +88 -0
- package/src/adapters/vitePlugin.ts +103 -0
- package/src/adapters/vitePort.ts +5 -0
- package/src/adapters/vitest/setup.ts +12 -0
- package/src/adapters/wireForm.ts +98 -0
- package/src/byText.ts +7 -0
- package/src/cache.ts +28 -0
- package/src/cli.ts +16 -0
- package/src/code.ts +23 -0
- package/src/command.ts +42 -0
- package/src/emit.ts +117 -0
- package/src/facts.ts +70 -0
- package/src/files.ts +24 -0
- package/src/formCode.ts +59 -0
- package/src/generate.ts +86 -0
- package/src/generateRound.ts +60 -0
- package/src/importNames.ts +61 -0
- package/src/inputs.ts +82 -0
- package/src/ownerOf.ts +40 -0
- package/src/packageScope.ts +35 -0
- package/src/plan.ts +138 -0
- package/src/project.ts +30 -0
- package/src/rejections.ts +57 -0
- package/src/report.ts +39 -0
- package/src/serverCode.ts +139 -0
- package/src/settleOrphans.ts +58 -0
- package/src/specifiers.ts +55 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
import { isReadonlyProperty } from "#adapters/isReadonlyProperty.ts";
|
|
3
|
+
import { declaredClass, isSchemaClass, type TypeReading } from "#adapters/typeReading.ts";
|
|
4
|
+
import { unionForm } from "#adapters/unionForm.ts";
|
|
5
|
+
import type { Field, WireForm } from "#facts.ts";
|
|
6
|
+
|
|
7
|
+
type Read = (type: ts.Type, path: string) => WireForm | undefined;
|
|
8
|
+
|
|
9
|
+
export function classForm(reading: TypeReading, type: ts.Type, symbol: ts.Symbol, path: string): WireForm | undefined {
|
|
10
|
+
if (!isSchemaClass(reading, symbol)) {
|
|
11
|
+
return reading.refuse(type, path, "return a plain object, or make the class a Schema class");
|
|
12
|
+
}
|
|
13
|
+
const declared = declaredClass(reading, symbol);
|
|
14
|
+
if (declared?.exportName === undefined) {
|
|
15
|
+
return reading.refuse(type, path, `export the Schema class ${symbol.name} from its module`);
|
|
16
|
+
}
|
|
17
|
+
return { declared, form: "class" };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// An exact optional property's only undefined member is the one optionality adds: TypeScript merges a written undefined
|
|
21
|
+
// into it, and the property is then not exact.
|
|
22
|
+
function optionalForm(reading: TypeReading, type: ts.Type, path: string, read: Read): WireForm | undefined {
|
|
23
|
+
const members = type.isUnion() ? type.types.filter((member) => !(member.flags & ts.TypeFlags.Undefined)) : [type];
|
|
24
|
+
const [only] = members;
|
|
25
|
+
if (only === undefined) {
|
|
26
|
+
return reading.refuse(type, path, "give the optional property a type besides undefined, or drop it");
|
|
27
|
+
}
|
|
28
|
+
return members.length === 1 ? read(only, path) : unionForm(reading, type, members, path, read);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function fieldOf(reading: TypeReading, property: ts.Symbol, exact: ReadonlySet<ts.Symbol>, path: string, read: Read): Field | undefined {
|
|
32
|
+
const optional = (property.flags & ts.SymbolFlags.Optional) !== 0;
|
|
33
|
+
const type = reading.checker.getTypeOfSymbol(property);
|
|
34
|
+
if (optional && !exact.has(property)) {
|
|
35
|
+
return reading.refuse(type, path, `drop undefined from the optional property ${property.name}, so an absent key is the only way to leave it out`);
|
|
36
|
+
}
|
|
37
|
+
const form = optional ? optionalForm(reading, type, path, read) : read(type, path);
|
|
38
|
+
return form === undefined ? undefined : { form, mutable: !isReadonlyProperty(property), name: property.name, optional };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function recordForm(reading: TypeReading, type: ts.Type, path: string, read: Read): WireForm | undefined {
|
|
42
|
+
const [index, ...others] = reading.checker.getIndexInfosOfType(type);
|
|
43
|
+
if (index === undefined || others.length > 0 || !(index.keyType.flags & ts.TypeFlags.String)) {
|
|
44
|
+
return reading.refuse(type, path, "use string keys only");
|
|
45
|
+
}
|
|
46
|
+
const value = read(index.type, `${path}[string]`);
|
|
47
|
+
return value === undefined ? undefined : { form: "record", mutable: !index.isReadonly, value };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function objectForm(reading: TypeReading, type: ts.Type, path: string, read: Read): WireForm | undefined {
|
|
51
|
+
if (type.getCallSignatures().length > 0 || type.getConstructSignatures().length > 0) {
|
|
52
|
+
return reading.refuse(type, path, "return data, not a function");
|
|
53
|
+
}
|
|
54
|
+
const symbol = type.getSymbol();
|
|
55
|
+
const builtIn =
|
|
56
|
+
symbol !== undefined
|
|
57
|
+
&& symbol.flags & ts.SymbolFlags.Interface
|
|
58
|
+
&& symbol.declarations?.some((declaration) => reading.program.isSourceFileDefaultLibrary(declaration.getSourceFile()));
|
|
59
|
+
if (builtIn === true) {
|
|
60
|
+
return reading.refuse(type, path, "return a plain object, an array or a string-keyed record");
|
|
61
|
+
}
|
|
62
|
+
const properties = reading.checker.getPropertiesOfType(type);
|
|
63
|
+
const indexes = reading.checker.getIndexInfosOfType(type);
|
|
64
|
+
if (indexes.length > 0) {
|
|
65
|
+
return properties.length === 0 ? recordForm(reading, type, path, read) : reading.refuse(type, path, "use either fixed properties or a record");
|
|
66
|
+
}
|
|
67
|
+
const methods = properties.filter((property) => reading.checker.getTypeOfSymbol(property).getCallSignatures().length > 0);
|
|
68
|
+
if (methods.length > 0 || properties.some((property) => property.name.startsWith("__@"))) {
|
|
69
|
+
return reading.refuse(type, path, "return plain data, without methods or symbol keys");
|
|
70
|
+
}
|
|
71
|
+
const exact = new Set(reading.checker.getExactOptionalProperties(type));
|
|
72
|
+
const fields = properties.map((property) => fieldOf(reading, property, exact, `${path}.${property.name}`, read));
|
|
73
|
+
return fields.every((field) => field !== undefined) ? { fields, form: "struct" } : undefined;
|
|
74
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
import { copiedCode } from "#adapters/copiedCode.ts";
|
|
3
|
+
import { failuresOf } from "#adapters/failuresOf.ts";
|
|
4
|
+
import { initializerOf, propertyOf } from "#adapters/moduleCalls.ts";
|
|
5
|
+
import { type TypeReading, typeArguments } from "#adapters/typeReading.ts";
|
|
6
|
+
import { wireForm } from "#adapters/wireForm.ts";
|
|
7
|
+
import type { CodePart, OperationFacts } from "#facts.ts";
|
|
8
|
+
|
|
9
|
+
export interface OperationReading {
|
|
10
|
+
readonly moduleName: string;
|
|
11
|
+
readonly reading: (label: string) => TypeReading;
|
|
12
|
+
readonly refuse: (message: string) => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function declarationOf(checker: ts.TypeChecker, element: ts.Expression) {
|
|
16
|
+
const symbol = checker.getSymbolAtLocation(element);
|
|
17
|
+
const target = symbol !== undefined && symbol.flags & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(symbol) : symbol;
|
|
18
|
+
const declaration = target?.valueDeclaration;
|
|
19
|
+
const call = declaration !== undefined && ts.isVariableDeclaration(declaration) ? declaration.initializer : undefined;
|
|
20
|
+
const literal = call !== undefined && ts.isCallExpression(call) ? call.arguments[1] : undefined;
|
|
21
|
+
return declaration === undefined || literal === undefined || !ts.isObjectLiteralExpression(literal) ? undefined : { declaration, literal };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// What an Effect-returning property of the operation succeeds and fails with.
|
|
25
|
+
function effectArguments(checker: ts.TypeChecker, owner: ts.Type, property: string, at: ts.Node): readonly ts.Type[] {
|
|
26
|
+
const symbol = owner.getProperty(property);
|
|
27
|
+
const [signature] = symbol === undefined ? [] : checker.getTypeOfSymbolAtLocation(symbol, at).getCallSignatures();
|
|
28
|
+
return signature === undefined ? [] : typeArguments(checker, signature.getReturnType());
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function textOf(checker: ts.TypeChecker, owner: ts.Type, property: string): string | undefined {
|
|
32
|
+
const symbol = owner.getProperty(property);
|
|
33
|
+
const value = symbol === undefined ? undefined : checker.getTypeOfSymbol(symbol);
|
|
34
|
+
return value?.isStringLiteral() === true ? value.value : undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Without run's types the operation did not compile; a run returning any is the common cause, and gets its own message.
|
|
38
|
+
function refuseUnread(context: OperationReading, label: string, literal: ts.ObjectLiteralExpression) {
|
|
39
|
+
const reading = context.reading(label);
|
|
40
|
+
const run = propertyOf(literal, "run");
|
|
41
|
+
const signature = run !== undefined && ts.isMethodDeclaration(run) ? reading.checker.getSignatureFromDeclaration(run) : undefined;
|
|
42
|
+
const returned = signature === undefined ? undefined : typeArguments(reading.checker, signature.getReturnType())[1];
|
|
43
|
+
if (returned !== undefined && returned.flags & ts.TypeFlags.Any) {
|
|
44
|
+
reading.refuse(returned, "success", "decode the value with a schema, or give it a type");
|
|
45
|
+
} else {
|
|
46
|
+
context.refuse(`${label}: run's result cannot be read; fix the compile errors on the operation first`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function operationFacts(context: OperationReading, element: ts.Expression): OperationFacts | undefined {
|
|
51
|
+
const reading = context.reading(context.moduleName);
|
|
52
|
+
const found = declarationOf(reading.checker, element);
|
|
53
|
+
if (found === undefined) {
|
|
54
|
+
context.refuse(`${context.moduleName}: ${element.getText()} in operations is not a const built by query or command with an object literal`);
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
const type = reading.checker.getTypeAtLocation(found.declaration);
|
|
58
|
+
const name = textOf(reading.checker, type, "name") ?? element.getText();
|
|
59
|
+
const kind = textOf(reading.checker, type, "kind") === "command" ? "command" : "query";
|
|
60
|
+
const label = `${context.moduleName}.${name}`;
|
|
61
|
+
const [success, runError] = effectArguments(reading.checker, type, "run", found.declaration);
|
|
62
|
+
if (success === undefined) {
|
|
63
|
+
refuseUnread(context, label, found.literal);
|
|
64
|
+
}
|
|
65
|
+
const [, policyError] = kind === "query" ? effectArguments(reading.checker, type, "readPolicy", found.declaration) : [];
|
|
66
|
+
const literal = found.literal;
|
|
67
|
+
function copy(field: string, member: boolean): readonly CodePart[] | undefined {
|
|
68
|
+
const property = propertyOf(literal, field);
|
|
69
|
+
const node = member ? property : initializerOf(property);
|
|
70
|
+
return node === undefined ? undefined : copiedCode({ checker: reading.checker, label: `${label} ${field}`, refuse: context.refuse }, node);
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
failures: failuresOf(
|
|
74
|
+
context.reading(label),
|
|
75
|
+
[runError, policyError].flatMap((error) => error ?? []),
|
|
76
|
+
),
|
|
77
|
+
invalidates: copy("invalidates", true),
|
|
78
|
+
kind,
|
|
79
|
+
name,
|
|
80
|
+
payload: copy("payload", false),
|
|
81
|
+
reads: copy("reads", true),
|
|
82
|
+
readsCollections: copy("readsCollections", true),
|
|
83
|
+
success: success === undefined ? undefined : wireForm(context.reading(label), success, "success"),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import * as NodeCrypto from "@effect/platform-node/NodeCrypto";
|
|
2
|
+
import * as NodeServices from "@effect/platform-node/NodeServices";
|
|
3
|
+
import { Cause, Effect, Exit, Layer } from "effect";
|
|
4
|
+
|
|
5
|
+
const services = Layer.mergeAll(NodeServices.layer, NodeCrypto.layer);
|
|
6
|
+
|
|
7
|
+
// Runs a generator effect for a tool that awaits a Promise, failing it with the effect's own error.
|
|
8
|
+
export async function runNode<A, E>(effect: Effect.Effect<A, E, Layer.Success<typeof services>>): Promise<A> {
|
|
9
|
+
const exit = await Effect.runPromiseExit(Effect.provide(effect, services));
|
|
10
|
+
if (Exit.isSuccess(exit)) {
|
|
11
|
+
return exit.value;
|
|
12
|
+
}
|
|
13
|
+
const failure = Cause.squash(exit.cause);
|
|
14
|
+
throw failure instanceof Error ? failure : new Error(String(failure), { cause: failure });
|
|
15
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
import type { DeclaredClass, Literal } from "#facts.ts";
|
|
3
|
+
|
|
4
|
+
export interface TypeReading {
|
|
5
|
+
readonly checker: ts.TypeChecker;
|
|
6
|
+
readonly program: ts.Program;
|
|
7
|
+
// Refuses the type at a path with what to write instead, and returns undefined so a caller can stop there.
|
|
8
|
+
readonly refuse: (type: ts.Type, path: string, instead: string) => undefined;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function declaredIn(type: ts.Type, module: string): boolean {
|
|
12
|
+
const pattern = new RegExp(`[/\\\\]effect[/\\\\](?:dist|src)[/\\\\]${module}\\.(?:d\\.)?ts$`, "u");
|
|
13
|
+
return (type.aliasSymbol ?? type.getSymbol())?.declarations?.some((declaration) => pattern.test(declaration.getSourceFile().fileName)) === true;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function isLibraryType(reading: TypeReading, type: ts.Type, name: string): boolean {
|
|
17
|
+
const symbol = type.getSymbol();
|
|
18
|
+
return (
|
|
19
|
+
symbol?.name === name
|
|
20
|
+
&& symbol.declarations?.some((declaration) => reading.program.isSourceFileDefaultLibrary(declaration.getSourceFile())) === true
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function declaredClass(reading: TypeReading, symbol: ts.Symbol): DeclaredClass | undefined {
|
|
25
|
+
const declaration = symbol.declarations?.find(ts.isClassDeclaration);
|
|
26
|
+
if (declaration === undefined) {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
const file = declaration.getSourceFile();
|
|
30
|
+
const module = reading.checker.getSymbolAtLocation(file);
|
|
31
|
+
const exported =
|
|
32
|
+
module === undefined
|
|
33
|
+
? undefined
|
|
34
|
+
: reading.checker
|
|
35
|
+
.getExportsOfModule(module)
|
|
36
|
+
.find((candidate) => (candidate.flags & ts.SymbolFlags.Alias ? reading.checker.getAliasedSymbol(candidate) : candidate) === symbol);
|
|
37
|
+
return { className: symbol.name, exportName: exported?.name, file: file.fileName };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function isSchemaClass(reading: TypeReading, symbol: ts.Symbol): boolean {
|
|
41
|
+
const declaration = symbol.declarations?.find(ts.isClassDeclaration);
|
|
42
|
+
if (declaration === undefined) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
const staticSide = reading.checker.getTypeOfSymbolAtLocation(symbol, declaration);
|
|
46
|
+
return staticSide.getProperty("ast") !== undefined && staticSide.getProperty("fields") !== undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Array, tuple and generic interface instances, such as Effect<A, E, R>, are references that carry their type arguments.
|
|
50
|
+
export function isReference(type: ts.Type): type is ts.TypeReference {
|
|
51
|
+
return "target" in type;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function typeArguments(checker: ts.TypeChecker, type: ts.Type): readonly ts.Type[] {
|
|
55
|
+
return isReference(type) ? checker.getTypeArguments(type) : [];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function literalOf(checker: ts.TypeChecker, type: ts.Type): Literal | undefined {
|
|
59
|
+
if (type.flags & ts.TypeFlags.BooleanLiteral) {
|
|
60
|
+
return checker.typeToString(type) === "true";
|
|
61
|
+
}
|
|
62
|
+
return type.isStringLiteral() || type.isNumberLiteral() ? type.value : undefined;
|
|
63
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import { Effect } from "effect";
|
|
4
|
+
import ts from "typescript";
|
|
5
|
+
import { moduleCalls } from "#adapters/moduleCalls.ts";
|
|
6
|
+
import { moduleFacts } from "#adapters/moduleFacts.ts";
|
|
7
|
+
import { BuildFailure } from "#BuildFailure.ts";
|
|
8
|
+
|
|
9
|
+
export interface ProjectConfig {
|
|
10
|
+
// The tsconfig and every file it extends, which decide how the project's types read.
|
|
11
|
+
readonly chain: readonly string[];
|
|
12
|
+
readonly declarations: readonly string[];
|
|
13
|
+
readonly files: readonly string[];
|
|
14
|
+
readonly options: ts.CompilerOptions;
|
|
15
|
+
readonly typeScriptVersion: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function diagnosticText(diagnostics: readonly ts.Diagnostic[]): string {
|
|
19
|
+
return ts.formatDiagnostics(diagnostics, {
|
|
20
|
+
getCanonicalFileName: (name) => name,
|
|
21
|
+
getCurrentDirectory: ts.sys.getCurrentDirectory,
|
|
22
|
+
getNewLine: () => "\n",
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const readConfig = Effect.fn("Build.readConfig")(function* (tsconfig: string) {
|
|
27
|
+
const parsed = yield* Effect.sync(() => {
|
|
28
|
+
const source = ts.readJsonConfigFile(tsconfig, ts.sys.readFile);
|
|
29
|
+
const config = ts.parseJsonSourceFileConfigFileContent(source, ts.sys, dirname(tsconfig), undefined, tsconfig);
|
|
30
|
+
return { config, problems: config.errors, source };
|
|
31
|
+
});
|
|
32
|
+
if (parsed.problems.length > 0) {
|
|
33
|
+
return yield* new BuildFailure({ message: `${tsconfig} cannot be read:\n${diagnosticText(parsed.problems)}` });
|
|
34
|
+
}
|
|
35
|
+
const config: ProjectConfig = {
|
|
36
|
+
chain: [tsconfig, ...(parsed.source.extendedSourceFiles ?? [])],
|
|
37
|
+
declarations: parsed.config.fileNames.filter((file) => file.endsWith(".d.ts")),
|
|
38
|
+
files: parsed.config.fileNames,
|
|
39
|
+
options: parsed.config.options,
|
|
40
|
+
typeScriptVersion: ts.version,
|
|
41
|
+
};
|
|
42
|
+
return config;
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
export function definesModule(file: string, text: string): boolean {
|
|
46
|
+
return text.includes("defineModule") && moduleCalls(ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true)).length > 0;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const resolvedImports = Effect.fn("Build.resolvedImports")(function* (config: ProjectConfig, file: string, text: string) {
|
|
50
|
+
return yield* Effect.sync(() =>
|
|
51
|
+
ts.preProcessFile(text, true, true).importedFiles.flatMap(({ fileName }) => {
|
|
52
|
+
const resolved = ts.resolveModuleName(fileName, file, config.options, ts.sys).resolvedModule;
|
|
53
|
+
return resolved === undefined ? [] : [resolved.resolvedFileName];
|
|
54
|
+
}),
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Building the program is the expensive part of a run, and parsing the installed packages' declarations is most of it. They rarely
|
|
59
|
+
// change between the runs of one process, such as a dev server's, so the registry hands a later program each file it parsed with the same text and settings.
|
|
60
|
+
const parsedFiles = ts.createDocumentRegistry(ts.sys.useCaseSensitiveFileNames);
|
|
61
|
+
|
|
62
|
+
function programHost(options: ts.CompilerOptions, rootNames: readonly string[]): ts.LanguageServiceHost {
|
|
63
|
+
const texts = new Map<string, string | undefined>();
|
|
64
|
+
function textOf(file: string) {
|
|
65
|
+
if (!texts.has(file)) {
|
|
66
|
+
texts.set(file, ts.sys.readFile(file));
|
|
67
|
+
}
|
|
68
|
+
return texts.get(file);
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
directoryExists: ts.sys.directoryExists,
|
|
72
|
+
fileExists: ts.sys.fileExists,
|
|
73
|
+
getCompilationSettings: () => options,
|
|
74
|
+
getCurrentDirectory: ts.sys.getCurrentDirectory,
|
|
75
|
+
getDefaultLibFileName: ts.getDefaultLibFilePath,
|
|
76
|
+
getDirectories: ts.sys.getDirectories,
|
|
77
|
+
getScriptFileNames: () => [...rootNames],
|
|
78
|
+
getScriptSnapshot: (file) => {
|
|
79
|
+
const text = textOf(file);
|
|
80
|
+
return text === undefined ? undefined : ts.ScriptSnapshot.fromString(text);
|
|
81
|
+
},
|
|
82
|
+
getScriptVersion: (file) =>
|
|
83
|
+
createHash("sha256")
|
|
84
|
+
.update(textOf(file) ?? "")
|
|
85
|
+
.digest("hex"),
|
|
86
|
+
readDirectory: ts.sys.readDirectory,
|
|
87
|
+
readFile: textOf,
|
|
88
|
+
...(ts.sys.realpath === undefined ? {} : { realpath: ts.sys.realpath }),
|
|
89
|
+
useCaseSensitiveFileNames: () => ts.sys.useCaseSensitiveFileNames,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export const deriveModules = Effect.fn("Build.deriveModules")(function* (config: ProjectConfig, modules: readonly string[]) {
|
|
94
|
+
const program = yield* Effect.sync(() =>
|
|
95
|
+
ts.createLanguageService(programHost({ ...config.options, noEmit: true }, [...modules, ...config.declarations]), parsedFiles).getProgram(),
|
|
96
|
+
);
|
|
97
|
+
if (program === undefined) {
|
|
98
|
+
return yield* new BuildFailure({ message: "TypeScript's language service built no program" });
|
|
99
|
+
}
|
|
100
|
+
return yield* Effect.sync(() => modules.map((module) => moduleFacts(program, module)));
|
|
101
|
+
});
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
import { declaredIn, literalOf, type TypeReading } from "#adapters/typeReading.ts";
|
|
3
|
+
import { byText } from "#byText.ts";
|
|
4
|
+
import type { Literal, WireForm } from "#facts.ts";
|
|
5
|
+
|
|
6
|
+
type Read = (type: ts.Type, path: string) => WireForm | undefined;
|
|
7
|
+
|
|
8
|
+
function discriminant(reading: TypeReading, members: readonly ts.Type[]): string | undefined {
|
|
9
|
+
const [first] = members;
|
|
10
|
+
const candidates = first === undefined ? [] : reading.checker.getPropertiesOfType(first).map((property) => property.name);
|
|
11
|
+
return candidates.sort().find((name) => {
|
|
12
|
+
const values = members.map((member) => {
|
|
13
|
+
const property = member.getProperty(name);
|
|
14
|
+
return property === undefined || property.flags & ts.SymbolFlags.Optional
|
|
15
|
+
? undefined
|
|
16
|
+
: literalOf(reading.checker, reading.checker.getTypeOfSymbol(property));
|
|
17
|
+
});
|
|
18
|
+
return values.every((value) => value !== undefined) && new Set(values).size === values.length;
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function byValue(left: Literal, right: Literal): number {
|
|
23
|
+
return byText(String(left), String(right));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function discriminated(reading: TypeReading, members: readonly ts.Type[], tag: string, path: string, read: Read): WireForm | undefined {
|
|
27
|
+
function tagOf(member: ts.Type) {
|
|
28
|
+
const property = member.getProperty(tag);
|
|
29
|
+
return (property === undefined ? undefined : literalOf(reading.checker, reading.checker.getTypeOfSymbol(property))) ?? "";
|
|
30
|
+
}
|
|
31
|
+
const forms = [...members]
|
|
32
|
+
.sort((left, right) => byValue(tagOf(left), tagOf(right)))
|
|
33
|
+
.map((member) => read(member, `${path}<${tag}: ${tagOf(member)}>`));
|
|
34
|
+
return forms.every((form) => form !== undefined) ? { form: "union", members: forms } : undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function optionValue(reading: TypeReading, members: readonly ts.Type[]): ts.Type | undefined {
|
|
38
|
+
const some = members.find((member) => member.getSymbol()?.name === "Some" && declaredIn(member, "Option"));
|
|
39
|
+
const none = members.find((member) => member.getSymbol()?.name === "None" && declaredIn(member, "Option"));
|
|
40
|
+
const value = some?.getProperty("value");
|
|
41
|
+
return none === undefined || value === undefined ? undefined : reading.checker.getTypeOfSymbol(value);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function withoutNull(reading: TypeReading, type: ts.Type, all: readonly ts.Type[], path: string, read: Read): WireForm | undefined {
|
|
45
|
+
const members = all.filter((member) => !(member.flags & ts.TypeFlags.Null));
|
|
46
|
+
const [only] = members;
|
|
47
|
+
if (members.length === 1 && only !== undefined && only.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Void)) {
|
|
48
|
+
return reading.refuse(type, path, "return either null or undefined, since the wire carries both as one");
|
|
49
|
+
}
|
|
50
|
+
const booleans = members.filter((member) => member.flags & ts.TypeFlags.BooleanLiteral);
|
|
51
|
+
const optionInner = optionValue(reading, members);
|
|
52
|
+
if (optionInner !== undefined && members.length === 2) {
|
|
53
|
+
const inner = read(optionInner, `${path}<Some>`);
|
|
54
|
+
return inner === undefined ? undefined : { form: "option", inner };
|
|
55
|
+
}
|
|
56
|
+
if (booleans.length === 2 && members.length === 2) {
|
|
57
|
+
return { form: "primitive", primitive: "Boolean" };
|
|
58
|
+
}
|
|
59
|
+
if (members.length === 1 && only !== undefined) {
|
|
60
|
+
return read(only, path);
|
|
61
|
+
}
|
|
62
|
+
if (members.some((member) => member.flags & ts.TypeFlags.Undefined)) {
|
|
63
|
+
return reading.refuse(type, path, "return null instead of undefined, or make the property optional");
|
|
64
|
+
}
|
|
65
|
+
const values = members.map((member) => literalOf(reading.checker, member));
|
|
66
|
+
if (values.every((value) => value !== undefined)) {
|
|
67
|
+
return { form: "literals", values: values.sort(byValue) };
|
|
68
|
+
}
|
|
69
|
+
const objects = members.every(
|
|
70
|
+
(member) => member.flags & ts.TypeFlags.Object && !reading.checker.isArrayType(member) && !reading.checker.isTupleType(member),
|
|
71
|
+
);
|
|
72
|
+
const tag = objects ? discriminant(reading, members) : undefined;
|
|
73
|
+
if (tag !== undefined) {
|
|
74
|
+
return discriminated(reading, members, tag, path, read);
|
|
75
|
+
}
|
|
76
|
+
return reading.refuse(type, path, "return string, number or boolean literals, or objects told apart by one literal field such as _tag");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// The members are the union's own, or an optional property's without the undefined its optionality adds.
|
|
80
|
+
export function unionForm(reading: TypeReading, type: ts.Type, members: readonly ts.Type[], path: string, read: Read): WireForm | undefined {
|
|
81
|
+
if (type.aliasSymbol?.name === "Option" && declaredIn(type, "Option") && type.aliasTypeArguments?.[0] !== undefined) {
|
|
82
|
+
const inner = read(type.aliasTypeArguments[0], `${path}<Some>`);
|
|
83
|
+
return inner === undefined ? undefined : { form: "option", inner };
|
|
84
|
+
}
|
|
85
|
+
const inner = withoutNull(reading, type, members, path, read);
|
|
86
|
+
const nullable = members.some((member) => member.flags & ts.TypeFlags.Null);
|
|
87
|
+
return inner === undefined || !nullable ? inner : { form: "nullOr", inner };
|
|
88
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { Option } from "effect";
|
|
2
|
+
import type { EnvironmentModuleNode, HookHandler, Plugin, ResolvedConfig, Rollup, ViteDevServer } from "vite";
|
|
3
|
+
import { bundleCheck } from "#adapters/bundleCheck.ts";
|
|
4
|
+
import { runNode } from "#adapters/runNode.ts";
|
|
5
|
+
import { type GenerateRound, type WatchedGeneration, watchedGeneration } from "#generateRound.ts";
|
|
6
|
+
|
|
7
|
+
export interface BuildPluginOptions {
|
|
8
|
+
readonly bundleCheck?: boolean;
|
|
9
|
+
readonly serverOnly?: readonly string[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type HotUpdateContext = ThisParameterType<HookHandler<NonNullable<Plugin["hotUpdate"]>>>;
|
|
13
|
+
|
|
14
|
+
interface SharedRound {
|
|
15
|
+
readonly round: Promise<Option.Option<GenerateRound>>;
|
|
16
|
+
seenBy: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function tellTheBrowser(environment: HotUpdateContext["environment"], devServer: ViteDevServer, refusal: Option.Option<string>, file: string) {
|
|
20
|
+
if (Option.isSome(refusal)) {
|
|
21
|
+
devServer.config.logger.error(refusal.value, { timestamp: true });
|
|
22
|
+
environment.hot.send({ err: { message: refusal.value, plugin: "shivaedev:build", stack: "" }, type: "error" });
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
environment.hot.send({ path: "*", triggeredBy: file, type: "full-reload" });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function generateFiles(): Plugin {
|
|
29
|
+
let config: ResolvedConfig | undefined;
|
|
30
|
+
let server: ViteDevServer | undefined;
|
|
31
|
+
let generation: Promise<WatchedGeneration> | undefined;
|
|
32
|
+
let started: Promise<GenerateRound> | undefined;
|
|
33
|
+
let outputs = new Set<string>();
|
|
34
|
+
// One change reaches hotUpdate once per environment; they share one run.
|
|
35
|
+
const shared = new Map<string, SharedRound>();
|
|
36
|
+
function remember(round: GenerateRound) {
|
|
37
|
+
outputs = new Set([...outputs, ...round.outputs]);
|
|
38
|
+
if (server !== undefined) {
|
|
39
|
+
const root = server.config.root;
|
|
40
|
+
server.watcher.add(round.inputs.filter((file) => !file.startsWith(root)));
|
|
41
|
+
}
|
|
42
|
+
return round;
|
|
43
|
+
}
|
|
44
|
+
function sharedRound(changed: WatchedGeneration["changed"], file: string, timestamp: number, environments: number) {
|
|
45
|
+
const key = `${timestamp}\u0000${file}`;
|
|
46
|
+
const entry = shared.get(key) ?? { round: runNode(changed(file)), seenBy: 0 };
|
|
47
|
+
entry.seenBy += 1;
|
|
48
|
+
shared.set(key, entry);
|
|
49
|
+
if (entry.seenBy === environments) {
|
|
50
|
+
shared.delete(key);
|
|
51
|
+
}
|
|
52
|
+
return entry.round;
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
async buildStart(this: Rollup.PluginContext) {
|
|
56
|
+
generation ??= runNode(watchedGeneration(config?.root ?? "."));
|
|
57
|
+
const { run } = await generation;
|
|
58
|
+
started ??= runNode(run);
|
|
59
|
+
const round = remember(await started);
|
|
60
|
+
if (Option.isSome(round.refusal)) {
|
|
61
|
+
this.error(round.refusal.value);
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
configResolved(resolved) {
|
|
65
|
+
config = resolved;
|
|
66
|
+
},
|
|
67
|
+
configureServer(devServer) {
|
|
68
|
+
server = devServer;
|
|
69
|
+
},
|
|
70
|
+
enforce: "pre",
|
|
71
|
+
async hotUpdate(this: HotUpdateContext, { file, modules, server: devServer, timestamp }) {
|
|
72
|
+
// A run hands Vite the modules of each file it rewrites and reloads the page when it creates one, since a page may have failed
|
|
73
|
+
// to resolve it, so the watcher's own event for that write would reload twice.
|
|
74
|
+
if (outputs.has(file)) {
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
if (generation === undefined) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const known = outputs;
|
|
81
|
+
const { changed } = await generation;
|
|
82
|
+
const round = Option.map(await sharedRound(changed, file, timestamp, Object.keys(devServer.environments).length), remember);
|
|
83
|
+
if (Option.isNone(round)) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (Option.isSome(round.value.refusal) || round.value.changed.some((output) => !known.has(output))) {
|
|
87
|
+
if (this.environment.config.consumer === "client") {
|
|
88
|
+
tellTheBrowser(this.environment, devServer, round.value.refusal, file);
|
|
89
|
+
}
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
92
|
+
const regenerated = round.value.changed.flatMap((output): EnvironmentModuleNode[] => [
|
|
93
|
+
...(this.environment.moduleGraph.getModulesByFile(output) ?? []),
|
|
94
|
+
]);
|
|
95
|
+
return [...modules, ...regenerated];
|
|
96
|
+
},
|
|
97
|
+
name: "shivaedev:build-generate",
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function buildPlugin({ bundleCheck: checked = true, serverOnly = [] }: BuildPluginOptions = {}): Plugin[] {
|
|
102
|
+
return checked ? [generateFiles(), bundleCheck(serverOnly)] : [generateFiles()];
|
|
103
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Option } from "effect";
|
|
2
|
+
import type { TestProject } from "vitest/node";
|
|
3
|
+
import { runNode } from "#adapters/runNode.ts";
|
|
4
|
+
import { generateRound } from "#generateRound.ts";
|
|
5
|
+
|
|
6
|
+
// A Vitest global setup: every module's files are written before the first test file loads, and a refusal stops the run.
|
|
7
|
+
export async function setup(project: TestProject): Promise<void> {
|
|
8
|
+
const round = await runNode(generateRound(project.config.root));
|
|
9
|
+
if (Option.isSome(round.refusal)) {
|
|
10
|
+
throw new Error(round.refusal.value);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
import { classForm, objectForm } from "#adapters/objectForm.ts";
|
|
3
|
+
import { declaredIn, isLibraryType, isReference, literalOf, type TypeReading, typeArguments } from "#adapters/typeReading.ts";
|
|
4
|
+
import { unionForm } from "#adapters/unionForm.ts";
|
|
5
|
+
import type { Primitive, WireForm } from "#facts.ts";
|
|
6
|
+
|
|
7
|
+
const PRIMITIVES: readonly (readonly [ts.TypeFlags, Primitive])[] = [
|
|
8
|
+
[ts.TypeFlags.String, "String"],
|
|
9
|
+
[ts.TypeFlags.Number, "Number"],
|
|
10
|
+
[ts.TypeFlags.Boolean, "Boolean"],
|
|
11
|
+
[ts.TypeFlags.BigInt, "BigInt"],
|
|
12
|
+
[ts.TypeFlags.Null, "Null"],
|
|
13
|
+
[ts.TypeFlags.Void, "Void"],
|
|
14
|
+
[ts.TypeFlags.Undefined, "Undefined"],
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
const UNMAPPED: readonly (readonly [ts.TypeFlags, string])[] = [
|
|
18
|
+
[ts.TypeFlags.Any, "decode the value with a schema, or give it a type"],
|
|
19
|
+
[ts.TypeFlags.Unknown, "decode the value with a schema, or give it a type"],
|
|
20
|
+
[ts.TypeFlags.Never, "return a value, or nothing"],
|
|
21
|
+
[ts.TypeFlags.EnumLike, "return string or number literals instead of an enum"],
|
|
22
|
+
[ts.TypeFlags.TemplateLiteral | ts.TypeFlags.StringMapping, "return string"],
|
|
23
|
+
[ts.TypeFlags.ESSymbolLike, "return a string instead of a symbol"],
|
|
24
|
+
[ts.TypeFlags.TypeParameter | ts.TypeFlags.Index | ts.TypeFlags.IndexedAccess | ts.TypeFlags.Conditional, "give run's return a concrete type"],
|
|
25
|
+
[ts.TypeFlags.NonPrimitive, "return a type with known properties"],
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
function arrayForm(reading: TypeReading, type: ts.Type, path: string, seen: ReadonlySet<ts.Type>): WireForm | undefined {
|
|
29
|
+
const [element] = typeArguments(reading.checker, type);
|
|
30
|
+
const item = element === undefined ? undefined : wireForm(reading, element, `${path}[number]`, seen);
|
|
31
|
+
return item === undefined ? undefined : { form: "array", item, mutable: type.getSymbol()?.name === "Array" };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function isTuple(target: ts.GenericType): target is ts.TupleType {
|
|
35
|
+
return "elementFlags" in target;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function tupleForm(reading: TypeReading, type: ts.TypeReference, path: string, seen: ReadonlySet<ts.Type>): WireForm | undefined {
|
|
39
|
+
const target = type.target;
|
|
40
|
+
if (!isTuple(target) || target.elementFlags.some((flags) => flags !== ts.ElementFlags.Required)) {
|
|
41
|
+
return reading.refuse(type, path, "use an array, or a tuple of required elements");
|
|
42
|
+
}
|
|
43
|
+
const elements = reading.checker
|
|
44
|
+
.getTypeArguments(type)
|
|
45
|
+
.slice(0, target.elementFlags.length)
|
|
46
|
+
.map((element, index) => wireForm(reading, element, `${path}[${index}]`, seen));
|
|
47
|
+
return elements.every((element) => element !== undefined) ? { elements, form: "tuple", mutable: !target.readonly } : undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function objectTypeForm(reading: TypeReading, type: ts.Type, path: string, seen: ReadonlySet<ts.Type>): WireForm | undefined {
|
|
51
|
+
if (isLibraryType(reading, type, "Date")) {
|
|
52
|
+
return { form: "primitive", primitive: "Date" };
|
|
53
|
+
}
|
|
54
|
+
if (declaredIn(type, "DateTime") && type.getSymbol()?.name === "Utc") {
|
|
55
|
+
return { form: "primitive", primitive: "DateTimeUtc" };
|
|
56
|
+
}
|
|
57
|
+
if (reading.checker.isTupleType(type) && isReference(type)) {
|
|
58
|
+
return tupleForm(reading, type, path, seen);
|
|
59
|
+
}
|
|
60
|
+
if (reading.checker.isArrayType(type)) {
|
|
61
|
+
return arrayForm(reading, type, path, seen);
|
|
62
|
+
}
|
|
63
|
+
const symbol = type.getSymbol();
|
|
64
|
+
if (symbol !== undefined && symbol.flags & ts.SymbolFlags.Class) {
|
|
65
|
+
return classForm(reading, type, symbol, path);
|
|
66
|
+
}
|
|
67
|
+
return objectForm(reading, type, path, (property, propertyPath) => wireForm(reading, property, propertyPath, seen));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// The schema whose Type is exactly this type, or undefined after refusing it: the wire never carries a looser form.
|
|
71
|
+
export function wireForm(reading: TypeReading, type: ts.Type, path: string, seen: ReadonlySet<ts.Type> = new Set()): WireForm | undefined {
|
|
72
|
+
if (seen.has(type)) {
|
|
73
|
+
return reading.refuse(type, path, "return a type that does not contain itself");
|
|
74
|
+
}
|
|
75
|
+
const within = new Set([...seen, type]);
|
|
76
|
+
const primitive = PRIMITIVES.find(([flags]) => type.flags & flags);
|
|
77
|
+
if (primitive !== undefined) {
|
|
78
|
+
return { form: "primitive", primitive: primitive[1] };
|
|
79
|
+
}
|
|
80
|
+
const unmapped = UNMAPPED.find(([flags]) => type.flags & flags);
|
|
81
|
+
if (unmapped !== undefined) {
|
|
82
|
+
return reading.refuse(type, path, unmapped[1]);
|
|
83
|
+
}
|
|
84
|
+
const literal = literalOf(reading.checker, type);
|
|
85
|
+
if (literal !== undefined) {
|
|
86
|
+
return { form: "literals", values: [literal] };
|
|
87
|
+
}
|
|
88
|
+
if (type.isUnion()) {
|
|
89
|
+
return unionForm(reading, type, type.types, path, (member, memberPath) => wireForm(reading, member, memberPath, within));
|
|
90
|
+
}
|
|
91
|
+
if (type.isIntersection() && type.types.some((member) => !(member.flags & ts.TypeFlags.Object))) {
|
|
92
|
+
return reading.refuse(type, path, "return the unbranded type, or wrap the value in an exported Schema class");
|
|
93
|
+
}
|
|
94
|
+
if (type.flags & ts.TypeFlags.Object || type.isIntersection()) {
|
|
95
|
+
return objectTypeForm(reading, type, path, within);
|
|
96
|
+
}
|
|
97
|
+
return reading.refuse(type, path, "return plain data");
|
|
98
|
+
}
|