@lukoweb/apitogo 0.2.30 → 0.2.34

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 (44) hide show
  1. package/dist/cli/cli.js +794 -248
  2. package/dist/declarations/app/entry.server.d.ts +2 -2
  3. package/dist/declarations/app/main.d.ts +3 -1
  4. package/dist/declarations/cli/platform/client/platform-api.d.ts +16 -5
  5. package/dist/declarations/cli/platform/helpers/deploy-artifact-hash.d.ts +1 -1
  6. package/dist/declarations/cli/platform/helpers/package-files.d.ts +1 -1
  7. package/dist/declarations/cli/platform/helpers/platform-plan.d.ts +1 -1
  8. package/dist/declarations/cli/platform/helpers/zip.d.ts +1 -1
  9. package/dist/declarations/cli/platform/tools/infrastructure.d.ts +0 -4
  10. package/dist/declarations/config/effective-redirects.d.ts +6 -0
  11. package/dist/declarations/config/header-navigation-guard.d.ts +2 -0
  12. package/dist/declarations/config/resolve-modules.d.ts +1 -4
  13. package/dist/declarations/config/validators/HeaderNavigationSchema.d.ts +1 -2
  14. package/docs/cli/commands.md +151 -75
  15. package/docs/cli/go-live.md +135 -123
  16. package/docs/cli/walkthrough.md +40 -1
  17. package/docs/configuration/gateway.md +52 -0
  18. package/docs/guides/customer-account.md +33 -2
  19. package/docs/mcp/tools.md +101 -102
  20. package/package.json +2 -1
  21. package/src/app/demo.tsx +78 -77
  22. package/src/app/entry.client.tsx +115 -107
  23. package/src/app/entry.server.tsx +164 -160
  24. package/src/app/main.tsx +175 -167
  25. package/src/app/standalone.tsx +61 -60
  26. package/src/config/effective-redirects.ts +31 -0
  27. package/src/config/header-navigation-guard.ts +13 -0
  28. package/src/config/resolve-modules.ts +331 -354
  29. package/src/config/v2/home-redirect.ts +104 -98
  30. package/src/config/v2/index.ts +127 -126
  31. package/src/config/v2/ledger.ts +242 -235
  32. package/src/config/v2/resolve.ts +12 -4
  33. package/src/config/v2/rules.ts +716 -714
  34. package/src/config/v2/services.ts +799 -663
  35. package/src/config/v2/workspace.ts +17 -1
  36. package/src/config/validators/HeaderNavigationSchema.ts +42 -44
  37. package/src/lib/assets/powered-by-icon.png +0 -0
  38. package/src/lib/authentication/providers/dev-portal-auth-pages.tsx +667 -602
  39. package/src/lib/authentication/providers/dev-portal.tsx +476 -448
  40. package/src/lib/components/HeaderNavigation.tsx +176 -176
  41. package/src/lib/components/MobileTopNavigation.tsx +283 -283
  42. package/src/lib/components/navigation/PoweredByZudoku.tsx +42 -39
  43. package/src/vite/plugin-offroot-resolve.ts +68 -67
  44. package/src/vite/prerender/prerender.ts +242 -242
@@ -1,235 +1,242 @@
1
- import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
2
- import path from "node:path";
3
- import { ApitogoError } from "../../lib/util/invariant.js";
4
- import type { StatePaths } from "./paths.js";
5
-
6
- /** Version of the `state.json` format this build reads and writes. A newer file is refused, never overwritten. */
7
- export const LEDGER_VERSION = 1;
8
-
9
- export type StepStatus = "running" | "succeeded" | "failed";
10
-
11
- /** One step of one service, for example `portal` / `build`. */
12
- export type StepRecord = {
13
- status: StepStatus;
14
- /** Hash of the inputs the step ran with. An equal hash means the result is still valid. */
15
- hash?: string;
16
- startedAt: string;
17
- finishedAt?: string;
18
- /** Why the step failed. */
19
- error?: string;
20
- /** Public facts the step produced, such as a deployed URL. */
21
- output?: Record<string, string>;
22
- };
23
-
24
- export type Ledger = {
25
- version: typeof LEDGER_VERSION;
26
- services: Record<string, { steps: Record<string, StepRecord> }>;
27
- };
28
-
29
- export class LedgerError extends ApitogoError {
30
- constructor(
31
- public readonly code: "LEDGER_CORRUPT" | "LEDGER_VERSION_UNSUPPORTED",
32
- message: string,
33
- developerHint?: string,
34
- ) {
35
- super(message, { developerHint, title: "Cannot read the project state" });
36
- this.name = "LedgerError";
37
- }
38
- }
39
-
40
- export const emptyLedger = (): Ledger => ({
41
- version: LEDGER_VERSION,
42
- services: {},
43
- });
44
-
45
- /** Reads `state.json`. A missing file is an empty ledger. A corrupt or newer file raises a {@link LedgerError}. */
46
- export async function readLedger(
47
- state: Pick<StatePaths, "file">,
48
- ): Promise<Ledger> {
49
- let text: string;
50
- try {
51
- text = await readFile(state.file, "utf8");
52
- } catch (error) {
53
- if ((error as NodeJS.ErrnoException).code === "ENOENT")
54
- return emptyLedger();
55
- throw error;
56
- }
57
-
58
- let raw: unknown;
59
- try {
60
- raw = JSON.parse(text);
61
- } catch {
62
- throw corrupt(state.file, "is not valid JSON");
63
- }
64
- if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
65
- throw corrupt(state.file, "is not an object");
66
- }
67
- const { version, services } = raw as Record<string, unknown>;
68
- if (typeof version !== "number") throw corrupt(state.file, "has no version");
69
- if (version > LEDGER_VERSION) {
70
- throw new LedgerError(
71
- "LEDGER_VERSION_UNSUPPORTED",
72
- `${state.file} has version ${version} but this CLI supports ${LEDGER_VERSION}.`,
73
- "Upgrade @lukoweb/apitogo.",
74
- );
75
- }
76
- if (version !== LEDGER_VERSION)
77
- throw corrupt(state.file, `has unknown version ${version}`);
78
- if (
79
- typeof services !== "object" ||
80
- services === null ||
81
- Array.isArray(services)
82
- ) {
83
- throw corrupt(state.file, "has no services object");
84
- }
85
- return { version: LEDGER_VERSION, services: services as Ledger["services"] };
86
- }
87
-
88
- function corrupt(file: string, why: string): LedgerError {
89
- return new LedgerError(
90
- "LEDGER_CORRUPT",
91
- `${file} ${why}.`,
92
- "It only holds regenerable state: delete it and run the command again.",
93
- );
94
- }
95
-
96
- /** Replaces `state.json` atomically, so a crash never leaves a half-written file. */
97
- export async function writeLedger(
98
- state: Pick<StatePaths, "file">,
99
- ledger: Ledger,
100
- ): Promise<void> {
101
- await mkdir(path.dirname(state.file), { recursive: true });
102
- const temporary = `${state.file}.${process.pid}.tmp`;
103
- try {
104
- await writeFile(temporary, `${JSON.stringify(ledger, null, 2)}\n`, "utf8");
105
- await rename(temporary, state.file);
106
- } catch (error) {
107
- await rm(temporary, { force: true });
108
- throw error;
109
- }
110
- }
111
-
112
- export function stepOf(
113
- ledger: Ledger,
114
- service: string,
115
- step: string,
116
- ): StepRecord | undefined {
117
- return ledger.services[service]?.steps[step];
118
- }
119
-
120
- function withStep(
121
- ledger: Ledger,
122
- service: string,
123
- step: string,
124
- record: StepRecord,
125
- ): Ledger {
126
- return {
127
- ...ledger,
128
- services: {
129
- ...ledger.services,
130
- [service]: {
131
- steps: { ...ledger.services[service]?.steps, [step]: record },
132
- },
133
- },
134
- };
135
- }
136
-
137
- export type PipelineStep = {
138
- name: string;
139
- /** Hash of this step's inputs. */
140
- hash: () => string | Promise<string>;
141
- run: () => Promise<Record<string, string> | undefined | void>;
142
- /** Extra proof the previous result still exists (for example the build folder). Defaults to true. */
143
- stillValid?: () => Promise<boolean>;
144
- };
145
-
146
- export type StepOutcome = {
147
- step: string;
148
- skipped: boolean;
149
- output?: Record<string, string>;
150
- };
151
-
152
- /**
153
- * Runs a service's steps in order against the ledger. A step is skipped when it already succeeded with the same
154
- * hash and nothing before it ran again. A failure is recorded with its message and rethrown; the next run starts
155
- * at that step, because the steps before it succeeded and are skipped.
156
- */
157
- export async function runPipeline(
158
- state: Pick<StatePaths, "file">,
159
- service: string,
160
- steps: readonly PipelineStep[],
161
- options: { force?: boolean; now?: () => Date } = {},
162
- ): Promise<StepOutcome[]> {
163
- const now = options.now ?? (() => new Date());
164
- let ledger = await readLedger(state);
165
- const outcomes: StepOutcome[] = [];
166
- let upstreamRan = false;
167
-
168
- for (const step of steps) {
169
- const hash = await step.hash();
170
- const previous = stepOf(ledger, service, step.name);
171
- const reusable =
172
- !options.force &&
173
- !upstreamRan &&
174
- previous?.status === "succeeded" &&
175
- previous.hash === hash &&
176
- (step.stillValid ? await step.stillValid() : true);
177
-
178
- if (reusable) {
179
- outcomes.push({
180
- step: step.name,
181
- skipped: true,
182
- output: previous?.output,
183
- });
184
- continue;
185
- }
186
-
187
- upstreamRan = true;
188
- const startedAt = now().toISOString();
189
- ledger = withStep(ledger, service, step.name, {
190
- status: "running",
191
- hash,
192
- startedAt,
193
- });
194
- await writeLedger(state, ledger);
195
- try {
196
- const output = (await step.run()) || undefined;
197
- ledger = withStep(ledger, service, step.name, {
198
- status: "succeeded",
199
- hash,
200
- startedAt,
201
- finishedAt: now().toISOString(),
202
- ...(output ? { output } : {}),
203
- });
204
- await writeLedger(state, ledger);
205
- outcomes.push({ step: step.name, skipped: false, output });
206
- } catch (error) {
207
- ledger = withStep(ledger, service, step.name, {
208
- status: "failed",
209
- hash,
210
- startedAt,
211
- finishedAt: now().toISOString(),
212
- error: error instanceof Error ? error.message : String(error),
213
- });
214
- await writeLedger(state, ledger);
215
- throw error;
216
- }
217
- }
218
- return outcomes;
219
- }
220
-
221
- /** Forgets the recorded steps of services that no longer exist on the platform. Other services keep their entries. */
222
- export async function clearServices(
223
- state: Pick<StatePaths, "file">,
224
- services: readonly string[],
225
- ): Promise<void> {
226
- const ledger = await readLedger(state);
227
- const remaining = Object.fromEntries(
228
- Object.entries(ledger.services).filter(
229
- ([name]) => !services.includes(name),
230
- ),
231
- );
232
- if (Object.keys(remaining).length === Object.keys(ledger.services).length)
233
- return;
234
- await writeLedger(state, { ...ledger, services: remaining });
235
- }
1
+ import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { ApitogoError } from "../../lib/util/invariant.js";
4
+ import type { StatePaths } from "./paths.js";
5
+
6
+ /** Version of the `state.json` format this build reads and writes. A newer file is refused, never overwritten. */
7
+ export const LEDGER_VERSION = 1;
8
+
9
+ export type StepStatus = "running" | "succeeded" | "failed";
10
+
11
+ /** One step of one service, for example `portal` / `build`. */
12
+ export type StepRecord = {
13
+ status: StepStatus;
14
+ /** Hash of the inputs the step ran with. An equal hash means the result is still valid. */
15
+ hash?: string;
16
+ startedAt: string;
17
+ finishedAt?: string;
18
+ /** Why the step failed. */
19
+ error?: string;
20
+ /** Public facts the step produced, such as a deployed URL. */
21
+ output?: Record<string, string>;
22
+ };
23
+
24
+ export type Ledger = {
25
+ version: typeof LEDGER_VERSION;
26
+ services: Record<string, { steps: Record<string, StepRecord> }>;
27
+ };
28
+
29
+ export class LedgerError extends ApitogoError {
30
+ constructor(
31
+ public readonly code: "LEDGER_CORRUPT" | "LEDGER_VERSION_UNSUPPORTED",
32
+ message: string,
33
+ developerHint?: string,
34
+ ) {
35
+ super(message, { developerHint, title: "Cannot read the project state" });
36
+ this.name = "LedgerError";
37
+ }
38
+ }
39
+
40
+ export const emptyLedger = (): Ledger => ({
41
+ version: LEDGER_VERSION,
42
+ services: {},
43
+ });
44
+
45
+ /** Reads `state.json`. A missing file is an empty ledger. A corrupt or newer file raises a {@link LedgerError}. */
46
+ export async function readLedger(
47
+ state: Pick<StatePaths, "file">,
48
+ ): Promise<Ledger> {
49
+ let text: string;
50
+ try {
51
+ text = await readFile(state.file, "utf8");
52
+ } catch (error) {
53
+ if ((error as NodeJS.ErrnoException).code === "ENOENT")
54
+ return emptyLedger();
55
+ throw error;
56
+ }
57
+
58
+ let raw: unknown;
59
+ try {
60
+ raw = JSON.parse(text);
61
+ } catch {
62
+ throw corrupt(state.file, "is not valid JSON");
63
+ }
64
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
65
+ throw corrupt(state.file, "is not an object");
66
+ }
67
+ const { version, services } = raw as Record<string, unknown>;
68
+ if (typeof version !== "number") throw corrupt(state.file, "has no version");
69
+ if (version > LEDGER_VERSION) {
70
+ throw new LedgerError(
71
+ "LEDGER_VERSION_UNSUPPORTED",
72
+ `${state.file} has version ${version} but this CLI supports ${LEDGER_VERSION}.`,
73
+ "Upgrade @lukoweb/apitogo.",
74
+ );
75
+ }
76
+ if (version !== LEDGER_VERSION)
77
+ throw corrupt(state.file, `has unknown version ${version}`);
78
+ if (
79
+ typeof services !== "object" ||
80
+ services === null ||
81
+ Array.isArray(services)
82
+ ) {
83
+ throw corrupt(state.file, "has no services object");
84
+ }
85
+ return { version: LEDGER_VERSION, services: services as Ledger["services"] };
86
+ }
87
+
88
+ function corrupt(file: string, why: string): LedgerError {
89
+ return new LedgerError(
90
+ "LEDGER_CORRUPT",
91
+ `${file} ${why}.`,
92
+ "It only holds regenerable state: delete it and run the command again.",
93
+ );
94
+ }
95
+
96
+ /** Replaces `state.json` atomically, so a crash never leaves a half-written file. */
97
+ export async function writeLedger(
98
+ state: Pick<StatePaths, "file">,
99
+ ledger: Ledger,
100
+ ): Promise<void> {
101
+ await mkdir(path.dirname(state.file), { recursive: true });
102
+ const temporary = `${state.file}.${process.pid}.tmp`;
103
+ try {
104
+ await writeFile(temporary, `${JSON.stringify(ledger, null, 2)}\n`, "utf8");
105
+ await rename(temporary, state.file);
106
+ } catch (error) {
107
+ await rm(temporary, { force: true });
108
+ throw error;
109
+ }
110
+ }
111
+
112
+ export function stepOf(
113
+ ledger: Ledger,
114
+ service: string,
115
+ step: string,
116
+ ): StepRecord | undefined {
117
+ return ledger.services[service]?.steps[step];
118
+ }
119
+
120
+ function withStep(
121
+ ledger: Ledger,
122
+ service: string,
123
+ step: string,
124
+ record: StepRecord,
125
+ ): Ledger {
126
+ return {
127
+ ...ledger,
128
+ services: {
129
+ ...ledger.services,
130
+ [service]: {
131
+ steps: { ...ledger.services[service]?.steps, [step]: record },
132
+ },
133
+ },
134
+ };
135
+ }
136
+
137
+ export type PipelineStep = {
138
+ name: string;
139
+ /** Hash of this step's inputs. */
140
+ hash: () => string | Promise<string>;
141
+ run: () => Promise<Record<string, string> | undefined | void>;
142
+ /** Extra proof the previous result still exists (for example the build folder). Defaults to true. */
143
+ stillValid?: () => Promise<boolean>;
144
+ };
145
+
146
+ export type StepOutcome = {
147
+ step: string;
148
+ skipped: boolean;
149
+ output?: Record<string, string>;
150
+ };
151
+
152
+ /**
153
+ * Runs a service's steps in order against the ledger. A step is skipped when it already succeeded with the same
154
+ * hash and nothing before it ran again. A failure is recorded with its message and rethrown; the next run starts
155
+ * at that step, because the steps before it succeeded and are skipped.
156
+ */
157
+ export async function runPipeline(
158
+ state: Pick<StatePaths, "file">,
159
+ service: string,
160
+ steps: readonly PipelineStep[],
161
+ options: { force?: boolean; now?: () => Date } = {},
162
+ ): Promise<StepOutcome[]> {
163
+ const now = options.now ?? (() => new Date());
164
+ let ledger = await readLedger(state);
165
+ const outcomes: StepOutcome[] = [];
166
+ let upstreamRan = false;
167
+
168
+ for (const step of steps) {
169
+ const hash = await step.hash();
170
+ const previous = stepOf(ledger, service, step.name);
171
+ const reusable =
172
+ !options.force &&
173
+ !upstreamRan &&
174
+ previous?.status === "succeeded" &&
175
+ previous.hash === hash &&
176
+ (step.stillValid ? await step.stillValid() : true);
177
+
178
+ if (reusable) {
179
+ outcomes.push({
180
+ step: step.name,
181
+ skipped: true,
182
+ output: previous?.output,
183
+ });
184
+ continue;
185
+ }
186
+
187
+ upstreamRan = true;
188
+ const startedAt = now().toISOString();
189
+ ledger = withStep(ledger, service, step.name, {
190
+ status: "running",
191
+ hash,
192
+ startedAt,
193
+ });
194
+ await writeLedger(state, ledger);
195
+ try {
196
+ const output = (await step.run()) || undefined;
197
+ ledger = withStep(ledger, service, step.name, {
198
+ status: "succeeded",
199
+ hash,
200
+ startedAt,
201
+ finishedAt: now().toISOString(),
202
+ ...(output ? { output } : {}),
203
+ });
204
+ await writeLedger(state, ledger);
205
+ outcomes.push({ step: step.name, skipped: false, output });
206
+ } catch (error) {
207
+ ledger = withStep(ledger, service, step.name, {
208
+ status: "failed",
209
+ hash,
210
+ startedAt,
211
+ finishedAt: now().toISOString(),
212
+ error: error instanceof Error ? error.message : String(error),
213
+ });
214
+ await writeLedger(state, ledger);
215
+ throw error;
216
+ }
217
+ }
218
+ return outcomes;
219
+ }
220
+
221
+ /**
222
+ * Forgets the recorded steps of services that no longer exist on the platform. Other services keep their entries.
223
+ * When no service is left the file is deleted: an empty ledger is the same as a missing one.
224
+ */
225
+ export async function clearServices(
226
+ state: Pick<StatePaths, "file">,
227
+ services: readonly string[],
228
+ ): Promise<void> {
229
+ const ledger = await readLedger(state);
230
+ const remaining = Object.fromEntries(
231
+ Object.entries(ledger.services).filter(
232
+ ([name]) => !services.includes(name),
233
+ ),
234
+ );
235
+ if (Object.keys(remaining).length === Object.keys(ledger.services).length)
236
+ return;
237
+ if (Object.keys(remaining).length === 0) {
238
+ await rm(state.file, { force: true });
239
+ return;
240
+ }
241
+ await writeLedger(state, { ...ledger, services: remaining });
242
+ }
@@ -4,11 +4,19 @@ import type { ConfigV2, Layout } from "./schema.js";
4
4
 
5
5
  export { layoutRoot };
6
6
 
7
- /** A path relative to the config file, made absolute. A leading `!` (negated glob) is preserved. */
7
+ /**
8
+ * A glob is written with forward slashes on every system: the glob library reads a backslash as an escape, so a Windows
9
+ * path such as `C:\site\portal\pages\**\*.md` matches nothing.
10
+ */
11
+ export function toGlobPath(value: string): string {
12
+ return value.split(path.sep).join("/");
13
+ }
14
+
15
+ /** A glob relative to the config file, made absolute. A leading `!` (negated glob) is preserved. */
8
16
  function fromConfig(configDir: string, value: string): string {
9
17
  return value.startsWith("!")
10
- ? `!${path.resolve(configDir, value.slice(1))}`
11
- : path.resolve(configDir, value);
18
+ ? `!${toGlobPath(path.resolve(configDir, value.slice(1)))}`
19
+ : toGlobPath(path.resolve(configDir, value));
12
20
  }
13
21
 
14
22
  export type ResolvedPortal = {
@@ -126,7 +134,7 @@ export function resolveConfigV2(
126
134
  ? []
127
135
  : portal.docs?.content
128
136
  ? portal.docs.content.map((glob) => fromConfig(configDir, glob))
129
- : [path.join(dir, "pages", "**", "*.{md,mdx}")],
137
+ : [toGlobPath(path.join(dir, "pages", "**", "*.{md,mdx}"))],
130
138
  url: portal.docs?.url,
131
139
  },
132
140
  console: { enabled: portal.console?.enabled ?? true },