robodev 0.28.0 → 0.30.0
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/package.json +1 -1
- package/src/collect-files.test.ts +22 -0
- package/src/collect-files.ts +58 -15
- package/src/frontend-build.test.ts +34 -7
- package/src/frontend-build.ts +29 -15
- package/src/index.ts +8 -1
- package/templates/auth-chat/.rulesync/rules/project-overview.md +1 -1
- package/templates/auth-chat/apps/fe/package.json +1 -1
- package/templates/auth-chat/package.json +2 -2
- package/templates/backend/package.json +2 -2
- package/templates/empty/package.json +2 -2
- package/templates/marketplace/.rulesync/rules/project-overview.md +1 -1
- package/templates/marketplace/apps/fe/package.json +1 -1
- package/templates/marketplace/package.json +2 -2
- package/templates/space/.rulesync/rules/project-overview.md +1 -1
- package/templates/space/apps/fe/package.json +1 -1
- package/templates/space/package.json +2 -2
package/package.json
CHANGED
|
@@ -11,6 +11,7 @@ test("collectFiles includes hosted sources and skips denied paths", async () =>
|
|
|
11
11
|
try {
|
|
12
12
|
await mkdir(join(root, "robodev/api/invoices"), { recursive: true });
|
|
13
13
|
await mkdir(join(root, "robodev/jobs"), { recursive: true });
|
|
14
|
+
await mkdir(join(root, "robodev/sockets"), { recursive: true });
|
|
14
15
|
await mkdir(join(root, "robodev/hooks"), { recursive: true });
|
|
15
16
|
await writeFile(join(root, "robodev/database.ts"), "export default {}");
|
|
16
17
|
await writeFile(join(root, "robodev/package.json"), '{"private":true,"dependencies":{}}');
|
|
@@ -21,6 +22,7 @@ test("collectFiles includes hosted sources and skips denied paths", async () =>
|
|
|
21
22
|
await writeFile(join(root, "robodev/api/x.ts"), "export {}");
|
|
22
23
|
await writeFile(join(root, "robodev/api/invoices/[id].ts"), "export {}");
|
|
23
24
|
await writeFile(join(root, "robodev/jobs/x.ts"), "export {}");
|
|
25
|
+
await writeFile(join(root, "robodev/sockets/chat.ts"), "export {}");
|
|
24
26
|
await writeFile(join(root, "robodev/hooks/auth.ts"), "export {}");
|
|
25
27
|
await writeFile(join(root, "robodev/hooks/lib.ts"), "export {}");
|
|
26
28
|
await writeFile(join(root, "package.json"), "{}");
|
|
@@ -41,6 +43,7 @@ test("collectFiles includes hosted sources and skips denied paths", async () =>
|
|
|
41
43
|
assert.ok(paths.has("robodev/api/x.ts"));
|
|
42
44
|
assert.ok(paths.has("robodev/api/invoices/[id].ts"));
|
|
43
45
|
assert.ok(paths.has("robodev/jobs/x.ts"));
|
|
46
|
+
assert.ok(paths.has("robodev/sockets/chat.ts"));
|
|
44
47
|
assert.ok(paths.has("robodev/hooks/auth.ts"));
|
|
45
48
|
assert.ok(paths.has("robodev/hooks/lib.ts"));
|
|
46
49
|
assert.ok(paths.has("package.json"));
|
|
@@ -54,6 +57,22 @@ test("collectFiles includes hosted sources and skips denied paths", async () =>
|
|
|
54
57
|
}
|
|
55
58
|
});
|
|
56
59
|
|
|
60
|
+
test("collectFiles includes public SPA binaries as base64", async () => {
|
|
61
|
+
const root = await mkdtemp(join(tmpdir(), "rd-cli-bin-"));
|
|
62
|
+
try {
|
|
63
|
+
await writeFile(join(root, "database.ts"), "export default {}");
|
|
64
|
+
await mkdir(join(root, "public/assets"), { recursive: true });
|
|
65
|
+
await writeFile(join(root, "public/assets/font.woff2"), Buffer.from("woff2-bytes"));
|
|
66
|
+
await writeFile(join(root, "public/icon.png"), Buffer.from("png-bytes"));
|
|
67
|
+
const files = await collectFiles(root);
|
|
68
|
+
const byPath = Object.fromEntries(files.map((file) => [file.path, file.content]));
|
|
69
|
+
assert.equal(byPath["public/assets/font.woff2"], Buffer.from("woff2-bytes").toString("base64"));
|
|
70
|
+
assert.equal(byPath["public/icon.png"], Buffer.from("png-bytes").toString("base64"));
|
|
71
|
+
} finally {
|
|
72
|
+
await rm(root, { recursive: true, force: true });
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
57
76
|
test("collectFiles still accepts a legacy root database.ts tree", async () => {
|
|
58
77
|
const root = await mkdtemp(join(tmpdir(), "rd-cli-"));
|
|
59
78
|
try {
|
|
@@ -67,6 +86,8 @@ test("collectFiles still accepts a legacy root database.ts tree", async () => {
|
|
|
67
86
|
await writeFile(join(root, "api/invoices/[id].ts"), "export {}");
|
|
68
87
|
await mkdir(join(root, "jobs"), { recursive: true });
|
|
69
88
|
await writeFile(join(root, "jobs/x.ts"), "export {}");
|
|
89
|
+
await mkdir(join(root, "sockets"), { recursive: true });
|
|
90
|
+
await writeFile(join(root, "sockets/chat.ts"), "export {}");
|
|
70
91
|
await mkdir(join(root, "hooks"), { recursive: true });
|
|
71
92
|
await writeFile(join(root, "hooks/auth.ts"), "export {}");
|
|
72
93
|
await writeFile(join(root, "hooks/lib.ts"), "export {}");
|
|
@@ -87,6 +108,7 @@ test("collectFiles still accepts a legacy root database.ts tree", async () => {
|
|
|
87
108
|
assert.ok(paths.has("api/x.ts"));
|
|
88
109
|
assert.ok(paths.has("api/invoices/[id].ts"));
|
|
89
110
|
assert.ok(paths.has("jobs/x.ts"));
|
|
111
|
+
assert.ok(paths.has("sockets/chat.ts"));
|
|
90
112
|
assert.ok(paths.has("hooks/auth.ts"));
|
|
91
113
|
assert.ok(paths.has("hooks/lib.ts"));
|
|
92
114
|
assert.ok(paths.has("package.json"));
|
package/src/collect-files.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { readdir, readFile, stat } from "node:fs/promises";
|
|
2
2
|
import { join, relative } from "node:path";
|
|
3
3
|
|
|
4
|
-
export const MAX_FILE_COUNT =
|
|
4
|
+
export const MAX_FILE_COUNT = 400;
|
|
5
5
|
export const MAX_FILE_BYTES = 1024 * 1024;
|
|
6
|
-
/** Hashed Vite JS from Tiny can exceed 1MB; keep API/source at 1MB. */
|
|
7
|
-
export const MAX_STATIC_FILE_BYTES =
|
|
8
|
-
export const MAX_TREE_BYTES =
|
|
6
|
+
/** Hashed Vite JS and SPA fonts from Tiny can exceed 1MB; keep API/source at 1MB. */
|
|
7
|
+
export const MAX_STATIC_FILE_BYTES = 8 * 1024 * 1024;
|
|
8
|
+
export const MAX_TREE_BYTES = 25 * 1024 * 1024;
|
|
9
9
|
export const MAX_PATH_LENGTH = 240;
|
|
10
10
|
export const MAX_PATH_DEPTH = 12;
|
|
11
11
|
|
|
@@ -33,6 +33,18 @@ const BINARY_EXTENSIONS = new Set([
|
|
|
33
33
|
".webm",
|
|
34
34
|
".mp3",
|
|
35
35
|
]);
|
|
36
|
+
const PUBLIC_BINARY_EXTENSIONS = new Set([
|
|
37
|
+
".woff",
|
|
38
|
+
".woff2",
|
|
39
|
+
".ttf",
|
|
40
|
+
".otf",
|
|
41
|
+
".png",
|
|
42
|
+
".jpg",
|
|
43
|
+
".jpeg",
|
|
44
|
+
".gif",
|
|
45
|
+
".webp",
|
|
46
|
+
".ico",
|
|
47
|
+
]);
|
|
36
48
|
const SRC_EXTENSIONS = new Set([
|
|
37
49
|
".ts",
|
|
38
50
|
".tsx",
|
|
@@ -102,16 +114,26 @@ function isDenied(path: string): boolean {
|
|
|
102
114
|
if (isRobodevLockfile(path)) return true;
|
|
103
115
|
const name = basename(path);
|
|
104
116
|
if (name.startsWith(".") && name !== ".gitignore") return true;
|
|
105
|
-
if (BINARY_EXTENSIONS.has(extname(path))) return true;
|
|
117
|
+
if (BINARY_EXTENSIONS.has(extname(path)) && !isPublicBinaryCollectedPath(path)) return true;
|
|
106
118
|
return false;
|
|
107
119
|
}
|
|
108
120
|
|
|
121
|
+
export function isPublicBinaryCollectedPath(path: string): boolean {
|
|
122
|
+
const normalized = normalize(path);
|
|
123
|
+
return normalized.startsWith("public/") && PUBLIC_BINARY_EXTENSIONS.has(extname(normalized));
|
|
124
|
+
}
|
|
125
|
+
|
|
109
126
|
function isBackendPath(path: string): boolean {
|
|
110
127
|
const inner = path.startsWith("robodev/") ? path.slice("robodev/".length) : path;
|
|
111
128
|
if (inner === "database.ts" && (path === "database.ts" || path === "robodev/database.ts")) {
|
|
112
129
|
return true;
|
|
113
130
|
}
|
|
114
|
-
if (
|
|
131
|
+
if (
|
|
132
|
+
inner.startsWith("api/") ||
|
|
133
|
+
inner.startsWith("jobs/") ||
|
|
134
|
+
inner.startsWith("hooks/") ||
|
|
135
|
+
inner.startsWith("sockets/")
|
|
136
|
+
) {
|
|
115
137
|
return inner.endsWith(".ts");
|
|
116
138
|
}
|
|
117
139
|
return path === "robodev/package.json";
|
|
@@ -122,7 +144,9 @@ function isAllowed(path: string): boolean {
|
|
|
122
144
|
if (path.startsWith("robodev/")) return false;
|
|
123
145
|
if (path === "index.html") return true;
|
|
124
146
|
if (path.startsWith("src/")) return SRC_EXTENSIONS.has(extname(path));
|
|
125
|
-
if (path.startsWith("public/"))
|
|
147
|
+
if (path.startsWith("public/")) {
|
|
148
|
+
return PUBLIC_EXTENSIONS.has(extname(path)) || PUBLIC_BINARY_EXTENSIONS.has(extname(path));
|
|
149
|
+
}
|
|
126
150
|
if (CONFIG_FILES.has(path) || CONFIG_FILES.has(basename(path))) return true;
|
|
127
151
|
if (/^tsconfig\..+\.json$/.test(basename(path)) && !path.includes("/")) return true;
|
|
128
152
|
if (!path.includes("/") && ROOT_STATIC_EXTENSIONS.has(extname(path))) return true;
|
|
@@ -133,9 +157,14 @@ export function mixedBackendLayoutError(paths: Iterable<string>): string | null
|
|
|
133
157
|
const list = [...paths].map(normalize);
|
|
134
158
|
const hasNewDb = list.includes("robodev/database.ts");
|
|
135
159
|
const hasLegacyDb = list.includes("database.ts");
|
|
136
|
-
const hasLegacyApiJobs = list.some(
|
|
160
|
+
const hasLegacyApiJobs = list.some(
|
|
161
|
+
(path) => path.startsWith("api/") || path.startsWith("jobs/") || path.startsWith("sockets/"),
|
|
162
|
+
);
|
|
137
163
|
const hasNewApiJobs = list.some(
|
|
138
|
-
(path) =>
|
|
164
|
+
(path) =>
|
|
165
|
+
path.startsWith("robodev/api/") ||
|
|
166
|
+
path.startsWith("robodev/jobs/") ||
|
|
167
|
+
path.startsWith("robodev/sockets/"),
|
|
139
168
|
);
|
|
140
169
|
if (hasNewDb && (hasLegacyDb || hasLegacyApiJobs)) {
|
|
141
170
|
return "Do not mix robodev/ backend files with root database.ts, api/, or jobs/";
|
|
@@ -167,6 +196,20 @@ function formatMaxBytes(bytes: number): string {
|
|
|
167
196
|
return `${Math.round(bytes / (1024 * 1024))}MB`;
|
|
168
197
|
}
|
|
169
198
|
|
|
199
|
+
export function collectedFileStoredBytes(file: CollectedFile): number {
|
|
200
|
+
if (isPublicBinaryCollectedPath(file.path)) {
|
|
201
|
+
return Buffer.from(file.content, "base64").byteLength;
|
|
202
|
+
}
|
|
203
|
+
return Buffer.byteLength(file.content, "utf8");
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export async function readCollectedContent(abs: string, deployPath: string): Promise<string> {
|
|
207
|
+
if (isPublicBinaryCollectedPath(deployPath)) {
|
|
208
|
+
return (await readFile(abs)).toString("base64");
|
|
209
|
+
}
|
|
210
|
+
return readFile(abs, "utf8");
|
|
211
|
+
}
|
|
212
|
+
|
|
170
213
|
export function assertCollectedFile(file: CollectedFile): void {
|
|
171
214
|
const path = normalize(file.path);
|
|
172
215
|
if (
|
|
@@ -177,7 +220,7 @@ export function assertCollectedFile(file: CollectedFile): void {
|
|
|
177
220
|
) {
|
|
178
221
|
throw new Error(`Invalid file path: ${path}`);
|
|
179
222
|
}
|
|
180
|
-
const bytes =
|
|
223
|
+
const bytes = collectedFileStoredBytes(file);
|
|
181
224
|
const max = maxBytesForCollectedPath(path);
|
|
182
225
|
if (bytes > max) {
|
|
183
226
|
throw new Error(`File must be under ${formatMaxBytes(max)}: ${path}`);
|
|
@@ -193,7 +236,7 @@ export function assertCollectedLimits(files: CollectedFile[]): void {
|
|
|
193
236
|
assertCollectedFile(file);
|
|
194
237
|
totalBytes += Buffer.byteLength(file.content, "utf8");
|
|
195
238
|
if (totalBytes > MAX_TREE_BYTES) {
|
|
196
|
-
throw new Error(
|
|
239
|
+
throw new Error(`Deploy tree must be under ${formatMaxBytes(MAX_TREE_BYTES)}`);
|
|
197
240
|
}
|
|
198
241
|
}
|
|
199
242
|
}
|
|
@@ -241,15 +284,15 @@ export async function collectFiles(root: string): Promise<CollectedFile[]> {
|
|
|
241
284
|
if (path.length > MAX_PATH_LENGTH || path.split("/").length > MAX_PATH_DEPTH) {
|
|
242
285
|
throw new Error(`Invalid file path: ${path}`);
|
|
243
286
|
}
|
|
244
|
-
const content = await
|
|
245
|
-
const bytes =
|
|
287
|
+
const content = await readCollectedContent(abs, path);
|
|
288
|
+
const bytes = collectedFileStoredBytes({ path, content });
|
|
246
289
|
const max = maxBytesForCollectedPath(path);
|
|
247
290
|
if (bytes > max) {
|
|
248
291
|
throw new Error(`File must be under ${formatMaxBytes(max)}: ${path}`);
|
|
249
292
|
}
|
|
250
|
-
totalBytes +=
|
|
293
|
+
totalBytes += Buffer.byteLength(content, "utf8");
|
|
251
294
|
if (totalBytes > MAX_TREE_BYTES) {
|
|
252
|
-
throw new Error(
|
|
295
|
+
throw new Error(`Deploy tree must be under ${formatMaxBytes(MAX_TREE_BYTES)}`);
|
|
253
296
|
}
|
|
254
297
|
files.push({ path, content });
|
|
255
298
|
if (files.length > MAX_FILE_COUNT) {
|
|
@@ -32,27 +32,54 @@ test("resolveFrontendPaths keeps outDir inside the project", () => {
|
|
|
32
32
|
);
|
|
33
33
|
});
|
|
34
34
|
|
|
35
|
-
test("mapFrontendOutDir remaps text files and
|
|
35
|
+
test("mapFrontendOutDir remaps text files and includes public binaries", async () => {
|
|
36
36
|
const out = await mkdtemp(join(tmpdir(), "rd-out-"));
|
|
37
37
|
try {
|
|
38
38
|
await writeFile(join(out, "index.html"), "<html><body>app</body></html>");
|
|
39
39
|
await mkdir(join(out, "assets"), { recursive: true });
|
|
40
40
|
await writeFile(join(out, "assets/index-abc.js"), "console.log(1)");
|
|
41
41
|
await writeFile(join(out, "assets/index-abc.css"), "body{color:red}");
|
|
42
|
-
await writeFile(join(out, "assets/font.woff2"), "binary-font");
|
|
43
|
-
await writeFile(join(out, "icon.png"), "binary-png");
|
|
44
|
-
const
|
|
42
|
+
await writeFile(join(out, "assets/font.woff2"), Buffer.from("binary-font"));
|
|
43
|
+
await writeFile(join(out, "icon.png"), Buffer.from("binary-png"));
|
|
44
|
+
const files = await mapFrontendOutDir(out);
|
|
45
45
|
const paths = new Set(files.map((file) => file.path));
|
|
46
46
|
assert.ok(paths.has("index.html"));
|
|
47
47
|
assert.ok(paths.has("public/assets/index-abc.js"));
|
|
48
48
|
assert.ok(paths.has("public/assets/index-abc.css"));
|
|
49
|
-
assert.ok(
|
|
50
|
-
assert.ok(
|
|
51
|
-
assert.equal(skippedBinaries, true);
|
|
49
|
+
assert.ok(paths.has("public/assets/font.woff2"));
|
|
50
|
+
assert.ok(paths.has("public/icon.png"));
|
|
52
51
|
assert.equal(
|
|
53
52
|
files.find((file) => file.path === "index.html")?.content,
|
|
54
53
|
"<html><body>app</body></html>",
|
|
55
54
|
);
|
|
55
|
+
assert.equal(
|
|
56
|
+
files.find((file) => file.path === "public/assets/font.woff2")?.content,
|
|
57
|
+
Buffer.from("binary-font").toString("base64"),
|
|
58
|
+
);
|
|
59
|
+
assert.equal(
|
|
60
|
+
files.find((file) => file.path === "public/icon.png")?.content,
|
|
61
|
+
Buffer.from("binary-png").toString("base64"),
|
|
62
|
+
);
|
|
63
|
+
} finally {
|
|
64
|
+
await rm(out, { recursive: true, force: true });
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("mapFrontendOutDir throws when dist still has denied files", async () => {
|
|
69
|
+
const out = await mkdtemp(join(tmpdir(), "rd-out-skip-"));
|
|
70
|
+
try {
|
|
71
|
+
await writeFile(join(out, "index.html"), "<html></html>");
|
|
72
|
+
await mkdir(join(out, "assets"), { recursive: true });
|
|
73
|
+
await writeFile(join(out, "assets/app.wasm"), Buffer.from("wasm"));
|
|
74
|
+
await writeFile(join(out, "clip.mp4"), Buffer.from("mp4"));
|
|
75
|
+
await assert.rejects(
|
|
76
|
+
() => mapFrontendOutDir(out),
|
|
77
|
+
(err: unknown) =>
|
|
78
|
+
err instanceof Error &&
|
|
79
|
+
/cannot be uploaded/.test(err.message) &&
|
|
80
|
+
err.message.includes("public/assets/app.wasm") &&
|
|
81
|
+
err.message.includes("public/clip.mp4"),
|
|
82
|
+
);
|
|
56
83
|
} finally {
|
|
57
84
|
await rm(out, { recursive: true, force: true });
|
|
58
85
|
}
|
package/src/frontend-build.ts
CHANGED
|
@@ -2,7 +2,12 @@ import { execFile, spawn } from "node:child_process";
|
|
|
2
2
|
import { readdir, readFile, stat } from "node:fs/promises";
|
|
3
3
|
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
4
4
|
import { promisify } from "node:util";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
assertCollectedFile,
|
|
7
|
+
type CollectedFile,
|
|
8
|
+
readCollectedContent,
|
|
9
|
+
shouldCollectPath,
|
|
10
|
+
} from "./collect-files.js";
|
|
6
11
|
import { isFrontendConfig, normalizeFrontendConfig, type FrontendConfig } from "./config.js";
|
|
7
12
|
|
|
8
13
|
const execFileAsync = promisify(execFile);
|
|
@@ -132,12 +137,9 @@ function mappedDeployPath(relFromOutDir: string): string {
|
|
|
132
137
|
return `public/${rel}`;
|
|
133
138
|
}
|
|
134
139
|
|
|
135
|
-
export async function mapFrontendOutDir(outDirAbs: string): Promise<{
|
|
136
|
-
files: CollectedFile[];
|
|
137
|
-
skippedBinaries: boolean;
|
|
138
|
-
}> {
|
|
140
|
+
export async function mapFrontendOutDir(outDirAbs: string): Promise<CollectedFile[]> {
|
|
139
141
|
const files: CollectedFile[] = [];
|
|
140
|
-
|
|
142
|
+
const skipped: string[] = [];
|
|
141
143
|
|
|
142
144
|
async function walk(dir: string): Promise<void> {
|
|
143
145
|
let entries;
|
|
@@ -156,10 +158,10 @@ export async function mapFrontendOutDir(outDirAbs: string): Promise<{
|
|
|
156
158
|
const rel = relative(outDirAbs, abs).replaceAll("\\", "/");
|
|
157
159
|
const deployPath = mappedDeployPath(rel);
|
|
158
160
|
if (!shouldCollectPath(deployPath)) {
|
|
159
|
-
|
|
161
|
+
skipped.push(deployPath);
|
|
160
162
|
continue;
|
|
161
163
|
}
|
|
162
|
-
const content = await
|
|
164
|
+
const content = await readCollectedContent(abs, deployPath);
|
|
163
165
|
const file = { path: deployPath, content };
|
|
164
166
|
assertCollectedFile(file);
|
|
165
167
|
files.push(file);
|
|
@@ -167,7 +169,24 @@ export async function mapFrontendOutDir(outDirAbs: string): Promise<{
|
|
|
167
169
|
}
|
|
168
170
|
|
|
169
171
|
await walk(outDirAbs);
|
|
170
|
-
|
|
172
|
+
if (skipped.length > 0) {
|
|
173
|
+
throw new Error(
|
|
174
|
+
`Frontend build produced files that cannot be uploaded: ${skipped.sort().join(", ")}`,
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
return files;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export async function frontendOutIndexExists(
|
|
181
|
+
projectRoot: string,
|
|
182
|
+
frontend: FrontendConfig,
|
|
183
|
+
): Promise<boolean> {
|
|
184
|
+
try {
|
|
185
|
+
const { outAbs } = resolveFrontendPaths(projectRoot, frontend);
|
|
186
|
+
return (await stat(join(outAbs, "index.html"))).isFile();
|
|
187
|
+
} catch {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
171
190
|
}
|
|
172
191
|
|
|
173
192
|
export async function buildAndCollectFrontend(
|
|
@@ -199,14 +218,9 @@ export async function buildAndCollectFrontend(
|
|
|
199
218
|
throw new Error(`Frontend build did not produce ${join(outAbs, "index.html")}`);
|
|
200
219
|
}
|
|
201
220
|
|
|
202
|
-
const
|
|
221
|
+
const files = await mapFrontendOutDir(outAbs);
|
|
203
222
|
if (!files.some((file) => file.path === "index.html")) {
|
|
204
223
|
throw new Error(`Frontend build did not produce ${join(outAbs, "index.html")}`);
|
|
205
224
|
}
|
|
206
|
-
if (skippedBinaries) {
|
|
207
|
-
console.log(
|
|
208
|
-
"Skipped binary frontend assets (fonts/images). Hosted UI may use system font fallback.",
|
|
209
|
-
);
|
|
210
|
-
}
|
|
211
225
|
return files;
|
|
212
226
|
}
|
package/src/index.ts
CHANGED
|
@@ -23,7 +23,11 @@ import {
|
|
|
23
23
|
} from "./config.js";
|
|
24
24
|
import { waitUntilLive } from "./wait-until-live.js";
|
|
25
25
|
import { collectFiles, mergeCollectedFiles, type CollectedFile } from "./collect-files.js";
|
|
26
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
buildAndCollectFrontend,
|
|
28
|
+
commandAvailable,
|
|
29
|
+
frontendOutIndexExists,
|
|
30
|
+
} from "./frontend-build.js";
|
|
27
31
|
import {
|
|
28
32
|
emitStarter,
|
|
29
33
|
loadCatalog,
|
|
@@ -538,6 +542,9 @@ async function runCreate(args: string[]): Promise<void> {
|
|
|
538
542
|
});
|
|
539
543
|
feHosted = true;
|
|
540
544
|
} catch (error) {
|
|
545
|
+
if (await frontendOutIndexExists(root, defaultFrontendConfig())) {
|
|
546
|
+
throw error;
|
|
547
|
+
}
|
|
541
548
|
console.log(error instanceof Error ? error.message : error);
|
|
542
549
|
console.log("Frontend build failed. Deploying schema and APIs only.");
|
|
543
550
|
skipFrontendBuild = true;
|
|
@@ -13,7 +13,7 @@ cursor:
|
|
|
13
13
|
|
|
14
14
|
This project is a Vite React frontend in `apps/fe` plus a Robodev backend under **`robodev/`** (`robodev/database.ts` + `robodev/api/**/*.ts` + `robodev/hooks/auth.ts` using `@robodev-ai/sdk`). There is no Expo / mobile app in this starter. End-user auth is reserved on the project host at `/api/user/*`. There is no in-browser fake backend.
|
|
15
15
|
|
|
16
|
-
`robodev deploy` from this root uploads schema and
|
|
16
|
+
`robodev deploy` from this root uploads schema, APIs, and (when `.robodev` has a `frontend` block) the local Vite build as a hosted static SPA. Use bun in `apps/fe` to iterate locally.
|
|
17
17
|
|
|
18
18
|
Shared AI rules and skills are authored in `.rulesync/`. Generated files for Codex, Claude Code, and Cursor are local outputs and should be regenerated with `bun rules:gen`.
|
|
19
19
|
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"description": "Auth chat starter frontend",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"scripts": {
|
|
8
|
-
"dev": "
|
|
8
|
+
"dev": "vite --host",
|
|
9
9
|
"build": "bun run openapi:gen && bunx --bun vite build",
|
|
10
10
|
"clean": "rimraf dist src/openapi",
|
|
11
11
|
"lint": "oxlint --type-aware . ../../robodev/api ../../robodev/database.ts --fix",
|
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
"rules:gen": "rulesync generate"
|
|
9
9
|
},
|
|
10
10
|
"dependencies": {
|
|
11
|
-
"@robodev-ai/sdk": "^0.
|
|
11
|
+
"@robodev-ai/sdk": "^0.10.0"
|
|
12
12
|
},
|
|
13
13
|
"devDependencies": {
|
|
14
|
-
"robodev": "^0.
|
|
14
|
+
"robodev": "^0.30.0",
|
|
15
15
|
"rulesync": "^8.18.0",
|
|
16
16
|
"typescript": "^5.9.2"
|
|
17
17
|
}
|
|
@@ -13,7 +13,7 @@ cursor:
|
|
|
13
13
|
|
|
14
14
|
This project is a Vite React frontend in `apps/fe` plus a Robodev backend under **`robodev/`** (`robodev/database.ts` + `robodev/api/**/*.ts` using `@robodev-ai/sdk`). There is no Expo / mobile app in this starter. End-user auth is reserved on the project host at `/api/user/*`. There is no in-browser fake backend.
|
|
15
15
|
|
|
16
|
-
`robodev deploy` from this root uploads schema and
|
|
16
|
+
`robodev deploy` from this root uploads schema, APIs, and (when `.robodev` has a `frontend` block) the local Vite build as a hosted static SPA. Use bun in `apps/fe` to iterate locally.
|
|
17
17
|
|
|
18
18
|
Shared AI rules and skills are authored in `.rulesync/`. Generated files for Codex, Claude Code, and Cursor are local outputs and should be regenerated with `bun rules:gen`.
|
|
19
19
|
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"description": "Marketplace starter frontend",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"scripts": {
|
|
8
|
-
"dev": "
|
|
8
|
+
"dev": "vite --host",
|
|
9
9
|
"build": "bun run openapi:gen && bunx --bun vite build",
|
|
10
10
|
"clean": "rimraf dist src/openapi",
|
|
11
11
|
"lint": "oxlint --type-aware . ../../robodev/api ../../robodev/database.ts --fix",
|
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
"rules:gen": "rulesync generate"
|
|
9
9
|
},
|
|
10
10
|
"dependencies": {
|
|
11
|
-
"@robodev-ai/sdk": "^0.
|
|
11
|
+
"@robodev-ai/sdk": "^0.10.0"
|
|
12
12
|
},
|
|
13
13
|
"devDependencies": {
|
|
14
|
-
"robodev": "^0.
|
|
14
|
+
"robodev": "^0.30.0",
|
|
15
15
|
"rulesync": "^8.18.0",
|
|
16
16
|
"typescript": "^5.9.2"
|
|
17
17
|
}
|
|
@@ -13,7 +13,7 @@ cursor:
|
|
|
13
13
|
|
|
14
14
|
This project is a Vite React frontend in `apps/fe` plus a Robodev backend under **`robodev/`** (`robodev/database.ts` + `robodev/api/**/*.ts` using `@robodev-ai/sdk`). There is no Expo / mobile app in this starter. End-user auth is reserved on the project host at `/api/user/*`. There is no in-browser fake backend.
|
|
15
15
|
|
|
16
|
-
`robodev deploy` from this root uploads schema and
|
|
16
|
+
`robodev deploy` from this root uploads schema, APIs, and (when `.robodev` has a `frontend` block) the local Vite build as a hosted static SPA. Use bun in `apps/fe` to iterate locally.
|
|
17
17
|
|
|
18
18
|
Shared AI rules and skills are authored in `.rulesync/`. Generated files for Codex, Claude Code, and Cursor are local outputs and should be regenerated with `bun rules:gen`.
|
|
19
19
|
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"description": "Space starter frontend",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"scripts": {
|
|
8
|
-
"dev": "
|
|
8
|
+
"dev": "vite --host",
|
|
9
9
|
"build": "bun run openapi:gen && bunx --bun vite build",
|
|
10
10
|
"clean": "rimraf dist src/openapi",
|
|
11
11
|
"lint": "oxlint --type-aware . ../../robodev/api ../../robodev/database.ts --fix",
|
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
"rules:gen": "rulesync generate"
|
|
9
9
|
},
|
|
10
10
|
"dependencies": {
|
|
11
|
-
"@robodev-ai/sdk": "^0.
|
|
11
|
+
"@robodev-ai/sdk": "^0.10.0"
|
|
12
12
|
},
|
|
13
13
|
"devDependencies": {
|
|
14
|
-
"robodev": "^0.
|
|
14
|
+
"robodev": "^0.30.0",
|
|
15
15
|
"rulesync": "^8.18.0",
|
|
16
16
|
"typescript": "^5.9.2"
|
|
17
17
|
}
|