ignotum 0.0.3 → 0.0.5

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 (64) hide show
  1. package/README.md +26 -7
  2. package/dist/cli/bin.mjs +27353 -1490
  3. package/dist/cli/bin.mjs.map +1 -1
  4. package/dist/runtime/{api-Cpx57mk3.d.ts → api-D2bsAz4T.d.ts} +3 -2
  5. package/dist/runtime/{api-BXXIZc_Q.js → api-DtX8qPrq.js} +58 -27
  6. package/dist/runtime/api-DtX8qPrq.js.map +1 -0
  7. package/dist/runtime/client.d.ts +11 -4
  8. package/dist/runtime/client.js +352 -79
  9. package/dist/runtime/client.js.map +1 -1
  10. package/dist/runtime/{result-B2W-z2wG.js → descriptor-t6BOEGw9-BTHR-ZMv.js} +119 -4
  11. package/dist/runtime/descriptor-t6BOEGw9-BTHR-ZMv.js.map +1 -0
  12. package/dist/runtime/id-Btwac71X-B9OeBzvq.d.ts +9 -0
  13. package/dist/runtime/id-D570vudg.js +26 -0
  14. package/dist/runtime/id-D570vudg.js.map +1 -0
  15. package/dist/runtime/{index-C6KO3ZdI.d.ts → index-Dl_VQGmA.d.ts} +90 -29
  16. package/dist/runtime/internal/api.d.ts +1 -1
  17. package/dist/runtime/internal/api.js +1 -1
  18. package/dist/runtime/internal/host.d.ts +44 -0
  19. package/dist/runtime/internal/host.js +213 -0
  20. package/dist/runtime/internal/host.js.map +1 -0
  21. package/dist/runtime/internal/server.d.ts +1 -1
  22. package/dist/runtime/internal/server.js +1 -1
  23. package/dist/runtime/internal/types.d.ts +1 -1
  24. package/dist/runtime/internal/types.js +1 -1
  25. package/dist/runtime/{result-C1ZdsM6Y.d.ts → result-BgcHn25t.d.ts} +21 -6
  26. package/dist/runtime/{schema-wiFI7rgx.js → schema-B-jMZEbs.js} +95 -35
  27. package/dist/runtime/schema-B-jMZEbs.js.map +1 -0
  28. package/dist/runtime/server.d.ts +2 -2
  29. package/dist/runtime/server.js +2 -2
  30. package/package.json +13 -3
  31. package/src/cli/agent-files.ts +6 -2
  32. package/src/cli/app-configuration.ts +135 -0
  33. package/src/cli/bin.ts +24 -1
  34. package/src/cli/build/artifact.ts +95 -0
  35. package/src/cli/build/client.ts +111 -0
  36. package/src/cli/build/public.ts +182 -0
  37. package/src/cli/build/server.ts +527 -0
  38. package/src/cli/client-config.ts +148 -0
  39. package/src/cli/client-entry.ts +152 -0
  40. package/src/cli/client-plugin.ts +86 -16
  41. package/src/cli/codegen.ts +5 -5
  42. package/src/cli/command.ts +41 -9
  43. package/src/cli/control-client.ts +343 -0
  44. package/src/cli/deploy.ts +217 -0
  45. package/src/cli/dev.ts +43 -108
  46. package/src/cli/module-boundaries.ts +183 -0
  47. package/src/cli/{new-project.ts → new-app.ts} +86 -86
  48. package/src/cli/package-manager.ts +9 -9
  49. package/src/client/app.ts +15 -0
  50. package/src/client/errors.ts +1 -17
  51. package/src/client/index.ts +2 -0
  52. package/src/client/sync.ts +172 -120
  53. package/src/dev-runtime/database.ts +253 -96
  54. package/src/dev-runtime/dev-database.ts +14 -14
  55. package/src/dev-runtime/functions.ts +57 -88
  56. package/src/dev-runtime/id.ts +2 -10
  57. package/src/dev-runtime/migrations.ts +68 -0
  58. package/src/dev-runtime/sync.ts +266 -83
  59. package/src/internal/function-definition.ts +77 -0
  60. package/src/internal/host.ts +1 -0
  61. package/src/internal/http-paths.ts +3 -1
  62. package/dist/runtime/api-BXXIZc_Q.js.map +0 -1
  63. package/dist/runtime/result-B2W-z2wG.js.map +0 -1
  64. package/dist/runtime/schema-wiFI7rgx.js.map +0 -1
@@ -0,0 +1,111 @@
1
+ import { Array, Effect, FileSystem, Path, String } from "effect";
2
+ import tailwindcss from "@tailwindcss/vite";
3
+ import { sha256 } from "@ignotum/deployment";
4
+ import type { ClientRoute } from "@ignotum/contracts/deployment";
5
+
6
+ import { resolveClientBuildConfig, validateClientFiles } from "../client-config.js";
7
+ import { loadClientEntryTitle } from "../client-entry.js";
8
+ import { clientEntryId, ignotumClientPlugin, renderClientDocument } from "../client-plugin.js";
9
+ import { moduleBoundariesPlugin } from "../module-boundaries.js";
10
+ import { InvalidBuildOutput, runViteBuild } from "./artifact.js";
11
+ import { copyPublicFiles } from "./public.js";
12
+
13
+ export interface ClientBuildResult {
14
+ readonly assets: number;
15
+ readonly routes: ReadonlyArray<ClientRoute>;
16
+ readonly shellPath: string;
17
+ }
18
+
19
+ export const buildClient = Effect.fn("Deploy.buildClient")(function* (
20
+ appDirectory: string,
21
+ outputDirectory: string,
22
+ ) {
23
+ const fileSystem = yield* FileSystem.FileSystem;
24
+ const path = yield* Path.Path;
25
+ const clientFiles = yield* validateClientFiles(appDirectory);
26
+ const clientDirectory = clientFiles.clientDirectory;
27
+ const title = yield* loadClientEntryTitle(clientFiles.entryPath);
28
+ const clientConfig = yield* resolveClientBuildConfig(false);
29
+ const boundaries = yield* moduleBoundariesPlugin("Client", appDirectory);
30
+
31
+ yield* fileSystem.makeDirectory(outputDirectory, { recursive: true });
32
+
33
+ const output = yield* runViteBuild("client", {
34
+ appType: "spa",
35
+ build: {
36
+ assetsDir: "_ignotum/assets",
37
+ emptyOutDir: true,
38
+ outDir: outputDirectory,
39
+ rolldownOptions: {
40
+ input: clientEntryId,
41
+ output: {
42
+ assetFileNames: (asset) =>
43
+ asset.names.some((name) => name.endsWith(".css"))
44
+ ? "_ignotum/assets/styles-[hash][extname]"
45
+ : "_ignotum/assets/[name]-[hash][extname]",
46
+ chunkFileNames: "_ignotum/assets/chunks/[name]-[hash].js",
47
+ entryFileNames: "_ignotum/assets/main-[hash].js",
48
+ },
49
+ },
50
+ sourcemap: false,
51
+ },
52
+ configFile: false,
53
+ logLevel: "warn",
54
+ mode: "production",
55
+ oxc: {
56
+ jsx: {
57
+ importSource: "ignotum/client",
58
+ runtime: "automatic",
59
+ },
60
+ },
61
+ plugins: [
62
+ boundaries,
63
+ ignotumClientPlugin({ development: false, icon: undefined, loadTitle: undefined }),
64
+ tailwindcss(),
65
+ ],
66
+ publicDir: false,
67
+ resolve: {
68
+ alias: clientConfig.aliases,
69
+ conditions: [...clientConfig.conditions],
70
+ dedupe: ["preact"],
71
+ tsconfigPaths: true,
72
+ },
73
+ root: clientDirectory,
74
+ });
75
+ const entry = Array.findFirst(output, (item) => item.type === "chunk" && item.isEntry);
76
+ const entryFile = yield* Effect.fromOption(entry, () =>
77
+ InvalidBuildOutput.make({ message: "The client build did not emit an entry chunk." }),
78
+ ).pipe(Effect.map((item) => item.fileName));
79
+ const styles = Array.sort(String.Order)(
80
+ Array.map(
81
+ Array.filter(output, (item) => item.type === "asset" && item.fileName.endsWith(".css")),
82
+ (item) => item.fileName,
83
+ ),
84
+ );
85
+ let icon: string | undefined;
86
+ if (clientFiles.iconPath !== undefined) {
87
+ const bytes = yield* fileSystem.readFile(clientFiles.iconPath);
88
+ const digest = yield* sha256(bytes);
89
+ const fileName = `icon-${digest}.svg`;
90
+ const assetsDirectory = path.join(outputDirectory, "_ignotum", "assets");
91
+ yield* fileSystem.makeDirectory(assetsDirectory, { recursive: true });
92
+ yield* fileSystem.writeFile(path.join(assetsDirectory, fileName), bytes);
93
+ icon = `/_ignotum/assets/${fileName}`;
94
+ }
95
+ const shellPath = path.join(outputDirectory, "_shell.html");
96
+ const document = renderClientDocument({
97
+ icon,
98
+ scripts: [{ source: entryFile, type: "External" }],
99
+ styles,
100
+ title,
101
+ });
102
+
103
+ yield* fileSystem.writeFileString(shellPath, document);
104
+ const publicBuild = yield* copyPublicFiles(appDirectory, outputDirectory);
105
+
106
+ return {
107
+ assets: output.length + (icon === undefined ? 0 : 1) + publicBuild.files,
108
+ routes: publicBuild.routes,
109
+ shellPath,
110
+ } satisfies ClientBuildResult;
111
+ });
@@ -0,0 +1,182 @@
1
+ import {
2
+ ArtifactPath,
3
+ ClientPath,
4
+ clientPublicFileExtensions,
5
+ deploymentArtifactLimits,
6
+ type ClientRoute,
7
+ } from "@ignotum/contracts/deployment";
8
+ import { Effect, FileSystem, Option, Path, Schema } from "effect";
9
+
10
+ export class InvalidPublicFile extends Schema.TaggedError<InvalidPublicFile>()(
11
+ "InvalidPublicFile",
12
+ {
13
+ message: Schema.String,
14
+ path: Schema.String,
15
+ },
16
+ ) {}
17
+
18
+ export interface PublicBuildResult {
19
+ readonly files: number;
20
+ readonly routes: ReadonlyArray<ClientRoute>;
21
+ }
22
+
23
+ interface PublicFile {
24
+ readonly bytes: Uint8Array;
25
+ readonly destination: string;
26
+ readonly route: ClientRoute;
27
+ readonly source: string;
28
+ }
29
+
30
+ const pathSegmentPattern = /^[A-Za-z0-9._~-]+$/;
31
+
32
+ const startsWith = (bytes: Uint8Array, signature: ReadonlyArray<number>, offset = 0): boolean =>
33
+ signature.every((byte, index) => bytes[offset + index] === byte);
34
+
35
+ const asciiAt = (bytes: Uint8Array, value: string, offset = 0): boolean =>
36
+ startsWith(
37
+ bytes,
38
+ globalThis.Array.from(value, (character) => character.charCodeAt(0)),
39
+ offset,
40
+ );
41
+
42
+ const hasValidSignature = (extension: string, bytes: Uint8Array): boolean => {
43
+ switch (extension) {
44
+ case ".avif": {
45
+ if (!asciiAt(bytes, "ftyp", 4)) return false;
46
+ const headerLength = Math.min(bytes.length, 64);
47
+ for (let offset = 8; offset + 4 <= headerLength; offset += 4) {
48
+ if (asciiAt(bytes, "avif", offset) || asciiAt(bytes, "avis", offset)) return true;
49
+ }
50
+ return false;
51
+ }
52
+ case ".gif":
53
+ return asciiAt(bytes, "GIF87a") || asciiAt(bytes, "GIF89a");
54
+ case ".ico":
55
+ return startsWith(bytes, [0x00, 0x00, 0x01, 0x00]);
56
+ case ".jpeg":
57
+ case ".jpg":
58
+ return startsWith(bytes, [0xff, 0xd8, 0xff]);
59
+ case ".pdf":
60
+ return asciiAt(bytes, "%PDF-");
61
+ case ".png":
62
+ return startsWith(bytes, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
63
+ case ".webp":
64
+ return asciiAt(bytes, "RIFF") && asciiAt(bytes, "WEBP", 8);
65
+ default:
66
+ return false;
67
+ }
68
+ };
69
+
70
+ const invalid = (path: string, message: string) => InvalidPublicFile.make({ message, path });
71
+
72
+ export const copyPublicFiles = Effect.fn("Deploy.copyPublicFiles")(function* (
73
+ appDirectory: string,
74
+ outputDirectory: string,
75
+ ) {
76
+ const fileSystem = yield* FileSystem.FileSystem;
77
+ const path = yield* Path.Path;
78
+ const publicDirectory = path.join(appDirectory, "public");
79
+ if (!(yield* fileSystem.exists(publicDirectory))) {
80
+ return { files: 0, routes: [] } satisfies PublicBuildResult;
81
+ }
82
+
83
+ if (Option.isSome(yield* Effect.option(fileSystem.readLink(publicDirectory)))) {
84
+ return yield* invalid(
85
+ publicDirectory,
86
+ "The top-level public directory cannot be a symbolic link.",
87
+ );
88
+ }
89
+ const publicInfo = yield* fileSystem.stat(publicDirectory);
90
+ if (publicInfo.type !== "Directory") {
91
+ return yield* invalid(publicDirectory, "The top-level public path must be a directory.");
92
+ }
93
+
94
+ const entries = (yield* fileSystem.readDirectory(publicDirectory, {
95
+ recursive: true,
96
+ })).toSorted();
97
+ const publicFiles = yield* Effect.forEach(entries, (entry) =>
98
+ Effect.gen(function* () {
99
+ const relativePath = entry.replaceAll("\\", "/");
100
+ const source = path.join(publicDirectory, entry);
101
+ if (Option.isSome(yield* Effect.option(fileSystem.readLink(source)))) {
102
+ return yield* invalid(source, "Public entries cannot be symbolic links.");
103
+ }
104
+ const info = yield* fileSystem.stat(source);
105
+ if (info.type === "Directory") return undefined;
106
+ if (info.type !== "File") {
107
+ return yield* invalid(source, "Public entries must be regular files or directories.");
108
+ }
109
+
110
+ const segments = relativePath.split("/");
111
+ if (segments.some((segment) => !pathSegmentPattern.test(segment))) {
112
+ return yield* invalid(
113
+ source,
114
+ "Public file paths may only contain letters, numbers, dots, underscores, tildes, hyphens, and directory separators.",
115
+ );
116
+ }
117
+ if (segments[0] === "_ignotum") {
118
+ return yield* invalid(source, "The /_ignotum namespace is reserved by the platform.");
119
+ }
120
+
121
+ const extension = path.extname(relativePath).toLowerCase();
122
+ if (!clientPublicFileExtensions.some((allowedExtension) => allowedExtension === extension)) {
123
+ return yield* invalid(
124
+ source,
125
+ `Unsupported public file type '${extension || "(none)"}'. Allowed types: ${clientPublicFileExtensions.join(", ")}.`,
126
+ );
127
+ }
128
+ if (info.size > BigInt(deploymentArtifactLimits.fileBytes)) {
129
+ return yield* invalid(
130
+ source,
131
+ `Public files cannot exceed ${deploymentArtifactLimits.fileBytes} bytes.`,
132
+ );
133
+ }
134
+
135
+ const bytes = yield* fileSystem.readFile(source);
136
+ if (!hasValidSignature(extension, bytes)) {
137
+ return yield* invalid(
138
+ source,
139
+ `The contents do not match the '${extension}' file extension.`,
140
+ );
141
+ }
142
+
143
+ const pathname = `/${relativePath}`;
144
+ return {
145
+ bytes,
146
+ destination: path.join(outputDirectory, entry),
147
+ route: {
148
+ pathname: ClientPath.make(pathname),
149
+ artifact: ArtifactPath.make(`client/${relativePath}`),
150
+ },
151
+ source,
152
+ } satisfies PublicFile;
153
+ }),
154
+ );
155
+ const files = publicFiles.filter((file): file is PublicFile => file !== undefined);
156
+
157
+ yield* Effect.forEach(files, (file) =>
158
+ Effect.gen(function* () {
159
+ if (yield* fileSystem.exists(file.destination)) {
160
+ return yield* invalid(
161
+ file.source,
162
+ `Public route '${file.route.pathname}' overlaps generated client output.`,
163
+ );
164
+ }
165
+ }),
166
+ );
167
+
168
+ yield* Effect.forEach(
169
+ files,
170
+ (file) =>
171
+ Effect.gen(function* () {
172
+ yield* fileSystem.makeDirectory(path.dirname(file.destination), { recursive: true });
173
+ yield* fileSystem.writeFile(file.destination, file.bytes);
174
+ }),
175
+ { discard: true },
176
+ );
177
+
178
+ return {
179
+ files: files.length,
180
+ routes: files.map((file) => file.route),
181
+ } satisfies PublicBuildResult;
182
+ });