@shivaedev/build 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (209) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/LICENSE +21 -0
  3. package/README.md +216 -2
  4. package/dist/BuildFailure.d.ts +9 -0
  5. package/dist/BuildFailure.d.ts.map +1 -0
  6. package/dist/BuildFailure.js +4 -0
  7. package/dist/BuildFailure.js.map +1 -0
  8. package/dist/adapters/appTestConfig.d.ts +4 -0
  9. package/dist/adapters/appTestConfig.d.ts.map +1 -0
  10. package/dist/adapters/appTestConfig.js +17 -0
  11. package/dist/adapters/appTestConfig.js.map +1 -0
  12. package/dist/adapters/appViteConfig.d.ts +26 -0
  13. package/dist/adapters/appViteConfig.d.ts.map +1 -0
  14. package/dist/adapters/appViteConfig.js +18 -0
  15. package/dist/adapters/appViteConfig.js.map +1 -0
  16. package/dist/adapters/bundleCheck.d.ts +3 -0
  17. package/dist/adapters/bundleCheck.d.ts.map +1 -0
  18. package/dist/adapters/bundleCheck.js +53 -0
  19. package/dist/adapters/bundleCheck.js.map +1 -0
  20. package/dist/adapters/copiedCode.d.ts +9 -0
  21. package/dist/adapters/copiedCode.d.ts.map +1 -0
  22. package/dist/adapters/copiedCode.js +84 -0
  23. package/dist/adapters/copiedCode.js.map +1 -0
  24. package/dist/adapters/failuresOf.d.ts +5 -0
  25. package/dist/adapters/failuresOf.d.ts.map +1 -0
  26. package/dist/adapters/failuresOf.js +29 -0
  27. package/dist/adapters/failuresOf.js.map +1 -0
  28. package/dist/adapters/isReadonlyProperty.d.ts +10 -0
  29. package/dist/adapters/isReadonlyProperty.d.ts.map +1 -0
  30. package/dist/adapters/isReadonlyProperty.js +7 -0
  31. package/dist/adapters/isReadonlyProperty.js.map +1 -0
  32. package/dist/adapters/moduleCalls.d.ts +10 -0
  33. package/dist/adapters/moduleCalls.d.ts.map +1 -0
  34. package/dist/adapters/moduleCalls.js +47 -0
  35. package/dist/adapters/moduleCalls.js.map +1 -0
  36. package/dist/adapters/moduleFacts.d.ts +4 -0
  37. package/dist/adapters/moduleFacts.d.ts.map +1 -0
  38. package/dist/adapters/moduleFacts.js +63 -0
  39. package/dist/adapters/moduleFacts.js.map +1 -0
  40. package/dist/adapters/objectForm.d.ts +8 -0
  41. package/dist/adapters/objectForm.d.ts.map +1 -0
  42. package/dist/adapters/objectForm.js +66 -0
  43. package/dist/adapters/objectForm.js.map +1 -0
  44. package/dist/adapters/operationFacts.d.ts +10 -0
  45. package/dist/adapters/operationFacts.d.ts.map +1 -0
  46. package/dist/adapters/operationFacts.js +72 -0
  47. package/dist/adapters/operationFacts.js.map +1 -0
  48. package/dist/adapters/runNode.d.ts +6 -0
  49. package/dist/adapters/runNode.d.ts.map +1 -0
  50. package/dist/adapters/runNode.js +14 -0
  51. package/dist/adapters/runNode.js.map +1 -0
  52. package/dist/adapters/typeReading.d.ts +15 -0
  53. package/dist/adapters/typeReading.d.ts.map +1 -0
  54. package/dist/adapters/typeReading.js +46 -0
  55. package/dist/adapters/typeReading.js.map +1 -0
  56. package/dist/adapters/typeScript.d.ts +15 -0
  57. package/dist/adapters/typeScript.d.ts.map +1 -0
  58. package/dist/adapters/typeScript.js +81 -0
  59. package/dist/adapters/typeScript.js.map +1 -0
  60. package/dist/adapters/unionForm.d.ts +7 -0
  61. package/dist/adapters/unionForm.d.ts.map +1 -0
  62. package/dist/adapters/unionForm.js +78 -0
  63. package/dist/adapters/unionForm.js.map +1 -0
  64. package/dist/adapters/vitePlugin.d.ts +7 -0
  65. package/dist/adapters/vitePlugin.d.ts.map +1 -0
  66. package/dist/adapters/vitePlugin.js +88 -0
  67. package/dist/adapters/vitePlugin.js.map +1 -0
  68. package/dist/adapters/vitePort.d.ts +2 -0
  69. package/dist/adapters/vitePort.d.ts.map +1 -0
  70. package/dist/adapters/vitePort.js +5 -0
  71. package/dist/adapters/vitePort.js.map +1 -0
  72. package/dist/adapters/vitest/setup.d.ts +3 -0
  73. package/dist/adapters/vitest/setup.d.ts.map +1 -0
  74. package/dist/adapters/vitest/setup.js +11 -0
  75. package/dist/adapters/vitest/setup.js.map +1 -0
  76. package/dist/adapters/wireForm.d.ts +5 -0
  77. package/dist/adapters/wireForm.d.ts.map +1 -0
  78. package/dist/adapters/wireForm.js +91 -0
  79. package/dist/adapters/wireForm.js.map +1 -0
  80. package/dist/byText.d.ts +2 -0
  81. package/dist/byText.d.ts.map +1 -0
  82. package/dist/byText.js +8 -0
  83. package/dist/byText.js.map +1 -0
  84. package/dist/cache.d.ts +4 -0
  85. package/dist/cache.d.ts.map +1 -0
  86. package/dist/cache.js +19 -0
  87. package/dist/cache.js.map +1 -0
  88. package/dist/cli.d.ts +3 -0
  89. package/dist/cli.d.ts.map +1 -0
  90. package/dist/cli.js +11 -0
  91. package/dist/cli.js.map +1 -0
  92. package/dist/code.d.ts +11 -0
  93. package/dist/code.d.ts.map +1 -0
  94. package/dist/code.js +11 -0
  95. package/dist/code.js.map +1 -0
  96. package/dist/command.d.ts +6 -0
  97. package/dist/command.d.ts.map +1 -0
  98. package/dist/command.js +28 -0
  99. package/dist/command.js.map +1 -0
  100. package/dist/emit.d.ts +23 -0
  101. package/dist/emit.d.ts.map +1 -0
  102. package/dist/emit.js +69 -0
  103. package/dist/emit.js.map +1 -0
  104. package/dist/facts.d.ts +92 -0
  105. package/dist/facts.d.ts.map +1 -0
  106. package/dist/facts.js +3 -0
  107. package/dist/facts.js.map +1 -0
  108. package/dist/files.d.ts +4 -0
  109. package/dist/files.d.ts.map +1 -0
  110. package/dist/files.js +15 -0
  111. package/dist/files.js.map +1 -0
  112. package/dist/formCode.d.ts +6 -0
  113. package/dist/formCode.d.ts.map +1 -0
  114. package/dist/formCode.js +46 -0
  115. package/dist/formCode.js.map +1 -0
  116. package/dist/generate.d.ts +7 -0
  117. package/dist/generate.d.ts.map +1 -0
  118. package/dist/generate.js +69 -0
  119. package/dist/generate.js.map +1 -0
  120. package/dist/generateRound.d.ts +14 -0
  121. package/dist/generateRound.d.ts.map +1 -0
  122. package/dist/generateRound.js +44 -0
  123. package/dist/generateRound.js.map +1 -0
  124. package/dist/importNames.d.ts +7 -0
  125. package/dist/importNames.d.ts.map +1 -0
  126. package/dist/importNames.js +57 -0
  127. package/dist/importNames.js.map +1 -0
  128. package/dist/inputs.d.ts +15 -0
  129. package/dist/inputs.d.ts.map +1 -0
  130. package/dist/inputs.js +64 -0
  131. package/dist/inputs.js.map +1 -0
  132. package/dist/ownerOf.d.ts +7 -0
  133. package/dist/ownerOf.d.ts.map +1 -0
  134. package/dist/ownerOf.js +30 -0
  135. package/dist/ownerOf.js.map +1 -0
  136. package/dist/packageScope.d.ts +13 -0
  137. package/dist/packageScope.d.ts.map +1 -0
  138. package/dist/packageScope.js +28 -0
  139. package/dist/packageScope.js.map +1 -0
  140. package/dist/plan.d.ts +12 -0
  141. package/dist/plan.d.ts.map +1 -0
  142. package/dist/plan.js +115 -0
  143. package/dist/plan.js.map +1 -0
  144. package/dist/project.d.ts +11 -0
  145. package/dist/project.d.ts.map +1 -0
  146. package/dist/project.js +24 -0
  147. package/dist/project.js.map +1 -0
  148. package/dist/rejections.d.ts +11 -0
  149. package/dist/rejections.d.ts.map +1 -0
  150. package/dist/rejections.js +46 -0
  151. package/dist/rejections.js.map +1 -0
  152. package/dist/report.d.ts +22 -0
  153. package/dist/report.d.ts.map +1 -0
  154. package/dist/report.js +20 -0
  155. package/dist/report.js.map +1 -0
  156. package/dist/serverCode.d.ts +20 -0
  157. package/dist/serverCode.d.ts.map +1 -0
  158. package/dist/serverCode.js +90 -0
  159. package/dist/serverCode.js.map +1 -0
  160. package/dist/settleOrphans.d.ts +5 -0
  161. package/dist/settleOrphans.d.ts.map +1 -0
  162. package/dist/settleOrphans.js +43 -0
  163. package/dist/settleOrphans.js.map +1 -0
  164. package/dist/specifiers.d.ts +4 -0
  165. package/dist/specifiers.d.ts.map +1 -0
  166. package/dist/specifiers.js +52 -0
  167. package/dist/specifiers.js.map +1 -0
  168. package/package.json +89 -3
  169. package/src/BuildFailure.ts +5 -0
  170. package/src/adapters/appTestConfig.ts +18 -0
  171. package/src/adapters/appViteConfig.ts +31 -0
  172. package/src/adapters/bundleCheck.ts +57 -0
  173. package/src/adapters/copiedCode.ts +103 -0
  174. package/src/adapters/failuresOf.ts +31 -0
  175. package/src/adapters/isReadonlyProperty.ts +21 -0
  176. package/src/adapters/moduleCalls.ts +58 -0
  177. package/src/adapters/moduleFacts.ts +66 -0
  178. package/src/adapters/objectForm.ts +74 -0
  179. package/src/adapters/operationFacts.ts +85 -0
  180. package/src/adapters/runNode.ts +15 -0
  181. package/src/adapters/typeReading.ts +63 -0
  182. package/src/adapters/typeScript.ts +101 -0
  183. package/src/adapters/unionForm.ts +88 -0
  184. package/src/adapters/vitePlugin.ts +103 -0
  185. package/src/adapters/vitePort.ts +5 -0
  186. package/src/adapters/vitest/setup.ts +12 -0
  187. package/src/adapters/wireForm.ts +98 -0
  188. package/src/byText.ts +7 -0
  189. package/src/cache.ts +28 -0
  190. package/src/cli.ts +16 -0
  191. package/src/code.ts +23 -0
  192. package/src/command.ts +42 -0
  193. package/src/emit.ts +117 -0
  194. package/src/facts.ts +70 -0
  195. package/src/files.ts +19 -0
  196. package/src/formCode.ts +59 -0
  197. package/src/generate.ts +86 -0
  198. package/src/generateRound.ts +60 -0
  199. package/src/importNames.ts +61 -0
  200. package/src/inputs.ts +82 -0
  201. package/src/ownerOf.ts +40 -0
  202. package/src/packageScope.ts +35 -0
  203. package/src/plan.ts +138 -0
  204. package/src/project.ts +30 -0
  205. package/src/rejections.ts +57 -0
  206. package/src/report.ts +39 -0
  207. package/src/serverCode.ts +139 -0
  208. package/src/settleOrphans.ts +58 -0
  209. package/src/specifiers.ts +55 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"settleOrphans.js","sourceRoot":"","sources":["../src/settleOrphans.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ import { Effect, Path } from "effect";
2
+ export declare function matchingKey(entries: readonly (readonly [string, unknown])[], relativeFile: string): string | undefined;
3
+ export declare const specifierFor: (root: string, from: string, file: string) => Effect.Effect<string | undefined, import("effect/PlatformError").PlatformError | import("effect/Schema").SchemaError, import("effect/FileSystem").FileSystem | Path.Path>;
4
+ //# sourceMappingURL=specifiers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"specifiers.d.ts","sourceRoot":"","sources":["../src/specifiers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAU,IAAI,EAAE,MAAM,QAAQ,CAAC;AAsB9C,wBAAgB,WAAW,CAAC,OAAO,EAAE,SAAS,CAAC,SAAS,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EAAE,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CActH;AAGD,eAAO,MAAM,YAAY,yNAevB,CAAC"}
@@ -0,0 +1,52 @@
1
+ import { Effect, Option, Path } from "effect";
2
+ import { packageScopeOf } from "#packageScope.ts";
3
+ function targetsOf(value) {
4
+ if (typeof value === "string") {
5
+ return [value];
6
+ }
7
+ if (Array.isArray(value)) {
8
+ return value.flatMap(targetsOf);
9
+ }
10
+ return typeof value === "object" && value !== null ? Object.values(value).flatMap(targetsOf) : [];
11
+ }
12
+ function exportEntries(exports) {
13
+ if (typeof exports !== "object" || exports === null || Array.isArray(exports)) {
14
+ return exports === undefined ? [] : [[".", exports]];
15
+ }
16
+ const entries = Object.entries(exports);
17
+ return entries.every(([key]) => key.startsWith(".")) ? entries : [[".", exports]];
18
+ }
19
+ // The key of the first entry whose target names the file, with its wildcard filled in.
20
+ export function matchingKey(entries, relativeFile) {
21
+ for (const [key, value] of entries) {
22
+ for (const target of targetsOf(value)) {
23
+ const [prefix = "", suffix, ...rest] = target.split("*");
24
+ if (suffix === undefined && target === relativeFile) {
25
+ return key;
26
+ }
27
+ const fits = suffix !== undefined && rest.length === 0 && relativeFile.length >= prefix.length + suffix.length;
28
+ if (fits && relativeFile.startsWith(prefix) && relativeFile.endsWith(suffix)) {
29
+ return key.replace("*", relativeFile.slice(prefix.length, relativeFile.length - suffix.length));
30
+ }
31
+ }
32
+ }
33
+ return undefined;
34
+ }
35
+ // How the generated file at `from` imports `file`: the project's own imports, another package's exports, else a relative path.
36
+ export const specifierFor = Effect.fn("Build.specifierFor")(function* (root, from, file) {
37
+ const path = yield* Path.Path;
38
+ const scope = yield* packageScopeOf(file);
39
+ if (Option.isNone(scope)) {
40
+ return undefined;
41
+ }
42
+ const { directory, manifest } = scope.value;
43
+ const relativeFile = `./${path.relative(directory, file)}`;
44
+ if (directory === root) {
45
+ const own = matchingKey(Object.entries(manifest.imports ?? {}), relativeFile);
46
+ const relative = path.relative(path.dirname(from), file);
47
+ return own ?? (relative.startsWith(".") ? relative : `./${relative}`);
48
+ }
49
+ const exported = matchingKey(exportEntries(manifest.exports), relativeFile);
50
+ return exported === undefined || manifest.name === undefined ? undefined : `${manifest.name}${exported.slice(1)}`;
51
+ });
52
+ //# sourceMappingURL=specifiers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"specifiers.js","sourceRoot":"","sources":["../src/specifiers.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,92 @@
1
1
  {
2
2
  "name": "@shivaedev/build",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
4
+ "description": "An application's build setup: its Vite and Vitest configuration, and the generated wire and browser contract of each defined module",
5
+ "homepage": "https://github.com/ShivaeDev/platform/tree/main/packages/build#readme",
6
+ "bugs": {
7
+ "url": "https://github.com/ShivaeDev/platform/issues"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/ShivaeDev/platform.git",
12
+ "directory": "packages/build"
13
+ },
14
+ "license": "MIT",
15
+ "sideEffects": false,
16
+ "type": "module",
17
+ "imports": {
18
+ "#test/*": "./src/test-support/*",
19
+ "#*.ts": {
20
+ "@shivaedev/source": "./src/*.ts",
21
+ "types": "./dist/*.d.ts",
22
+ "default": "./dist/*.js"
23
+ },
24
+ "#package.json": "./package.json"
25
+ },
26
+ "exports": {
27
+ "./*.ts": {
28
+ "@shivaedev/source": "./src/*.ts",
29
+ "types": "./dist/*.d.ts",
30
+ "default": "./dist/*.js"
31
+ },
32
+ "./package.json": "./package.json"
33
+ },
34
+ "bin": {
35
+ "shivaedev-build": "./dist/cli.js"
36
+ },
37
+ "files": [
38
+ "dist",
39
+ "src",
40
+ "!src/**/*.test.ts",
41
+ "!src/**/*.spec.ts",
42
+ "!src/**/test-support",
43
+ "CHANGELOG.md",
44
+ "README.md"
45
+ ],
46
+ "dependencies": {
47
+ "@shivaedev/heavy-lock": "0.11.2",
48
+ "@shivaedev/quality": "0.77.0",
49
+ "typescript": "npm:@typescript/typescript6@6.0.2"
50
+ },
51
+ "devDependencies": {
52
+ "@effect/platform-node": "4.1.2",
53
+ "@effect/platform-node-shared": "4.1.2",
54
+ "@shivaedev/changes": "0.12.0",
55
+ "@shivaedev/contract": "0.44.0",
56
+ "@shivaedev/errors": "0.12.0",
57
+ "@shivaedev/test-story": "0.47.0",
58
+ "@types/node": "24.19.2",
59
+ "@typescript/native": "npm:typescript@7.0.2",
60
+ "effect": "4.1.2",
61
+ "vite": "8.3.5",
62
+ "vitest": "5.1.0"
63
+ },
64
+ "peerDependencies": {
65
+ "@effect/platform-node": "4.1.2",
66
+ "@effect/platform-node-shared": "4.1.2",
67
+ "effect": "4.1.2",
68
+ "vite": "8.3.5",
69
+ "vitest": "5.1.0"
70
+ },
71
+ "peerDependenciesMeta": {
72
+ "vite": {
73
+ "optional": true
74
+ },
75
+ "vitest": {
76
+ "optional": true
77
+ }
78
+ },
79
+ "engines": {
80
+ "node": ">=24"
81
+ },
82
+ "publishConfig": {
83
+ "access": "public",
84
+ "provenance": true
85
+ },
86
+ "scripts": {
87
+ "build": "node --eval \"import('node:fs').then(({ rmSync }) => rmSync('dist', { force: true, recursive: true }))\" && tsc --project tsconfig.build.json",
88
+ "ready": "pnpm typecheck && pnpm test && pnpm build",
89
+ "test": "vitest run",
90
+ "typecheck": "tsc --noEmit"
91
+ }
6
92
  }
@@ -0,0 +1,5 @@
1
+ import { Data } from "effect";
2
+
3
+ export class BuildFailure extends Data.TaggedError("BuildFailure")<{
4
+ readonly message: string;
5
+ }> {}
@@ -0,0 +1,18 @@
1
+ import { fileURLToPath } from "node:url";
2
+ import type { ViteUserConfig } from "vitest/config";
3
+ import { type TestProjectsOptions, testProjects } from "@shivaedev/quality/adapters/testProjects.ts";
4
+
5
+ export function appTestConfig(conditions: string[] = [], options: TestProjectsOptions = {}): ViteUserConfig {
6
+ const server = { resolve: { conditions: [...conditions, "node", "development|production"] } };
7
+ return {
8
+ // Vitest loads the global setup in its own environment, which needs the application's conditions to find the generator's sources.
9
+ environments: { "__vitest__": server },
10
+ resolve: { conditions: [...conditions, "module", "browser", "development|production"] },
11
+ ssr: server,
12
+ test: {
13
+ ...testProjects(options),
14
+ globalSetup: [fileURLToPath(import.meta.resolve("#adapters/vitest/setup.ts"))],
15
+ server: { deps: { inline: [/^@shivaedev\//u] } },
16
+ },
17
+ };
18
+ }
@@ -0,0 +1,31 @@
1
+ import type { PluginOption } from "vite";
2
+ import { type BuildPluginOptions, buildPlugin } from "#adapters/vitePlugin.ts";
3
+ import { vitePort } from "#adapters/vitePort.ts";
4
+
5
+ export interface AppViteOptions extends BuildPluginOptions {
6
+ readonly apiPort?: number;
7
+ readonly webPort?: number;
8
+ }
9
+
10
+ export function appViteConfig(
11
+ plugins: PluginOption[],
12
+ conditions: string[] = [],
13
+ proxyPaths: string[] = [],
14
+ { webPort = vitePort("VITE_PORT", 5173), apiPort = vitePort("PORT", 3000), ...build }: AppViteOptions = {},
15
+ ) {
16
+ return {
17
+ plugins: [...plugins, ...buildPlugin(build)],
18
+ resolve: { conditions, tsconfigPaths: true },
19
+ server: {
20
+ host: "127.0.0.1",
21
+ port: webPort,
22
+ proxy: Object.fromEntries(
23
+ ["/api/auth", "/rpc", ...proxyPaths].map((path) => [
24
+ path,
25
+ { changeOrigin: false, target: `http://127.0.0.1:${apiPort}`, ws: path === "/rpc" },
26
+ ]),
27
+ ),
28
+ strictPort: true,
29
+ },
30
+ };
31
+ }
@@ -0,0 +1,57 @@
1
+ import { isBuiltin } from "node:module";
2
+ import { dirname, join } from "node:path";
3
+ import { Option } from "effect";
4
+ import type { Plugin, Rollup } from "vite";
5
+ import { runNode } from "#adapters/runNode.ts";
6
+ import { type BundleGraph, type BundleModule, type ServerMarkers, serverCodeIn } from "#serverCode.ts";
7
+
8
+ const BROWSER_EXTERNAL = "__vite-browser-external:";
9
+
10
+ function graphOf(context: Rollup.PluginContext): BundleGraph {
11
+ const modules = new Map<string, BundleModule>();
12
+ const entries: string[] = [];
13
+ for (const id of context.getModuleIds()) {
14
+ const info = context.getModuleInfo(id);
15
+ if (info !== null) {
16
+ if (info.isEntry) {
17
+ entries.push(id);
18
+ }
19
+ const bare = id.startsWith(BROWSER_EXTERNAL) ? id.slice(BROWSER_EXTERNAL.length) : id;
20
+ modules.set(id, { builtin: isBuiltin(bare), imports: [...info.importedIds, ...info.dynamicallyImportedIds] });
21
+ }
22
+ }
23
+ return { entries, modules };
24
+ }
25
+
26
+ async function markersFrom(context: Rollup.PluginContext, root: string): Promise<ServerMarkers> {
27
+ const importer = join(root, "index.html");
28
+ async function resolved(specifier: string) {
29
+ return Option.fromNullishOr((await context.resolve(specifier, importer))?.id);
30
+ }
31
+ return {
32
+ defineModule: await resolved("@shivaedev/contract/defineModule.ts"),
33
+ operation: await resolved("@shivaedev/contract/operation.ts"),
34
+ sql: Option.map(await resolved("effect/sql"), dirname),
35
+ };
36
+ }
37
+
38
+ export function bundleCheck(serverOnly: readonly string[]): Plugin {
39
+ let root = ".";
40
+ return {
41
+ apply: "build",
42
+ applyToEnvironment: (environment) => environment.config.consumer === "client",
43
+ async buildEnd(this: Rollup.PluginContext, error) {
44
+ if (error !== undefined) {
45
+ return;
46
+ }
47
+ const found = await runNode(serverCodeIn(graphOf(this), await markersFrom(this, root), { root, serverOnly }));
48
+ if (Option.isSome(found)) {
49
+ this.error(found.value);
50
+ }
51
+ },
52
+ configResolved(resolved) {
53
+ root = resolved.root;
54
+ },
55
+ name: "shivaedev:build-bundle-check",
56
+ };
57
+ }
@@ -0,0 +1,103 @@
1
+ import ts from "typescript";
2
+ import type { CodePart, ImportFact } from "#facts.ts";
3
+
4
+ export interface Copying {
5
+ readonly checker: ts.TypeChecker;
6
+ // Names the copied part in a refusal, such as "notes.get reads".
7
+ readonly label: string;
8
+ readonly refuse: (message: string) => void;
9
+ }
10
+
11
+ const LOCAL_DECLARATIONS = new Set([
12
+ ts.SyntaxKind.VariableDeclaration,
13
+ ts.SyntaxKind.FunctionDeclaration,
14
+ ts.SyntaxKind.ClassDeclaration,
15
+ ts.SyntaxKind.InterfaceDeclaration,
16
+ ts.SyntaxKind.TypeAliasDeclaration,
17
+ ts.SyntaxKind.EnumDeclaration,
18
+ ts.SyntaxKind.ModuleDeclaration,
19
+ ts.SyntaxKind.Parameter,
20
+ ts.SyntaxKind.BindingElement,
21
+ ]);
22
+
23
+ type ImportBinding = ts.ImportSpecifier | ts.ImportClause | ts.NamespaceImport;
24
+
25
+ function importBinding(declaration: ts.Declaration): ImportBinding | undefined {
26
+ return ts.isImportSpecifier(declaration) || ts.isImportClause(declaration) || ts.isNamespaceImport(declaration) ? declaration : undefined;
27
+ }
28
+
29
+ function clauseOf(binding: ImportBinding): ts.ImportClause {
30
+ if (ts.isImportClause(binding)) {
31
+ return binding;
32
+ }
33
+ return ts.isNamespaceImport(binding) ? binding.parent : binding.parent.parent;
34
+ }
35
+
36
+ function importedName(binding: ImportBinding): string {
37
+ if (ts.isImportClause(binding)) {
38
+ return "default";
39
+ }
40
+ return ts.isNamespaceImport(binding) ? "*" : (binding.propertyName ?? binding.name).text;
41
+ }
42
+
43
+ function importFact(copying: Copying, binding: ImportBinding, local: string): ImportFact | undefined {
44
+ const clause = clauseOf(binding);
45
+ const declaration = clause.parent;
46
+ if (!ts.isStringLiteral(declaration.moduleSpecifier)) {
47
+ return undefined;
48
+ }
49
+ const specifier = declaration.moduleSpecifier.text;
50
+ const file = declaration.getSourceFile().fileName;
51
+ if (specifier.startsWith(".")) {
52
+ copying.refuse(
53
+ `${copying.label} imports ${local} by the relative path "${specifier}" in ${file}; import it through a package import, such as a # specifier, so the generated file can import it too`,
54
+ );
55
+ return undefined;
56
+ }
57
+ const name = importedName(binding);
58
+ const target = copying.checker.getSymbolAtLocation(declaration.moduleSpecifier)?.valueDeclaration?.getSourceFile().fileName;
59
+ const typeOnly = clause.isTypeOnly || (ts.isImportSpecifier(binding) && binding.isTypeOnly);
60
+ return { local, name, specifier, target, typeOnly, writtenIn: file };
61
+ }
62
+
63
+ function referenceOf(copying: Copying, node: ts.Node, identifier: ts.Identifier): ImportFact | "local" | undefined {
64
+ const symbol = ts.isShorthandPropertyAssignment(identifier.parent)
65
+ ? copying.checker.getShorthandAssignmentValueSymbol(identifier.parent)
66
+ : copying.checker.getSymbolAtLocation(identifier);
67
+ const declaration = symbol?.declarations?.[0];
68
+ if (declaration === undefined) {
69
+ return undefined;
70
+ }
71
+ const binding = importBinding(declaration);
72
+ if (binding !== undefined) {
73
+ return importFact(copying, binding, identifier.text);
74
+ }
75
+ const inside = declaration.pos >= node.pos && declaration.end <= node.end;
76
+ const sameFile = declaration.getSourceFile() === node.getSourceFile();
77
+ return sameFile && !inside && LOCAL_DECLARATIONS.has(declaration.kind) ? "local" : undefined;
78
+ }
79
+
80
+ // The code as written, with each imported name kept as its import, so the generated file can import and rename it.
81
+ export function copiedCode(copying: Copying, node: ts.Node): readonly CodePart[] {
82
+ const source = node.getSourceFile();
83
+ const parts: CodePart[] = [];
84
+ let position = node.getStart(source);
85
+ function visit(child: ts.Node) {
86
+ if (ts.isIdentifier(child)) {
87
+ const reference = referenceOf(copying, node, child);
88
+ if (reference === "local") {
89
+ copying.refuse(
90
+ `${copying.label} uses ${child.text}, which is declared in ${source.fileName}; move it to a module the operation imports it from, such as the module's model file`,
91
+ );
92
+ } else if (reference !== undefined) {
93
+ const shorthand = ts.isShorthandPropertyAssignment(child.parent) ? `${child.text}: ` : "";
94
+ parts.push(source.text.slice(position, child.getStart(source)) + shorthand, reference);
95
+ position = child.end;
96
+ }
97
+ }
98
+ ts.forEachChild(child, visit);
99
+ }
100
+ visit(node);
101
+ parts.push(source.text.slice(position, node.end));
102
+ return parts.filter((part) => part !== "");
103
+ }
@@ -0,0 +1,31 @@
1
+ import ts from "typescript";
2
+ import { declaredClass, isSchemaClass, type TypeReading } from "#adapters/typeReading.ts";
3
+ import type { Failure } from "#facts.ts";
4
+
5
+ function failureOf(reading: TypeReading, member: ts.Type): Failure | undefined {
6
+ if (member.flags & ts.TypeFlags.Never) {
7
+ return undefined;
8
+ }
9
+ if (member.getProperty("isRetryable") !== undefined) {
10
+ return { kind: "retryable" };
11
+ }
12
+ const text = reading.checker.typeToString(member);
13
+ const tagProperty = member.getProperty("_tag");
14
+ const tag = tagProperty === undefined ? undefined : reading.checker.getTypeOfSymbol(tagProperty);
15
+ if (tag === undefined || !tag.isStringLiteral()) {
16
+ return { kind: "untagged", text };
17
+ }
18
+ // Object.assign(new NoteChanged(...), { cause }) raises a NoteChanged whose type is an intersection.
19
+ const parts = member.isIntersection() ? member.types : [member];
20
+ const symbol = parts.map((part) => part.getSymbol()).find((candidate) => candidate !== undefined && candidate.flags & ts.SymbolFlags.Class);
21
+ const declared = symbol === undefined ? undefined : declaredClass(reading, symbol);
22
+ if (symbol === undefined || declared?.exportName === undefined) {
23
+ return { kind: "unexported", tag: tag.value, text };
24
+ }
25
+ return { declared, kind: "class", schema: isSchemaClass(reading, symbol), tag: tag.value };
26
+ }
27
+
28
+ // A rejection is the class run raises, so each failure keeps the declaration it came from, never just its shape.
29
+ export function failuresOf(reading: TypeReading, errors: readonly ts.Type[]): readonly Failure[] {
30
+ return errors.flatMap((error) => (error.isUnion() ? error.types : [error])).flatMap((member) => failureOf(reading, member) ?? []);
31
+ }
@@ -0,0 +1,21 @@
1
+ import ts from "typescript";
2
+
3
+ // TypeScript 6 answers two questions only through internal API: whether a property is readonly (a mapped type such as
4
+ // Readonly<T> or a Schema's Type marks it in internal flags, not on a declaration) and which optional properties exclude
5
+ // undefined. The compiler-api catalog pins the version, so these stay put.
6
+ declare module "typescript" {
7
+ function getCheckFlags(symbol: ts.Symbol): number;
8
+ function getDeclarationModifierFlagsFromSymbol(symbol: ts.Symbol): ts.ModifierFlags;
9
+ interface TypeChecker {
10
+ getExactOptionalProperties: (type: ts.Type) => ts.Symbol[];
11
+ }
12
+ }
13
+
14
+ const READONLY_CHECK_FLAG = 8;
15
+
16
+ export function isReadonlyProperty(property: ts.Symbol): boolean {
17
+ return (
18
+ (ts.getCheckFlags(property) & READONLY_CHECK_FLAG) !== 0
19
+ || ((property.flags & ts.SymbolFlags.Property) !== 0 && (ts.getDeclarationModifierFlagsFromSymbol(property) & ts.ModifierFlags.Readonly) !== 0)
20
+ );
21
+ }
@@ -0,0 +1,58 @@
1
+ import ts from "typescript";
2
+
3
+ export interface ModuleCall {
4
+ readonly definition: ts.Expression | undefined;
5
+ readonly exported: boolean;
6
+ readonly variable: ts.VariableDeclaration;
7
+ }
8
+
9
+ function importsDefineModule(source: ts.SourceFile): ReadonlySet<string> {
10
+ const locals = new Set<string>();
11
+ for (const statement of source.statements) {
12
+ const bindings = ts.isImportDeclaration(statement) ? statement.importClause?.namedBindings : undefined;
13
+ if (bindings !== undefined && ts.isNamedImports(bindings)) {
14
+ for (const element of bindings.elements) {
15
+ if ((element.propertyName ?? element.name).text === "defineModule") {
16
+ locals.add(element.name.text);
17
+ }
18
+ }
19
+ }
20
+ }
21
+ return locals;
22
+ }
23
+
24
+ // Found by syntax alone, so discovery and the cache need no program.
25
+ export function moduleCalls(source: ts.SourceFile): readonly ModuleCall[] {
26
+ const locals = importsDefineModule(source);
27
+ if (locals.size === 0) {
28
+ return [];
29
+ }
30
+ return source.statements.flatMap((statement) => {
31
+ if (!ts.isVariableStatement(statement)) {
32
+ return [];
33
+ }
34
+ const exported = statement.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword) === true;
35
+ return statement.declarationList.declarations.flatMap((variable) => {
36
+ const call = variable.initializer;
37
+ return call !== undefined && ts.isCallExpression(call) && ts.isIdentifier(call.expression) && locals.has(call.expression.text)
38
+ ? [{ definition: call.arguments[0], exported, variable }]
39
+ : [];
40
+ });
41
+ });
42
+ }
43
+
44
+ export function propertyOf(literal: ts.ObjectLiteralExpression, name: string): ts.ObjectLiteralElementLike | undefined {
45
+ return literal.properties.find(
46
+ (property) => property.name !== undefined && (ts.isIdentifier(property.name) || ts.isStringLiteral(property.name)) && property.name.text === name,
47
+ );
48
+ }
49
+
50
+ export function initializerOf(property: ts.ObjectLiteralElementLike | undefined): ts.Expression | undefined {
51
+ if (property === undefined) {
52
+ return undefined;
53
+ }
54
+ if (ts.isPropertyAssignment(property)) {
55
+ return property.initializer;
56
+ }
57
+ return ts.isShorthandPropertyAssignment(property) ? property.name : undefined;
58
+ }
@@ -0,0 +1,66 @@
1
+ import ts from "typescript";
2
+ import { copiedCode } from "#adapters/copiedCode.ts";
3
+ import { initializerOf, moduleCalls, propertyOf } from "#adapters/moduleCalls.ts";
4
+ import { operationFacts } from "#adapters/operationFacts.ts";
5
+ import type { TypeReading } from "#adapters/typeReading.ts";
6
+ import type { ModuleFacts } from "#facts.ts";
7
+
8
+ function readingFor(program: ts.Program, refuse: (message: string) => void) {
9
+ const checker = program.getTypeChecker();
10
+ return (label: string): TypeReading => ({
11
+ checker,
12
+ program,
13
+ refuse: (type, path, instead) => {
14
+ refuse(`${label}: run returns ${checker.typeToString(type)} at ${path}, which has no exact wire form; ${instead}`);
15
+ return undefined;
16
+ },
17
+ });
18
+ }
19
+
20
+ export function moduleFacts(program: ts.Program, file: string): ModuleFacts {
21
+ // Creating the checker binds the files, which sets the parent links that reading a node's text needs.
22
+ const checker = program.getTypeChecker();
23
+ const refusals = new Set<string>();
24
+ function refuse(message: string) {
25
+ refusals.add(message);
26
+ }
27
+ const empty = { exportName: "", file, keyScope: undefined, middleware: undefined, name: "", operations: [], subject: undefined };
28
+ const source = program.getSourceFile(file);
29
+ const calls = source === undefined ? [] : moduleCalls(source);
30
+ const [call, ...others] = calls;
31
+ if (call === undefined || others.length > 0) {
32
+ return { ...empty, refusals: [`${file}: defines ${calls.length} modules; define exactly one module per file`] };
33
+ }
34
+ const definition = call.definition;
35
+ const nameValue = definition !== undefined && ts.isObjectLiteralExpression(definition) ? initializerOf(propertyOf(definition, "name")) : undefined;
36
+ if (definition === undefined || !ts.isObjectLiteralExpression(definition) || nameValue === undefined || !ts.isStringLiteral(nameValue)) {
37
+ return { ...empty, refusals: [`${file}: defineModule takes an object literal whose name is a string literal`] };
38
+ }
39
+ const name = nameValue.text;
40
+ const exportName = call.variable.name.getText();
41
+ if (!(call.exported && ts.isIdentifier(call.variable.name))) {
42
+ refuse(`${name}: export the module as a const, such as export const ${exportName} = defineModule(...)`);
43
+ }
44
+ const literal = definition;
45
+ function copy(field: string) {
46
+ const node = initializerOf(propertyOf(literal, field));
47
+ return node === undefined ? undefined : copiedCode({ checker, label: `${name} ${field}`, refuse }, node);
48
+ }
49
+ const list = initializerOf(propertyOf(definition, "operations"));
50
+ if (list === undefined || !ts.isArrayLiteralExpression(list)) {
51
+ refuse(`${name}: operations is an array literal of the module's operations`);
52
+ }
53
+ const reading = readingFor(program, refuse);
54
+ const elements = list !== undefined && ts.isArrayLiteralExpression(list) ? list.elements : [];
55
+ const operations = elements.flatMap((element) => operationFacts({ moduleName: name, reading, refuse }, element) ?? []);
56
+ return {
57
+ exportName,
58
+ file,
59
+ keyScope: copy("keyScope"),
60
+ middleware: copy("middleware"),
61
+ name,
62
+ operations,
63
+ refusals: [...refusals],
64
+ subject: copy("subject"),
65
+ };
66
+ }
@@ -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
+ }