ignotum 0.0.13 → 0.0.15
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/README.md +6 -6
- package/dist/cli/bin.mjs +6111 -4448
- package/dist/cli/bin.mjs.map +1 -1
- package/dist/cli/emscripten-module-D5GzfUNv.mjs +533 -0
- package/dist/cli/emscripten-module-D5GzfUNv.mjs.map +1 -0
- package/dist/cli/ffi-7IElU4Co.mjs +148 -0
- package/dist/cli/ffi-7IElU4Co.mjs.map +1 -0
- package/dist/cli/managed-release-WHYLZwaS.mjs +10902 -0
- package/dist/cli/managed-release-WHYLZwaS.mjs.map +1 -0
- package/dist/cli/managed-release.d.mts +1 -0
- package/dist/cli/managed-release.mjs +14 -0
- package/dist/cli/managed-release.mjs.map +1 -0
- package/dist/cli/module-ES6BEMUI-D36I4NVn.mjs +1289 -0
- package/dist/cli/module-ES6BEMUI-D36I4NVn.mjs.map +1 -0
- package/dist/runtime/{api-Cv3hMbzo.d.ts → api-B4fwp5a2.d.ts} +4 -4
- package/dist/runtime/bootstrap-BISQSn_O.js +916 -0
- package/dist/runtime/bootstrap-BISQSn_O.js.map +1 -0
- package/dist/runtime/bootstrap-BTFScKAc.d.ts +545 -0
- package/dist/runtime/client.d.ts +50 -15
- package/dist/runtime/client.js +7 -1138
- package/dist/runtime/client.js.map +1 -1
- package/dist/runtime/descriptor-CTY4mtwS-cS43EbwS.js +269 -0
- package/dist/runtime/descriptor-CTY4mtwS-cS43EbwS.js.map +1 -0
- package/dist/runtime/{id-Bt9XWRGL.js → id--1m0NuUL.js} +4 -4
- package/dist/runtime/id--1m0NuUL.js.map +1 -0
- package/dist/runtime/{id-BzFHf3Wo-DGPCjgrf.d.ts → id-7an3nxTu-DZJSM5xh.d.ts} +2 -2
- package/dist/runtime/id-CBOt2kDo.d.ts +1 -0
- package/dist/runtime/identity-QjtJRxBD.d.ts +2 -0
- package/dist/runtime/image-Djtjy4Z8.js +19 -0
- package/dist/runtime/image-Djtjy4Z8.js.map +1 -0
- package/dist/runtime/index-D4seNHlX.d.ts +184 -0
- package/dist/runtime/internal/api.d.ts +1 -1
- package/dist/runtime/internal/api.js +30 -1
- package/dist/runtime/internal/api.js.map +1 -0
- package/dist/runtime/internal/bootstrap.d.ts +2 -0
- package/dist/runtime/internal/bootstrap.js +2 -0
- package/dist/runtime/internal/client.d.ts +2 -0
- package/dist/runtime/internal/client.js +75 -0
- package/dist/runtime/internal/client.js.map +1 -0
- package/dist/runtime/internal/host.d.ts +25 -8
- package/dist/runtime/internal/host.js +27 -6
- package/dist/runtime/internal/host.js.map +1 -1
- package/dist/runtime/internal/routes.d.ts +2 -0
- package/dist/runtime/internal/routes.js +2 -0
- package/dist/runtime/internal/server.d.ts +1 -1
- package/dist/runtime/internal/server.js +1 -1
- package/dist/runtime/internal/types.d.ts +2 -1
- package/dist/runtime/internal/types.js +1 -1
- package/dist/runtime/pagination-CBOt2kDo.d.ts +1 -0
- package/dist/runtime/pagination-D3qd6s9N.js +33 -0
- package/dist/runtime/pagination-D3qd6s9N.js.map +1 -0
- package/dist/runtime/result-BkziOG1L.d.ts +1 -0
- package/dist/runtime/router-BSBI1oN1.js +2377 -0
- package/dist/runtime/router-BSBI1oN1.js.map +1 -0
- package/dist/runtime/routes-Ce8TVP-T.js +690 -0
- package/dist/runtime/routes-Ce8TVP-T.js.map +1 -0
- package/dist/runtime/{schema-1Zs03-iS.js → schema-D1wOqiNH.js} +40 -12
- package/dist/runtime/schema-D1wOqiNH.js.map +1 -0
- package/dist/runtime/server.d.ts +5 -5
- package/dist/runtime/server.js +2 -2
- package/dist/runtime/{sync-Bs8J3fIr.d.ts → sync-D-GK2sv9.d.ts} +3 -2
- package/dist/runtime/{api-CAgKDij7.js → sync-DzUwA8W9.js} +22 -41
- package/dist/runtime/sync-DzUwA8W9.js.map +1 -0
- package/dist/runtime/types-DeCCyqLU-BIm7rU-0.d.ts +341 -0
- package/package.json +29 -4
- package/src/cli/agent-files.ts +14 -8
- package/src/cli/build/managed-client.ts +411 -0
- package/src/cli/build/managed-server.ts +294 -0
- package/src/cli/build/managed.ts +67 -0
- package/src/cli/build/output.ts +47 -0
- package/src/cli/build/server.ts +51 -220
- package/src/cli/client-config.ts +1 -140
- package/src/cli/client-styles.ts +1 -0
- package/src/cli/codegen.ts +9 -4
- package/src/cli/command.ts +28 -5
- package/src/cli/control-client.ts +84 -101
- package/src/cli/deploy.ts +133 -177
- package/src/cli/dev.ts +506 -107
- package/src/cli/image-assets.ts +499 -0
- package/src/cli/managed-client-boundaries.ts +103 -0
- package/src/cli/managed-dev-platform.ts +118 -0
- package/src/cli/managed-exports.ts +219 -0
- package/src/cli/managed-release-bin.ts +13 -0
- package/src/cli/managed-release.ts +260 -0
- package/src/cli/module-boundaries.ts +11 -1
- package/src/cli/new-app.ts +111 -87
- package/src/cli/route-codegen.ts +311 -0
- package/src/cli/route-static.ts +710 -0
- package/src/client/bootstrap.ts +110 -0
- package/src/client/errors.ts +13 -7
- package/src/client/files.ts +64 -0
- package/src/client/id.ts +9 -5
- package/src/client/image.ts +28 -0
- package/src/client/index.ts +25 -2
- package/src/client/managed-client.ts +729 -0
- package/src/client/managed-upgrade.ts +283 -0
- package/src/client/recovery-journal.ts +195 -0
- package/src/client/route-boundaries.ts +184 -0
- package/src/client/router-history.ts +152 -0
- package/src/client/router-store.ts +644 -0
- package/src/client/router.ts +324 -0
- package/src/client/routes.ts +28 -0
- package/src/client/sync.ts +66 -592
- package/src/dev-runtime/database.ts +77 -2
- package/src/dev-runtime/functions.ts +3 -3
- package/src/dev-runtime/id.ts +19 -4
- package/src/dev-runtime/managed-functions.ts +255 -0
- package/src/dev-runtime/managed-sync.ts +263 -0
- package/src/dev-runtime/managed-websocket.ts +71 -0
- package/src/dev-runtime/migrations.ts +20 -0
- package/src/dev-runtime/sync.ts +236 -236
- package/src/image-assets.d.ts +36 -0
- package/src/internal/client.ts +186 -0
- package/src/internal/host.ts +1 -1
- package/src/internal/http-paths.ts +0 -3
- package/src/internal/routes.ts +45 -0
- package/dist/runtime/api-CAgKDij7.js.map +0 -1
- package/dist/runtime/descriptor-Cyo9FP9n-C0SRVXNW.js +0 -154
- package/dist/runtime/descriptor-Cyo9FP9n-C0SRVXNW.js.map +0 -1
- package/dist/runtime/id-Bt9XWRGL.js.map +0 -1
- package/dist/runtime/id-Dz0apuB3.d.ts +0 -1
- package/dist/runtime/index-D54flWtH.d.ts +0 -402
- package/dist/runtime/pagination-DnKg3dkI-r5ZUxBBx.d.ts +0 -112
- package/dist/runtime/pagination-Dz0apuB3.d.ts +0 -1
- package/dist/runtime/result-DKAA4gpS.d.ts +0 -1
- package/dist/runtime/schema-1Zs03-iS.js.map +0 -1
- package/src/cli/build/client.ts +0 -119
- package/src/cli/build/public.ts +0 -186
- package/src/cli/client-entry.ts +0 -152
- package/src/cli/client-plugin.ts +0 -136
- package/src/client/app.ts +0 -15
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
import { Crypto, Effect, FileSystem, Path, Predicate, Schema } from "effect";
|
|
2
|
+
import { versionArtifactLimits } from "@ignotum/contracts/version";
|
|
3
|
+
import type { ArtifactReference } from "@ignotum/contracts/version";
|
|
4
|
+
import { artifactReference, sha256 } from "@ignotum/versions";
|
|
5
|
+
import type { Plugin as PostcssPlugin } from "postcss";
|
|
6
|
+
import valueParser from "postcss-value-parser";
|
|
7
|
+
import { normalizePath } from "vite";
|
|
8
|
+
import type { Plugin, ResolvedConfig, ViteDevServer } from "vite";
|
|
9
|
+
|
|
10
|
+
const imageModulePrefix = "\0ignotum:image:";
|
|
11
|
+
const assetUrlPrefix = "/_ignotum/";
|
|
12
|
+
const imageModuleId = (sourcePath: string): string =>
|
|
13
|
+
`${imageModulePrefix}${encodeURIComponent(normalizePath(sourcePath))}.js`;
|
|
14
|
+
|
|
15
|
+
const imageFormats = {
|
|
16
|
+
avif: { canonicalExtension: "avif", contentType: "image/avif" },
|
|
17
|
+
gif: { canonicalExtension: "gif", contentType: "image/gif" },
|
|
18
|
+
ico: { canonicalExtension: "ico", contentType: "image/x-icon" },
|
|
19
|
+
jpeg: { canonicalExtension: "jpg", contentType: "image/jpeg" },
|
|
20
|
+
jpg: { canonicalExtension: "jpg", contentType: "image/jpeg" },
|
|
21
|
+
png: { canonicalExtension: "png", contentType: "image/png" },
|
|
22
|
+
webp: { canonicalExtension: "webp", contentType: "image/webp" },
|
|
23
|
+
} as const;
|
|
24
|
+
|
|
25
|
+
const ImageExtension = Schema.Literals(["avif", "gif", "ico", "jpeg", "jpg", "png", "webp"]);
|
|
26
|
+
type ImageExtension = typeof ImageExtension.Type;
|
|
27
|
+
const isImageExtension = Schema.is(ImageExtension);
|
|
28
|
+
|
|
29
|
+
const sourceExtensions = new Set([
|
|
30
|
+
"cjs",
|
|
31
|
+
"css",
|
|
32
|
+
"cts",
|
|
33
|
+
"html",
|
|
34
|
+
"js",
|
|
35
|
+
"json",
|
|
36
|
+
"jsx",
|
|
37
|
+
"mjs",
|
|
38
|
+
"mts",
|
|
39
|
+
"ts",
|
|
40
|
+
"tsx",
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
export interface ImageAsset {
|
|
44
|
+
readonly bytes: Uint8Array;
|
|
45
|
+
readonly contentType: string;
|
|
46
|
+
readonly path: string;
|
|
47
|
+
readonly reference: ArtifactReference;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface ImageAssetsPlugin {
|
|
51
|
+
readonly assets: ReadonlyMap<string, ImageAsset>;
|
|
52
|
+
readonly plugin: Plugin;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export class InvalidImageAsset extends Schema.TaggedError<InvalidImageAsset>()(
|
|
56
|
+
"InvalidImageAsset",
|
|
57
|
+
{
|
|
58
|
+
importer: Schema.optional(Schema.String),
|
|
59
|
+
message: Schema.String,
|
|
60
|
+
path: Schema.String,
|
|
61
|
+
},
|
|
62
|
+
) {}
|
|
63
|
+
|
|
64
|
+
const extensionOf = (specifier: string): string | undefined => {
|
|
65
|
+
const clean = specifier.split(/[?#]/, 1)[0] ?? specifier;
|
|
66
|
+
const slash = clean.lastIndexOf("/");
|
|
67
|
+
const dot = clean.lastIndexOf(".");
|
|
68
|
+
return dot > slash ? clean.slice(dot + 1).toLowerCase() : undefined;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const imageExtension = (specifier: string): ImageExtension | undefined => {
|
|
72
|
+
const extension = extensionOf(specifier);
|
|
73
|
+
return isImageExtension(extension) ? extension : undefined;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const hasImageSignature = (extension: ImageExtension, bytes: Uint8Array): boolean => {
|
|
77
|
+
const ascii = (start: number, length: number): string =>
|
|
78
|
+
String.fromCharCode(...bytes.subarray(start, start + length));
|
|
79
|
+
|
|
80
|
+
switch (extension) {
|
|
81
|
+
case "avif": {
|
|
82
|
+
if (bytes.length < 12 || ascii(4, 4) !== "ftyp") return false;
|
|
83
|
+
for (let offset = 8; offset + 4 <= Math.min(bytes.length, 40); offset += 4) {
|
|
84
|
+
const brand = ascii(offset, 4);
|
|
85
|
+
if (brand === "avif" || brand === "avis") return true;
|
|
86
|
+
}
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
case "gif":
|
|
90
|
+
return ascii(0, 6) === "GIF87a" || ascii(0, 6) === "GIF89a";
|
|
91
|
+
case "ico":
|
|
92
|
+
return (
|
|
93
|
+
bytes.length >= 6 &&
|
|
94
|
+
bytes[0] === 0 &&
|
|
95
|
+
bytes[1] === 0 &&
|
|
96
|
+
bytes[2] === 1 &&
|
|
97
|
+
bytes[3] === 0 &&
|
|
98
|
+
(bytes[4] ?? 0) + ((bytes[5] ?? 0) << 8) > 0
|
|
99
|
+
);
|
|
100
|
+
case "jpeg":
|
|
101
|
+
case "jpg":
|
|
102
|
+
return bytes.length >= 3 && bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff;
|
|
103
|
+
case "png":
|
|
104
|
+
return (
|
|
105
|
+
bytes.length >= 8 &&
|
|
106
|
+
bytes[0] === 0x89 &&
|
|
107
|
+
ascii(1, 3) === "PNG" &&
|
|
108
|
+
bytes[4] === 0x0d &&
|
|
109
|
+
bytes[5] === 0x0a &&
|
|
110
|
+
bytes[6] === 0x1a &&
|
|
111
|
+
bytes[7] === 0x0a
|
|
112
|
+
);
|
|
113
|
+
case "webp":
|
|
114
|
+
return bytes.length >= 12 && ascii(0, 4) === "RIFF" && ascii(8, 4) === "WEBP";
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const isExternalBrowserUrl = (url: string): boolean =>
|
|
119
|
+
url.startsWith("#") || url.startsWith("//") || /^[A-Za-z][A-Za-z0-9+.-]*:/.test(url);
|
|
120
|
+
|
|
121
|
+
const decodeCssEscapes = (value: string): string =>
|
|
122
|
+
value.replace(/\\([0-9A-Fa-f]{1,6})\s?|\\([^\r\n])/g, (_match, hex: string, escaped: string) =>
|
|
123
|
+
hex === undefined ? escaped : String.fromCodePoint(Number.parseInt(hex, 16)),
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
const cssImageNodes = (parsed: ReturnType<typeof valueParser>): ReadonlyArray<valueParser.Node> => {
|
|
127
|
+
const nodes: valueParser.Node[] = [];
|
|
128
|
+
parsed.walk((node) => {
|
|
129
|
+
if (node.type === "function" && node.value.toLowerCase() === "url") {
|
|
130
|
+
const valueNode = node.nodes.find(
|
|
131
|
+
(child) => child.type === "string" || child.type === "word",
|
|
132
|
+
);
|
|
133
|
+
if (valueNode !== undefined) nodes.push(valueNode);
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
if (
|
|
137
|
+
node.type === "function" &&
|
|
138
|
+
["image-set", "-webkit-image-set"].includes(node.value.toLowerCase())
|
|
139
|
+
) {
|
|
140
|
+
for (const child of node.nodes) {
|
|
141
|
+
if (child.type === "string") nodes.push(child);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
return nodes;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
export const createImageAssetsPlugin = Effect.fn("Deploy.createImageAssetsPlugin")(function* (
|
|
149
|
+
appDirectory: string,
|
|
150
|
+
) {
|
|
151
|
+
const fileSystem = yield* FileSystem.FileSystem;
|
|
152
|
+
const path = yield* Path.Path;
|
|
153
|
+
const runPromise = Effect.runPromiseWith(yield* Effect.context<Crypto.Crypto>());
|
|
154
|
+
const assets = new Map<string, ImageAsset>();
|
|
155
|
+
const sources = new Map<string, ImageAsset>();
|
|
156
|
+
const emitted = new Set<string>();
|
|
157
|
+
let emitAsset: ((asset: ImageAsset) => void) | undefined;
|
|
158
|
+
let resolvedConfig: ResolvedConfig | undefined;
|
|
159
|
+
let devServer: ViteDevServer | undefined;
|
|
160
|
+
|
|
161
|
+
const fail = (assetPath: string, message: string, importer?: string): InvalidImageAsset =>
|
|
162
|
+
InvalidImageAsset.make({ importer, message, path: assetPath });
|
|
163
|
+
|
|
164
|
+
const validateNoSymlinks = Effect.fn("Deploy.validateImageAssetSymlinks")(function* (
|
|
165
|
+
absolutePath: string,
|
|
166
|
+
importer?: string,
|
|
167
|
+
) {
|
|
168
|
+
let current = absolutePath;
|
|
169
|
+
for (;;) {
|
|
170
|
+
const info = yield* fileSystem
|
|
171
|
+
.stat(current)
|
|
172
|
+
.pipe(
|
|
173
|
+
Effect.mapError((cause) =>
|
|
174
|
+
fail(
|
|
175
|
+
absolutePath,
|
|
176
|
+
`Could not inspect imported image ${absolutePath}: ${cause.message}`,
|
|
177
|
+
importer,
|
|
178
|
+
),
|
|
179
|
+
),
|
|
180
|
+
);
|
|
181
|
+
if (info.type === "SymbolicLink") {
|
|
182
|
+
return yield* fail(
|
|
183
|
+
absolutePath,
|
|
184
|
+
`Imported image paths cannot contain symbolic links (${current})`,
|
|
185
|
+
importer,
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
const parent = path.dirname(current);
|
|
189
|
+
if (parent === current) break;
|
|
190
|
+
current = parent;
|
|
191
|
+
}
|
|
192
|
+
const realPath = yield* fileSystem
|
|
193
|
+
.realPath(absolutePath)
|
|
194
|
+
.pipe(
|
|
195
|
+
Effect.mapError((cause) =>
|
|
196
|
+
fail(
|
|
197
|
+
absolutePath,
|
|
198
|
+
`Could not resolve imported image ${absolutePath}: ${cause.message}`,
|
|
199
|
+
importer,
|
|
200
|
+
),
|
|
201
|
+
),
|
|
202
|
+
);
|
|
203
|
+
if (normalizePath(realPath) !== normalizePath(absolutePath)) {
|
|
204
|
+
return yield* fail(
|
|
205
|
+
absolutePath,
|
|
206
|
+
"Imported image paths cannot contain symbolic links",
|
|
207
|
+
importer,
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
const validateImage = Effect.fn("Deploy.validateImageAsset")(function* (
|
|
213
|
+
sourcePath: string,
|
|
214
|
+
importer?: string,
|
|
215
|
+
) {
|
|
216
|
+
const absolutePath = path.resolve(sourcePath);
|
|
217
|
+
const cached = sources.get(absolutePath);
|
|
218
|
+
if (cached !== undefined) return cached;
|
|
219
|
+
|
|
220
|
+
const extension = imageExtension(absolutePath);
|
|
221
|
+
if (extension === undefined) {
|
|
222
|
+
return yield* fail(
|
|
223
|
+
absolutePath,
|
|
224
|
+
"Imported assets must be AVIF, GIF, ICO, JPEG, PNG, or WebP images",
|
|
225
|
+
importer,
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
yield* validateNoSymlinks(absolutePath, importer);
|
|
229
|
+
const info = yield* fileSystem
|
|
230
|
+
.stat(absolutePath)
|
|
231
|
+
.pipe(
|
|
232
|
+
Effect.mapError((cause) =>
|
|
233
|
+
fail(
|
|
234
|
+
absolutePath,
|
|
235
|
+
`Could not inspect imported image ${absolutePath}: ${cause.message}`,
|
|
236
|
+
importer,
|
|
237
|
+
),
|
|
238
|
+
),
|
|
239
|
+
);
|
|
240
|
+
if (info.type !== "File") {
|
|
241
|
+
return yield* fail(absolutePath, "Imported images must be regular files", importer);
|
|
242
|
+
}
|
|
243
|
+
if (info.size > BigInt(versionArtifactLimits.fileBytes)) {
|
|
244
|
+
return yield* fail(absolutePath, "Imported images cannot be larger than 16 MiB", importer);
|
|
245
|
+
}
|
|
246
|
+
const bytes = yield* fileSystem
|
|
247
|
+
.readFile(absolutePath)
|
|
248
|
+
.pipe(
|
|
249
|
+
Effect.mapError((cause) =>
|
|
250
|
+
fail(
|
|
251
|
+
absolutePath,
|
|
252
|
+
`Could not read imported image ${absolutePath}: ${cause.message}`,
|
|
253
|
+
importer,
|
|
254
|
+
),
|
|
255
|
+
),
|
|
256
|
+
);
|
|
257
|
+
if (bytes.byteLength > versionArtifactLimits.fileBytes) {
|
|
258
|
+
return yield* fail(absolutePath, "Imported images cannot be larger than 16 MiB", importer);
|
|
259
|
+
}
|
|
260
|
+
if (!hasImageSignature(extension, bytes)) {
|
|
261
|
+
return yield* fail(
|
|
262
|
+
absolutePath,
|
|
263
|
+
`The file contents do not match the .${extension} image extension`,
|
|
264
|
+
importer,
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const digest = yield* sha256(bytes);
|
|
269
|
+
const format = imageFormats[extension];
|
|
270
|
+
const artifactPath = `assets/${digest}/image.${format.canonicalExtension}`;
|
|
271
|
+
let asset = assets.get(artifactPath);
|
|
272
|
+
if (asset === undefined) {
|
|
273
|
+
asset = {
|
|
274
|
+
bytes,
|
|
275
|
+
contentType: format.contentType,
|
|
276
|
+
path: artifactPath,
|
|
277
|
+
reference: yield* artifactReference(artifactPath, bytes),
|
|
278
|
+
};
|
|
279
|
+
assets.set(artifactPath, asset);
|
|
280
|
+
}
|
|
281
|
+
sources.set(absolutePath, asset);
|
|
282
|
+
return asset;
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
const emit = (asset: ImageAsset): void => {
|
|
286
|
+
if (resolvedConfig?.command !== "build" || emitted.has(asset.path)) return;
|
|
287
|
+
emitAsset?.(asset);
|
|
288
|
+
emitted.add(asset.path);
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
const cssPlugin: PostcssPlugin = {
|
|
292
|
+
postcssPlugin: "ignotum-image-assets",
|
|
293
|
+
Once: async (root, { result }) => {
|
|
294
|
+
const declarations: Array<{
|
|
295
|
+
file: string;
|
|
296
|
+
get: () => string;
|
|
297
|
+
set: (value: string) => void;
|
|
298
|
+
}> = [];
|
|
299
|
+
root.walkDecls((declaration) => {
|
|
300
|
+
const file = declaration.source?.input.file;
|
|
301
|
+
if (file === undefined || file.startsWith("<")) return;
|
|
302
|
+
declarations.push({
|
|
303
|
+
file,
|
|
304
|
+
get: () => declaration.value,
|
|
305
|
+
set: (value) => {
|
|
306
|
+
declaration.value = value;
|
|
307
|
+
},
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
for (const owner of declarations) {
|
|
312
|
+
const parsed = valueParser(owner.get());
|
|
313
|
+
const nodes = cssImageNodes(parsed);
|
|
314
|
+
for (const node of nodes) {
|
|
315
|
+
const url = decodeCssEscapes(node.value.trim());
|
|
316
|
+
if (url.length === 0 || isExternalBrowserUrl(url)) continue;
|
|
317
|
+
if (url.startsWith(assetUrlPrefix) && assets.has(url.slice(assetUrlPrefix.length)))
|
|
318
|
+
continue;
|
|
319
|
+
if (url.includes("?") || url.includes("#")) {
|
|
320
|
+
throw fail(url, "Image import queries and fragments are not supported", owner.file);
|
|
321
|
+
}
|
|
322
|
+
const extension = extensionOf(url);
|
|
323
|
+
if (!isImageExtension(extension)) {
|
|
324
|
+
throw fail(
|
|
325
|
+
url,
|
|
326
|
+
"CSS can only import local AVIF, GIF, ICO, JPEG, PNG, or WebP images",
|
|
327
|
+
owner.file,
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
const sourcePath = url.startsWith("/")
|
|
331
|
+
? path.resolve(resolvedConfig?.root ?? path.join(appDirectory, "client"), url.slice(1))
|
|
332
|
+
: path.resolve(path.dirname(owner.file), url);
|
|
333
|
+
const asset = await runPromise(validateImage(sourcePath, owner.file));
|
|
334
|
+
result.messages.push({
|
|
335
|
+
type: "dependency",
|
|
336
|
+
plugin: "ignotum-image-assets",
|
|
337
|
+
file: sourcePath,
|
|
338
|
+
parent: owner.file,
|
|
339
|
+
});
|
|
340
|
+
emit(asset);
|
|
341
|
+
node.value = `${assetUrlPrefix}${asset.path}`;
|
|
342
|
+
}
|
|
343
|
+
owner.set(valueParser.stringify(parsed.nodes));
|
|
344
|
+
}
|
|
345
|
+
},
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
const plugin: Plugin = {
|
|
349
|
+
name: "ignotum-image-assets",
|
|
350
|
+
enforce: "pre",
|
|
351
|
+
config: (config) => {
|
|
352
|
+
const external = config.build?.rolldownOptions?.external;
|
|
353
|
+
const postcss = config.css?.postcss;
|
|
354
|
+
const postcssOptions = Predicate.isString(postcss) ? undefined : postcss;
|
|
355
|
+
return {
|
|
356
|
+
build: {
|
|
357
|
+
assetsInlineLimit: 0,
|
|
358
|
+
rolldownOptions: {
|
|
359
|
+
external: (id, importer, isResolved) => {
|
|
360
|
+
if (id.startsWith(assetUrlPrefix) && assets.has(id.slice(assetUrlPrefix.length)))
|
|
361
|
+
return true;
|
|
362
|
+
if (Predicate.isFunction(external)) return external(id, importer, isResolved);
|
|
363
|
+
if (external === undefined) return false;
|
|
364
|
+
const patterns = Array.isArray(external) ? external : [external];
|
|
365
|
+
return patterns.some((pattern) =>
|
|
366
|
+
Predicate.isString(pattern) ? pattern === id : pattern.test(id),
|
|
367
|
+
);
|
|
368
|
+
},
|
|
369
|
+
},
|
|
370
|
+
},
|
|
371
|
+
css: {
|
|
372
|
+
postcss: {
|
|
373
|
+
...postcssOptions,
|
|
374
|
+
plugins: [...(postcssOptions?.plugins ?? []), cssPlugin],
|
|
375
|
+
},
|
|
376
|
+
},
|
|
377
|
+
};
|
|
378
|
+
},
|
|
379
|
+
configResolved: (config) => {
|
|
380
|
+
resolvedConfig = config;
|
|
381
|
+
},
|
|
382
|
+
buildStart() {
|
|
383
|
+
emitAsset = (asset) => {
|
|
384
|
+
this.emitFile({ type: "asset", fileName: asset.path, source: asset.bytes });
|
|
385
|
+
};
|
|
386
|
+
assets.clear();
|
|
387
|
+
sources.clear();
|
|
388
|
+
emitted.clear();
|
|
389
|
+
},
|
|
390
|
+
resolveId: async function (source, importer) {
|
|
391
|
+
if (source.startsWith("/@id/__x00__")) {
|
|
392
|
+
return `\0${source.slice("/@id/__x00__".length)}`;
|
|
393
|
+
}
|
|
394
|
+
if (
|
|
395
|
+
source.startsWith("\0") ||
|
|
396
|
+
source.startsWith("virtual:") ||
|
|
397
|
+
source.startsWith("//") ||
|
|
398
|
+
/^[A-Za-z][A-Za-z0-9+.-]*:/.test(source)
|
|
399
|
+
)
|
|
400
|
+
return;
|
|
401
|
+
const extension = extensionOf(source);
|
|
402
|
+
const hasQuery = source.includes("?") || source.includes("#");
|
|
403
|
+
if (imageExtension(source) !== undefined) {
|
|
404
|
+
if (hasQuery) {
|
|
405
|
+
throw fail(source, "Image import queries and fragments are not supported", importer);
|
|
406
|
+
}
|
|
407
|
+
if (source.startsWith(".") && importer !== undefined && !importer.startsWith("\0")) {
|
|
408
|
+
await runPromise(validateImage(path.resolve(path.dirname(importer), source), importer));
|
|
409
|
+
} else if (source.startsWith("/")) {
|
|
410
|
+
const sourcePath = (await runPromise(fileSystem.exists(source)))
|
|
411
|
+
? source
|
|
412
|
+
: path.resolve(
|
|
413
|
+
resolvedConfig?.root ?? path.join(appDirectory, "client"),
|
|
414
|
+
source.slice(1),
|
|
415
|
+
);
|
|
416
|
+
await runPromise(validateImage(sourcePath, importer));
|
|
417
|
+
}
|
|
418
|
+
const resolution = await this.resolve(source, importer, { skipSelf: true });
|
|
419
|
+
if (
|
|
420
|
+
resolution === null ||
|
|
421
|
+
(resolution.external !== undefined && resolution.external !== false)
|
|
422
|
+
) {
|
|
423
|
+
throw fail(source, "Imported image could not be resolved", importer);
|
|
424
|
+
}
|
|
425
|
+
return imageModuleId(resolution.id);
|
|
426
|
+
}
|
|
427
|
+
if (hasQuery || (extension !== undefined && !sourceExtensions.has(extension))) {
|
|
428
|
+
throw fail(
|
|
429
|
+
source,
|
|
430
|
+
"Client imports only support source modules, stylesheets, and validated image assets",
|
|
431
|
+
importer,
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
},
|
|
435
|
+
load: async function (id) {
|
|
436
|
+
if (!id.startsWith(imageModulePrefix)) {
|
|
437
|
+
if (!id.startsWith("\0") && !id.startsWith("virtual:")) {
|
|
438
|
+
const extension = extensionOf(id);
|
|
439
|
+
if (extension !== undefined && !sourceExtensions.has(extension)) {
|
|
440
|
+
throw fail(
|
|
441
|
+
id,
|
|
442
|
+
"Client imports only support source modules, stylesheets, and validated image assets",
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
const sourcePath = decodeURIComponent(id.slice(imageModulePrefix.length, -3));
|
|
449
|
+
if (resolvedConfig?.command === "build") this.addWatchFile(sourcePath);
|
|
450
|
+
else devServer?.watcher.add(sourcePath);
|
|
451
|
+
const asset = await runPromise(validateImage(sourcePath));
|
|
452
|
+
emit(asset);
|
|
453
|
+
return [
|
|
454
|
+
'import { createImageSource } from "ignotum/internal/client";',
|
|
455
|
+
`export default createImageSource(${JSON.stringify(`${assetUrlPrefix}${asset.path}`)});`,
|
|
456
|
+
].join("\n");
|
|
457
|
+
},
|
|
458
|
+
configureServer(server: ViteDevServer) {
|
|
459
|
+
devServer = server;
|
|
460
|
+
server.middlewares.use((request, response, next) => {
|
|
461
|
+
if (request.method !== "GET" && request.method !== "HEAD") return next();
|
|
462
|
+
const pathname = request.url?.split(/[?#]/, 1)[0];
|
|
463
|
+
if (pathname === undefined || !pathname.startsWith(`${assetUrlPrefix}assets/`))
|
|
464
|
+
return next();
|
|
465
|
+
const asset = assets.get(pathname.slice(assetUrlPrefix.length));
|
|
466
|
+
if (asset === undefined) return next();
|
|
467
|
+
response.statusCode = 200;
|
|
468
|
+
response.setHeader("Content-Type", asset.contentType);
|
|
469
|
+
response.setHeader("Content-Length", String(asset.bytes.byteLength));
|
|
470
|
+
response.setHeader("Cache-Control", "public, max-age=31536000, immutable");
|
|
471
|
+
response.end(request.method === "HEAD" ? undefined : asset.bytes);
|
|
472
|
+
});
|
|
473
|
+
},
|
|
474
|
+
watchChange(id) {
|
|
475
|
+
sources.delete(path.resolve(id));
|
|
476
|
+
const graph = devServer?.environments.client.moduleGraph;
|
|
477
|
+
const module = graph?.getModuleById(imageModuleId(path.resolve(id)));
|
|
478
|
+
if (module !== undefined) graph?.invalidateModule(module);
|
|
479
|
+
},
|
|
480
|
+
hotUpdate(context) {
|
|
481
|
+
const module = this.environment.moduleGraph.getModuleById(
|
|
482
|
+
imageModuleId(path.resolve(context.file)),
|
|
483
|
+
);
|
|
484
|
+
if (module !== undefined) return [...new Set([...context.modules, module])];
|
|
485
|
+
},
|
|
486
|
+
generateBundle(_options, bundle) {
|
|
487
|
+
for (const output of Object.values(bundle)) {
|
|
488
|
+
if (output.type !== "asset") continue;
|
|
489
|
+
if (output.fileName.endsWith(".css") || assets.has(output.fileName)) continue;
|
|
490
|
+
throw fail(
|
|
491
|
+
output.fileName,
|
|
492
|
+
"The client build emitted an unsupported asset; import supported images through Ignotum",
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
},
|
|
496
|
+
};
|
|
497
|
+
|
|
498
|
+
return { assets, plugin } satisfies ImageAssetsPlugin;
|
|
499
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { Effect, FileSystem, Path, Schema } from "effect";
|
|
2
|
+
import { normalizePath, type Plugin } from "vite";
|
|
3
|
+
import { managedBrowserExports } from "./managed-exports.js";
|
|
4
|
+
import { InvalidBuildOutput } from "./build/artifact.js";
|
|
5
|
+
|
|
6
|
+
const PackageMetadata = Schema.Struct({ name: Schema.optional(Schema.String) });
|
|
7
|
+
const externals = new Set<string>(
|
|
8
|
+
managedBrowserExports
|
|
9
|
+
.filter((entry) => entry.specifier.startsWith("ignotum/"))
|
|
10
|
+
.map((entry) => entry.specifier),
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
/** Resolve before workspace conditions or package aliases can embed SDK implementation. */
|
|
14
|
+
export const managedClientBoundariesPlugin = Effect.fn("Deploy.managedClientBoundariesPlugin")(
|
|
15
|
+
function* (appDirectory: string) {
|
|
16
|
+
const fs = yield* FileSystem.FileSystem;
|
|
17
|
+
const path = yield* Path.Path;
|
|
18
|
+
const runPromise = Effect.runPromiseWith(yield* Effect.context<never>());
|
|
19
|
+
const generatedDirectory = normalizePath(
|
|
20
|
+
path.join(yield* fs.realPath(appDirectory), "_generated"),
|
|
21
|
+
);
|
|
22
|
+
const compilerImportAllowed = (specifier: string, importer: string | undefined): boolean => {
|
|
23
|
+
if (importer === undefined) return false;
|
|
24
|
+
const source = normalizePath(importer.split("?", 1)[0]!);
|
|
25
|
+
switch (specifier) {
|
|
26
|
+
case "ignotum/internal/api":
|
|
27
|
+
return source === `${generatedDirectory}/api.ts`;
|
|
28
|
+
case "ignotum/internal/client":
|
|
29
|
+
return (
|
|
30
|
+
source === `${generatedDirectory}/client.ts` ||
|
|
31
|
+
source === `${generatedDirectory}/routes.ts` ||
|
|
32
|
+
source.startsWith("\0ignotum:image:")
|
|
33
|
+
);
|
|
34
|
+
case "ignotum/internal/routes":
|
|
35
|
+
return source === `${generatedDirectory}/routes.ts`;
|
|
36
|
+
default:
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
const owners = new Map<string, string | undefined>();
|
|
41
|
+
const ownerOf = Effect.fn("Deploy.managedClientOwner")(function* (id: string) {
|
|
42
|
+
let directory = path.dirname(id);
|
|
43
|
+
const start = directory;
|
|
44
|
+
if (owners.has(start)) return owners.get(start);
|
|
45
|
+
while (path.dirname(directory) !== directory) {
|
|
46
|
+
const manifest = path.join(directory, "package.json");
|
|
47
|
+
if (yield* fs.exists(manifest)) {
|
|
48
|
+
const metadata = yield* Schema.decodeUnknownEffect(
|
|
49
|
+
Schema.fromJsonString(PackageMetadata),
|
|
50
|
+
)(yield* fs.readFileString(manifest));
|
|
51
|
+
if (metadata.name !== undefined) {
|
|
52
|
+
owners.set(start, metadata.name);
|
|
53
|
+
return metadata.name;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
directory = path.dirname(directory);
|
|
57
|
+
}
|
|
58
|
+
owners.set(start, undefined);
|
|
59
|
+
return undefined;
|
|
60
|
+
});
|
|
61
|
+
return {
|
|
62
|
+
name: "ignotum:managed-client-platform-boundary",
|
|
63
|
+
enforce: "pre",
|
|
64
|
+
resolveId(specifier, importer) {
|
|
65
|
+
if (
|
|
66
|
+
specifier.startsWith("ignotum/internal/") &&
|
|
67
|
+
!compilerImportAllowed(specifier, importer)
|
|
68
|
+
) {
|
|
69
|
+
throw InvalidBuildOutput.make({
|
|
70
|
+
message: `Managed client module '${importer ?? "<entry>"}' imports unsupported platform entry '${specifier}'. Compiler-owned entries must come from generated bindings.`,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
if (externals.has(specifier)) return { id: specifier, external: true };
|
|
74
|
+
if (
|
|
75
|
+
specifier === "ignotum" ||
|
|
76
|
+
specifier.startsWith("ignotum/") ||
|
|
77
|
+
specifier.startsWith("@ignotum/")
|
|
78
|
+
) {
|
|
79
|
+
throw InvalidBuildOutput.make({
|
|
80
|
+
message: `Managed client module '${importer ?? "<entry>"}' imports unsupported platform entry '${specifier}'. Use ignotum/client or generated client bindings.`,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
async transform(_code, id) {
|
|
85
|
+
const clean = id.split("?", 1)[0]!;
|
|
86
|
+
if (!path.isAbsolute(clean)) return;
|
|
87
|
+
const relative = normalizePath(path.relative(appDirectory, clean));
|
|
88
|
+
if (
|
|
89
|
+
!relative.startsWith("../") &&
|
|
90
|
+
!path.isAbsolute(relative) &&
|
|
91
|
+
!relative.split("/").includes("node_modules")
|
|
92
|
+
)
|
|
93
|
+
return;
|
|
94
|
+
const owner = await runPromise(ownerOf(clean));
|
|
95
|
+
if (owner === "ignotum" || owner?.startsWith("@ignotum/")) {
|
|
96
|
+
throw InvalidBuildOutput.make({
|
|
97
|
+
message: `App client graph embeds private platform implementation '${id}'. Import supported ignotum/client entries instead.`,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
} satisfies Plugin;
|
|
102
|
+
},
|
|
103
|
+
);
|