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.
- package/README.md +23 -7
- package/dist/cli/bin.mjs +32313 -751
- package/dist/cli/bin.mjs.map +1 -1
- package/dist/runtime/{api-Cpx57mk3.d.ts → api-D2bsAz4T.d.ts} +3 -2
- package/dist/runtime/{api-BXXIZc_Q.js → api-DtX8qPrq.js} +58 -27
- package/dist/runtime/api-DtX8qPrq.js.map +1 -0
- package/dist/runtime/client.d.ts +11 -4
- package/dist/runtime/client.js +343 -79
- package/dist/runtime/client.js.map +1 -1
- package/dist/runtime/{result-B2W-z2wG.js → descriptor-t6BOEGw9-BTHR-ZMv.js} +119 -4
- package/dist/runtime/descriptor-t6BOEGw9-BTHR-ZMv.js.map +1 -0
- package/dist/runtime/id-Btwac71X-B9OeBzvq.d.ts +9 -0
- package/dist/runtime/id-D570vudg.js +26 -0
- package/dist/runtime/id-D570vudg.js.map +1 -0
- package/dist/runtime/{index-BgWROoyk.d.ts → index-Dl_VQGmA.d.ts} +114 -31
- package/dist/runtime/internal/api.d.ts +1 -1
- package/dist/runtime/internal/api.js +1 -1
- package/dist/runtime/internal/host.d.ts +44 -0
- package/dist/runtime/internal/host.js +213 -0
- package/dist/runtime/internal/host.js.map +1 -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 +1 -1
- package/dist/runtime/internal/types.js +1 -1
- package/dist/runtime/{result-C1ZdsM6Y.d.ts → result-BgcHn25t.d.ts} +21 -6
- package/dist/runtime/schema-B-jMZEbs.js +227 -0
- package/dist/runtime/schema-B-jMZEbs.js.map +1 -0
- package/dist/runtime/server.d.ts +2 -2
- package/dist/runtime/server.js +2 -2
- package/package.json +15 -5
- package/src/cli/agent-files.ts +6 -2
- package/src/cli/app-configuration.ts +135 -0
- package/src/cli/bin.ts +24 -1
- package/src/cli/build/artifact.ts +95 -0
- package/src/cli/build/client.ts +103 -0
- package/src/cli/build/server.ts +526 -0
- package/src/cli/client-config.ts +141 -0
- package/src/cli/client-entry.ts +152 -0
- package/src/cli/client-plugin.ts +86 -16
- package/src/cli/codegen.ts +5 -5
- package/src/cli/command.ts +41 -9
- package/src/cli/control-client.ts +343 -0
- package/src/cli/deploy.ts +217 -0
- package/src/cli/dev.ts +43 -108
- package/src/cli/module-boundaries.ts +183 -0
- package/src/cli/{new-project.ts → new-app.ts} +86 -86
- package/src/cli/package-manager.ts +9 -9
- package/src/client/app.ts +15 -0
- package/src/client/errors.ts +1 -17
- package/src/client/index.ts +2 -0
- package/src/client/sync.ts +172 -120
- package/src/dev-runtime/database.ts +253 -96
- package/src/dev-runtime/dev-database.ts +14 -14
- package/src/dev-runtime/functions.ts +57 -88
- package/src/dev-runtime/id.ts +2 -10
- package/src/dev-runtime/migrations.ts +68 -0
- package/src/dev-runtime/sync.ts +266 -83
- package/src/internal/function-definition.ts +77 -0
- package/src/internal/host.ts +1 -0
- package/src/internal/http-paths.ts +3 -1
- package/dist/runtime/api-BXXIZc_Q.js.map +0 -1
- package/dist/runtime/result-B2W-z2wG.js.map +0 -1
- package/dist/runtime/schema-CNEVLF7D.js +0 -116
- package/dist/runtime/schema-CNEVLF7D.js.map +0 -1
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
|
|
4
|
+
import { Effect, FileSystem, Path, Schema } from "effect";
|
|
5
|
+
import { defaultClientConditions, normalizePath } from "vite";
|
|
6
|
+
import type { Alias } from "vite";
|
|
7
|
+
|
|
8
|
+
const clientEntryFileName = "index.tsx";
|
|
9
|
+
const clientIconFileName = "icon.svg";
|
|
10
|
+
const preactSpecifiers = [
|
|
11
|
+
"preact/jsx-dev-runtime",
|
|
12
|
+
"preact/jsx-runtime",
|
|
13
|
+
"preact/devtools",
|
|
14
|
+
"preact/debug",
|
|
15
|
+
"preact/hooks",
|
|
16
|
+
"preact",
|
|
17
|
+
] as const;
|
|
18
|
+
const prefreshSpecifiers = ["@prefresh/core", "@prefresh/utils"] as const;
|
|
19
|
+
|
|
20
|
+
export class ClientFileNotFound extends Schema.TaggedError<ClientFileNotFound>()(
|
|
21
|
+
"ClientFileNotFound",
|
|
22
|
+
{
|
|
23
|
+
message: Schema.String,
|
|
24
|
+
path: Schema.String,
|
|
25
|
+
},
|
|
26
|
+
) {}
|
|
27
|
+
|
|
28
|
+
export class InvalidClientFile extends Schema.TaggedError<InvalidClientFile>()(
|
|
29
|
+
"InvalidClientFile",
|
|
30
|
+
{
|
|
31
|
+
message: Schema.String,
|
|
32
|
+
path: Schema.String,
|
|
33
|
+
},
|
|
34
|
+
) {}
|
|
35
|
+
|
|
36
|
+
export class ClientRuntimeResolutionFailed extends Schema.TaggedError<ClientRuntimeResolutionFailed>()(
|
|
37
|
+
"ClientRuntimeResolutionFailed",
|
|
38
|
+
{
|
|
39
|
+
cause: Schema.Defect(),
|
|
40
|
+
message: Schema.String,
|
|
41
|
+
},
|
|
42
|
+
) {}
|
|
43
|
+
|
|
44
|
+
export interface ClientBuildConfig {
|
|
45
|
+
readonly aliases: ReadonlyArray<Alias>;
|
|
46
|
+
readonly conditions: ReadonlyArray<string>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface ClientFiles {
|
|
50
|
+
readonly clientDirectory: string;
|
|
51
|
+
readonly entryPath: string;
|
|
52
|
+
readonly iconPath: string | undefined;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const validateClientFiles = Effect.fn("ClientConfig.validateFiles")(function* (
|
|
56
|
+
appDirectory: string,
|
|
57
|
+
) {
|
|
58
|
+
const fileSystem = yield* FileSystem.FileSystem;
|
|
59
|
+
const path = yield* Path.Path;
|
|
60
|
+
const clientDirectory = path.join(appDirectory, "client");
|
|
61
|
+
const entryPath = path.join(clientDirectory, clientEntryFileName);
|
|
62
|
+
if (!(yield* fileSystem.exists(entryPath))) {
|
|
63
|
+
return yield* ClientFileNotFound.make({
|
|
64
|
+
message: `Required client file not found: ${entryPath}`,
|
|
65
|
+
path: entryPath,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
const entryInfo = yield* fileSystem.stat(entryPath);
|
|
69
|
+
if (entryInfo.type !== "File") {
|
|
70
|
+
return yield* InvalidClientFile.make({
|
|
71
|
+
message: `Required client entry is not a regular file: ${entryPath}`,
|
|
72
|
+
path: entryPath,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const iconPath = path.join(clientDirectory, clientIconFileName);
|
|
77
|
+
const hasIcon = yield* fileSystem.exists(iconPath);
|
|
78
|
+
if (hasIcon) {
|
|
79
|
+
const iconInfo = yield* fileSystem.stat(iconPath);
|
|
80
|
+
if (iconInfo.type !== "File") {
|
|
81
|
+
return yield* InvalidClientFile.make({
|
|
82
|
+
message: `Optional client icon is not a regular file: ${iconPath}`,
|
|
83
|
+
path: iconPath,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
clientDirectory,
|
|
90
|
+
entryPath,
|
|
91
|
+
iconPath: hasIcon ? iconPath : undefined,
|
|
92
|
+
} satisfies ClientFiles;
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
export const resolveClientBuildConfig = Effect.fn("ClientConfig.resolve")(function* (
|
|
96
|
+
includePrefresh: boolean,
|
|
97
|
+
) {
|
|
98
|
+
const fileSystem = yield* FileSystem.FileSystem;
|
|
99
|
+
const sourceClientEntry = fileURLToPath(new URL("../../src/client/index.ts", import.meta.url));
|
|
100
|
+
const sourceContractsEntry = fileURLToPath(
|
|
101
|
+
new URL("../../../contracts/src/schema/index.ts", import.meta.url),
|
|
102
|
+
);
|
|
103
|
+
const hasSourceClient = yield* fileSystem.exists(sourceClientEntry);
|
|
104
|
+
const hasSourceContracts = yield* fileSystem.exists(sourceContractsEntry);
|
|
105
|
+
const hasWorkspaceSourceExports = hasSourceClient && hasSourceContracts;
|
|
106
|
+
|
|
107
|
+
return yield* Effect.try({
|
|
108
|
+
try: (): ClientBuildConfig => {
|
|
109
|
+
const prefreshRequire = createRequire(import.meta.resolve("@prefresh/vite"));
|
|
110
|
+
const aliases: ReadonlyArray<Alias> = [
|
|
111
|
+
{
|
|
112
|
+
find: /^tailwindcss$/,
|
|
113
|
+
replacement: normalizePath(fileURLToPath(import.meta.resolve("tailwindcss/index.css"))),
|
|
114
|
+
},
|
|
115
|
+
...preactSpecifiers.map((specifier) => ({
|
|
116
|
+
find: specifier,
|
|
117
|
+
replacement: normalizePath(fileURLToPath(import.meta.resolve(specifier))),
|
|
118
|
+
})),
|
|
119
|
+
...(includePrefresh
|
|
120
|
+
? prefreshSpecifiers.map((specifier) => ({
|
|
121
|
+
find: specifier,
|
|
122
|
+
replacement: normalizePath(prefreshRequire.resolve(specifier)),
|
|
123
|
+
}))
|
|
124
|
+
: []),
|
|
125
|
+
];
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
aliases,
|
|
129
|
+
conditions: [
|
|
130
|
+
...(hasWorkspaceSourceExports ? ["@ignotum/source"] : []),
|
|
131
|
+
...defaultClientConditions,
|
|
132
|
+
],
|
|
133
|
+
};
|
|
134
|
+
},
|
|
135
|
+
catch: (cause) =>
|
|
136
|
+
ClientRuntimeResolutionFailed.make({
|
|
137
|
+
cause,
|
|
138
|
+
message: "Ignotum could not resolve its client runtime.",
|
|
139
|
+
}),
|
|
140
|
+
});
|
|
141
|
+
});
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { Effect, FileSystem, Predicate, Result, Schema } from "effect";
|
|
2
|
+
import { parseSync, type Node, type ObjectProperty, type ObjectPropertyKind } from "oxc-parser";
|
|
3
|
+
|
|
4
|
+
export class InvalidClientEntry extends Schema.TaggedError<InvalidClientEntry>()(
|
|
5
|
+
"InvalidClientEntry",
|
|
6
|
+
{
|
|
7
|
+
column: Schema.Int,
|
|
8
|
+
line: Schema.Int,
|
|
9
|
+
message: Schema.String,
|
|
10
|
+
path: Schema.String,
|
|
11
|
+
},
|
|
12
|
+
) {}
|
|
13
|
+
|
|
14
|
+
export interface ClientEntrySource {
|
|
15
|
+
readonly path: string;
|
|
16
|
+
readonly source: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const requiredForm = 'export default app({ title: "My App", component: App });';
|
|
20
|
+
|
|
21
|
+
const location = (source: string, offset: number) => {
|
|
22
|
+
const prefix = source.slice(0, offset);
|
|
23
|
+
const line = prefix.split("\n").length;
|
|
24
|
+
const lastNewline = prefix.lastIndexOf("\n");
|
|
25
|
+
return { column: offset - lastNewline, line };
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const invalid = (entry: ClientEntrySource, node: Pick<Node, "start">, detail: string) => {
|
|
29
|
+
const position = location(entry.source, node.start);
|
|
30
|
+
return InvalidClientEntry.make({
|
|
31
|
+
...position,
|
|
32
|
+
message: `${entry.path}:${position.line}:${position.column}: ${detail} Required form: ${requiredForm}`,
|
|
33
|
+
path: entry.path,
|
|
34
|
+
});
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const isNamedProperty = (property: ObjectProperty, name: string): boolean =>
|
|
38
|
+
property.kind === "init" &&
|
|
39
|
+
!property.computed &&
|
|
40
|
+
!property.method &&
|
|
41
|
+
!property.shorthand &&
|
|
42
|
+
property.key.type === "Identifier" &&
|
|
43
|
+
property.key.name === name;
|
|
44
|
+
|
|
45
|
+
const isObjectProperty = (property: ObjectPropertyKind): property is ObjectProperty =>
|
|
46
|
+
property.type === "Property";
|
|
47
|
+
|
|
48
|
+
export const extractClientEntryTitle = (
|
|
49
|
+
entry: ClientEntrySource,
|
|
50
|
+
): Result.Result<string, InvalidClientEntry> => {
|
|
51
|
+
const parsed = parseSync(entry.path, entry.source, {
|
|
52
|
+
lang: "tsx",
|
|
53
|
+
preserveParens: true,
|
|
54
|
+
showSemanticErrors: true,
|
|
55
|
+
sourceType: "module",
|
|
56
|
+
});
|
|
57
|
+
const parserError = parsed.errors[0];
|
|
58
|
+
if (parserError !== undefined) {
|
|
59
|
+
const label = parserError.labels[0];
|
|
60
|
+
return Result.fail(
|
|
61
|
+
invalid(
|
|
62
|
+
entry,
|
|
63
|
+
{ start: label?.start ?? 0 },
|
|
64
|
+
`The client entry is invalid: ${parserError.message}.`,
|
|
65
|
+
),
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const importsApp = parsed.program.body.some(
|
|
70
|
+
(statement) =>
|
|
71
|
+
statement.type === "ImportDeclaration" &&
|
|
72
|
+
statement.source.value === "ignotum/client" &&
|
|
73
|
+
statement.importKind !== "type" &&
|
|
74
|
+
statement.specifiers.some(
|
|
75
|
+
(specifier) =>
|
|
76
|
+
specifier.type === "ImportSpecifier" &&
|
|
77
|
+
specifier.importKind !== "type" &&
|
|
78
|
+
specifier.imported.type === "Identifier" &&
|
|
79
|
+
specifier.imported.name === "app" &&
|
|
80
|
+
specifier.local.name === "app",
|
|
81
|
+
),
|
|
82
|
+
);
|
|
83
|
+
if (!importsApp) {
|
|
84
|
+
return Result.fail(
|
|
85
|
+
invalid(entry, parsed.program, 'Import { app } from "ignotum/client" without an alias.'),
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const defaultExports = parsed.program.body.filter(
|
|
90
|
+
(statement) => statement.type === "ExportDefaultDeclaration",
|
|
91
|
+
);
|
|
92
|
+
const defaultExport = defaultExports[0];
|
|
93
|
+
if (defaultExports.length !== 1 || defaultExport === undefined) {
|
|
94
|
+
return Result.fail(
|
|
95
|
+
invalid(entry, parsed.program, "The client entry needs one default export."),
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
const call = defaultExport.declaration;
|
|
99
|
+
if (
|
|
100
|
+
call.type !== "CallExpression" ||
|
|
101
|
+
call.optional ||
|
|
102
|
+
call.typeArguments != null ||
|
|
103
|
+
call.callee.type !== "Identifier" ||
|
|
104
|
+
call.callee.name !== "app" ||
|
|
105
|
+
call.arguments.length !== 1
|
|
106
|
+
) {
|
|
107
|
+
return Result.fail(invalid(entry, defaultExport, "The default export must be an app call."));
|
|
108
|
+
}
|
|
109
|
+
const definition = call.arguments[0];
|
|
110
|
+
if (definition?.type !== "ObjectExpression" || definition.properties.length !== 2) {
|
|
111
|
+
return Result.fail(
|
|
112
|
+
invalid(entry, call, "The app call needs exactly title and component properties."),
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
const properties = definition.properties;
|
|
116
|
+
if (!properties.every(isObjectProperty)) {
|
|
117
|
+
return Result.fail(invalid(entry, definition, "Spread properties are not supported."));
|
|
118
|
+
}
|
|
119
|
+
const titleProperties = properties.filter((property) => isNamedProperty(property, "title"));
|
|
120
|
+
const componentProperties = properties.filter((property) =>
|
|
121
|
+
isNamedProperty(property, "component"),
|
|
122
|
+
);
|
|
123
|
+
if (titleProperties.length !== 1 || componentProperties.length !== 1) {
|
|
124
|
+
return Result.fail(
|
|
125
|
+
invalid(entry, definition, "Use unquoted title and component properties with no extras."),
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
const titleProperty = titleProperties[0];
|
|
129
|
+
if (titleProperty === undefined) {
|
|
130
|
+
return Result.fail(invalid(entry, definition, "The title property is missing."));
|
|
131
|
+
}
|
|
132
|
+
const title = titleProperty.value;
|
|
133
|
+
if (
|
|
134
|
+
title.type !== "Literal" ||
|
|
135
|
+
!Predicate.isString(title.value) ||
|
|
136
|
+
(title.raw?.startsWith('"') !== true && title.raw?.startsWith("'") !== true)
|
|
137
|
+
) {
|
|
138
|
+
return Result.fail(invalid(entry, title, "The title must be a quoted string literal."));
|
|
139
|
+
}
|
|
140
|
+
if (title.value.trim().length === 0) {
|
|
141
|
+
return Result.fail(invalid(entry, title, "The title must contain a non-whitespace character."));
|
|
142
|
+
}
|
|
143
|
+
return Result.succeed(title.value);
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
export const loadClientEntryTitle = Effect.fn("ClientEntry.loadTitle")(function* (path: string) {
|
|
147
|
+
const fileSystem = yield* FileSystem.FileSystem;
|
|
148
|
+
const source = yield* fileSystem.readFileString(path);
|
|
149
|
+
const extracted = extractClientEntryTitle({ path, source });
|
|
150
|
+
if (Result.isFailure(extracted)) return yield* extracted.failure;
|
|
151
|
+
return extracted.success;
|
|
152
|
+
});
|
package/src/cli/client-plugin.ts
CHANGED
|
@@ -1,29 +1,70 @@
|
|
|
1
1
|
import type { Plugin } from "vite";
|
|
2
2
|
|
|
3
|
-
const clientEntryId = "virtual:ignotum/client-entry";
|
|
3
|
+
export const clientEntryId = "virtual:ignotum/client-entry";
|
|
4
4
|
const resolvedClientEntryId = `\0${clientEntryId}`;
|
|
5
|
+
export const clientStylesId = "virtual:ignotum/_styles.css";
|
|
6
|
+
const resolvedClientStylesId = `\0${clientStylesId}`;
|
|
7
|
+
export const clientStylesSource = '@import "tailwindcss";\n';
|
|
5
8
|
|
|
6
|
-
|
|
9
|
+
interface ExternalClientScript {
|
|
10
|
+
readonly source: string;
|
|
11
|
+
readonly type: "External";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface InlineClientScript {
|
|
15
|
+
readonly source: string;
|
|
16
|
+
readonly type: "Inline";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface ClientDocumentOptions {
|
|
20
|
+
readonly icon: string | undefined;
|
|
21
|
+
readonly scripts: ReadonlyArray<ExternalClientScript | InlineClientScript>;
|
|
22
|
+
readonly styles: ReadonlyArray<string>;
|
|
23
|
+
readonly title: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ClientPluginOptions {
|
|
27
|
+
readonly development: boolean;
|
|
28
|
+
readonly icon: string | undefined;
|
|
29
|
+
readonly loadTitle: (() => Promise<string>) | undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const escapeHtml = (value: string): string =>
|
|
33
|
+
value
|
|
34
|
+
.replaceAll("&", "&")
|
|
35
|
+
.replaceAll("<", "<")
|
|
36
|
+
.replaceAll(">", ">")
|
|
37
|
+
.replaceAll('"', """)
|
|
38
|
+
.replaceAll("'", "'");
|
|
39
|
+
|
|
40
|
+
export const renderClientDocument = (options: ClientDocumentOptions): string => `<!doctype html>
|
|
7
41
|
<html lang="en">
|
|
8
42
|
<head>
|
|
9
43
|
<meta charset="UTF-8" />
|
|
10
44
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
11
|
-
<title
|
|
45
|
+
<title>${escapeHtml(options.title)}</title>
|
|
46
|
+
${options.icon === undefined ? "" : ` <link rel="icon" type="image/svg+xml" href="${escapeHtml(options.icon)}" />`}
|
|
47
|
+
${options.styles.map((href) => ` <link rel="stylesheet" crossorigin href="/${escapeHtml(href)}" />`).join("\n")}
|
|
12
48
|
</head>
|
|
13
49
|
<body>
|
|
14
50
|
<div id="app"></div>
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
51
|
+
${options.scripts
|
|
52
|
+
.map((script) =>
|
|
53
|
+
script.type === "External"
|
|
54
|
+
? ` <script type="module" crossorigin src="/${escapeHtml(script.source)}"></script>`
|
|
55
|
+
: ` <script type="module">\n ${script.source}\n </script>`,
|
|
56
|
+
)
|
|
57
|
+
.join("\n")}
|
|
18
58
|
</body>
|
|
19
59
|
</html>`;
|
|
20
60
|
|
|
21
|
-
const
|
|
22
|
-
|
|
61
|
+
export const renderClientEntry = (
|
|
62
|
+
development: boolean,
|
|
63
|
+
): string => `${development ? 'import "preact/debug"\n' : ""}import { render } from "preact"
|
|
23
64
|
import { jsx } from "preact/jsx-runtime"
|
|
24
65
|
|
|
25
|
-
import "
|
|
26
|
-
import
|
|
66
|
+
import "${clientStylesId}"
|
|
67
|
+
import application from "/index.tsx"
|
|
27
68
|
|
|
28
69
|
const root = document.getElementById("app")
|
|
29
70
|
|
|
@@ -31,10 +72,23 @@ if (root === null) {
|
|
|
31
72
|
throw new Error("Ignotum mount element is missing.")
|
|
32
73
|
}
|
|
33
74
|
|
|
34
|
-
|
|
75
|
+
document.title = application.title
|
|
76
|
+
render(jsx(application.component, {}), root)
|
|
35
77
|
`;
|
|
36
78
|
|
|
37
|
-
|
|
79
|
+
const loadClientModule = (id: string, development: boolean): string | undefined => {
|
|
80
|
+
if (id === resolvedClientEntryId) return renderClientEntry(development);
|
|
81
|
+
if (id === resolvedClientStylesId) return clientStylesSource;
|
|
82
|
+
return undefined;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export const resolveClientModule = (id: string): string | undefined => {
|
|
86
|
+
if (id === clientEntryId) return resolvedClientEntryId;
|
|
87
|
+
if (id === clientStylesId) return resolvedClientStylesId;
|
|
88
|
+
return undefined;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export const ignotumClientPlugin = (options: ClientPluginOptions): Plugin => ({
|
|
38
92
|
name: "ignotum:client",
|
|
39
93
|
|
|
40
94
|
configureServer: (server) => {
|
|
@@ -49,8 +103,24 @@ export const ignotumClientPlugin = (): Plugin => ({
|
|
|
49
103
|
|
|
50
104
|
const requestUrl = request.originalUrl ?? request.url ?? "/";
|
|
51
105
|
|
|
52
|
-
|
|
53
|
-
|
|
106
|
+
const loadTitle = options.loadTitle;
|
|
107
|
+
if (loadTitle === undefined) {
|
|
108
|
+
next(new Error("Ignotum's development title loader is missing."));
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
void loadTitle()
|
|
113
|
+
.then((title) =>
|
|
114
|
+
server.transformIndexHtml(
|
|
115
|
+
requestUrl,
|
|
116
|
+
renderClientDocument({
|
|
117
|
+
icon: options.icon,
|
|
118
|
+
scripts: [{ source: `import "${clientEntryId}"`, type: "Inline" }],
|
|
119
|
+
styles: [],
|
|
120
|
+
title,
|
|
121
|
+
}),
|
|
122
|
+
),
|
|
123
|
+
)
|
|
54
124
|
.then((html) => {
|
|
55
125
|
response.statusCode = 200;
|
|
56
126
|
response.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
@@ -60,7 +130,7 @@ export const ignotumClientPlugin = (): Plugin => ({
|
|
|
60
130
|
});
|
|
61
131
|
},
|
|
62
132
|
|
|
63
|
-
load: (id) => (id
|
|
133
|
+
load: (id) => loadClientModule(id, options.development),
|
|
64
134
|
|
|
65
|
-
resolveId:
|
|
135
|
+
resolveId: resolveClientModule,
|
|
66
136
|
});
|
package/src/cli/codegen.ts
CHANGED
|
@@ -152,10 +152,10 @@ const writeOutput = Effect.fn("Codegen.writeOutput")(function* (output: Generate
|
|
|
152
152
|
);
|
|
153
153
|
});
|
|
154
154
|
|
|
155
|
-
export const generate = Effect.fn("Codegen.generate")(function* (
|
|
155
|
+
export const generate = Effect.fn("Codegen.generate")(function* (appDirectory: string) {
|
|
156
156
|
const fileSystem = yield* FileSystem.FileSystem;
|
|
157
157
|
const path = yield* Path.Path;
|
|
158
|
-
const serverDirectory = path.join(
|
|
158
|
+
const serverDirectory = path.join(appDirectory, "server");
|
|
159
159
|
const schemaPath = path.join(serverDirectory, "schema.ts");
|
|
160
160
|
const schemaExists = yield* fileSystem.exists(schemaPath);
|
|
161
161
|
|
|
@@ -174,15 +174,15 @@ export const generate = Effect.fn("Codegen.generate")(function* (projectDirector
|
|
|
174
174
|
const outputs: ReadonlyArray<GeneratedOutput> = [
|
|
175
175
|
{
|
|
176
176
|
content: renderServerBindings(),
|
|
177
|
-
path: path.join(
|
|
177
|
+
path: path.join(appDirectory, "_generated", "server.ts"),
|
|
178
178
|
},
|
|
179
179
|
{
|
|
180
180
|
content: renderApi(functionModules),
|
|
181
|
-
path: path.join(
|
|
181
|
+
path: path.join(appDirectory, "_generated", "api.ts"),
|
|
182
182
|
},
|
|
183
183
|
{
|
|
184
184
|
content: renderTypes(),
|
|
185
|
-
path: path.join(
|
|
185
|
+
path: path.join(appDirectory, "_generated", "types.ts"),
|
|
186
186
|
},
|
|
187
187
|
];
|
|
188
188
|
const inspections = yield* Effect.forEach(outputs, inspectOutput);
|
package/src/cli/command.ts
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
|
-
import { NodeRuntime, NodeServices } from "@effect/platform-node";
|
|
2
|
-
import { Effect, Path, Schema, Terminal } from "effect";
|
|
1
|
+
import { NodeHttpClient, NodeRuntime, NodeServices } from "@effect/platform-node";
|
|
2
|
+
import { Effect, Layer, Option, Path, Schema, Terminal } from "effect";
|
|
3
3
|
import { Argument, CliError, Command, Flag } from "effect/unstable/cli";
|
|
4
|
+
import { AppSlug } from "@ignotum/contracts/control";
|
|
5
|
+
import { IdGenerator } from "@ignotum/shared/id";
|
|
4
6
|
|
|
5
7
|
import packageJson from "../../package.json" with { type: "json" };
|
|
6
8
|
import { generate } from "./codegen.js";
|
|
9
|
+
import { ControlClient } from "./control-client.js";
|
|
10
|
+
import { deploy as runDeploy } from "./deploy.js";
|
|
7
11
|
import { resetDevDatabase } from "../dev-runtime/dev-database.js";
|
|
8
12
|
import { dev as runDev } from "./dev.js";
|
|
9
|
-
import {
|
|
13
|
+
import { createApp } from "./new-app.js";
|
|
10
14
|
import { installDependencies } from "./package-manager.js";
|
|
11
15
|
|
|
12
16
|
const DevPort = Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 65_535 }));
|
|
@@ -67,7 +71,7 @@ const dev = Command.make(
|
|
|
67
71
|
host,
|
|
68
72
|
open,
|
|
69
73
|
port,
|
|
70
|
-
|
|
74
|
+
appDirectory: path.resolve("."),
|
|
71
75
|
}).pipe(
|
|
72
76
|
Effect.mapError((error) =>
|
|
73
77
|
CliError.UserError.make({ cause: error, userMessage: error.message }),
|
|
@@ -79,6 +83,32 @@ const dev = Command.make(
|
|
|
79
83
|
Command.withSubcommands([devDatabase]),
|
|
80
84
|
);
|
|
81
85
|
|
|
86
|
+
const deploy = Command.make(
|
|
87
|
+
"deploy",
|
|
88
|
+
{
|
|
89
|
+
app: Flag.string("app").pipe(
|
|
90
|
+
Flag.withDescription("Create or link this app slug on the first deploy."),
|
|
91
|
+
Flag.withSchema(AppSlug),
|
|
92
|
+
Flag.optional,
|
|
93
|
+
),
|
|
94
|
+
},
|
|
95
|
+
Effect.fn("deploy")(function* ({ app }) {
|
|
96
|
+
const path = yield* Path.Path;
|
|
97
|
+
const terminal = yield* Terminal.Terminal;
|
|
98
|
+
const result = yield* runDeploy(path.resolve("."), Option.getOrUndefined(app)).pipe(
|
|
99
|
+
// @effect-diagnostics-next-line strictEffectProvide:off The deploy subcommand owns this client layer for its full lifetime.
|
|
100
|
+
Effect.provide(ControlClient.layer),
|
|
101
|
+
Effect.mapError((error) =>
|
|
102
|
+
CliError.UserError.make({ cause: error, userMessage: error.message }),
|
|
103
|
+
),
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
yield* terminal.display(
|
|
107
|
+
`Deployed ${result.clientAssets} client artifact${result.clientAssets === 1 ? "" : "s"} and ${result.serverFunctions} server function${result.serverFunctions === 1 ? "" : "s"}.\n\nApp: ${result.appUrl}\nDeployment: ${result.deploymentId}\n`,
|
|
108
|
+
);
|
|
109
|
+
}),
|
|
110
|
+
).pipe(Command.withDescription("Build and deploy the application to Ignotum Cloud."));
|
|
111
|
+
|
|
82
112
|
const install = Command.make(
|
|
83
113
|
"install",
|
|
84
114
|
{},
|
|
@@ -95,7 +125,7 @@ const install = Command.make(
|
|
|
95
125
|
}),
|
|
96
126
|
).pipe(Command.withDescription("Install dependencies with pnpm, or npm when pnpm is unavailable."));
|
|
97
127
|
|
|
98
|
-
const
|
|
128
|
+
const newApp = Command.make(
|
|
99
129
|
"new",
|
|
100
130
|
{
|
|
101
131
|
directory: Argument.string("directory").pipe(
|
|
@@ -114,7 +144,7 @@ const newProject = Command.make(
|
|
|
114
144
|
const path = yield* Path.Path;
|
|
115
145
|
const terminal = yield* Terminal.Terminal;
|
|
116
146
|
const currentDirectory = path.resolve(".");
|
|
117
|
-
const result = yield*
|
|
147
|
+
const result = yield* createApp({ currentDirectory, directory, git, install }).pipe(
|
|
118
148
|
Effect.mapError((error) =>
|
|
119
149
|
CliError.UserError.make({ cause: error, userMessage: error.message }),
|
|
120
150
|
),
|
|
@@ -124,7 +154,7 @@ const newProject = Command.make(
|
|
|
124
154
|
const installCommand = result.packageManager === null ? " npx ignotum install\n" : "";
|
|
125
155
|
|
|
126
156
|
yield* terminal.display(
|
|
127
|
-
`Created ${result.
|
|
157
|
+
`Created ${result.appName} in ${result.directory}.\n\nNext steps:\n${changeDirectory}${installCommand} npx ignotum dev\n`,
|
|
128
158
|
);
|
|
129
159
|
}),
|
|
130
160
|
).pipe(
|
|
@@ -142,14 +172,16 @@ const newProject = Command.make(
|
|
|
142
172
|
);
|
|
143
173
|
|
|
144
174
|
const ignotum = Command.make("ignotum").pipe(
|
|
145
|
-
Command.withSubcommands([
|
|
175
|
+
Command.withSubcommands([newApp, install, codegen, dev, deploy]),
|
|
146
176
|
);
|
|
147
177
|
|
|
148
178
|
export const main = (): void => {
|
|
149
179
|
ignotum.pipe(
|
|
150
180
|
Command.run({ version: packageJson.version }),
|
|
151
181
|
// @effect-diagnostics-next-line strictEffectProvide:off
|
|
152
|
-
Effect.provide(
|
|
182
|
+
Effect.provide(
|
|
183
|
+
Layer.mergeAll(NodeServices.layer, NodeHttpClient.layerUndici, IdGenerator.layer),
|
|
184
|
+
),
|
|
153
185
|
NodeRuntime.runMain,
|
|
154
186
|
);
|
|
155
187
|
};
|