ignotum 0.0.2 → 0.0.4

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 +23 -7
  2. package/dist/cli/bin.mjs +32313 -751
  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 +343 -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-BgWROoyk.d.ts → index-Dl_VQGmA.d.ts} +114 -31
  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-B-jMZEbs.js +227 -0
  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 +15 -5
  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 +103 -0
  36. package/src/cli/build/server.ts +526 -0
  37. package/src/cli/client-config.ts +141 -0
  38. package/src/cli/client-entry.ts +152 -0
  39. package/src/cli/client-plugin.ts +86 -16
  40. package/src/cli/codegen.ts +5 -5
  41. package/src/cli/command.ts +41 -9
  42. package/src/cli/control-client.ts +343 -0
  43. package/src/cli/deploy.ts +217 -0
  44. package/src/cli/dev.ts +43 -108
  45. package/src/cli/module-boundaries.ts +183 -0
  46. package/src/cli/{new-project.ts → new-app.ts} +86 -86
  47. package/src/cli/package-manager.ts +9 -9
  48. package/src/client/app.ts +15 -0
  49. package/src/client/errors.ts +1 -17
  50. package/src/client/index.ts +2 -0
  51. package/src/client/sync.ts +172 -120
  52. package/src/dev-runtime/database.ts +253 -96
  53. package/src/dev-runtime/dev-database.ts +14 -14
  54. package/src/dev-runtime/functions.ts +57 -88
  55. package/src/dev-runtime/id.ts +2 -10
  56. package/src/dev-runtime/migrations.ts +68 -0
  57. package/src/dev-runtime/sync.ts +266 -83
  58. package/src/internal/function-definition.ts +77 -0
  59. package/src/internal/host.ts +1 -0
  60. package/src/internal/http-paths.ts +3 -1
  61. package/dist/runtime/api-BXXIZc_Q.js.map +0 -1
  62. package/dist/runtime/result-B2W-z2wG.js.map +0 -1
  63. package/dist/runtime/schema-CNEVLF7D.js +0 -116
  64. package/dist/runtime/schema-CNEVLF7D.js.map +0 -1
package/src/cli/dev.ts CHANGED
@@ -1,44 +1,20 @@
1
- import { createRequire } from "node:module";
2
- import { fileURLToPath } from "node:url";
3
-
4
1
  import prefresh from "@prefresh/vite";
5
2
  import tailwindcss from "@tailwindcss/vite";
6
3
  import { Effect, FileSystem, Path, Schema } from "effect";
7
- import { createServer, defaultClientConditions, normalizePath } from "vite";
8
- import type { Alias, ViteDevServer } from "vite";
4
+ import { createServer } from "vite";
5
+ import type { ViteDevServer } from "vite";
9
6
 
10
7
  import { generate } from "./codegen.js";
8
+ import {
9
+ resolveClientBuildConfig,
10
+ validateClientFiles,
11
+ type ClientFiles,
12
+ } from "./client-config.js";
13
+ import { loadClientEntryTitle } from "./client-entry.js";
11
14
  import { ignotumClientPlugin } from "./client-plugin.js";
12
15
  import { acquireDevDatabaseLock, devDatabasePath } from "../dev-runtime/dev-database.js";
13
16
  import { ignotumSyncPlugin } from "../dev-runtime/sync.js";
14
17
 
15
- const requiredClientFiles = ["App.tsx", "styles.css"] as const;
16
- const preactSpecifiers = [
17
- "preact/jsx-dev-runtime",
18
- "preact/jsx-runtime",
19
- "preact/devtools",
20
- "preact/debug",
21
- "preact/hooks",
22
- "preact",
23
- ] as const;
24
- const prefreshSpecifiers = ["@prefresh/core", "@prefresh/utils"] as const;
25
-
26
- export class ClientFileNotFound extends Schema.TaggedError<ClientFileNotFound>()(
27
- "ClientFileNotFound",
28
- {
29
- message: Schema.String,
30
- path: Schema.String,
31
- },
32
- ) {}
33
-
34
- export class ClientRuntimeResolutionFailed extends Schema.TaggedError<ClientRuntimeResolutionFailed>()(
35
- "ClientRuntimeResolutionFailed",
36
- {
37
- cause: Schema.Defect(),
38
- message: Schema.String,
39
- },
40
- ) {}
41
-
42
18
  export class ViteStartupFailed extends Schema.TaggedError<ViteStartupFailed>()(
43
19
  "ViteStartupFailed",
44
20
  {
@@ -51,76 +27,20 @@ export interface DevOptions {
51
27
  readonly host: string;
52
28
  readonly open: boolean;
53
29
  readonly port: number;
54
- readonly projectDirectory: string;
30
+ readonly appDirectory: string;
55
31
  }
56
32
 
57
- const validateClientFiles = Effect.fn("Dev.validateClientFiles")(function* (
58
- projectDirectory: string,
33
+ const acquireViteServer = Effect.fn("Dev.acquireViteServer")(function* (
34
+ options: DevOptions,
35
+ clientFiles: ClientFiles,
59
36
  ) {
60
37
  const fileSystem = yield* FileSystem.FileSystem;
61
38
  const path = yield* Path.Path;
62
- const clientDirectory = path.join(projectDirectory, "client");
63
-
64
- yield* Effect.forEach(
65
- requiredClientFiles,
66
- Effect.fn("Dev.validateClientFile")(function* (fileName) {
67
- const filePath = path.join(clientDirectory, fileName);
68
- const exists = yield* fileSystem.exists(filePath);
69
-
70
- if (!exists) {
71
- return yield* ClientFileNotFound.make({
72
- message: `Required client file not found: ${filePath}`,
73
- path: filePath,
74
- });
75
- }
76
- }),
77
- { discard: true },
78
- );
79
- });
80
-
81
- const resolveClientAliases = Effect.fn("Dev.resolveClientAliases")(function* () {
82
- return yield* Effect.try({
83
- try: (): Array<Alias> => {
84
- const prefreshRequire = createRequire(import.meta.resolve("@prefresh/vite"));
85
-
86
- return [
87
- {
88
- find: /^tailwindcss$/,
89
- replacement: normalizePath(fileURLToPath(import.meta.resolve("tailwindcss/index.css"))),
90
- },
91
- ...preactSpecifiers.map((specifier) => ({
92
- find: specifier,
93
- replacement: normalizePath(fileURLToPath(import.meta.resolve(specifier))),
94
- })),
95
- ...prefreshSpecifiers.map((specifier) => ({
96
- find: specifier,
97
- replacement: normalizePath(prefreshRequire.resolve(specifier)),
98
- })),
99
- ];
100
- },
101
- catch: (cause) =>
102
- ClientRuntimeResolutionFailed.make({
103
- cause,
104
- message: "Ignotum could not resolve its client runtime.",
105
- }),
106
- });
107
- });
108
-
109
- const acquireViteServer = Effect.fn("Dev.acquireViteServer")(function* (options: DevOptions) {
110
- const fileSystem = yield* FileSystem.FileSystem;
111
- const path = yield* Path.Path;
112
- const aliases = yield* resolveClientAliases();
113
- const clientDirectory = path.join(options.projectDirectory, "client");
114
- const databasePath = yield* devDatabasePath(options.projectDirectory);
39
+ const runPromise = Effect.runPromiseWith(yield* Effect.context<never>());
40
+ const clientConfig = yield* resolveClientBuildConfig(true);
41
+ const clientDirectory = clientFiles.clientDirectory;
42
+ const databasePath = yield* devDatabasePath(options.appDirectory);
115
43
  const stateDirectory = path.dirname(databasePath);
116
- const sourceClientEntry = fileURLToPath(new URL("../../src/client/index.ts", import.meta.url));
117
- const sourceContractsEntry = fileURLToPath(
118
- new URL("../../../contracts/src/schema/index.ts", import.meta.url),
119
- );
120
- const hasSourceClient = yield* fileSystem.exists(sourceClientEntry);
121
- const hasSourceContracts = yield* fileSystem.exists(sourceContractsEntry);
122
- const hasWorkspaceSourceExports = hasSourceClient && hasSourceContracts;
123
-
124
44
  yield* fileSystem.makeDirectory(stateDirectory, { recursive: true });
125
45
 
126
46
  return yield* Effect.acquireRelease(
@@ -133,8 +53,17 @@ const acquireViteServer = Effect.fn("Dev.acquireViteServer")(function* (options:
133
53
  mode: "development",
134
54
  root: clientDirectory,
135
55
  plugins: [
136
- ignotumSyncPlugin(options.projectDirectory, databasePath),
137
- ignotumClientPlugin(),
56
+ ignotumSyncPlugin(options.appDirectory, databasePath),
57
+ ignotumClientPlugin({
58
+ development: true,
59
+ icon: clientFiles.iconPath === undefined ? undefined : "/icon.svg",
60
+ loadTitle: () =>
61
+ runPromise(
62
+ loadClientEntryTitle(clientFiles.entryPath).pipe(
63
+ Effect.provideService(FileSystem.FileSystem, fileSystem),
64
+ ),
65
+ ),
66
+ }),
138
67
  prefreshPlugin,
139
68
  tailwindcss(),
140
69
  ],
@@ -144,13 +73,19 @@ const acquireViteServer = Effect.fn("Dev.acquireViteServer")(function* (options:
144
73
  runtime: "automatic",
145
74
  },
146
75
  },
147
- resolve: {
148
- alias: aliases,
149
- conditions: [
150
- ...(hasWorkspaceSourceExports ? ["@ignotum/source"] : []),
151
- ...defaultClientConditions,
76
+ optimizeDeps: {
77
+ include: [
78
+ "preact",
79
+ "preact/hooks",
80
+ "preact/jsx-runtime",
81
+ "ignotum/client",
82
+ "ignotum/client/jsx-dev-runtime",
152
83
  ],
153
- dedupe: ["preact"],
84
+ },
85
+ resolve: {
86
+ alias: clientConfig.aliases,
87
+ conditions: [...clientConfig.conditions],
88
+ dedupe: ["preact", "preact/hooks"],
154
89
  tsconfigPaths: true,
155
90
  },
156
91
  server: {
@@ -187,13 +122,13 @@ const listen = Effect.fn("Dev.listen")(function* (server: ViteDevServer) {
187
122
  });
188
123
 
189
124
  export const dev = Effect.fn("Dev.run")(function* (options: DevOptions) {
190
- yield* validateClientFiles(options.projectDirectory);
191
- yield* generate(options.projectDirectory);
125
+ const clientFiles = yield* validateClientFiles(options.appDirectory);
126
+ yield* generate(options.appDirectory);
192
127
 
193
128
  return yield* Effect.scoped(
194
129
  Effect.gen(function* () {
195
- yield* acquireDevDatabaseLock(options.projectDirectory);
196
- const server = yield* acquireViteServer(options);
130
+ yield* acquireDevDatabaseLock(options.appDirectory);
131
+ const server = yield* acquireViteServer(options, clientFiles);
197
132
 
198
133
  yield* listen(server);
199
134
  yield* Effect.sync(() => {
@@ -0,0 +1,183 @@
1
+ import { isBuiltin } from "node:module";
2
+
3
+ import * as Config from "effect/Config";
4
+ import { Effect, FileSystem, Predicate, Schema, String } from "effect";
5
+ import { normalizePath } from "vite";
6
+ import type { Plugin } from "vite";
7
+
8
+ const BuildGraph = Schema.Literals(["Client", "Server"]);
9
+ type BuildGraph = typeof BuildGraph.Type;
10
+ type PackageGraph = BuildGraph | "Shared";
11
+
12
+ interface PackageEntry {
13
+ readonly graph: PackageGraph;
14
+ readonly specifier: string;
15
+ }
16
+
17
+ const packageEntries: ReadonlyArray<PackageEntry> = [
18
+ { graph: "Client", specifier: "ignotum/client" },
19
+ { graph: "Client", specifier: "ignotum/internal/api" },
20
+ { graph: "Server", specifier: "ignotum/server" },
21
+ { graph: "Server", specifier: "ignotum/internal/host" },
22
+ { graph: "Server", specifier: "ignotum/internal/server" },
23
+ { graph: "Shared", specifier: "ignotum/internal/types" },
24
+ ];
25
+
26
+ export class BuildBoundaryViolation extends Schema.TaggedError<BuildBoundaryViolation>()(
27
+ "BuildBoundaryViolation",
28
+ {
29
+ graph: BuildGraph,
30
+ importer: Schema.String,
31
+ message: Schema.String,
32
+ specifier: Schema.String,
33
+ },
34
+ ) {}
35
+
36
+ const cleanModuleId = (id: string): string => normalizePath(id.split("?", 1)[0] ?? id);
37
+
38
+ const caseVariant = (path: string): string | undefined => {
39
+ for (let index = 0; index < path.length; index += 1) {
40
+ const character = path[index];
41
+ if (character === undefined) continue;
42
+ const lower: string = String.toLowerCase(character);
43
+ const upper: string = String.toUpperCase(character);
44
+ if (lower === upper) continue;
45
+ return `${path.slice(0, index)}${character === lower ? upper : lower}${path.slice(index + 1)}`;
46
+ }
47
+ };
48
+
49
+ const isWithin = (id: string, directory: string): boolean =>
50
+ id === directory || id.startsWith(`${directory}/`);
51
+
52
+ const packageGraph = (specifier: string): PackageGraph | undefined => {
53
+ for (const entry of packageEntries) {
54
+ if (specifier === entry.specifier || specifier.startsWith(`${entry.specifier}/`)) {
55
+ return entry.graph;
56
+ }
57
+ }
58
+ };
59
+
60
+ const isForbiddenPackage = (graph: BuildGraph, specifier: string): boolean => {
61
+ const owner = packageGraph(specifier);
62
+ return owner !== undefined && owner !== "Shared" && owner !== graph;
63
+ };
64
+
65
+ export const moduleBoundariesPlugin = Effect.fn("Deploy.moduleBoundariesPlugin")(function* (
66
+ graph: BuildGraph,
67
+ appDirectory: string,
68
+ ) {
69
+ const fileSystem = yield* FileSystem.FileSystem;
70
+ const runPromise = Effect.runPromiseWith(yield* Effect.context<never>());
71
+ const isolatedDiscovery =
72
+ (yield* Config.string("IGNOTUM_ISOLATED_DISCOVERY").pipe(Config.withDefault("0"))) === "1";
73
+ const realAppDirectory = isolatedDiscovery
74
+ ? normalizePath(cleanModuleId(appDirectory))
75
+ : normalizePath(yield* fileSystem.realPath(cleanModuleId(appDirectory)));
76
+ const appCaseVariant = caseVariant(realAppDirectory);
77
+ const caseInsensitive =
78
+ !isolatedDiscovery &&
79
+ appCaseVariant !== undefined &&
80
+ (yield* fileSystem.exists(appCaseVariant));
81
+
82
+ const canonicalModuleId = Effect.fn("Deploy.canonicalModuleId")(function* (id: string) {
83
+ const cleaned = cleanModuleId(id);
84
+ const exists = yield* fileSystem.exists(cleaned);
85
+ const canonical =
86
+ exists && !isolatedDiscovery
87
+ ? normalizePath(yield* fileSystem.realPath(cleaned))
88
+ : normalizePath(cleaned);
89
+ return caseInsensitive ? String.toLowerCase(canonical) : canonical;
90
+ });
91
+
92
+ const normalizedApp = yield* canonicalModuleId(appDirectory);
93
+ const clientDirectory = yield* canonicalModuleId(`${normalizedApp}/client`);
94
+ const serverDirectory = yield* canonicalModuleId(`${normalizedApp}/server`);
95
+ const generatedApi = yield* canonicalModuleId(`${normalizedApp}/_generated/api.ts`);
96
+ const generatedServer = yield* canonicalModuleId(`${normalizedApp}/_generated/server.ts`);
97
+
98
+ const reject = (specifier: string, importer: string, reason: string): never => {
99
+ throw BuildBoundaryViolation.make({
100
+ graph,
101
+ importer,
102
+ message: `${graph.toLowerCase()} build cannot import ${specifier} from ${importer}: ${reason}.`,
103
+ specifier,
104
+ });
105
+ };
106
+
107
+ const validateSpecifier = (specifier: string, importer: string): void => {
108
+ const cleaned = cleanModuleId(specifier);
109
+ if (isBuiltin(cleaned)) reject(specifier, importer, "Node built-ins are unavailable");
110
+ if (cleaned.endsWith(".node")) reject(specifier, importer, "native add-ons are unavailable");
111
+ if (isForbiddenPackage(graph, cleaned)) {
112
+ reject(
113
+ specifier,
114
+ importer,
115
+ `the package entry belongs to the ${graph === "Client" ? "server" : "client"}`,
116
+ );
117
+ }
118
+ };
119
+
120
+ const validateCanonicalId = (cleaned: string, importer: string, specifier: string): void => {
121
+ if (cleaned.endsWith(".node")) {
122
+ reject(specifier, importer, "native add-ons are unavailable");
123
+ }
124
+
125
+ if (graph === "Client" && (isWithin(cleaned, serverDirectory) || cleaned === generatedServer)) {
126
+ reject(specifier, importer, "the resolved module belongs to the server");
127
+ }
128
+
129
+ if (graph === "Server" && (isWithin(cleaned, clientDirectory) || cleaned === generatedApi)) {
130
+ reject(specifier, importer, "the resolved module belongs to the client");
131
+ }
132
+ };
133
+
134
+ const validateResolvedId = (id: string, importer: string, specifier: string): Promise<void> =>
135
+ id.startsWith("\0")
136
+ ? Promise.resolve()
137
+ : runPromise(canonicalModuleId(id)).then((cleaned) =>
138
+ validateCanonicalId(cleaned, importer, specifier),
139
+ );
140
+
141
+ return {
142
+ name: `ignotum:${graph.toLowerCase()}-boundaries`,
143
+ enforce: "pre",
144
+
145
+ moduleParsed: (moduleInfo) =>
146
+ moduleInfo.id.startsWith("\0")
147
+ ? undefined
148
+ : runPromise(canonicalModuleId(moduleInfo.id)).then((cleaned) => {
149
+ validateCanonicalId(cleaned, moduleInfo.id, moduleInfo.id);
150
+ if (
151
+ graph === "Server" &&
152
+ isWithin(cleaned, normalizedApp) &&
153
+ moduleInfo.dynamicallyImportedIds.length > 0
154
+ ) {
155
+ reject(
156
+ moduleInfo.dynamicallyImportedIds[0] ?? "dynamic import",
157
+ moduleInfo.id,
158
+ "dynamic imports are unavailable in server functions",
159
+ );
160
+ }
161
+ }),
162
+
163
+ resolveId: function (specifier, importer, options) {
164
+ const importingModule = importer ?? "<entry>";
165
+ validateSpecifier(specifier, importingModule);
166
+ return this.resolve(specifier, importer, { ...options, skipSelf: true }).then((resolved) => {
167
+ if (resolved === null) return resolved;
168
+ return validateResolvedId(resolved.id, importingModule, specifier).then(() => resolved);
169
+ });
170
+ },
171
+
172
+ resolveDynamicImport: (specifier, importer) => {
173
+ if (graph === "Server") {
174
+ reject(
175
+ Predicate.isString(specifier) ? specifier : "dynamic import",
176
+ importer ?? "<entry>",
177
+ "dynamic imports are unavailable in server functions",
178
+ );
179
+ }
180
+ return null;
181
+ },
182
+ } satisfies Plugin;
183
+ });