ignotum 0.0.16 → 0.0.18

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 (126) hide show
  1. package/README.md +6 -6
  2. package/dist/cli/bin.mjs +13263 -2689
  3. package/dist/cli/bin.mjs.map +1 -1
  4. package/dist/platform/_headers +3 -0
  5. package/dist/platform/_ignotum/v1.0/client.js +12 -0
  6. package/dist/runtime/{api-B4fwp5a2.d.ts → api-BnUKx9cj-D49Tf0rX.d.ts} +4 -3
  7. package/dist/runtime/api-C-tltz6s.js +31 -0
  8. package/dist/runtime/api-C-tltz6s.js.map +1 -0
  9. package/dist/runtime/artifact-BB5qzoa6.js +42 -0
  10. package/dist/runtime/artifact-BB5qzoa6.js.map +1 -0
  11. package/dist/runtime/{bootstrap-BTFScKAc.d.ts → bootstrap-DjTwGJao-DBx5zdFI.d.ts} +64 -64
  12. package/dist/runtime/{bootstrap-BISQSn_O.js → bootstrap-DlNIfH-r.js} +9 -9
  13. package/dist/runtime/bootstrap-DlNIfH-r.js.map +1 -0
  14. package/dist/runtime/client.d.ts +24 -20
  15. package/dist/runtime/client.js +9 -9
  16. package/dist/runtime/client.js.map +1 -1
  17. package/dist/runtime/id-BI08JkmR.d.ts +1 -0
  18. package/dist/runtime/{id--1m0NuUL.js → id-BrEnxY-r.js} +118 -53
  19. package/dist/runtime/id-BrEnxY-r.js.map +1 -0
  20. package/dist/runtime/{image-Djtjy4Z8.js → image-gofzZeCm.js} +2 -2
  21. package/dist/runtime/image-gofzZeCm.js.map +1 -0
  22. package/dist/runtime/{index-D4seNHlX.d.ts → index-Biaw5vdF.d.ts} +2 -2
  23. package/dist/runtime/internal/api.d.ts +1 -1
  24. package/dist/runtime/internal/api.js +1 -30
  25. package/dist/runtime/internal/bootstrap.d.ts +1 -1
  26. package/dist/runtime/internal/bootstrap.js +1 -1
  27. package/dist/runtime/internal/client.d.ts +2 -2
  28. package/dist/runtime/internal/client.js +4 -4
  29. package/dist/runtime/internal/client.js.map +1 -1
  30. package/dist/runtime/internal/host.d.ts +4 -4
  31. package/dist/runtime/internal/host.js +4 -4
  32. package/dist/runtime/internal/routes.d.ts +1 -1
  33. package/dist/runtime/internal/routes.js +1 -1
  34. package/dist/runtime/internal/server.d.ts +3 -2
  35. package/dist/runtime/internal/server.js +2 -2
  36. package/dist/runtime/internal/server.js.map +1 -1
  37. package/dist/runtime/internal/types.d.ts +2 -2
  38. package/dist/runtime/internal/types.js +1 -1
  39. package/dist/runtime/{pagination-D3qd6s9N.js → pagination--f4kMKf6.js} +2 -2
  40. package/dist/runtime/{pagination-D3qd6s9N.js.map → pagination--f4kMKf6.js.map} +1 -1
  41. package/dist/runtime/pagination-BI08JkmR.d.ts +1 -0
  42. package/dist/runtime/{router-BSBI1oN1.js → router-C1eC9yy5.js} +462 -419
  43. package/dist/runtime/router-C1eC9yy5.js.map +1 -0
  44. package/dist/runtime/routes-DnU39R7c.js +152 -0
  45. package/dist/runtime/routes-DnU39R7c.js.map +1 -0
  46. package/dist/runtime/{schema-D1wOqiNH.js → schema-BSjepoNF.js} +271 -6
  47. package/dist/runtime/schema-BSjepoNF.js.map +1 -0
  48. package/dist/runtime/server.d.ts +5 -4
  49. package/dist/runtime/server.js +3 -3
  50. package/dist/runtime/server.js.map +1 -1
  51. package/dist/runtime/{sync-DzUwA8W9.js → sync-BwgS19Xy.js} +4 -16
  52. package/dist/runtime/sync-BwgS19Xy.js.map +1 -0
  53. package/dist/runtime/{sync-D-GK2sv9.d.ts → sync-By2hX5ev.d.ts} +1 -2
  54. package/dist/runtime/{types-DeCCyqLU-BIm7rU-0.d.ts → types-DeCCyqLU-CHx7KFtx.d.ts} +2 -2
  55. package/package.json +7 -1
  56. package/src/cli/agent-files.ts +2 -0
  57. package/src/cli/build/asset-names.ts +190 -0
  58. package/src/cli/build/managed-client.ts +151 -81
  59. package/src/cli/build/managed-server.ts +4 -4
  60. package/src/cli/build/managed.ts +5 -6
  61. package/src/cli/command.ts +7 -15
  62. package/src/cli/control-client.ts +7 -9
  63. package/src/cli/deploy.ts +45 -15
  64. package/src/cli/dev.ts +50 -99
  65. package/src/cli/image-assets.ts +5 -2
  66. package/src/cli/managed-client-boundaries.ts +6 -6
  67. package/src/cli/managed-dev-platform.ts +52 -66
  68. package/src/cli/managed-exports.ts +5 -66
  69. package/src/cli/new-app.ts +7 -2
  70. package/src/cli/path.ts +11 -0
  71. package/src/cli/route-codegen.ts +2 -1
  72. package/src/cli/route-static.ts +3 -2
  73. package/src/cli/upload-journal.ts +130 -0
  74. package/src/client/bootstrap.ts +1 -110
  75. package/src/client/errors.ts +1 -70
  76. package/src/client/files.ts +1 -233
  77. package/src/client/hooks.ts +1 -367
  78. package/src/client/id.ts +1 -263
  79. package/src/client/image.ts +1 -28
  80. package/src/client/index.ts +1 -126
  81. package/src/client/jsx-dev-runtime.ts +1 -2
  82. package/src/client/jsx-runtime.ts +1 -2
  83. package/src/client/managed-client.ts +1 -729
  84. package/src/client/managed-upgrade.ts +1 -283
  85. package/src/client/page-observers.ts +1 -77
  86. package/src/client/query.ts +1 -17
  87. package/src/client/recovery-journal.ts +1 -195
  88. package/src/client/route-boundaries.ts +1 -184
  89. package/src/client/router-history.ts +1 -152
  90. package/src/client/router-store.ts +1 -644
  91. package/src/client/router.ts +1 -324
  92. package/src/client/routes.ts +1 -28
  93. package/src/client/sync.ts +1 -286
  94. package/src/dev-runtime/id.ts +1 -1
  95. package/src/dev-runtime/managed-functions.ts +10 -18
  96. package/src/dev-runtime/managed-sync.ts +16 -16
  97. package/src/dev-runtime/sync.ts +6 -4
  98. package/src/internal/api.ts +1 -156
  99. package/src/internal/client.ts +1 -186
  100. package/src/internal/host.ts +1 -1
  101. package/src/internal/http-paths.ts +1 -4
  102. package/src/internal/routes.ts +1 -45
  103. package/src/internal/server.ts +1 -3
  104. package/src/internal/types.ts +1 -2
  105. package/src/server/index.ts +1 -27
  106. package/dist/cli/managed-release-WHYLZwaS.mjs +0 -10902
  107. package/dist/cli/managed-release-WHYLZwaS.mjs.map +0 -1
  108. package/dist/cli/managed-release.d.mts +0 -1
  109. package/dist/cli/managed-release.mjs +0 -14
  110. package/dist/cli/managed-release.mjs.map +0 -1
  111. package/dist/runtime/bootstrap-BISQSn_O.js.map +0 -1
  112. package/dist/runtime/descriptor-CTY4mtwS-cS43EbwS.js +0 -269
  113. package/dist/runtime/descriptor-CTY4mtwS-cS43EbwS.js.map +0 -1
  114. package/dist/runtime/id--1m0NuUL.js.map +0 -1
  115. package/dist/runtime/id-CBOt2kDo.d.ts +0 -1
  116. package/dist/runtime/identity-QjtJRxBD.d.ts +0 -2
  117. package/dist/runtime/image-Djtjy4Z8.js.map +0 -1
  118. package/dist/runtime/internal/api.js.map +0 -1
  119. package/dist/runtime/pagination-CBOt2kDo.d.ts +0 -1
  120. package/dist/runtime/router-BSBI1oN1.js.map +0 -1
  121. package/dist/runtime/routes-Ce8TVP-T.js +0 -690
  122. package/dist/runtime/routes-Ce8TVP-T.js.map +0 -1
  123. package/dist/runtime/schema-D1wOqiNH.js.map +0 -1
  124. package/dist/runtime/sync-DzUwA8W9.js.map +0 -1
  125. package/src/cli/managed-release-bin.ts +0 -13
  126. package/src/cli/managed-release.ts +0 -260
@@ -0,0 +1,190 @@
1
+ import MagicString from "magic-string";
2
+ import remapping from "@jridgewell/remapping";
3
+ import { createHash } from "node:crypto";
4
+ import { posix } from "node:path";
5
+ import { Graph, Schema } from "effect";
6
+ import { parseSync } from "oxc-parser";
7
+
8
+ export interface ClientOutputChunk {
9
+ readonly fileName: string;
10
+ readonly code: string;
11
+ readonly sourceMap?: string;
12
+ readonly imports: readonly string[];
13
+ readonly dynamicImports: readonly string[];
14
+ readonly modules: Readonly<Record<string, { readonly renderedLength: number }>>;
15
+ }
16
+
17
+ export class AssetNamingError extends Schema.TaggedError<AssetNamingError>()("AssetNamingError", {
18
+ message: Schema.String,
19
+ }) {}
20
+
21
+ /** The bundler must merge these source groups before assigning final-byte hashes. */
22
+ export const cyclicChunkGroups = (
23
+ chunks: readonly ClientOutputChunk[],
24
+ ): readonly (readonly string[])[] => {
25
+ const ids = new Map<string, Graph.NodeIndex>();
26
+ const byIndex = new Map<Graph.NodeIndex, ClientOutputChunk>();
27
+ const graph = Graph.directed<string, void>((mutable) => {
28
+ for (const chunk of chunks) {
29
+ const index = Graph.addNode(mutable, chunk.fileName);
30
+ ids.set(chunk.fileName, index);
31
+ byIndex.set(index, chunk);
32
+ }
33
+ for (const chunk of chunks) {
34
+ for (const dependency of [...chunk.imports, ...chunk.dynamicImports]) {
35
+ const target = ids.get(dependency);
36
+ if (target !== undefined)
37
+ Graph.addEdge(mutable, ids.get(chunk.fileName)!, target, undefined);
38
+ }
39
+ }
40
+ });
41
+ return Graph.stronglyConnectedComponents(graph)
42
+ .filter((component) => component.length > 1)
43
+ .map((component) =>
44
+ [...new Set(component.flatMap((index) => Object.keys(byIndex.get(index)!.modules)))].sort(),
45
+ );
46
+ };
47
+
48
+ export interface NamedClientChunk {
49
+ readonly fileName: string;
50
+ readonly code: string;
51
+ readonly sha256: string;
52
+ readonly sourceMap?: string;
53
+ }
54
+
55
+ /** Call only after CSS/media URLs and chunk grouping have been finalized. */
56
+ export const nameClientChunks = (
57
+ chunks: readonly ClientOutputChunk[],
58
+ entry: string,
59
+ ): ReadonlyMap<string, NamedClientChunk> => {
60
+ if (cyclicChunkGroups(chunks).length !== 0) {
61
+ throw AssetNamingError.make({
62
+ message: "Merge cyclic emitted chunks before hashing their bytes.",
63
+ });
64
+ }
65
+ const sources = new Map(chunks.map((chunk) => [chunk.fileName, chunk]));
66
+ const named = new Map<string, NamedClientChunk>();
67
+ const visiting = new Set<string>();
68
+ const name = (fileName: string): NamedClientChunk => {
69
+ const found = named.get(fileName);
70
+ if (found !== undefined) return found;
71
+ const chunk = sources.get(fileName);
72
+ if (chunk === undefined || visiting.has(fileName)) {
73
+ throw AssetNamingError.make({ message: `Unresolved or cyclic app chunk '${fileName}'.` });
74
+ }
75
+ visiting.add(fileName);
76
+ const parsed = parseSync(fileName, chunk.code);
77
+ if (parsed.errors.length > 0)
78
+ throw AssetNamingError.make({ message: `Invalid app chunk '${fileName}'.` });
79
+ const requests = [
80
+ ...parsed.module.staticImports.map((item) => item.moduleRequest),
81
+ ...parsed.module.staticExports.flatMap((item) =>
82
+ item.entries.flatMap((item) => (item.moduleRequest === null ? [] : [item.moduleRequest])),
83
+ ),
84
+ ...parsed.module.dynamicImports.map((item) => {
85
+ const expression = parseSync(
86
+ "import.js",
87
+ chunk.code.slice(item.moduleRequest.start, item.moduleRequest.end),
88
+ ).program.body[0];
89
+ const value =
90
+ expression?.type === "ExpressionStatement" ? expression.expression : undefined;
91
+ const address =
92
+ value?.type === "Literal"
93
+ ? value.value
94
+ : value?.type === "TemplateLiteral" &&
95
+ value.expressions.length === 0 &&
96
+ value.quasis.length === 1
97
+ ? value.quasis[0]!.value.cooked
98
+ : undefined;
99
+ if (address === undefined)
100
+ throw AssetNamingError.make({
101
+ message: `App dynamic import in '${fileName}' must have a literal module address.`,
102
+ });
103
+ return { ...item.moduleRequest, value: Schema.decodeUnknownSync(Schema.String)(address) };
104
+ }),
105
+ ];
106
+ const edited = new MagicString(chunk.code);
107
+ const replacements = new Map<number, { readonly end: number; readonly text: string }>();
108
+ for (const request of requests) {
109
+ if (!request.value.startsWith(".")) continue;
110
+ const dependency = posix.normalize(posix.join(posix.dirname(fileName), request.value));
111
+ const target = name(dependency);
112
+ if (dependency === entry)
113
+ throw AssetNamingError.make({
114
+ message: "An app dependency cannot import the publication entry.",
115
+ });
116
+ const relative = posix.relative(posix.dirname(fileName), target.fileName);
117
+ replacements.set(request.start, {
118
+ end: request.end,
119
+ text: JSON.stringify(relative.startsWith(".") ? relative : `./${relative}`),
120
+ });
121
+ }
122
+ for (const [start, replacement] of [...replacements].sort(([a], [b]) => b - a)) {
123
+ edited.overwrite(start, replacement.end, replacement.text);
124
+ }
125
+ const code = edited.toString();
126
+ const sha256 = createHash("sha256").update(code).digest("hex");
127
+ const outputName =
128
+ fileName === entry ? "entry.js" : fileName.replace(/\.(?:m?js)$/, `-${sha256}.js`);
129
+ if (fileName !== entry && outputName === fileName)
130
+ throw AssetNamingError.make({
131
+ message: `App chunk '${fileName}' must have a JavaScript extension.`,
132
+ });
133
+ const sourceMap =
134
+ chunk.sourceMap === undefined
135
+ ? undefined
136
+ : remapping(
137
+ [edited.generateMap({ hires: true, source: fileName }).toString(), chunk.sourceMap],
138
+ () => null,
139
+ ).toString();
140
+ const output = { fileName: outputName, code, sha256, sourceMap };
141
+ const collision = [...named.values()].find(
142
+ (other) => other.fileName === outputName && other.code !== code,
143
+ );
144
+ if (collision !== undefined)
145
+ throw AssetNamingError.make({ message: `Conflicting public path '${outputName}'.` });
146
+ named.set(fileName, output);
147
+ visiting.delete(fileName);
148
+ return output;
149
+ };
150
+ for (const chunk of chunks) name(chunk.fileName);
151
+ return named;
152
+ };
153
+
154
+ /** Group source cycles before rendering so final-byte dependency hashing terminates. */
155
+ export const clientCycleGroupsPlugin = (appDirectory: string): import("vite").Plugin => {
156
+ const groups = new Map<string, string>();
157
+ return {
158
+ name: "ignotum:client-cycle-groups",
159
+ buildEnd() {
160
+ groups.clear();
161
+ const sources = [...this.getModuleIds()].flatMap((id) => {
162
+ const info = this.getModuleInfo(id);
163
+ return info === null
164
+ ? []
165
+ : [
166
+ {
167
+ fileName: id,
168
+ code: "",
169
+ imports: info.importedIds,
170
+ dynamicImports: info.dynamicallyImportedIds,
171
+ modules: { [id]: { renderedLength: 1 } },
172
+ },
173
+ ];
174
+ });
175
+ for (const ids of cyclicChunkGroups(sources)) {
176
+ const identity = ids
177
+ .map((id) => posix.relative(appDirectory.replaceAll("\\", "/"), id.replaceAll("\\", "/")))
178
+ .sort();
179
+ const name = `cycle-${createHash("sha256").update(JSON.stringify(identity)).digest("hex").slice(0, 16)}`;
180
+ for (const id of ids) groups.set(id, name);
181
+ }
182
+ },
183
+ outputOptions(options) {
184
+ return {
185
+ ...options,
186
+ codeSplitting: { groups: [{ name: (id: string) => groups.get(id) ?? null }] },
187
+ };
188
+ },
189
+ };
190
+ };
@@ -1,6 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { fileURLToPath } from "node:url";
3
- import { Effect, FileSystem, Option, Path, Predicate, Schema } from "effect";
3
+ import { Effect, FileSystem, Option, Path, Schema } from "effect";
4
4
  import tailwindcss from "@tailwindcss/vite";
5
5
  import { defaultClientConditions, normalizePath, type Plugin } from "vite";
6
6
  import {
@@ -16,7 +16,9 @@ import { InvalidClientFile } from "../client-config.js";
16
16
  import { clientStylesSource } from "../client-styles.js";
17
17
  import { createImageAssetsPlugin } from "../image-assets.js";
18
18
  import { managedClientBoundariesPlugin } from "../managed-client-boundaries.js";
19
- import { managedPreactPeerPlugin } from "../managed-exports.js";
19
+ import { platformImportsPlugin } from "@ignotum/platform/compiler";
20
+ import { compilerExports } from "@ignotum/platform/compiler-exports";
21
+ import { clientCycleGroupsPlugin, nameClientChunks } from "./asset-names.js";
20
22
  import { moduleBoundariesPlugin } from "../module-boundaries.js";
21
23
  import { generateClientRoutes } from "../route-codegen.js";
22
24
  import { InvalidBuildOutput, runViteBuild, sourceMapPathTransform } from "./artifact.js";
@@ -38,14 +40,7 @@ interface ClientChunk {
38
40
 
39
41
  const literal = Schema.encodeSync(Schema.fromJsonString(Schema.String));
40
42
  const globalStylesId = "virtual:ignotum/managed-global.css";
41
-
42
- const stylesheetFileName = (source: string | Uint8Array): string => {
43
- // Vite removes its internal hash-update marker before writing CSS assets.
44
- const finalSource = Predicate.isString(source)
45
- ? source.replace(/\/\*\$vite\$:\d+\*\//, "")
46
- : source;
47
- return `assets/${createHash("sha256").update(finalSource).digest("hex")}/style.css`;
48
- };
43
+ const globalEntryId = "virtual:ignotum/managed-global.js";
49
44
 
50
45
  /** Compile route definitions and app dependencies without embedding browser runtime code. */
51
46
  export const buildManagedClient = Effect.fn("Deploy.buildManagedClient")(function* (
@@ -74,16 +69,34 @@ export const buildManagedClient = Effect.fn("Deploy.buildManagedClient")(functio
74
69
  }
75
70
  const discovery = yield* generateClientRoutes(appDirectory);
76
71
  const stage = yield* fs.makeTempDirectoryScoped({ prefix: "ignotum-managed-client-" });
77
- const images = yield* createImageAssetsPlugin(appDirectory);
72
+ const images = yield* createImageAssetsPlugin(appDirectory, true);
78
73
  const boundaries = yield* moduleBoundariesPlugin("Client", appDirectory);
79
- const platform = yield* managedClientBoundariesPlugin(appDirectory);
80
- const peers = yield* managedPreactPeerPlugin({ resolvePeer: (specifier) => specifier });
74
+ const platform = yield* managedClientBoundariesPlugin(appDirectory, true);
81
75
  const entrySources = new Map<string, string>();
82
76
  const input: Record<string, string> = {};
83
- input.global = globalStylesId;
77
+ input.global = globalEntryId;
84
78
  const sourceEntries = new Map<AppModuleId, string>();
85
- for (const [index, module] of discovery.modules.entries()) {
86
- const name = `${module.kind}-${index}`;
79
+ const segmentName = (segment: string): string => {
80
+ const escape = (value: string) =>
81
+ value.replace(/[^a-zA-Z0-9.-]/g, (value) => `_${value.codePointAt(0)!.toString(16)}_`);
82
+ const parameter = /^\[([^\]]+)\]$/.exec(segment);
83
+ return parameter ? `_param_${escape(parameter[1]!)}` : escape(segment);
84
+ };
85
+ for (const module of discovery.modules) {
86
+ const relative = normalizePath(
87
+ path.relative(path.join(appDirectory, "client"), module.path),
88
+ ).replace(/\.[^.]+$/, "");
89
+ const ownership =
90
+ module.kind === "layout"
91
+ ? path.dirname(relative) === "."
92
+ ? "root"
93
+ : normalizePath(path.dirname(relative))
94
+ : relative;
95
+ const name = `${module.kind === "route" ? "routes" : "layouts"}/${ownership.split("/").map(segmentName).join("/")}`;
96
+ if (Object.hasOwn(input, name))
97
+ return yield* InvalidBuildOutput.make({
98
+ message: `Conflicting client module ownership '${name}'.`,
99
+ });
87
100
  const id = `virtual:ignotum/managed-client/${name}`;
88
101
  input[name] = id;
89
102
  sourceEntries.set(module.id, `${name}.mjs`);
@@ -115,9 +128,11 @@ export const buildManagedClient = Effect.fn("Deploy.buildManagedClient")(functio
115
128
  name: "ignotum:managed-client-entries",
116
129
  enforce: "post",
117
130
  resolveId(id) {
118
- if (id === globalStylesId || entrySources.has(`\0${id}`)) return `\0${id}`;
131
+ if (id === globalStylesId || id === globalEntryId || entrySources.has(`\0${id}`))
132
+ return `\0${id}`;
119
133
  },
120
134
  load(id) {
135
+ if (id === `\0${globalEntryId}`) return `import ${literal(globalStylesId)};`;
121
136
  if (id === `\0${globalStylesId}`) return clientStylesSource;
122
137
  return entrySources.get(id);
123
138
  },
@@ -139,7 +154,13 @@ export const buildManagedClient = Effect.fn("Deploy.buildManagedClient")(functio
139
154
  isEntry: item.isEntry,
140
155
  });
141
156
  if (item.map === null) {
142
- if (Object.values(item.modules).some((module) => module.renderedLength > 0))
157
+ const applicationRoot = `${normalizePath(appDirectory)}/`;
158
+ if (
159
+ Object.entries(item.modules).some(
160
+ ([id, module]) =>
161
+ normalizePath(id).startsWith(applicationRoot) && module.renderedLength > 0,
162
+ )
163
+ )
143
164
  this.error(`Missing source map for app implementation '${item.fileName}'.`);
144
165
  continue;
145
166
  }
@@ -163,7 +184,7 @@ export const buildManagedClient = Effect.fn("Deploy.buildManagedClient")(functio
163
184
  "import.meta.env.DEV": "false",
164
185
  "import.meta.env.PROD": "true",
165
186
  },
166
- base: "/_ignotum/",
187
+ base: "/_ignotum/assets/",
167
188
  oxc: { jsx: { importSource: "ignotum/client", runtime: "automatic", development: false } },
168
189
  resolve: {
169
190
  alias: [
@@ -177,11 +198,21 @@ export const buildManagedClient = Effect.fn("Deploy.buildManagedClient")(functio
177
198
  ),
178
199
  tsconfigPaths: true,
179
200
  },
180
- plugins: [privateMaps, images.plugin, boundaries, platform, peers, collect, tailwindcss()],
201
+ plugins: [
202
+ privateMaps,
203
+ images.plugin,
204
+ boundaries,
205
+ platform,
206
+ platformImportsPlugin(compilerExports),
207
+ clientCycleGroupsPlugin(appDirectory),
208
+ collect,
209
+ tailwindcss(),
210
+ ],
181
211
  build: {
182
212
  emptyOutDir: false,
183
213
  outDir: stage,
184
214
  modulePreload: false,
215
+ cssCodeSplit: false,
185
216
  sourcemap: "hidden",
186
217
  target: "es2022",
187
218
  rolldownOptions: {
@@ -189,18 +220,31 @@ export const buildManagedClient = Effect.fn("Deploy.buildManagedClient")(functio
189
220
  preserveEntrySignatures: "strict",
190
221
  output: {
191
222
  entryFileNames: "[name].mjs",
192
- chunkFileNames: "shared-[hash].mjs",
223
+ chunkFileNames: (chunk) => {
224
+ const owners = chunk.moduleIds
225
+ .map((id) => {
226
+ const normalized = normalizePath(id);
227
+ const dependency = normalized.lastIndexOf("/node_modules/");
228
+ return dependency < 0
229
+ ? normalized
230
+ .replace(normalizePath(appDirectory), "app")
231
+ .replace(encodeURIComponent(normalizePath(appDirectory)), "app")
232
+ : normalized.slice(dependency + 14);
233
+ })
234
+ .sort();
235
+ return `shared/chunk-${createHash("sha256").update(JSON.stringify(owners)).digest("hex").slice(0, 16)}.mjs`;
236
+ },
193
237
  assetFileNames: (asset) =>
194
238
  asset.names.some((name) => name.endsWith(".css"))
195
- ? stylesheetFileName(asset.source)
196
- : "assets/[name]-[hash][extname]",
239
+ ? "styles.css"
240
+ : "media/[name]-[hash][extname]",
197
241
  sourcemapPathTransform: yield* sourceMapPathTransform(appDirectory),
198
242
  },
199
243
  },
200
244
  },
201
245
  });
202
246
  const chunks = output.flatMap((item) =>
203
- item.type === "chunk"
247
+ item.type === "chunk" && item.fileName !== "global.mjs"
204
248
  ? [
205
249
  {
206
250
  fileName: item.fileName,
@@ -214,6 +258,7 @@ export const buildManagedClient = Effect.fn("Deploy.buildManagedClient")(functio
214
258
  );
215
259
  const files = new Map<string, Uint8Array>();
216
260
  for (const item of output) {
261
+ if (item.fileName === "global.mjs") continue;
217
262
  if (
218
263
  item.type === "asset" &&
219
264
  !item.fileName.endsWith(".css") &&
@@ -224,53 +269,81 @@ export const buildManagedClient = Effect.fn("Deploy.buildManagedClient")(functio
224
269
  });
225
270
  files.set(item.fileName, yield* fs.readFile(path.join(stage, item.fileName)));
226
271
  }
272
+ for (const [name, contents] of [...files]) {
273
+ if (!name.endsWith(".css")) continue;
274
+ files.delete(name);
275
+ files.set(`styles-${yield* sha256(contents)}.css`, contents);
276
+ }
227
277
  for (const [name, bytes] of maps) files.set(name, bytes);
228
- const knownChunks = new Map(chunks.map((chunk) => [chunk.fileName, chunk]));
229
- const stylesFor = (fileName: string, visited = new Set<string>()): ReadonlyArray<string> => {
230
- if (visited.has(fileName)) return [];
231
- visited.add(fileName);
232
- const chunk = knownChunks.get(fileName);
233
- if (chunk === undefined) return [];
234
- return [
235
- ...new Set([
236
- ...chunk.styles,
237
- ...chunk.imports.flatMap((dependency) => stylesFor(dependency, visited)),
238
- ]),
239
- ].sort();
278
+ const styles = [...files.keys()].filter((name) => name.endsWith(".css")).sort();
279
+ const registration = `const registration = {
280
+ applicationName: ${literal(appName)},
281
+ routes: ${JSON.stringify(discovery.routes)},
282
+ layouts: ${JSON.stringify(discovery.layouts)},
283
+ styles: ${JSON.stringify(styles.map((name) => `/_ignotum/assets/${name}`))},
284
+ modules: new Map([
285
+ ${discovery.modules.map((module) => ` [${literal(module.id)}, { load: () => import(${literal(`./${sourceEntries.get(module.id)!}`)}), styles: [] }]`).join(",\n")}
286
+ ])
240
287
  };
241
- const globalCss = new Set(chunkMetadata.get("global.mjs")?.styles ?? []);
242
- const routeStyles = new Set(
243
- chunks.filter((chunk) => chunk.fileName !== "global.mjs").flatMap((chunk) => chunk.styles),
244
- );
245
- const styles = [...files.keys()]
246
- .filter((name) => name.endsWith(".css") && (globalCss.has(name) || !routeStyles.has(name)))
247
- .sort();
248
- const registration = `import { mountClient } from "ignotum/internal/bootstrap";
249
- const registration = {
250
- applicationName: ${literal(appName)},
251
- routes: ${JSON.stringify(discovery.routes)},
252
- layouts: ${JSON.stringify(discovery.layouts)},
253
- styles: ${JSON.stringify(styles.map((name) => `/_ignotum/${name}`))},
254
- modules: new Map([
255
- ${discovery.modules
256
- .map((module) => {
257
- const name = sourceEntries.get(module.id)!;
258
- return ` [${literal(module.id)}, { load: () => import(${literal(`./${name}`)}), styles: ${JSON.stringify(stylesFor(name).map((style) => `/_ignotum/${style}`))} }]`;
259
- })
260
- .join(",\n")}
261
- ])
262
- };
263
- export default function mount(selection) { return mountClient(registration, selection); }
288
+ export default registration;
264
289
  `;
265
- files.set("registration.mjs", utf8Bytes(registration));
266
290
  chunks.push({
267
- fileName: "registration.mjs",
268
- imports: ["ignotum/internal/bootstrap"],
291
+ fileName: "entry.mjs",
292
+ imports: [],
269
293
  dynamicImports: [...sourceEntries.values()],
270
294
  styles,
271
295
  isEntry: true,
272
296
  });
273
- chunks.sort((a, b) => a.fileName.localeCompare(b.fileName));
297
+ const names = yield* Effect.try({
298
+ try: () =>
299
+ nameClientChunks(
300
+ [
301
+ ...output.flatMap((item) =>
302
+ item.type === "chunk" && item.fileName !== "global.mjs"
303
+ ? [
304
+ {
305
+ fileName: item.fileName,
306
+ code: new TextDecoder().decode(files.get(item.fileName)!),
307
+ imports: item.imports,
308
+ dynamicImports: item.dynamicImports,
309
+ modules: {},
310
+ sourceMap: maps.has(`${item.fileName}.map`)
311
+ ? new TextDecoder().decode(maps.get(`${item.fileName}.map`))
312
+ : undefined,
313
+ },
314
+ ]
315
+ : [],
316
+ ),
317
+ {
318
+ fileName: "entry.mjs",
319
+ code: registration,
320
+ imports: [],
321
+ dynamicImports: [...sourceEntries.values()],
322
+ modules: {},
323
+ },
324
+ ],
325
+ "entry.mjs",
326
+ ),
327
+ catch: (cause) =>
328
+ InvalidBuildOutput.make({ message: `Client dependency naming failed: ${String(cause)}` }),
329
+ });
330
+ for (const [original, named] of names) {
331
+ files.delete(original);
332
+ files.delete(`${original}.map`);
333
+ maps.delete(`${original}.map`);
334
+ files.set(named.fileName, utf8Bytes(named.code));
335
+ if (named.sourceMap !== undefined)
336
+ maps.set(`${named.fileName}.map`, utf8Bytes(named.sourceMap));
337
+ }
338
+ for (const [id, original] of sourceEntries) sourceEntries.set(id, names.get(original)!.fileName);
339
+ const finalChunks = chunks.map((chunk) => ({
340
+ ...chunk,
341
+ fileName: names.get(chunk.fileName)!.fileName,
342
+ imports: chunk.imports.map((name) => names.get(name)?.fileName ?? name),
343
+ dynamicImports: chunk.dynamicImports.map((name) => names.get(name)?.fileName ?? name),
344
+ styles: [],
345
+ }));
346
+ const knownChunks = new Map(finalChunks.map((chunk) => [chunk.fileName, chunk]));
274
347
  let icon: AppArtifactPath | undefined;
275
348
  const iconPath = path.join(appDirectory, "client/icon.svg");
276
349
  if (yield* fs.exists(iconPath)) {
@@ -280,23 +353,16 @@ export default function mount(selection) { return mountClient(registration, sele
280
353
  message: "The client icon must be a regular file.",
281
354
  });
282
355
  const bytes = yield* fs.readFile(iconPath);
283
- icon = AppArtifactPath.make(`assets/${yield* sha256(bytes)}/icon.svg`);
356
+ icon = AppArtifactPath.make(`media/icon-${yield* sha256(bytes)}.svg`);
284
357
  files.set(icon, bytes);
285
358
  }
286
- // App module identities include the completed inventory digest. Its source retains relative
287
- // imports and content-named asset URLs, so cycles and registration never hash their own identity.
288
- const inventory = yield* Effect.forEach(
289
- [...files].sort(([a], [b]) => a.localeCompare(b)),
290
- ([name, bytes]) => sha256(bytes).pipe(Effect.map((digest) => ({ name, digest }))),
291
- );
292
- const graphDigest = yield* sha256(utf8Bytes(JSON.stringify(inventory)));
293
- const moduleId = (name: string) => AppModuleId.make(`app/client/${graphDigest}/${name}`);
294
- const modulePath = (name: string) => AppArtifactPath.make(`client/${graphDigest}/${name}`);
359
+ const moduleId = (name: string) => AppModuleId.make(`app/client/${name}`);
360
+ const modulePath = (name: string) => AppArtifactPath.make(name);
295
361
  const artifacts: AppArtifact[] = [];
296
362
  const bytes = new Map<string, Uint8Array>();
297
363
  const modules: AppModule[] = [];
298
364
  const imagePaths = [...images.assets.keys()];
299
- for (const chunk of chunks) {
365
+ for (const chunk of finalChunks) {
300
366
  const source = files.get(chunk.fileName)!;
301
367
  const id = moduleId(chunk.fileName);
302
368
  const artifact = {
@@ -317,7 +383,7 @@ export default function mount(selection) { return mountClient(registration, sele
317
383
  } else {
318
384
  const kind = imported.startsWith("ignotum/internal/")
319
385
  ? "Compiler"
320
- : imported.startsWith("ignotum/client")
386
+ : imported.startsWith("ignotum/client") || imported === "ignotum/platform"
321
387
  ? "Runtime"
322
388
  : "RenderingPeer";
323
389
  imports.push(
@@ -327,7 +393,7 @@ export default function mount(selection) { return mountClient(registration, sele
327
393
  }
328
394
  const map = maps.get(`${chunk.fileName}.map`);
329
395
  const mapPath = modulePath(`${chunk.fileName}.map`);
330
- const moduleStyles = chunk.fileName === "registration.mjs" ? styles : stylesFor(chunk.fileName);
396
+ const moduleStyles = chunk.fileName === "entry.js" ? styles : [];
331
397
  const assetSources = [
332
398
  new TextDecoder().decode(source),
333
399
  ...moduleStyles.map((style) => new TextDecoder().decode(files.get(style))),
@@ -338,7 +404,9 @@ export default function mount(selection) { return mountClient(registration, sele
338
404
  imports,
339
405
  styles: moduleStyles.map((name) => AppArtifactPath.make(name)),
340
406
  assets: imagePaths
341
- .filter((name) => assetSources.some((contents) => contents.includes(`/_ignotum/${name}`)))
407
+ .filter((name) =>
408
+ assetSources.some((contents) => contents.includes(`/_ignotum/assets/${name}`)),
409
+ )
342
410
  .map((name) => AppArtifactPath.make(name)),
343
411
  };
344
412
  const module = map === undefined ? baseModule : { ...baseModule, sourceMap: mapPath };
@@ -358,7 +426,7 @@ export default function mount(selection) { return mountClient(registration, sele
358
426
  }
359
427
  }
360
428
  for (const [name, contents] of [...files].sort(([a], [b]) => a.localeCompare(b))) {
361
- if (knownChunks.has(name) || name === "registration.mjs" || name.endsWith(".map")) continue;
429
+ if (knownChunks.has(name) || name === "entry.js" || name.endsWith(".map")) continue;
362
430
  const kind = name === icon ? "Icon" : name.endsWith(".css") ? "Style" : "Image";
363
431
  const contentType =
364
432
  kind === "Icon"
@@ -369,7 +437,7 @@ export default function mount(selection) { return mountClient(registration, sele
369
437
  if (contentType === undefined)
370
438
  return yield* InvalidBuildOutput.make({ message: `Unknown client asset '${name}'.` });
371
439
  const reference = yield* artifactReference(name, contents);
372
- if (kind === "Style" && name !== `assets/${reference.sha256}/style.css`)
440
+ if (kind === "Style" && name !== `styles-${reference.sha256}.css`)
373
441
  return yield* InvalidBuildOutput.make({
374
442
  message: "The bundler changed stylesheet bytes after their content identity was assigned.",
375
443
  });
@@ -387,9 +455,11 @@ export default function mount(selection) { return mountClient(registration, sele
387
455
  exportName: "default",
388
456
  });
389
457
  const baseClient = {
390
- registration: { module: moduleId("registration.mjs"), exportName: "default" },
458
+ registration: { module: moduleId("entry.js"), exportName: "default" },
391
459
  graph: {
392
- entries: chunks.filter((chunk) => chunk.isEntry).map((chunk) => moduleId(chunk.fileName)),
460
+ entries: finalChunks
461
+ .filter((chunk) => chunk.isEntry)
462
+ .map((chunk) => moduleId(chunk.fileName)),
393
463
  modules,
394
464
  },
395
465
  routes: discovery.routes.map((route) => ({ ...route, page: reference(route.page.module) })),
@@ -19,7 +19,7 @@ import {
19
19
  validateAppModuleBytes,
20
20
  validateManagedServerDefinition,
21
21
  } from "@ignotum/versions";
22
- import { managedGuestExports } from "../managed-exports.js";
22
+ import { guestEntries } from "@ignotum/platform/registry";
23
23
  import { moduleBoundariesPlugin } from "../module-boundaries.js";
24
24
  import { InvalidBuildOutput, runViteBuild, sourceMapPathTransform } from "./artifact.js";
25
25
  import type { ServerDiscoveryManifest } from "./server.js";
@@ -36,9 +36,9 @@ export interface ManagedServerBuildResult {
36
36
  }
37
37
 
38
38
  const literal = Schema.encodeSync(Schema.fromJsonString(Schema.String));
39
- const externals = managedGuestExports
40
- .filter((entry) => entry.specifier !== "ignotum/internal/host")
41
- .map((entry) => entry.specifier);
39
+ const externals = Object.keys(guestEntries).filter(
40
+ (specifier) => specifier !== "ignotum/internal/host",
41
+ );
42
42
 
43
43
  export const compileManagedServer = Effect.fn("Deploy.compileManagedServer")(function* (
44
44
  appDirectory: string,
@@ -1,10 +1,10 @@
1
1
  import { Effect, FileSystem, Path, Schema } from "effect";
2
2
  import { UploadDeclaration } from "@ignotum/contracts/app-definition";
3
- import { RuntimeRequirements } from "@ignotum/contracts/platform";
3
+ import { PlatformRequirement } from "@ignotum/contracts/deployment";
4
4
  import { encodeCanonical, validateUploadDeclaration, utf8Bytes } from "@ignotum/versions";
5
5
  import { generate } from "../codegen.js";
6
6
  import { replaceBuildDirectory } from "./output.js";
7
- import { managedCompatibility } from "../managed-exports.js";
7
+
8
8
  import { buildManagedClient } from "./managed-client.js";
9
9
  import { buildManagedServer } from "./server.js";
10
10
 
@@ -35,10 +35,9 @@ export const buildManagedVersion = Effect.fn("Deploy.buildManagedVersion")(funct
35
35
  );
36
36
  const client = yield* buildManagedClient(appDirectory, staging, options);
37
37
  const server = yield* buildManagedServer(appDirectory, staging, generated.functionModules);
38
- const requirements = yield* Schema.decodeUnknownEffect(RuntimeRequirements)({
39
- clientApiVersion: managedCompatibility.clientApiVersion,
40
- serverApiVersion: managedCompatibility.serverApiVersion,
41
- capabilities: managedCompatibility.capabilities,
38
+ const requirements = yield* Schema.decodeEffect(PlatformRequirement)({
39
+ major: 1,
40
+ minimumMinor: 0,
42
41
  });
43
42
  const declaration = yield* validateUploadDeclaration({
44
43
  formatVersion: 1,