@sansavision/aurora 0.1.0-alpha.20260212.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 +4 -0
- package/package.json +17 -0
- package/src/ai-diagnostics.ts +156 -0
- package/src/ai.ts +574 -0
- package/src/analyze.ts +669 -0
- package/src/bin/aurora.ts +15 -0
- package/src/build.ts +431 -0
- package/src/bun-test-shims.d.ts +17 -0
- package/src/create-feature.ts +419 -0
- package/src/create-route.ts +581 -0
- package/src/create.ts +425 -0
- package/src/dev.ts +126 -0
- package/src/devtools.ts +1143 -0
- package/src/doctor.ts +611 -0
- package/src/explain.ts +855 -0
- package/src/help.ts +39 -0
- package/src/index.ts +34 -0
- package/src/init.ts +1011 -0
- package/src/inspect-cache.ts +464 -0
- package/src/lsp-inline-hints.ts +254 -0
- package/src/node-shims.d.ts +26 -0
- package/src/process.d.ts +11 -0
- package/src/query-profiler.ts +520 -0
- package/src/realtime-monitor.ts +389 -0
- package/src/registry.ts +303 -0
- package/src/run.ts +37 -0
- package/src/start.ts +56 -0
- package/src/test.ts +289 -0
- package/templates/basic/README.md +16 -0
- package/templates/basic/package.json +10 -0
- package/templates/basic/src/actions/createMessage.action.server.ts +22 -0
- package/templates/basic/src/lib/auth.server.ts +11 -0
- package/templates/basic/src/queries/listMessages.server.ts +17 -0
- package/templates/basic/src/routes/index.tsx +12 -0
- package/templates/blog/README.md +17 -0
- package/templates/blog/package.json +12 -0
- package/templates/blog/public/assets/og-default.svg +17 -0
- package/templates/blog/src/content/loadPosts.server.ts +22 -0
- package/templates/blog/src/content/posts/hello-world.md +11 -0
- package/templates/blog/src/content/posts/release-notes.md +9 -0
- package/templates/blog/src/routes/index.tsx +22 -0
- package/templates/blog/src/routes/posts/[slug].tsx +19 -0
- package/templates/blog/src/seo/meta.ts +19 -0
- package/templates/dashboard/README.md +18 -0
- package/templates/dashboard/package.json +10 -0
- package/templates/dashboard/src/actions/acknowledgeAlert.action.server.ts +6 -0
- package/templates/dashboard/src/queries/getDashboardMetrics.server.ts +30 -0
- package/templates/dashboard/src/realtime/useDashboardRealtime.client.ts +13 -0
- package/templates/dashboard/src/routes/index.tsx +19 -0
- package/templates/dashboard/src/widgets/DataGrid.client.ts +8 -0
- package/templates/dashboard/src/widgets/MetricChart.client.ts +8 -0
- package/templates/desktop/README.md +18 -0
- package/templates/desktop/package.json +11 -0
- package/templates/desktop/src/actions/saveDesktopPreference.action.server.ts +28 -0
- package/templates/desktop/src/desktop/secureStorage.client.ts +20 -0
- package/templates/desktop/src/desktop/tauriBridge.client.ts +14 -0
- package/templates/desktop/src/queries/getDesktopSyncStatus.server.ts +9 -0
- package/templates/desktop/src/routes/index.tsx +27 -0
- package/templates/desktop/src/sync/offlineSyncBoundary.server.ts +27 -0
- package/templates/feature-skeleton/README.md +13 -0
- package/templates/feature-skeleton/actions/createFeature.action.server.ts +19 -0
- package/templates/feature-skeleton/index.ts +8 -0
- package/templates/feature-skeleton/queries/listFeature.server.ts +15 -0
- package/templates/feature-skeleton/realtime/useFeatureRealtime.client.ts +16 -0
- package/templates/feature-skeleton/template.manifest.json +15 -0
- package/templates/feature-skeleton/ui/FeatureView.client.tsx +14 -0
- package/templates/mobile/README.md +17 -0
- package/templates/mobile/package.json +11 -0
- package/templates/mobile/src/mobile/auth/session-handoff.client.ts +69 -0
- package/templates/mobile/src/mobile/generated/mobile-api-sdk.ts +62 -0
- package/templates/mobile/src/mobile/transport/mobile-api-transport.client.ts +122 -0
- package/templates/mobile/src/routes/index.tsx +134 -0
- package/templates/monorepo/README.md +18 -0
- package/templates/monorepo/apps/web/package.json +9 -0
- package/templates/monorepo/apps/web/src/routes/index.tsx +1 -0
- package/templates/monorepo/package.json +13 -0
- package/templates/monorepo/packages/shared/README.md +3 -0
- package/templates/monorepo/packages/ui/README.md +3 -0
- package/templates/saas/README.md +17 -0
- package/templates/saas/package.json +10 -0
- package/templates/saas/src/admin/getDashboard.server.ts +18 -0
- package/templates/saas/src/auth/session.server.ts +13 -0
- package/templates/saas/src/billing/checkout.server.ts +11 -0
- package/templates/saas/src/email/sendWelcome.server.ts +8 -0
- package/templates/saas/src/realtime/notifications.server.ts +8 -0
- package/templates/saas/src/routes/index.tsx +20 -0
- package/test/ai.test.ts +94 -0
- package/test/analyze.test.ts +301 -0
- package/test/build.test.ts +135 -0
- package/test/create-feature.test.ts +145 -0
- package/test/create-route.test.ts +117 -0
- package/test/create.test.ts +222 -0
- package/test/dev.test.ts +52 -0
- package/test/devtools.test.ts +130 -0
- package/test/doctor.test.ts +129 -0
- package/test/explain.test.ts +232 -0
- package/test/feature-skeleton.test.ts +53 -0
- package/test/fixtures/analyze/cache-input.invalid.json +1 -0
- package/test/fixtures/analyze/cache-input.missing-keyhash.v1.json +10 -0
- package/test/fixtures/analyze/cache-input.unsupported-version.v2.json +10 -0
- package/test/fixtures/analyze/cache-input.v1.json +12 -0
- package/test/fixtures/analyze/compiler-manifest/manifest.json +11 -0
- package/test/fixtures/analyze/guardrails-input.unsupported-version.v2.json +4 -0
- package/test/fixtures/analyze/guardrails-input.v1.json +49 -0
- package/test/fixtures/analyze/query-input.invalid-cache-status.v1.json +11 -0
- package/test/fixtures/analyze/query-input.unsupported-version.v2.json +11 -0
- package/test/fixtures/analyze/query-input.v1.json +18 -0
- package/test/fixtures/analyze/realtime-input.missing-lag-p95.v1.json +10 -0
- package/test/fixtures/analyze/realtime-input.unsupported-version.v2.json +8 -0
- package/test/fixtures/analyze/realtime-input.v1.json +12 -0
- package/test/fixtures/cache-inspector/cache-input.v1.json +23 -0
- package/test/fixtures/cache-inspector/invalid.json +1 -0
- package/test/fixtures/cache-inspector/snapshot.v1.json +34 -0
- package/test/fixtures/cache-inspector/unsupported-version.v2.json +13 -0
- package/test/fixtures/devtools/healthy.v1.json +130 -0
- package/test/fixtures/devtools/invalid.json +1 -0
- package/test/fixtures/devtools/unsupported-version.v2.json +8 -0
- package/test/fixtures/devtools/warn.v1.json +114 -0
- package/test/fixtures/doctor/clean/src/page.tsx +3 -0
- package/test/fixtures/doctor/findings/src/accessibility.client.tsx +7 -0
- package/test/fixtures/doctor/findings/src/migration.config.ts +3 -0
- package/test/fixtures/doctor/findings/src/page.client.tsx +5 -0
- package/test/fixtures/doctor/findings/src/perf.server.ts +15 -0
- package/test/fixtures/doctor/findings/src/routes.js +3 -0
- package/test/fixtures/doctor/findings/src/security.server.ts +7 -0
- package/test/fixtures/doctor/findings/src/users.server.ts +3 -0
- package/test/fixtures/doctor/governance/src/features/analytics/OWNERS.ts +2 -0
- package/test/fixtures/doctor/governance/src/features/analytics/page.tsx +3 -0
- package/test/fixtures/doctor/governance/src/features/billing/page.tsx +3 -0
- package/test/fixtures/explain/invalid.json +1 -0
- package/test/fixtures/explain/module-report.unsupported-version.v2.json +6 -0
- package/test/fixtures/explain/module-report.v1.json +72 -0
- package/test/fixtures/query-profiler/healthy.v1.json +11 -0
- package/test/fixtures/query-profiler/invalid.json +1 -0
- package/test/fixtures/query-profiler/unsupported-version.v2.json +6 -0
- package/test/fixtures/query-profiler/warning.v1.json +10 -0
- package/test/fixtures/realtime-monitor/healthy.v1.json +8 -0
- package/test/fixtures/realtime-monitor/invalid.json +1 -0
- package/test/fixtures/realtime-monitor/unsupported-version.v2.json +8 -0
- package/test/fixtures/realtime-monitor/warning.v1.json +8 -0
- package/test/help-parity.test.ts +104 -0
- package/test/init.test.ts +164 -0
- package/test/inspect-cache.test.ts +112 -0
- package/test/lsp-inline-hints.test.ts +65 -0
- package/test/query-profiler.test.ts +123 -0
- package/test/realtime-monitor.test.ts +115 -0
- package/test/registry.test.ts +41 -0
- package/test/start.test.ts +23 -0
- package/test/test-command.test.ts +65 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,581 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
import { type CommandContext, type CommandResult } from "./registry";
|
|
5
|
+
|
|
6
|
+
export type RouteKind = "crud" | "read-only" | "form" | "dashboard" | "empty";
|
|
7
|
+
export type RouteDataSource = "database" | "api" | "none";
|
|
8
|
+
|
|
9
|
+
export interface RouteScaffoldOptions {
|
|
10
|
+
routeSlug: string;
|
|
11
|
+
routeName: string;
|
|
12
|
+
kind: RouteKind;
|
|
13
|
+
dataSource: RouteDataSource;
|
|
14
|
+
auth: string;
|
|
15
|
+
realtime: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface RouteScaffoldReport {
|
|
19
|
+
mode: "create-route";
|
|
20
|
+
cwd: string;
|
|
21
|
+
routeSlug: string;
|
|
22
|
+
routeName: string;
|
|
23
|
+
routeRoot: string;
|
|
24
|
+
kind: RouteKind;
|
|
25
|
+
dataSource: RouteDataSource;
|
|
26
|
+
auth: string;
|
|
27
|
+
realtime: boolean;
|
|
28
|
+
createdFileCount: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface ParsedCreateRouteOptions extends RouteScaffoldOptions {
|
|
32
|
+
rawName: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface ScaffoldRouteExecutionOptions {
|
|
36
|
+
commandName?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const ROUTE_KINDS: readonly RouteKind[] = [
|
|
40
|
+
"crud",
|
|
41
|
+
"read-only",
|
|
42
|
+
"form",
|
|
43
|
+
"dashboard",
|
|
44
|
+
"empty",
|
|
45
|
+
];
|
|
46
|
+
const ROUTE_DATA_SOURCES: readonly RouteDataSource[] = ["database", "api", "none"];
|
|
47
|
+
|
|
48
|
+
function parseCreateRouteOptions(
|
|
49
|
+
args: ReadonlyArray<string>,
|
|
50
|
+
): ParsedCreateRouteOptions | CommandResult {
|
|
51
|
+
if (args.length === 0) {
|
|
52
|
+
return {
|
|
53
|
+
exitCode: 2,
|
|
54
|
+
stderr: "aurora create-route: route name is required",
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const firstArg = args[0];
|
|
59
|
+
if (!firstArg || firstArg.startsWith("-")) {
|
|
60
|
+
return {
|
|
61
|
+
exitCode: 2,
|
|
62
|
+
stderr: "aurora create-route: route name must be provided before options",
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const routeSlug = toRouteSlug(firstArg);
|
|
67
|
+
if (!routeSlug) {
|
|
68
|
+
return {
|
|
69
|
+
exitCode: 2,
|
|
70
|
+
stderr:
|
|
71
|
+
"aurora create-route: route name must include at least one letter or number",
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const options: ParsedCreateRouteOptions = {
|
|
76
|
+
rawName: firstArg,
|
|
77
|
+
routeSlug,
|
|
78
|
+
routeName: toRouteName(routeSlug),
|
|
79
|
+
kind: "form",
|
|
80
|
+
dataSource: "database",
|
|
81
|
+
auth: "public",
|
|
82
|
+
realtime: false,
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
for (let i = 1; i < args.length; i += 1) {
|
|
86
|
+
const arg = args[i];
|
|
87
|
+
|
|
88
|
+
if (arg === "--kind") {
|
|
89
|
+
const value = args[i + 1];
|
|
90
|
+
if (!value || !isRouteKind(value)) {
|
|
91
|
+
return {
|
|
92
|
+
exitCode: 2,
|
|
93
|
+
stderr:
|
|
94
|
+
`aurora create-route: --kind requires one of ${ROUTE_KINDS.join("|")}`,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
options.kind = value;
|
|
99
|
+
i += 1;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (arg === "--data") {
|
|
104
|
+
const value = args[i + 1];
|
|
105
|
+
if (!value || !isRouteDataSource(value)) {
|
|
106
|
+
return {
|
|
107
|
+
exitCode: 2,
|
|
108
|
+
stderr:
|
|
109
|
+
`aurora create-route: --data requires one of ${ROUTE_DATA_SOURCES.join("|")}`,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
options.dataSource = value;
|
|
114
|
+
i += 1;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (arg === "--auth") {
|
|
119
|
+
const value = args[i + 1];
|
|
120
|
+
if (!value) {
|
|
121
|
+
return {
|
|
122
|
+
exitCode: 2,
|
|
123
|
+
stderr:
|
|
124
|
+
"aurora create-route: --auth requires 'public', 'user', 'admin', or 'permission:<name>'",
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const parsedAuth = parseAuthMode(value);
|
|
129
|
+
if (!parsedAuth) {
|
|
130
|
+
return {
|
|
131
|
+
exitCode: 2,
|
|
132
|
+
stderr:
|
|
133
|
+
"aurora create-route: --auth requires 'public', 'user', 'admin', or 'permission:<name>'",
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
options.auth = parsedAuth;
|
|
138
|
+
i += 1;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (arg === "--realtime") {
|
|
143
|
+
options.realtime = true;
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (arg === "--no-realtime") {
|
|
148
|
+
options.realtime = false;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
exitCode: 2,
|
|
154
|
+
stderr: `aurora create-route: unknown option '${arg}'`,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return options;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function isRouteKind(value: string): value is RouteKind {
|
|
162
|
+
return ROUTE_KINDS.includes(value as RouteKind);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function isRouteDataSource(value: string): value is RouteDataSource {
|
|
166
|
+
return ROUTE_DATA_SOURCES.includes(value as RouteDataSource);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function parseAuthMode(value: string): string | undefined {
|
|
170
|
+
const normalized = value.trim();
|
|
171
|
+
if (normalized === "public" || normalized === "user" || normalized === "admin") {
|
|
172
|
+
return normalized;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (/^permission:[a-z0-9:_-]+$/i.test(normalized)) {
|
|
176
|
+
return normalized;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return undefined;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function toRouteSlug(rawName: string): string {
|
|
183
|
+
return rawName
|
|
184
|
+
.trim()
|
|
185
|
+
.replace(/[^a-zA-Z0-9]+/g, "-")
|
|
186
|
+
.replace(/^-+|-+$/g, "")
|
|
187
|
+
.replace(/-+/g, "-")
|
|
188
|
+
.toLowerCase();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function toRouteName(routeSlug: string): string {
|
|
192
|
+
const pascal = routeSlug
|
|
193
|
+
.split("-")
|
|
194
|
+
.filter((part) => part.length > 0)
|
|
195
|
+
.map((part) => part[0].toUpperCase() + part.slice(1))
|
|
196
|
+
.join("");
|
|
197
|
+
|
|
198
|
+
if (/^[A-Za-z_]/.test(pascal)) {
|
|
199
|
+
return pascal;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return `Route${pascal}`;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function isNonEmptyDirectory(path: string): boolean {
|
|
206
|
+
if (!existsSync(path)) {
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
try {
|
|
211
|
+
return readdirSync(path).length > 0;
|
|
212
|
+
} catch {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function buildRouteScaffoldFiles(
|
|
218
|
+
options: RouteScaffoldOptions,
|
|
219
|
+
): ReadonlyArray<{ path: string; content: string }> {
|
|
220
|
+
return [
|
|
221
|
+
{
|
|
222
|
+
path: "page.tsx",
|
|
223
|
+
content: buildPageModule(options),
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
path: `${options.routeSlug}.client.tsx`,
|
|
227
|
+
content: buildClientModule(options),
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
path: `${options.routeSlug}.server.ts`,
|
|
231
|
+
content: buildServerModule(options),
|
|
232
|
+
},
|
|
233
|
+
];
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function buildServerModule(options: RouteScaffoldOptions): string {
|
|
237
|
+
const lines = [
|
|
238
|
+
"// Safe route defaults: explicit runtime/auth/scope for predictable behavior.",
|
|
239
|
+
'export const runtime = "node";',
|
|
240
|
+
`export const auth = "${options.auth}";`,
|
|
241
|
+
`export const scope = "${options.routeSlug}";`,
|
|
242
|
+
`export const dataSource = "${options.dataSource}";`,
|
|
243
|
+
"",
|
|
244
|
+
];
|
|
245
|
+
|
|
246
|
+
if (options.kind === "form") {
|
|
247
|
+
lines.push(...buildFormServerModule(options));
|
|
248
|
+
} else if (options.kind === "read-only") {
|
|
249
|
+
lines.push(...buildReadOnlyServerModule(options));
|
|
250
|
+
} else if (options.kind === "crud") {
|
|
251
|
+
lines.push(...buildCrudServerModule(options));
|
|
252
|
+
} else if (options.kind === "dashboard") {
|
|
253
|
+
lines.push(...buildDashboardServerModule(options));
|
|
254
|
+
} else {
|
|
255
|
+
lines.push(...buildEmptyServerModule(options));
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (options.realtime) {
|
|
259
|
+
lines.push(
|
|
260
|
+
"",
|
|
261
|
+
`export function ${options.routeName}RealtimeChannel(): string {`,
|
|
262
|
+
` return "${options.routeSlug}";`,
|
|
263
|
+
"}",
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return lines.join("\n");
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function buildFormServerModule(options: RouteScaffoldOptions): string[] {
|
|
271
|
+
const itemType = `${options.routeName}Item`;
|
|
272
|
+
const listName = `list${options.routeName}`;
|
|
273
|
+
const submitInputType = `Submit${options.routeName}Input`;
|
|
274
|
+
const submitName = `submit${options.routeName}`;
|
|
275
|
+
|
|
276
|
+
return [
|
|
277
|
+
`export interface ${itemType} {`,
|
|
278
|
+
" id: string;",
|
|
279
|
+
" label: string;",
|
|
280
|
+
" createdAt: number;",
|
|
281
|
+
"}",
|
|
282
|
+
"",
|
|
283
|
+
`const seed${options.routeName}: ${itemType}[] = [`,
|
|
284
|
+
" {",
|
|
285
|
+
` id: "${options.routeSlug}-seed-1",`,
|
|
286
|
+
" label: " + JSON.stringify(`${options.routeName} starter entry`) + ",",
|
|
287
|
+
" createdAt: 1707609600000,",
|
|
288
|
+
" },",
|
|
289
|
+
"];",
|
|
290
|
+
"",
|
|
291
|
+
`export async function ${listName}(): Promise<${itemType}[]> {`,
|
|
292
|
+
` return seed${options.routeName};`,
|
|
293
|
+
"}",
|
|
294
|
+
"",
|
|
295
|
+
`export interface ${submitInputType} {`,
|
|
296
|
+
" label: string;",
|
|
297
|
+
"}",
|
|
298
|
+
"",
|
|
299
|
+
`export async function ${submitName}(input: ${submitInputType}): Promise<${itemType}> {`,
|
|
300
|
+
" return {",
|
|
301
|
+
" id: `${scope}-${Date.now()}`,",
|
|
302
|
+
" label: input.label,",
|
|
303
|
+
" createdAt: Date.now(),",
|
|
304
|
+
" };",
|
|
305
|
+
"}",
|
|
306
|
+
];
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function buildReadOnlyServerModule(options: RouteScaffoldOptions): string[] {
|
|
310
|
+
const itemType = `${options.routeName}Item`;
|
|
311
|
+
const listName = `list${options.routeName}`;
|
|
312
|
+
|
|
313
|
+
return [
|
|
314
|
+
`export interface ${itemType} {`,
|
|
315
|
+
" id: string;",
|
|
316
|
+
" label: string;",
|
|
317
|
+
" updatedAt: number;",
|
|
318
|
+
"}",
|
|
319
|
+
"",
|
|
320
|
+
`export async function ${listName}(): Promise<${itemType}[]> {`,
|
|
321
|
+
" return [",
|
|
322
|
+
" {",
|
|
323
|
+
` id: "${options.routeSlug}-readonly-1",`,
|
|
324
|
+
" label: " + JSON.stringify(`${options.routeName} readonly sample`) + ",",
|
|
325
|
+
" updatedAt: 1707609600000,",
|
|
326
|
+
" },",
|
|
327
|
+
" ];",
|
|
328
|
+
"}",
|
|
329
|
+
];
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function buildCrudServerModule(options: RouteScaffoldOptions): string[] {
|
|
333
|
+
const itemType = `${options.routeName}Item`;
|
|
334
|
+
const listName = `list${options.routeName}`;
|
|
335
|
+
const createInputType = `Create${options.routeName}Input`;
|
|
336
|
+
const createName = `create${options.routeName}`;
|
|
337
|
+
const updateName = `update${options.routeName}`;
|
|
338
|
+
const removeName = `remove${options.routeName}`;
|
|
339
|
+
|
|
340
|
+
return [
|
|
341
|
+
`export interface ${itemType} {`,
|
|
342
|
+
" id: string;",
|
|
343
|
+
" label: string;",
|
|
344
|
+
" createdAt: number;",
|
|
345
|
+
"}",
|
|
346
|
+
"",
|
|
347
|
+
`export interface ${createInputType} {`,
|
|
348
|
+
" label: string;",
|
|
349
|
+
"}",
|
|
350
|
+
"",
|
|
351
|
+
`export async function ${listName}(): Promise<${itemType}[]> {`,
|
|
352
|
+
" return [];",
|
|
353
|
+
"}",
|
|
354
|
+
"",
|
|
355
|
+
`export async function ${createName}(input: ${createInputType}): Promise<${itemType}> {`,
|
|
356
|
+
" return {",
|
|
357
|
+
" id: `${scope}-${Date.now()}`,",
|
|
358
|
+
" label: input.label,",
|
|
359
|
+
" createdAt: Date.now(),",
|
|
360
|
+
" };",
|
|
361
|
+
"}",
|
|
362
|
+
"",
|
|
363
|
+
`export async function ${updateName}(id: string, input: ${createInputType}): Promise<${itemType}> {`,
|
|
364
|
+
" return {",
|
|
365
|
+
" id,",
|
|
366
|
+
" label: input.label,",
|
|
367
|
+
" createdAt: Date.now(),",
|
|
368
|
+
" };",
|
|
369
|
+
"}",
|
|
370
|
+
"",
|
|
371
|
+
`export async function ${removeName}(id: string): Promise<{ id: string; removed: boolean }> {`,
|
|
372
|
+
" return { id, removed: true };",
|
|
373
|
+
"}",
|
|
374
|
+
];
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function buildDashboardServerModule(options: RouteScaffoldOptions): string[] {
|
|
378
|
+
const metricType = `${options.routeName}Metric`;
|
|
379
|
+
const loadName = `load${options.routeName}Metrics`;
|
|
380
|
+
|
|
381
|
+
return [
|
|
382
|
+
`export interface ${metricType} {`,
|
|
383
|
+
" id: string;",
|
|
384
|
+
" label: string;",
|
|
385
|
+
" value: number;",
|
|
386
|
+
"}",
|
|
387
|
+
"",
|
|
388
|
+
`export async function ${loadName}(): Promise<${metricType}[]> {`,
|
|
389
|
+
" return [",
|
|
390
|
+
" { id: \"throughput\", label: \"Throughput\", value: 24 },",
|
|
391
|
+
" { id: \"error-rate\", label: \"Error Rate\", value: 1.2 },",
|
|
392
|
+
" ];",
|
|
393
|
+
"}",
|
|
394
|
+
];
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function buildEmptyServerModule(options: RouteScaffoldOptions): string[] {
|
|
398
|
+
const loadName = `load${options.routeName}Page`;
|
|
399
|
+
|
|
400
|
+
return [
|
|
401
|
+
`export async function ${loadName}(): Promise<{ ready: boolean }> {`,
|
|
402
|
+
" return { ready: true };",
|
|
403
|
+
"}",
|
|
404
|
+
];
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function buildClientModule(options: RouteScaffoldOptions): string {
|
|
408
|
+
const lines = [
|
|
409
|
+
"// Client shell generated with novice-friendly defaults.",
|
|
410
|
+
];
|
|
411
|
+
|
|
412
|
+
if (options.kind === "dashboard") {
|
|
413
|
+
const metricType = `${options.routeName}Metric`;
|
|
414
|
+
lines.push(
|
|
415
|
+
`import type { ${metricType} } from "./${options.routeSlug}.server";`,
|
|
416
|
+
"",
|
|
417
|
+
`export interface ${options.routeName}ClientProps {`,
|
|
418
|
+
` metrics: ReadonlyArray<${metricType}>;`,
|
|
419
|
+
"}",
|
|
420
|
+
"",
|
|
421
|
+
`export function render${options.routeName}Client(props: ${options.routeName}ClientProps): string {`,
|
|
422
|
+
` return \`<section data-route=\"${options.routeSlug}\"><h1>${options.routeName}</h1><p>metrics: \${props.metrics.length}</p></section>\`;`,
|
|
423
|
+
"}",
|
|
424
|
+
);
|
|
425
|
+
|
|
426
|
+
return lines.join("\n");
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (options.kind === "empty") {
|
|
430
|
+
lines.push(
|
|
431
|
+
"",
|
|
432
|
+
`export function render${options.routeName}Client(): string {`,
|
|
433
|
+
` return \`<section data-route=\"${options.routeSlug}\"><h1>${options.routeName}</h1><p>route scaffold ready</p></section>\`;`,
|
|
434
|
+
"}",
|
|
435
|
+
);
|
|
436
|
+
|
|
437
|
+
return lines.join("\n");
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const itemType = `${options.routeName}Item`;
|
|
441
|
+
|
|
442
|
+
lines.push(
|
|
443
|
+
`import type { ${itemType} } from "./${options.routeSlug}.server";`,
|
|
444
|
+
"",
|
|
445
|
+
`export interface ${options.routeName}ClientProps {`,
|
|
446
|
+
` items: ReadonlyArray<${itemType}>;`,
|
|
447
|
+
"}",
|
|
448
|
+
"",
|
|
449
|
+
`export function render${options.routeName}Client(props: ${options.routeName}ClientProps): string {`,
|
|
450
|
+
` return \`<section data-route=\"${options.routeSlug}\"><h1>${options.routeName}</h1><p>items: \${props.items.length}</p></section>\`;`,
|
|
451
|
+
"}",
|
|
452
|
+
);
|
|
453
|
+
|
|
454
|
+
return lines.join("\n");
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function buildPageModule(options: RouteScaffoldOptions): string {
|
|
458
|
+
const pageName = `${options.routeName}Page`;
|
|
459
|
+
const lines = [
|
|
460
|
+
`import { render${options.routeName}Client } from "./${options.routeSlug}.client";`,
|
|
461
|
+
];
|
|
462
|
+
|
|
463
|
+
if (options.kind === "form" || options.kind === "read-only" || options.kind === "crud") {
|
|
464
|
+
lines.push(
|
|
465
|
+
`import { list${options.routeName} } from "./${options.routeSlug}.server";`,
|
|
466
|
+
"",
|
|
467
|
+
`export async function ${pageName}(): Promise<string> {`,
|
|
468
|
+
` const items = await list${options.routeName}();`,
|
|
469
|
+
` return render${options.routeName}Client({ items });`,
|
|
470
|
+
"}",
|
|
471
|
+
);
|
|
472
|
+
|
|
473
|
+
return lines.join("\n");
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (options.kind === "dashboard") {
|
|
477
|
+
lines.push(
|
|
478
|
+
`import { load${options.routeName}Metrics } from "./${options.routeSlug}.server";`,
|
|
479
|
+
"",
|
|
480
|
+
`export async function ${pageName}(): Promise<string> {`,
|
|
481
|
+
` const metrics = await load${options.routeName}Metrics();`,
|
|
482
|
+
` return render${options.routeName}Client({ metrics });`,
|
|
483
|
+
"}",
|
|
484
|
+
);
|
|
485
|
+
|
|
486
|
+
return lines.join("\n");
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
lines.push(
|
|
490
|
+
`import { load${options.routeName}Page } from "./${options.routeSlug}.server";`,
|
|
491
|
+
"",
|
|
492
|
+
`export async function ${pageName}(): Promise<string> {`,
|
|
493
|
+
` await load${options.routeName}Page();`,
|
|
494
|
+
` return render${options.routeName}Client();`,
|
|
495
|
+
"}",
|
|
496
|
+
);
|
|
497
|
+
|
|
498
|
+
return lines.join("\n");
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
export function scaffoldRoute(
|
|
502
|
+
options: RouteScaffoldOptions,
|
|
503
|
+
context: CommandContext,
|
|
504
|
+
executionOptions: ScaffoldRouteExecutionOptions = {},
|
|
505
|
+
): RouteScaffoldReport | CommandResult {
|
|
506
|
+
const commandName = executionOptions.commandName ?? "aurora create-route";
|
|
507
|
+
const routeRoot = resolve(context.cwd, "src/routes", options.routeSlug);
|
|
508
|
+
|
|
509
|
+
if (isNonEmptyDirectory(routeRoot)) {
|
|
510
|
+
return {
|
|
511
|
+
exitCode: 2,
|
|
512
|
+
stderr: `${commandName}: target directory already exists and is not empty: ${routeRoot}`,
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const files = buildRouteScaffoldFiles(options);
|
|
517
|
+
|
|
518
|
+
try {
|
|
519
|
+
mkdirSync(routeRoot, { recursive: true });
|
|
520
|
+
|
|
521
|
+
for (const file of files) {
|
|
522
|
+
const absolutePath = join(routeRoot, file.path);
|
|
523
|
+
mkdirSync(dirname(absolutePath), { recursive: true });
|
|
524
|
+
writeFileSync(absolutePath, `${file.content}\n`, "utf8");
|
|
525
|
+
}
|
|
526
|
+
} catch (error) {
|
|
527
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
528
|
+
return {
|
|
529
|
+
exitCode: 1,
|
|
530
|
+
stderr: `${commandName}: failed to scaffold route at ${routeRoot}: ${message}`,
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
return {
|
|
535
|
+
mode: "create-route",
|
|
536
|
+
cwd: context.cwd,
|
|
537
|
+
routeSlug: options.routeSlug,
|
|
538
|
+
routeName: options.routeName,
|
|
539
|
+
routeRoot,
|
|
540
|
+
kind: options.kind,
|
|
541
|
+
dataSource: options.dataSource,
|
|
542
|
+
auth: options.auth,
|
|
543
|
+
realtime: options.realtime,
|
|
544
|
+
createdFileCount: files.length,
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function renderCreateRouteReport(report: RouteScaffoldReport): string {
|
|
549
|
+
return [
|
|
550
|
+
"aurora create-route bootstrap",
|
|
551
|
+
`cwd: ${report.cwd}`,
|
|
552
|
+
`route_slug: ${report.routeSlug}`,
|
|
553
|
+
`route_name: ${report.routeName}`,
|
|
554
|
+
`route_kind: ${report.kind}`,
|
|
555
|
+
`data_source: ${report.dataSource}`,
|
|
556
|
+
`auth: ${report.auth}`,
|
|
557
|
+
`realtime: ${report.realtime ? "enabled" : "disabled"}`,
|
|
558
|
+
`route_root: ${report.routeRoot}`,
|
|
559
|
+
`created_files: ${report.createdFileCount}`,
|
|
560
|
+
].join("\n");
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
export function runCreateRouteCommand(
|
|
564
|
+
args: ReadonlyArray<string>,
|
|
565
|
+
context: CommandContext,
|
|
566
|
+
): CommandResult {
|
|
567
|
+
const options = parseCreateRouteOptions(args);
|
|
568
|
+
if ("exitCode" in options) {
|
|
569
|
+
return options;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
const report = scaffoldRoute(options, context);
|
|
573
|
+
if ("exitCode" in report) {
|
|
574
|
+
return report;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
return {
|
|
578
|
+
exitCode: 0,
|
|
579
|
+
stdout: renderCreateRouteReport(report),
|
|
580
|
+
};
|
|
581
|
+
}
|