@flaredev/cli 0.4.1 → 0.5.1
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/dist/index.js +698 -341
- package/package.json +2 -2
- package/templates/app/app/dashboard/costs/page.tsx +93 -0
- package/templates/app/app/dashboard/page.tsx +4 -7
- package/templates/app/app/page.tsx +10 -0
- package/templates/app/components/dashboard/cost-estimator.tsx +77 -0
- package/templates/app/components/dashboard/fields/field-widget.tsx +237 -186
- package/templates/app/components/dashboard/record-actions.tsx +7 -7
- package/templates/app/components/dashboard/record-detail.tsx +1 -1
- package/templates/app/components/dashboard/resource-form-sheet.tsx +86 -0
- package/templates/app/components/dashboard/resource-form.tsx +129 -33
- package/templates/app/components/dashboard/resource-icon.tsx +87 -85
- package/templates/app/components/dashboard/resource-stats.tsx +10 -9
- package/templates/app/components/dashboard/resource-table.tsx +4 -4
- package/templates/app/components/dashboard/row-actions.tsx +3 -3
- package/templates/app/lib/costs.ts +138 -0
- package/templates/app/lib/dashboard-nav.ts +5 -1
- package/templates/app/lib/dashboard.ts +66 -61
- package/templates/app/lib/number.ts +33 -0
- package/templates/app/lib/site.ts +56 -56
- package/templates/app/lib/suggest.ts +35 -0
- package/templates/app/lib/usage.ts +32 -0
- package/templates/next/.env.example +21 -0
- package/templates/next/README.md +16 -0
- package/templates/next/app/api/health/route.ts +7 -0
- package/templates/next/app/dashboard/account/password/page.tsx +19 -0
- package/templates/next/app/dashboard/account/security/page.tsx +31 -0
- package/templates/next/app/dashboard/actions.ts +277 -0
- package/templates/next/app/dashboard/layout.tsx +64 -0
- package/templates/next/app/dashboard/page.tsx +127 -0
- package/templates/next/components/dashboard/resource-chart.tsx +86 -0
- package/templates/next/lib/analytics.ts +71 -0
- package/templates/next/lib/audit.ts +80 -0
- package/templates/next/lib/auth-emails.ts +92 -0
- package/templates/next/lib/auth.ts +156 -0
- package/templates/next/lib/dashboard.ts +161 -0
- package/templates/next/lib/db.ts +24 -0
- package/templates/next/lib/mail.ts +35 -0
- package/templates/next/lib/realtime.ts +23 -0
- package/templates/next/lib/redis.ts +17 -0
- package/templates/next/lib/storage.ts +57 -0
- package/templates/next/lib/theme.ts +83 -0
- package/templates/next/lib/views.ts +23 -0
- package/templates/next/next.config.ts +13 -0
- package/templates/next/prisma/schema/base.prisma +162 -0
- package/templates/next/prisma.config.ts +15 -0
- package/templates/next/tsconfig.json +24 -0
- package/templates/app/components/dashboard/resource-form-dialog.tsx +0 -80
package/dist/index.js
CHANGED
|
@@ -9,8 +9,8 @@ import { FLARE_VERSION as FLARE_VERSION3 } from "@flaredev/core";
|
|
|
9
9
|
|
|
10
10
|
// src/commands/create.ts
|
|
11
11
|
import { randomBytes } from "crypto";
|
|
12
|
-
import { appendFileSync, copyFileSync, existsSync as
|
|
13
|
-
import { basename, dirname as dirname2, isAbsolute, join as
|
|
12
|
+
import { appendFileSync, copyFileSync, existsSync as existsSync3, readdirSync as readdirSync2, rmSync, writeFileSync as writeFileSync3 } from "fs";
|
|
13
|
+
import { basename, dirname as dirname2, isAbsolute, join as join3, relative, resolve as resolve2 } from "path";
|
|
14
14
|
import * as prompts from "@clack/prompts";
|
|
15
15
|
import pc from "picocolors";
|
|
16
16
|
import { FLARE_VERSION } from "@flaredev/core";
|
|
@@ -186,42 +186,94 @@ var APP_DEV_DEPENDENCIES = {
|
|
|
186
186
|
vite: "^8.3.0",
|
|
187
187
|
wrangler: "^4.132.0"
|
|
188
188
|
};
|
|
189
|
+
var NEXT_DEPENDENCIES = {
|
|
190
|
+
next: "16.0.4",
|
|
191
|
+
"@prisma/client": "7.10.0",
|
|
192
|
+
"@prisma/adapter-pg": "7.10.0",
|
|
193
|
+
pg: "^8.16.3",
|
|
194
|
+
"@upstash/redis": "^1.36.0",
|
|
195
|
+
"@better-auth/prisma-adapter": "1.7.5",
|
|
196
|
+
// Signs S3 requests in a few kilobytes, where the AWS SDK is megabytes.
|
|
197
|
+
aws4fetch: "^1.0.20",
|
|
198
|
+
dotenv: "^17.2.3"
|
|
199
|
+
};
|
|
200
|
+
var NEXT_DEV_DEPENDENCIES = {
|
|
201
|
+
prisma: "7.10.0",
|
|
202
|
+
// Runs seeds: the generated Prisma client is TypeScript a bundler compiles.
|
|
203
|
+
tsx: "^4.23.13",
|
|
204
|
+
"@types/node": "^22.20.3",
|
|
205
|
+
"@types/react": "^19.3.0",
|
|
206
|
+
"@types/react-dom": "^19.3.0",
|
|
207
|
+
"@tailwindcss/postcss": "^4.3.3",
|
|
208
|
+
tailwindcss: "^4.3.3",
|
|
209
|
+
typescript: "~5.9.3"
|
|
210
|
+
};
|
|
211
|
+
var CLOUDFLARE_ONLY = [
|
|
212
|
+
"wrangler.jsonc",
|
|
213
|
+
"vite.config.ts",
|
|
214
|
+
"worker",
|
|
215
|
+
"db",
|
|
216
|
+
"migrations",
|
|
217
|
+
"drizzle.config.ts",
|
|
218
|
+
".dev.vars.example",
|
|
219
|
+
"worker-configuration.d.ts"
|
|
220
|
+
];
|
|
221
|
+
|
|
222
|
+
// src/stack.ts
|
|
223
|
+
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
224
|
+
import { join } from "path";
|
|
225
|
+
var STACKS = ["cloudflare", "next"];
|
|
226
|
+
var isStack = (value) => STACKS.includes(value);
|
|
227
|
+
var STACK_LABELS = {
|
|
228
|
+
cloudflare: "Cloudflare Workers, D1 and Drizzle \u2014 cheapest to run, deploys everywhere",
|
|
229
|
+
next: "Next.js on Vercel, Neon Postgres and Prisma \u2014 real Postgres, Node runtime"
|
|
230
|
+
};
|
|
231
|
+
function readStack(appRoot) {
|
|
232
|
+
const file = join(appRoot, "package.json");
|
|
233
|
+
if (!existsSync(file)) return "cloudflare";
|
|
234
|
+
try {
|
|
235
|
+
const parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
236
|
+
return isStack(parsed.flare?.stack) ? parsed.flare.stack : "cloudflare";
|
|
237
|
+
} catch {
|
|
238
|
+
return "cloudflare";
|
|
239
|
+
}
|
|
240
|
+
}
|
|
189
241
|
|
|
190
242
|
// src/utils/fs.ts
|
|
191
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "fs";
|
|
192
|
-
import { dirname, join, resolve } from "path";
|
|
243
|
+
import { existsSync as existsSync2, mkdirSync, readdirSync, readFileSync as readFileSync2, statSync, writeFileSync as writeFileSync2 } from "fs";
|
|
244
|
+
import { dirname, join as join2, resolve } from "path";
|
|
193
245
|
import { fileURLToPath } from "url";
|
|
194
246
|
var RENAMES = { _gitignore: ".gitignore" };
|
|
195
247
|
function copyTemplate(from, to, tokens) {
|
|
196
248
|
mkdirSync(to, { recursive: true });
|
|
197
249
|
let files = 0;
|
|
198
250
|
for (const entry of readdirSync(from)) {
|
|
199
|
-
const src =
|
|
200
|
-
const dest =
|
|
251
|
+
const src = join2(from, entry);
|
|
252
|
+
const dest = join2(to, RENAMES[entry] ?? entry);
|
|
201
253
|
if (statSync(src).isDirectory()) {
|
|
202
254
|
files += copyTemplate(src, dest, tokens);
|
|
203
255
|
continue;
|
|
204
256
|
}
|
|
205
|
-
const content =
|
|
206
|
-
|
|
257
|
+
const content = readFileSync2(src, "utf8").replace(/__([A-Z_]+)__/g, (match, key) => tokens[key] ?? match);
|
|
258
|
+
writeFileSync2(dest, content);
|
|
207
259
|
files++;
|
|
208
260
|
}
|
|
209
261
|
return files;
|
|
210
262
|
}
|
|
211
263
|
function writeJson(path, value) {
|
|
212
264
|
mkdirSync(dirname(path), { recursive: true });
|
|
213
|
-
|
|
265
|
+
writeFileSync2(path, JSON.stringify(value, null, 2) + "\n");
|
|
214
266
|
}
|
|
215
267
|
function findUp(file, start) {
|
|
216
268
|
let dir = resolve(start);
|
|
217
269
|
while (true) {
|
|
218
|
-
if (
|
|
270
|
+
if (existsSync2(join2(dir, file))) return dir;
|
|
219
271
|
const parent = dirname(dir);
|
|
220
272
|
if (parent === dir) return void 0;
|
|
221
273
|
dir = parent;
|
|
222
274
|
}
|
|
223
275
|
}
|
|
224
|
-
var templatesDir =
|
|
276
|
+
var templatesDir = join2(findUp("templates", dirname(fileURLToPath(import.meta.url))), "templates");
|
|
225
277
|
|
|
226
278
|
// src/utils/pm.ts
|
|
227
279
|
import { spawnSync } from "child_process";
|
|
@@ -260,7 +312,7 @@ function toAppName(dir) {
|
|
|
260
312
|
function flarePackageSpec(pkg, appDir, packageManager) {
|
|
261
313
|
const repoPackage = resolve2(templatesDir, "../..", pkg);
|
|
262
314
|
const repoRoot = resolve2(repoPackage, "../..");
|
|
263
|
-
const fromCheckout =
|
|
315
|
+
const fromCheckout = existsSync3(join3(repoPackage, "package.json")) && existsSync3(join3(repoRoot, "pnpm-workspace.yaml")) && !repoPackage.split(/[\\/]/).includes("node_modules");
|
|
264
316
|
if (!fromCheckout) return `^${FLARE_VERSION}`;
|
|
265
317
|
const rel = relative(repoRoot, appDir);
|
|
266
318
|
if (rel && !rel.startsWith("..") && !isAbsolute(rel)) return "workspace:*";
|
|
@@ -270,32 +322,54 @@ function flarePackageSpec(pkg, appDir, packageManager) {
|
|
|
270
322
|
function createApp(target, options2 = {}) {
|
|
271
323
|
const dir = resolve2(target);
|
|
272
324
|
const name = toAppName(dir);
|
|
273
|
-
if (
|
|
325
|
+
if (existsSync3(dir) && readdirSync2(dir).length > 0) {
|
|
274
326
|
throw new Error(`Directory ${dir} already exists and is not empty.`);
|
|
275
327
|
}
|
|
276
328
|
if (options2.pm !== void 0 && !isPackageManager(options2.pm)) {
|
|
277
329
|
throw new Error(`Unknown package manager "${options2.pm}". Use pnpm, npm, yarn, or bun.`);
|
|
278
330
|
}
|
|
279
331
|
const packageManager = options2.pm ?? detectPackageManager();
|
|
332
|
+
if (options2.stack !== void 0 && !isStack(options2.stack)) {
|
|
333
|
+
throw new Error(`Unknown stack "${options2.stack}". Use ${STACKS.join(" or ")}.`);
|
|
334
|
+
}
|
|
335
|
+
const stack = options2.stack ?? "cloudflare";
|
|
280
336
|
const authProviders = parseAuthProviders(options2.authProviders);
|
|
281
337
|
const authMethods = parseAuthMethods(options2.auth);
|
|
282
338
|
const theme = parseTheme(options2.theme);
|
|
283
339
|
const inWorkspace = findUp("pnpm-workspace.yaml", dirname2(dir)) !== void 0;
|
|
284
340
|
const compatibilityDate = options2.compatibilityDate ?? COMPATIBILITY_DATE;
|
|
285
|
-
const files = copyTemplate(
|
|
341
|
+
const files = copyTemplate(join3(templatesDir, "app"), dir, {
|
|
286
342
|
APP_NAME: name,
|
|
287
343
|
COMPAT_DATE: compatibilityDate,
|
|
288
344
|
PM: packageManager,
|
|
289
345
|
THEME: theme
|
|
290
346
|
});
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
347
|
+
if (stack === "next") {
|
|
348
|
+
copyTemplate(join3(templatesDir, "next"), dir, { APP_NAME: name, PM: packageManager, THEME: theme });
|
|
349
|
+
for (const path of CLOUDFLARE_ONLY) rmSync(join3(dir, path), { recursive: true, force: true });
|
|
350
|
+
}
|
|
351
|
+
writeFileSync3(join3(dir, "lib", "auth-config.ts"), renderAuthConfig(authMethods, authProviders));
|
|
352
|
+
appendFileSync(join3(dir, stack === "next" ? ".env.example" : ".dev.vars.example"), devVarsExampleEntries(authProviders));
|
|
353
|
+
const shared = { react: APP_DEPENDENCIES.react, "react-dom": APP_DEPENDENCIES["react-dom"] };
|
|
354
|
+
const uiOnly = Object.fromEntries(
|
|
355
|
+
Object.entries(APP_DEPENDENCIES).filter(([key]) => !["vinext", "@vinext/cloudflare", "drizzle-orm", "react-server-dom-webpack"].includes(key))
|
|
356
|
+
);
|
|
357
|
+
writeJson(join3(dir, "package.json"), {
|
|
294
358
|
name,
|
|
295
359
|
version: "0.1.0",
|
|
296
360
|
private: true,
|
|
297
361
|
type: "module",
|
|
298
|
-
|
|
362
|
+
// Which stack this app targets. `flare gen` reads it; don't change it by hand.
|
|
363
|
+
flare: { stack },
|
|
364
|
+
scripts: stack === "next" ? {
|
|
365
|
+
dev: "next dev",
|
|
366
|
+
build: "prisma generate && next build",
|
|
367
|
+
start: "next start",
|
|
368
|
+
deploy: "vercel deploy --prod",
|
|
369
|
+
"db:generate": "prisma generate",
|
|
370
|
+
"db:migrate": "prisma migrate dev",
|
|
371
|
+
"db:studio": "prisma studio"
|
|
372
|
+
} : {
|
|
299
373
|
dev: "flare dev",
|
|
300
374
|
build: "flare build",
|
|
301
375
|
start: "flare start",
|
|
@@ -304,18 +378,25 @@ function createApp(target, options2 = {}) {
|
|
|
304
378
|
"db:generate": "drizzle-kit generate",
|
|
305
379
|
"db:migrate:local": "wrangler d1 migrations apply DB --local"
|
|
306
380
|
},
|
|
307
|
-
dependencies: {
|
|
308
|
-
|
|
381
|
+
dependencies: {
|
|
382
|
+
"@flaredev/core": flarePackageSpec("core", dir, packageManager),
|
|
383
|
+
...stack === "next" ? { ...uiOnly, ...shared, ...NEXT_DEPENDENCIES } : APP_DEPENDENCIES
|
|
384
|
+
},
|
|
385
|
+
devDependencies: {
|
|
386
|
+
"@flaredev/cli": flarePackageSpec("cli", dir, packageManager),
|
|
387
|
+
...stack === "next" ? NEXT_DEV_DEPENDENCIES : APP_DEV_DEPENDENCIES
|
|
388
|
+
}
|
|
309
389
|
});
|
|
310
|
-
|
|
311
|
-
|
|
390
|
+
writeFileSync3(
|
|
391
|
+
join3(dir, stack === "next" ? ".env" : ".dev.vars"),
|
|
312
392
|
`BETTER_AUTH_SECRET=${randomBytes(32).toString("base64")}
|
|
313
393
|
RESEND_API_KEY=
|
|
314
394
|
MAIL_FROM=
|
|
315
395
|
${devVarsEntries(authProviders)}`
|
|
316
396
|
);
|
|
317
397
|
if (packageManager === "pnpm" && !inWorkspace) {
|
|
318
|
-
|
|
398
|
+
const allowBuilds = stack === "next" ? "allowBuilds:\n '@prisma/engines': true\n prisma: true\n sharp: false\n" : "allowBuilds:\n esbuild: true\n workerd: true\n sharp: false\n";
|
|
399
|
+
writeFileSync3(join3(dir, "pnpm-workspace.yaml"), allowBuilds);
|
|
319
400
|
}
|
|
320
401
|
const pinned = flarePackageSpec("core", dir, packageManager).startsWith("^");
|
|
321
402
|
if (pinned) copyLockfile(packageManager, dir);
|
|
@@ -326,9 +407,9 @@ ${devVarsEntries(authProviders)}`
|
|
|
326
407
|
function copyLockfile(packageManager, dir) {
|
|
327
408
|
const file = packageManager === "pnpm" ? "pnpm-lock.yaml" : packageManager === "yarn" ? "yarn.lock" : "package-lock.json";
|
|
328
409
|
if (!file) return false;
|
|
329
|
-
const source =
|
|
330
|
-
if (!
|
|
331
|
-
copyFileSync(source,
|
|
410
|
+
const source = join3(templatesDir, "locks", file);
|
|
411
|
+
if (!existsSync3(source)) return false;
|
|
412
|
+
copyFileSync(source, join3(dir, file));
|
|
332
413
|
return true;
|
|
333
414
|
}
|
|
334
415
|
function printNextSteps(result, installed) {
|
|
@@ -348,8 +429,8 @@ function printNextSteps(result, installed) {
|
|
|
348
429
|
}
|
|
349
430
|
|
|
350
431
|
// src/commands/gen-billing.ts
|
|
351
|
-
import { appendFileSync as appendFileSync2, existsSync as
|
|
352
|
-
import { dirname as dirname6, join as
|
|
432
|
+
import { appendFileSync as appendFileSync2, existsSync as existsSync13, mkdirSync as mkdirSync5, readFileSync as readFileSync9, unlinkSync, writeFileSync as writeFileSync7 } from "fs";
|
|
433
|
+
import { dirname as dirname6, join as join12 } from "path";
|
|
353
434
|
import pc6 from "picocolors";
|
|
354
435
|
|
|
355
436
|
// src/generator/grammar.ts
|
|
@@ -513,7 +594,7 @@ import { defineResource, field as builders, kebabCase, pascalCase } from "@flare
|
|
|
513
594
|
|
|
514
595
|
// src/generator/markers.ts
|
|
515
596
|
import { createHash } from "crypto";
|
|
516
|
-
import { existsSync as
|
|
597
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
517
598
|
import { dirname as dirname3 } from "path";
|
|
518
599
|
var START = "// generated:start";
|
|
519
600
|
var END = "// generated:end";
|
|
@@ -563,8 +644,8 @@ ${indentBlock(body, indent)}${indent}${END}
|
|
|
563
644
|
${after}`;
|
|
564
645
|
}
|
|
565
646
|
function inspectGenerated(path, content) {
|
|
566
|
-
if (!
|
|
567
|
-
const parts = splitMarkers(
|
|
647
|
+
if (!existsSync4(path)) return "missing";
|
|
648
|
+
const parts = splitMarkers(readFileSync3(path, "utf8"));
|
|
568
649
|
if (!parts) return "unmarked";
|
|
569
650
|
if (parts.hash && hashBlock(parts.block) !== parts.hash) return "drift";
|
|
570
651
|
if (normalize(parts.block) !== normalize(content)) return "outdated";
|
|
@@ -580,18 +661,18 @@ var DriftError = class extends Error {
|
|
|
580
661
|
};
|
|
581
662
|
function writeGenerated(path, content, options2 = {}) {
|
|
582
663
|
const hash = hashBlock(content);
|
|
583
|
-
if (!
|
|
664
|
+
if (!existsSync4(path)) {
|
|
584
665
|
mkdirSync2(dirname3(path), { recursive: true });
|
|
585
|
-
|
|
666
|
+
writeFileSync4(path, joinMarkers({ before: options2.header ?? "", block: content, after: "", indent: "", hash }));
|
|
586
667
|
return "create";
|
|
587
668
|
}
|
|
588
|
-
const source =
|
|
669
|
+
const source = readFileSync3(path, "utf8");
|
|
589
670
|
const parts = splitMarkers(source);
|
|
590
671
|
if (!parts) throw new Error(`${path} exists but has no generated block; refusing to overwrite hand-written code.`);
|
|
591
672
|
if (parts.hash && hashBlock(parts.block) !== parts.hash && !options2.force) throw new DriftError(path);
|
|
592
673
|
const next = joinMarkers({ ...parts, block: content, hash });
|
|
593
674
|
if (next === source) return "identical";
|
|
594
|
-
|
|
675
|
+
writeFileSync4(path, next);
|
|
595
676
|
return "update";
|
|
596
677
|
}
|
|
597
678
|
|
|
@@ -1346,17 +1427,17 @@ var billingDevVarsExampleEntries = () => [
|
|
|
1346
1427
|
].join("\n");
|
|
1347
1428
|
|
|
1348
1429
|
// src/generator/load.ts
|
|
1349
|
-
import { existsSync as
|
|
1350
|
-
import { join as
|
|
1430
|
+
import { existsSync as existsSync5, readdirSync as readdirSync3 } from "fs";
|
|
1431
|
+
import { join as join4, resolve as resolve3 } from "path";
|
|
1351
1432
|
import { createJiti } from "jiti";
|
|
1352
1433
|
async function loadResources(root) {
|
|
1353
1434
|
const appRoot = resolve3(root);
|
|
1354
|
-
const dir =
|
|
1355
|
-
if (!
|
|
1356
|
-
const jiti = createJiti(
|
|
1435
|
+
const dir = join4(appRoot, "resources");
|
|
1436
|
+
if (!existsSync5(dir)) return [];
|
|
1437
|
+
const jiti = createJiti(join4(appRoot, "package.json"), { alias: { "@": appRoot }, moduleCache: false, fsCache: false });
|
|
1357
1438
|
const loaded = [];
|
|
1358
1439
|
for (const file of readdirSync3(dir).filter((name) => name.endsWith(".resource.ts")).sort()) {
|
|
1359
|
-
const path =
|
|
1440
|
+
const path = join4(dir, file);
|
|
1360
1441
|
const mod = await jiti.import(path);
|
|
1361
1442
|
const resource = mod.default;
|
|
1362
1443
|
if (!resource || typeof resource !== "object" || !("fields" in resource) || !("table" in resource)) {
|
|
@@ -1374,20 +1455,20 @@ async function loadResources(root) {
|
|
|
1374
1455
|
|
|
1375
1456
|
// src/generator/migrations.ts
|
|
1376
1457
|
import { spawn as spawn4 } from "child_process";
|
|
1377
|
-
import { existsSync as
|
|
1378
|
-
import { join as
|
|
1458
|
+
import { existsSync as existsSync10, readdirSync as readdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "fs";
|
|
1459
|
+
import { join as join9 } from "path";
|
|
1379
1460
|
|
|
1380
1461
|
// src/commands/run.ts
|
|
1381
1462
|
import { spawn as spawn3 } from "child_process";
|
|
1382
|
-
import { existsSync as
|
|
1383
|
-
import { dirname as dirname4, join as
|
|
1463
|
+
import { existsSync as existsSync9, readFileSync as readFileSync6 } from "fs";
|
|
1464
|
+
import { dirname as dirname4, join as join8, resolve as resolve4 } from "path";
|
|
1384
1465
|
import pc4 from "picocolors";
|
|
1385
1466
|
|
|
1386
1467
|
// src/tunnel.ts
|
|
1387
1468
|
import { spawn, spawnSync as spawnSync2 } from "child_process";
|
|
1388
|
-
import { chmodSync, existsSync as
|
|
1469
|
+
import { chmodSync, existsSync as existsSync6, mkdirSync as mkdirSync3, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync5 } from "fs";
|
|
1389
1470
|
import { homedir } from "os";
|
|
1390
|
-
import { join as
|
|
1471
|
+
import { join as join5 } from "path";
|
|
1391
1472
|
import pc2 from "picocolors";
|
|
1392
1473
|
var TUNNEL_URL = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/;
|
|
1393
1474
|
var LOCAL_URL = /https?:\/\/(?:localhost|127\.0\.0\.1|\[::1\]|0\.0\.0\.0):\d+/;
|
|
@@ -1412,7 +1493,7 @@ function releaseAsset(platform = process.platform, arch = process.arch) {
|
|
|
1412
1493
|
throw new Error(`No cloudflared build for ${platform}/${arch}. Install it yourself: https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/`);
|
|
1413
1494
|
}
|
|
1414
1495
|
var binName = process.platform === "win32" ? "cloudflared.exe" : "cloudflared";
|
|
1415
|
-
var cacheDir = () =>
|
|
1496
|
+
var cacheDir = () => join5(homedir(), ".flare", "bin");
|
|
1416
1497
|
function onPath() {
|
|
1417
1498
|
const probe = spawnSync2(process.platform === "win32" ? "where" : "which", ["cloudflared"], { encoding: "utf8" });
|
|
1418
1499
|
return probe.status === 0 ? probe.stdout.split(/\r?\n/)[0]?.trim() ?? null : null;
|
|
@@ -1421,19 +1502,19 @@ async function download(log2) {
|
|
|
1421
1502
|
const asset = releaseAsset();
|
|
1422
1503
|
const dir = cacheDir();
|
|
1423
1504
|
mkdirSync3(dir, { recursive: true });
|
|
1424
|
-
const target =
|
|
1505
|
+
const target = join5(dir, binName);
|
|
1425
1506
|
log2(pc2.dim(`Downloading cloudflared (one time) into ${dir}\u2026`));
|
|
1426
1507
|
const response = await fetch(`https://github.com/cloudflare/cloudflared/releases/latest/download/${asset}`);
|
|
1427
1508
|
if (!response.ok) throw new Error(`Downloading cloudflared failed: HTTP ${response.status}.`);
|
|
1428
1509
|
const bytes = Buffer.from(await response.arrayBuffer());
|
|
1429
1510
|
const partial = `${target}.download`;
|
|
1430
1511
|
if (asset.endsWith(".tgz")) {
|
|
1431
|
-
|
|
1512
|
+
writeFileSync5(`${partial}.tgz`, bytes);
|
|
1432
1513
|
const untar = spawnSync2("tar", ["-xzf", `${partial}.tgz`, "-C", dir], { encoding: "utf8" });
|
|
1433
|
-
|
|
1514
|
+
rmSync2(`${partial}.tgz`, { force: true });
|
|
1434
1515
|
if (untar.status !== 0) throw new Error(`Unpacking cloudflared failed: ${untar.stderr}`);
|
|
1435
1516
|
} else {
|
|
1436
|
-
|
|
1517
|
+
writeFileSync5(partial, bytes);
|
|
1437
1518
|
renameSync(partial, target);
|
|
1438
1519
|
}
|
|
1439
1520
|
if (process.platform !== "win32") chmodSync(target, 493);
|
|
@@ -1442,8 +1523,8 @@ async function download(log2) {
|
|
|
1442
1523
|
async function ensureCloudflared(log2 = console.log) {
|
|
1443
1524
|
const found = onPath();
|
|
1444
1525
|
if (found) return found;
|
|
1445
|
-
const cached =
|
|
1446
|
-
if (
|
|
1526
|
+
const cached = join5(cacheDir(), binName);
|
|
1527
|
+
if (existsSync6(cached)) return cached;
|
|
1447
1528
|
return download(log2);
|
|
1448
1529
|
}
|
|
1449
1530
|
async function openTunnel(localUrl, options2 = {}) {
|
|
@@ -1499,16 +1580,16 @@ function tunnelBanner(publicUrl, localUrl) {
|
|
|
1499
1580
|
|
|
1500
1581
|
// src/commands/deploy.ts
|
|
1501
1582
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
1502
|
-
import { existsSync as
|
|
1503
|
-
import { join as
|
|
1583
|
+
import { existsSync as existsSync8, readFileSync as readFileSync5 } from "fs";
|
|
1584
|
+
import { join as join7 } from "path";
|
|
1504
1585
|
import { createZoneClient } from "@flaredev/core/security";
|
|
1505
1586
|
import { createJiti as createJiti2 } from "jiti";
|
|
1506
1587
|
import pc3 from "picocolors";
|
|
1507
1588
|
|
|
1508
1589
|
// src/utils/wrangler.ts
|
|
1509
1590
|
import { spawn as spawn2 } from "child_process";
|
|
1510
|
-
import { existsSync as
|
|
1511
|
-
import { join as
|
|
1591
|
+
import { existsSync as existsSync7, readFileSync as readFileSync4 } from "fs";
|
|
1592
|
+
import { join as join6 } from "path";
|
|
1512
1593
|
import { parse as parseJsonc } from "jsonc-parser";
|
|
1513
1594
|
function runWrangler(wranglerBin, args, cwd, options2 = {}) {
|
|
1514
1595
|
return new Promise((resolve6, reject) => {
|
|
@@ -1533,9 +1614,9 @@ ${output.trim()}`);
|
|
|
1533
1614
|
return JSON.parse(output.slice(start, end + 1));
|
|
1534
1615
|
}
|
|
1535
1616
|
function readD1Databases(appRoot) {
|
|
1536
|
-
const path =
|
|
1537
|
-
if (!
|
|
1538
|
-
const config = parseJsonc(
|
|
1617
|
+
const path = join6(appRoot, "wrangler.jsonc");
|
|
1618
|
+
if (!existsSync7(path)) return [];
|
|
1619
|
+
const config = parseJsonc(readFileSync4(path, "utf8"));
|
|
1539
1620
|
return (config?.d1_databases ?? []).filter((db) => db.binding && db.database_name);
|
|
1540
1621
|
}
|
|
1541
1622
|
|
|
@@ -1575,15 +1656,15 @@ function declaredSecretNames(devVarsExample) {
|
|
|
1575
1656
|
return devVarsExample.split(/\r?\n/).map((line) => /^([A-Z][A-Z0-9_]*)=/.exec(line.trim())?.[1]).filter((name) => Boolean(name));
|
|
1576
1657
|
}
|
|
1577
1658
|
async function loadSecurityConfig(appRoot) {
|
|
1578
|
-
const path =
|
|
1579
|
-
if (!
|
|
1580
|
-
const jiti = createJiti2(
|
|
1659
|
+
const path = join7(appRoot, "security.config.ts");
|
|
1660
|
+
if (!existsSync8(path)) return null;
|
|
1661
|
+
const jiti = createJiti2(join7(appRoot, "package.json"), { moduleCache: false, fsCache: false });
|
|
1581
1662
|
const mod = await jiti.import(path);
|
|
1582
1663
|
if (!mod.default) throw new Error("security.config.ts must `export default defineSecurity({ ... })`.");
|
|
1583
1664
|
return mod.default;
|
|
1584
1665
|
}
|
|
1585
1666
|
async function provisionZoneSecurity(appRoot, config, credentials, options2) {
|
|
1586
|
-
const app = JSON.parse(
|
|
1667
|
+
const app = JSON.parse(readFileSync5(join7(appRoot, "package.json"), "utf8")).name ?? "app";
|
|
1587
1668
|
const zone = createZoneClient({ ...credentials, app, fetch: options2.fetch });
|
|
1588
1669
|
if (config.zone?.rateLimits) {
|
|
1589
1670
|
const result = await zone.syncRateLimits(config.zone.rateLimits);
|
|
@@ -1603,7 +1684,7 @@ async function runDeploy(args, context3) {
|
|
|
1603
1684
|
const envArgs = plan.env ? ["--env", plan.env] : [];
|
|
1604
1685
|
const step = (message) => log2(`
|
|
1605
1686
|
${pc3.bold(pc3.cyan("flare"))} ${message}`);
|
|
1606
|
-
const databases = plan.skipMigrations ? [] : readD1Databases(appRoot).filter((db) =>
|
|
1687
|
+
const databases = plan.skipMigrations ? [] : readD1Databases(appRoot).filter((db) => existsSync8(join7(appRoot, db.migrations_dir ?? "migrations")));
|
|
1607
1688
|
const migrateAfterDeploy = [];
|
|
1608
1689
|
const migrate2 = async (db) => {
|
|
1609
1690
|
step(`Applying migrations to ${db.database_name} (remote)`);
|
|
@@ -1662,8 +1743,8 @@ ${put.output.trim()}`);
|
|
|
1662
1743
|
existing.add(name);
|
|
1663
1744
|
}
|
|
1664
1745
|
}
|
|
1665
|
-
const examplePath =
|
|
1666
|
-
const optional =
|
|
1746
|
+
const examplePath = join7(appRoot, ".dev.vars.example");
|
|
1747
|
+
const optional = existsSync8(examplePath) ? declaredSecretNames(readFileSync5(examplePath, "utf8")).filter((name) => !existing.has(name)) : [];
|
|
1667
1748
|
if (optional.length > 0) {
|
|
1668
1749
|
step("Optional secrets not set in production (features using them stay off):");
|
|
1669
1750
|
for (const name of optional) log2(` wrangler secret put ${name}${plan.env ? ` --env ${plan.env}` : ""}`);
|
|
@@ -1698,25 +1779,30 @@ function findAppRoot(cwd) {
|
|
|
1698
1779
|
while (dir) {
|
|
1699
1780
|
const root = findUp("package.json", dir);
|
|
1700
1781
|
if (!root) break;
|
|
1701
|
-
const pkg = JSON.parse(
|
|
1702
|
-
|
|
1782
|
+
const pkg = JSON.parse(readFileSync6(join8(root, "package.json"), "utf8"));
|
|
1783
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
1784
|
+
if (deps["@flaredev/core"] || deps.vinext) return root;
|
|
1703
1785
|
const parent = dirname4(root);
|
|
1704
1786
|
dir = parent === root ? void 0 : parent;
|
|
1705
1787
|
}
|
|
1706
|
-
throw new Error("Not inside a Flare app (no package.json depending on
|
|
1788
|
+
throw new Error("Not inside a Flare app (no package.json depending on @flaredev/core found). Run this from your app directory.");
|
|
1707
1789
|
}
|
|
1708
1790
|
function resolveBin(appRoot, pkgName, binName2) {
|
|
1709
|
-
const pkgDir = findUp(
|
|
1791
|
+
const pkgDir = findUp(join8("node_modules", pkgName, "package.json"), appRoot);
|
|
1710
1792
|
if (!pkgDir) {
|
|
1711
1793
|
throw new Error(`Cannot find ${pkgName} in ${appRoot}. Install dependencies first (e.g. pnpm install).`);
|
|
1712
1794
|
}
|
|
1713
|
-
const manifestPath =
|
|
1714
|
-
const { bin } = JSON.parse(
|
|
1795
|
+
const manifestPath = join8(pkgDir, "node_modules", pkgName, "package.json");
|
|
1796
|
+
const { bin } = JSON.parse(readFileSync6(manifestPath, "utf8"));
|
|
1715
1797
|
const relativeBin = typeof bin === "string" ? bin : bin?.[binName2];
|
|
1716
1798
|
if (!relativeBin) throw new Error(`${pkgName} does not provide a "${binName2}" executable.`);
|
|
1717
|
-
return
|
|
1799
|
+
return join8(dirname4(manifestPath), relativeBin);
|
|
1718
1800
|
}
|
|
1719
1801
|
function delegatedArgv(appRoot, command, forwarded) {
|
|
1802
|
+
if (readStack(appRoot) === "next") {
|
|
1803
|
+
const script = { dev: "npm run dev", build: "npm run build", start: "npm start", deploy: "npm run deploy" }[command];
|
|
1804
|
+
throw new Error(`\`flare ${command}\` is for the Cloudflare stack. This app targets Next.js \u2014 run \`${script}\` instead.`);
|
|
1805
|
+
}
|
|
1720
1806
|
const spec = DELEGATED_COMMANDS[command];
|
|
1721
1807
|
return [resolveBin(appRoot, spec.pkg, spec.bin), ...spec.args, ...forwarded];
|
|
1722
1808
|
}
|
|
@@ -1775,7 +1861,7 @@ async function runDelegated(command, forwarded, cwd = process.cwd()) {
|
|
|
1775
1861
|
const tunnel = (command === "dev" || command === "start") && forwarded.includes("--tunnel");
|
|
1776
1862
|
if (tunnel) forwarded = forwarded.filter((arg) => arg !== "--tunnel");
|
|
1777
1863
|
const wantsHelp = forwarded.includes("--help") || forwarded.includes("-h");
|
|
1778
|
-
if (command === "start" && !wantsHelp && !
|
|
1864
|
+
if (command === "start" && !wantsHelp && !existsSync9(join8(appRoot, "dist/server/wrangler.json"))) {
|
|
1779
1865
|
console.log("No production build found; running `flare build` first.\n");
|
|
1780
1866
|
const code = await runNode(delegatedArgv(appRoot, "build", []), appRoot);
|
|
1781
1867
|
if (code !== 0) return code;
|
|
@@ -1825,9 +1911,9 @@ function snapshotColumns(snapshot) {
|
|
|
1825
1911
|
}
|
|
1826
1912
|
|
|
1827
1913
|
// src/generator/migrations.ts
|
|
1828
|
-
var listSql = (dir) =>
|
|
1914
|
+
var listSql = (dir) => existsSync10(dir) ? readdirSync4(dir).filter((file) => file.endsWith(".sql")) : [];
|
|
1829
1915
|
async function generateSchemaMigration(appRoot, name, options2 = {}) {
|
|
1830
|
-
const dir =
|
|
1916
|
+
const dir = join9(appRoot, "migrations");
|
|
1831
1917
|
const before = new Set(listSql(dir));
|
|
1832
1918
|
const bin = resolveBin(appRoot, "drizzle-kit", "drizzle-kit");
|
|
1833
1919
|
const { code, output } = await new Promise((resolve6, reject) => {
|
|
@@ -1855,10 +1941,10 @@ If it asked whether a column was renamed, run \`pnpm exec drizzle-kit generate\`
|
|
|
1855
1941
|
for (const file of created) {
|
|
1856
1942
|
const previous = previousSnapshot(dir, file);
|
|
1857
1943
|
if (!previous) continue;
|
|
1858
|
-
const path =
|
|
1859
|
-
const { sql: sql2, changes } = repairRebuildMigration(
|
|
1944
|
+
const path = join9(dir, file);
|
|
1945
|
+
const { sql: sql2, changes } = repairRebuildMigration(readFileSync7(path, "utf8"), snapshotColumns(previous));
|
|
1860
1946
|
if (changes.length) {
|
|
1861
|
-
|
|
1947
|
+
writeFileSync6(path, sql2);
|
|
1862
1948
|
repairs.push(...changes.map((change) => `${file}: ${change}`));
|
|
1863
1949
|
}
|
|
1864
1950
|
}
|
|
@@ -1866,32 +1952,32 @@ If it asked whether a column was renamed, run \`pnpm exec drizzle-kit generate\`
|
|
|
1866
1952
|
}
|
|
1867
1953
|
function previousSnapshot(dir, migrationFile) {
|
|
1868
1954
|
const prefix = /^(\d+)_/.exec(migrationFile)?.[1];
|
|
1869
|
-
const metaDir =
|
|
1870
|
-
if (!prefix || !
|
|
1955
|
+
const metaDir = join9(dir, "meta");
|
|
1956
|
+
if (!prefix || !existsSync10(metaDir)) return void 0;
|
|
1871
1957
|
const earlier = readdirSync4(metaDir).filter((file) => /^\d+_snapshot\.json$/.test(file) && file < `${prefix}_snapshot.json`).sort().pop();
|
|
1872
|
-
return earlier ? JSON.parse(
|
|
1958
|
+
return earlier ? JSON.parse(readFileSync7(join9(metaDir, earlier), "utf8")) : void 0;
|
|
1873
1959
|
}
|
|
1874
1960
|
|
|
1875
1961
|
// src/generator/plan.ts
|
|
1876
|
-
import { copyFileSync as copyFileSync2, existsSync as
|
|
1877
|
-
import { dirname as dirname5, join as
|
|
1962
|
+
import { copyFileSync as copyFileSync2, existsSync as existsSync12, mkdirSync as mkdirSync4, readdirSync as readdirSync6, readFileSync as readFileSync8 } from "fs";
|
|
1963
|
+
import { dirname as dirname5, join as join11 } from "path";
|
|
1878
1964
|
import pc5 from "picocolors";
|
|
1879
1965
|
|
|
1880
1966
|
// src/generator/policy.ts
|
|
1881
|
-
import { existsSync as
|
|
1882
|
-
import { join as
|
|
1967
|
+
import { existsSync as existsSync11, readdirSync as readdirSync5 } from "fs";
|
|
1968
|
+
import { join as join10, resolve as resolve5 } from "path";
|
|
1883
1969
|
import { camelCase as camelCase2, kebabCase as kebabCase2 } from "@flaredev/core";
|
|
1884
1970
|
import { createJiti as createJiti3 } from "jiti";
|
|
1885
1971
|
var policyPath = (name) => `policies/${kebabCase2(name)}.policy.ts`;
|
|
1886
1972
|
var policyLocal = (stem) => `${camelCase2(stem)}Policy`;
|
|
1887
1973
|
async function loadPolicies(root) {
|
|
1888
1974
|
const appRoot = resolve5(root);
|
|
1889
|
-
const dir =
|
|
1890
|
-
if (!
|
|
1891
|
-
const jiti = createJiti3(
|
|
1975
|
+
const dir = join10(appRoot, "policies");
|
|
1976
|
+
if (!existsSync11(dir)) return [];
|
|
1977
|
+
const jiti = createJiti3(join10(appRoot, "package.json"), { moduleCache: false, fsCache: false });
|
|
1892
1978
|
const loaded = [];
|
|
1893
1979
|
for (const file of readdirSync5(dir).filter((name) => name.endsWith(".policy.ts")).sort()) {
|
|
1894
|
-
const mod = await jiti.import(
|
|
1980
|
+
const mod = await jiti.import(join10(dir, file));
|
|
1895
1981
|
const policy = mod.default;
|
|
1896
1982
|
if (!policy || typeof policy !== "object" || !("resource" in policy) || !Array.isArray(policy.read)) {
|
|
1897
1983
|
throw new Error(`policies/${file} must \`export default definePolicy({ ... })\`.`);
|
|
@@ -1936,15 +2022,136 @@ function renderPolicyRegistry(all) {
|
|
|
1936
2022
|
].join("\n");
|
|
1937
2023
|
}
|
|
1938
2024
|
|
|
1939
|
-
// src/generator/
|
|
1940
|
-
import { camelCase as camelCase3, columnName,
|
|
1941
|
-
var resourceLocal = (stem) => `${camelCase3(stem)}Resource`;
|
|
1942
|
-
var tableExport = (resource) => camelCase3(resource.table);
|
|
2025
|
+
// src/generator/prisma.ts
|
|
2026
|
+
import { camelCase as camelCase3, columnName, pascalCase as pascalCase2, pluralize, storedFields } from "@flaredev/core";
|
|
1943
2027
|
var q = (value) => JSON.stringify(value);
|
|
2028
|
+
var enumTypeName = (resource, key) => `${pascalCase2(resource)}${pascalCase2(key)}`;
|
|
2029
|
+
var enumMember = (option) => {
|
|
2030
|
+
const safe = option.replace(/[^A-Za-z0-9_]/g, "_").replace(/^(?=\d)/, "v_");
|
|
2031
|
+
return safe === option ? ` ${safe}` : ` ${safe} @map(${q(option)})`;
|
|
2032
|
+
};
|
|
2033
|
+
function prismaType(resource, key, def) {
|
|
2034
|
+
switch (def.kind) {
|
|
2035
|
+
case "int":
|
|
2036
|
+
return "Int";
|
|
2037
|
+
case "float":
|
|
2038
|
+
return "Float";
|
|
2039
|
+
case "boolean":
|
|
2040
|
+
return "Boolean";
|
|
2041
|
+
case "date":
|
|
2042
|
+
case "datetime":
|
|
2043
|
+
return "DateTime";
|
|
2044
|
+
case "enum":
|
|
2045
|
+
return enumTypeName(resource.name, key);
|
|
2046
|
+
case "multiselect":
|
|
2047
|
+
return "String[]";
|
|
2048
|
+
default:
|
|
2049
|
+
return "String";
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
function attributes(key, def) {
|
|
2053
|
+
const parts = [];
|
|
2054
|
+
const column = columnName(key);
|
|
2055
|
+
if (column !== key) parts.push(`@map(${q(column)})`);
|
|
2056
|
+
if ("unique" in def && def.unique) parts.push("@unique");
|
|
2057
|
+
if (def.kind === "text") parts.push("@db.Text");
|
|
2058
|
+
if (def.kind === "date") parts.push("@db.Date");
|
|
2059
|
+
if ("default" in def && def.default !== void 0) {
|
|
2060
|
+
const value = def.default;
|
|
2061
|
+
if (def.kind === "enum") parts.push(`@default(${String(value).replace(/[^A-Za-z0-9_]/g, "_")})`);
|
|
2062
|
+
else if (def.kind === "multiselect") parts.push(`@default([${value.map(q).join(", ")}])`);
|
|
2063
|
+
else parts.push(`@default(${typeof value === "string" ? q(value) : String(value)})`);
|
|
2064
|
+
}
|
|
2065
|
+
return parts;
|
|
2066
|
+
}
|
|
2067
|
+
var onDeleteAction = (onDelete) => onDelete === "set null" ? "SetNull" : onDelete === "cascade" ? "Cascade" : "Restrict";
|
|
2068
|
+
function renderPrismaEnums(resource) {
|
|
2069
|
+
return storedFields(resource).filter(([, def]) => def.kind === "enum").map(([key, def]) => {
|
|
2070
|
+
if (def.kind !== "enum") return "";
|
|
2071
|
+
return [`enum ${enumTypeName(resource.name, key)} {`, ...def.options.map(enumMember), "}"].join("\n");
|
|
2072
|
+
}).filter(Boolean);
|
|
2073
|
+
}
|
|
2074
|
+
function backReferences(target, all) {
|
|
2075
|
+
const found = [];
|
|
2076
|
+
for (const { resource: source } of all) {
|
|
2077
|
+
const pointing = storedFields(source).filter(([, def]) => def.kind === "belongsTo" && def.target === target.name);
|
|
2078
|
+
const ambiguous = pointing.length > 1;
|
|
2079
|
+
for (const [key, def] of pointing) {
|
|
2080
|
+
if (def.kind !== "belongsTo") continue;
|
|
2081
|
+
const declared = Object.entries(target.fields).find(([, other]) => other.kind === "hasMany" && other.target === source.name)?.[0];
|
|
2082
|
+
const base = ambiguous ? camelCase3(`${key.replace(/Id$/, "")} ${pluralize(source.name)}`) : declared ?? camelCase3(pluralize(source.name));
|
|
2083
|
+
found.push({ field: base, from: source.name, relationName: ambiguous ? `${source.name}_${key}` : void 0 });
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
return found;
|
|
2087
|
+
}
|
|
2088
|
+
function renderPrismaModel(entry, all) {
|
|
2089
|
+
const resource = entry.resource;
|
|
2090
|
+
const byName = new Map(all.map((item) => [item.resource.name, item.resource]));
|
|
2091
|
+
const lines = [`model ${resource.name} {`, ` id String @id @default(uuid())`];
|
|
2092
|
+
const indexes = [];
|
|
2093
|
+
for (const [key, def] of Object.entries(resource.fields)) {
|
|
2094
|
+
if (def.kind === "hasMany") {
|
|
2095
|
+
if (!byName.get(def.target)) {
|
|
2096
|
+
throw new Error(`${resource.name}.${key} has many "${def.target}", but there is no ${def.target} resource. Generate it first.`);
|
|
2097
|
+
}
|
|
2098
|
+
continue;
|
|
2099
|
+
}
|
|
2100
|
+
if (def.kind === "belongsTo") {
|
|
2101
|
+
const target = byName.get(def.target);
|
|
2102
|
+
if (!target) throw new Error(`${resource.name}.${key} belongs to "${def.target}", but there is no ${def.target} resource. Generate it first.`);
|
|
2103
|
+
const optional2 = def.required ? "" : "?";
|
|
2104
|
+
const relationField = key.replace(/Id$/, "");
|
|
2105
|
+
const named = backReferences(target, all).find((back) => back.from === resource.name && back.relationName?.endsWith(`_${key}`));
|
|
2106
|
+
const relationArgs = [
|
|
2107
|
+
...named?.relationName ? [q(named.relationName)] : [],
|
|
2108
|
+
`fields: [${key}]`,
|
|
2109
|
+
"references: [id]",
|
|
2110
|
+
`onDelete: ${onDeleteAction(def.onDelete)}`
|
|
2111
|
+
];
|
|
2112
|
+
lines.push(` ${key} String${optional2} @map(${q(columnName(key))})`);
|
|
2113
|
+
lines.push(` ${relationField} ${target.name}${optional2} @relation(${relationArgs.join(", ")})`);
|
|
2114
|
+
indexes.push(` @@index([${key}])`);
|
|
2115
|
+
continue;
|
|
2116
|
+
}
|
|
2117
|
+
const optional = def.required || def.kind === "multiselect" ? "" : "?";
|
|
2118
|
+
lines.push(` ${key} ${prismaType(resource, key, def)}${optional} ${attributes(key, def).join(" ")}`.trimEnd());
|
|
2119
|
+
}
|
|
2120
|
+
for (const back of backReferences(resource, all)) {
|
|
2121
|
+
const args = back.relationName ? `@relation(${q(back.relationName)})` : "";
|
|
2122
|
+
lines.push(` ${back.field} ${back.from}[] ${args}`.trimEnd());
|
|
2123
|
+
}
|
|
2124
|
+
lines.push(` createdAt DateTime @default(now()) @map("created_at")`);
|
|
2125
|
+
lines.push(` updatedAt DateTime @updatedAt @map("updated_at")`);
|
|
2126
|
+
indexes.push(` @@index([createdAt])`);
|
|
2127
|
+
const sortField = resource.defaultSort.field;
|
|
2128
|
+
if (!["createdAt", "updatedAt"].includes(sortField) && resource.fields[sortField]) indexes.push(` @@index([${sortField}])`);
|
|
2129
|
+
for (const [key, def] of storedFields(resource)) {
|
|
2130
|
+
if (def.kind === "enum" && def.filterable !== false) indexes.push(` @@index([${key}])`);
|
|
2131
|
+
}
|
|
2132
|
+
lines.push("", ...[...new Set(indexes)], ` @@map(${q(resource.table)})`, "}");
|
|
2133
|
+
return lines.join("\n");
|
|
2134
|
+
}
|
|
2135
|
+
function renderPrismaSchema(all) {
|
|
2136
|
+
const ordered = [...all].sort((a, b) => a.resource.name.localeCompare(b.resource.name));
|
|
2137
|
+
const blocks = [];
|
|
2138
|
+
for (const entry of ordered) {
|
|
2139
|
+
blocks.push(...renderPrismaEnums(entry.resource));
|
|
2140
|
+
blocks.push(renderPrismaModel(entry, all));
|
|
2141
|
+
}
|
|
2142
|
+
return `${blocks.join("\n\n")}
|
|
2143
|
+
`;
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2146
|
+
// src/generator/render.ts
|
|
2147
|
+
import { camelCase as camelCase4, columnName as columnName2, relationGraph, storedFields as storedFields2 } from "@flaredev/core";
|
|
2148
|
+
var resourceLocal = (stem) => `${camelCase4(stem)}Resource`;
|
|
2149
|
+
var tableExport = (resource) => camelCase4(resource.table);
|
|
2150
|
+
var q2 = (value) => JSON.stringify(value);
|
|
1944
2151
|
var sqlString = (value) => `'${value.replace(/'/g, "''")}'`;
|
|
1945
2152
|
var TIMESTAMP_DEFAULT = "sql`(cast(unixepoch('subsecond') * 1000 as integer))`";
|
|
1946
2153
|
function columnExpression(key, def, resources, selfName) {
|
|
1947
|
-
const col =
|
|
2154
|
+
const col = q2(columnName2(key));
|
|
1948
2155
|
let expr;
|
|
1949
2156
|
switch (def.kind) {
|
|
1950
2157
|
case "int":
|
|
@@ -1957,10 +2164,10 @@ function columnExpression(key, def, resources, selfName) {
|
|
|
1957
2164
|
expr = `integer(${col}, { mode: "boolean" })`;
|
|
1958
2165
|
break;
|
|
1959
2166
|
case "enum":
|
|
1960
|
-
expr = `text(${col}, { enum: ${
|
|
2167
|
+
expr = `text(${col}, { enum: ${q2(def.options)} })`;
|
|
1961
2168
|
break;
|
|
1962
2169
|
case "multiselect":
|
|
1963
|
-
expr = `text(${col}, { mode: "json" }).$type<(${def.options.map((option) =>
|
|
2170
|
+
expr = `text(${col}, { mode: "json" }).$type<(${def.options.map((option) => q2(option)).join(" | ")})[]>()`;
|
|
1964
2171
|
break;
|
|
1965
2172
|
case "belongsTo": {
|
|
1966
2173
|
const target = resources.get(def.target);
|
|
@@ -1968,7 +2175,7 @@ function columnExpression(key, def, resources, selfName) {
|
|
|
1968
2175
|
throw new Error(`${selfName}.${key} belongs to "${def.target}", but there is no ${def.target} resource. Generate it first.`);
|
|
1969
2176
|
}
|
|
1970
2177
|
const onDelete = def.onDelete ?? "restrict";
|
|
1971
|
-
expr = `text(${col}).references(() => ${tableExport(target)}.id, { onDelete: ${
|
|
2178
|
+
expr = `text(${col}).references(() => ${tableExport(target)}.id, { onDelete: ${q2(onDelete)} })`;
|
|
1972
2179
|
break;
|
|
1973
2180
|
}
|
|
1974
2181
|
default:
|
|
@@ -1976,13 +2183,13 @@ function columnExpression(key, def, resources, selfName) {
|
|
|
1976
2183
|
}
|
|
1977
2184
|
if (def.required) expr += ".notNull()";
|
|
1978
2185
|
if ("unique" in def && def.unique) expr += ".unique()";
|
|
1979
|
-
if ("default" in def && def.default !== void 0) expr += `.default(${
|
|
2186
|
+
if ("default" in def && def.default !== void 0) expr += `.default(${q2(def.default)})`;
|
|
1980
2187
|
return expr;
|
|
1981
2188
|
}
|
|
1982
2189
|
function renderTableModule(entry, all) {
|
|
1983
2190
|
const { resource } = entry;
|
|
1984
2191
|
const resources = new Map(all.map((item) => [item.resource.name, item.resource]));
|
|
1985
|
-
const fields =
|
|
2192
|
+
const fields = storedFields2(resource);
|
|
1986
2193
|
const name = tableExport(resource);
|
|
1987
2194
|
const columns = fields.map(([key, def]) => ` ${key}: ${columnExpression(key, def, resources, resource.name)},`);
|
|
1988
2195
|
const extras = [];
|
|
@@ -1990,12 +2197,12 @@ function renderTableModule(entry, all) {
|
|
|
1990
2197
|
const addIndex = (key, column) => {
|
|
1991
2198
|
if (indexed.has(key)) return;
|
|
1992
2199
|
indexed.add(key);
|
|
1993
|
-
extras.push(` index(${
|
|
2200
|
+
extras.push(` index(${q2(`${resource.table}_${columnName2(column)}_idx`)}).on(table.${key}),`);
|
|
1994
2201
|
};
|
|
1995
2202
|
for (const [key, def] of fields) {
|
|
1996
2203
|
if (def.kind === "enum") {
|
|
1997
2204
|
const options2 = def.options.map(sqlString).join(", ");
|
|
1998
|
-
extras.push(` check(${
|
|
2205
|
+
extras.push(` check(${q2(`${resource.table}_${columnName2(key)}_check`)}, sql\`\${table.${key}} in (${options2})\`),`);
|
|
1999
2206
|
if (def.filterable !== false) addIndex(key, key);
|
|
2000
2207
|
}
|
|
2001
2208
|
if (def.kind === "belongsTo") addIndex(key, key);
|
|
@@ -2017,7 +2224,7 @@ function renderTableModule(entry, all) {
|
|
|
2017
2224
|
...targets.map((target) => `import { ${tableExport(target)} } from "./${target.table}";`),
|
|
2018
2225
|
"",
|
|
2019
2226
|
`export const ${name} = sqliteTable(`,
|
|
2020
|
-
` ${
|
|
2227
|
+
` ${q2(resource.table)},`,
|
|
2021
2228
|
" {",
|
|
2022
2229
|
` id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),`,
|
|
2023
2230
|
...columns.map(
|
|
@@ -2057,12 +2264,12 @@ function renderRelations(all) {
|
|
|
2057
2264
|
const target = byName.get(relation.target);
|
|
2058
2265
|
used.add(target);
|
|
2059
2266
|
const other = tableExport(target);
|
|
2060
|
-
return ` ${relation.name}: one(${other}, { fields: [${self}.${relation.key}], references: [${other}.id], relationName: ${
|
|
2267
|
+
return ` ${relation.name}: one(${other}, { fields: [${self}.${relation.key}], references: [${other}.id], relationName: ${q2(relation.relationName)} }),`;
|
|
2061
2268
|
}),
|
|
2062
2269
|
...hasMany.map((relation) => {
|
|
2063
2270
|
const target = byName.get(relation.target);
|
|
2064
2271
|
used.add(target);
|
|
2065
|
-
return ` ${relation.key}: many(${tableExport(target)}, { relationName: ${
|
|
2272
|
+
return ` ${relation.key}: many(${tableExport(target)}, { relationName: ${q2(relation.relationName)} }),`;
|
|
2066
2273
|
})
|
|
2067
2274
|
];
|
|
2068
2275
|
const helpers = [belongsTo.length ? "one" : "", hasMany.length ? "many" : ""].filter(Boolean).join(", ");
|
|
@@ -2075,32 +2282,40 @@ ${lines.join("\n")}
|
|
|
2075
2282
|
const imports = [...used].sort((a, b) => a.table.localeCompare(b.table)).map((resource) => `import { ${tableExport(resource)} } from "./schema/${resource.table}";`);
|
|
2076
2283
|
return [`import { relations } from "drizzle-orm";`, ...imports, "", blocks.join("\n")].join("\n");
|
|
2077
2284
|
}
|
|
2078
|
-
var handlerImports = (entry) =>
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2285
|
+
var handlerImports = (entry, stack = "cloudflare") => {
|
|
2286
|
+
const local = resourceLocal(entry.stem);
|
|
2287
|
+
const source = stack === "next" ? {
|
|
2288
|
+
imports: [`import { prismaRows } from "@flaredev/core/server";`, `import { prisma } from "@/lib/db";`],
|
|
2289
|
+
wiring: [` rows: prismaRows(prisma.${camelCase4(entry.resource.name)}, prisma),`]
|
|
2290
|
+
} : {
|
|
2291
|
+
imports: [`import { getDb } from "@/db";`, `import { ${tableExport(entry.resource)} } from "@/db/schema";`],
|
|
2292
|
+
wiring: [` table: ${tableExport(entry.resource)},`, ` getDb,`]
|
|
2293
|
+
};
|
|
2294
|
+
return [
|
|
2295
|
+
`import { createResourceHandlers } from "@flaredev/core/server";`,
|
|
2296
|
+
...source.imports,
|
|
2297
|
+
`import { authorize, currentUser } from "@/lib/api";`,
|
|
2298
|
+
`import { revalidateResource } from "@/lib/cache";`,
|
|
2299
|
+
`import ${local} from "@/resources/${entry.stem}.resource";`,
|
|
2300
|
+
"",
|
|
2301
|
+
`const handlers = createResourceHandlers({`,
|
|
2302
|
+
` resource: ${local},`,
|
|
2303
|
+
...source.wiring,
|
|
2304
|
+
` authorize,`,
|
|
2305
|
+
` currentUser,`,
|
|
2306
|
+
` onChange: revalidateResource,`,
|
|
2307
|
+
`});`,
|
|
2308
|
+
""
|
|
2309
|
+
];
|
|
2310
|
+
};
|
|
2311
|
+
function renderCollectionRoute(entry, stack = "cloudflare") {
|
|
2312
|
+
return [...handlerImports(entry, stack), "export const GET = handlers.collection.GET;", "export const POST = handlers.collection.POST;", ""].join(
|
|
2098
2313
|
"\n"
|
|
2099
2314
|
);
|
|
2100
2315
|
}
|
|
2101
|
-
function renderItemRoute(entry) {
|
|
2316
|
+
function renderItemRoute(entry, stack = "cloudflare") {
|
|
2102
2317
|
return [
|
|
2103
|
-
...handlerImports(entry),
|
|
2318
|
+
...handlerImports(entry, stack),
|
|
2104
2319
|
"export const GET = handlers.item.GET;",
|
|
2105
2320
|
"export const PATCH = handlers.item.PATCH;",
|
|
2106
2321
|
"export const PUT = handlers.item.PUT;",
|
|
@@ -2179,7 +2394,7 @@ function renderClient(entry) {
|
|
|
2179
2394
|
`import { createResourceClient } from "@flaredev/core/client";`,
|
|
2180
2395
|
`import type ${local} from "./${stem}.resource";`,
|
|
2181
2396
|
"",
|
|
2182
|
-
`export const ${
|
|
2397
|
+
`export const ${camelCase4(stem)}Client = createResourceClient<typeof ${local}>(${q2(`/api/${resource.slug}`)});`,
|
|
2183
2398
|
"",
|
|
2184
2399
|
`export type ${resource.name} = typeof ${local}.$types.record;`,
|
|
2185
2400
|
`export type ${resource.name}Create = typeof ${local}.$types.create;`,
|
|
@@ -2193,7 +2408,7 @@ function renderValidators(entry) {
|
|
|
2193
2408
|
`import { createValidators } from "@flaredev/core";`,
|
|
2194
2409
|
`import ${local} from "./${entry.stem}.resource";`,
|
|
2195
2410
|
"",
|
|
2196
|
-
`export const ${
|
|
2411
|
+
`export const ${camelCase4(entry.stem)}Validators = createValidators(${local});`,
|
|
2197
2412
|
""
|
|
2198
2413
|
].join("\n");
|
|
2199
2414
|
}
|
|
@@ -2223,12 +2438,31 @@ function renderServerRegistry(all) {
|
|
|
2223
2438
|
""
|
|
2224
2439
|
].join("\n");
|
|
2225
2440
|
}
|
|
2226
|
-
function
|
|
2441
|
+
function renderPrismaServerRegistry(all) {
|
|
2442
|
+
if (all.length === 0) return "export const resourceTables = {} as const;\n";
|
|
2443
|
+
const sorted = [...all].sort((a, b) => a.resource.name.localeCompare(b.resource.name));
|
|
2444
|
+
return [
|
|
2445
|
+
`import { prisma } from "@/lib/db";`,
|
|
2446
|
+
`import { ${sorted.map(({ stem }) => resourceLocal(stem)).join(", ")} } from "./index";`,
|
|
2447
|
+
"",
|
|
2448
|
+
"export const resourceTables = {",
|
|
2449
|
+
...sorted.map(
|
|
2450
|
+
({ resource, stem }) => ` ${resource.name}: { resource: ${resourceLocal(stem)}, delegate: prisma.${camelCase4(resource.name)} },`
|
|
2451
|
+
),
|
|
2452
|
+
"} as const;",
|
|
2453
|
+
"",
|
|
2454
|
+
"export type ResourceName = keyof typeof resourceTables;",
|
|
2455
|
+
""
|
|
2456
|
+
].join("\n");
|
|
2457
|
+
}
|
|
2458
|
+
function resourceFiles(entry, all, stack = "cloudflare") {
|
|
2227
2459
|
const { resource, stem } = entry;
|
|
2228
2460
|
return [
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
{ path: `
|
|
2461
|
+
// The schema is the one file that belongs to a stack. On Next.js every model lives
|
|
2462
|
+
// in one prisma/schema.prisma, written by planFiles rather than per resource.
|
|
2463
|
+
...stack === "cloudflare" ? [{ path: `db/schema/${resource.table}.ts`, content: renderTableModule(entry, all) }] : [],
|
|
2464
|
+
{ path: `app/api/${resource.slug}/route.ts`, content: renderCollectionRoute(entry, stack) },
|
|
2465
|
+
{ path: `app/api/${resource.slug}/[id]/route.ts`, content: renderItemRoute(entry, stack) },
|
|
2232
2466
|
{ path: `resources/${stem}.client.ts`, content: renderClient(entry) },
|
|
2233
2467
|
{ path: `resources/${stem}.validators.ts`, content: renderValidators(entry) },
|
|
2234
2468
|
{ path: `app/dashboard/${resource.slug}/page.tsx`, content: renderAdminListPage(entry) },
|
|
@@ -2243,21 +2477,42 @@ var resourceHeader = (name) => `// Generated by \`flare gen resource ${name}\`.
|
|
|
2243
2477
|
// rewritten on regeneration; add your own code outside it.
|
|
2244
2478
|
`;
|
|
2245
2479
|
var HEADER_PATTERN = /^\/\/ Generated by `flare gen resource (\w+)`/;
|
|
2246
|
-
function planFiles(all, policies) {
|
|
2480
|
+
function planFiles(all, policies, stack = "cloudflare") {
|
|
2247
2481
|
const files = [];
|
|
2248
2482
|
for (const entry of all) {
|
|
2249
|
-
for (const file of resourceFiles(entry, all)) files.push({ ...file, header: resourceHeader(entry.resource.name) });
|
|
2250
|
-
}
|
|
2251
|
-
files.push(
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2483
|
+
for (const file of resourceFiles(entry, all, stack)) files.push({ ...file, header: resourceHeader(entry.resource.name) });
|
|
2484
|
+
}
|
|
2485
|
+
files.push({
|
|
2486
|
+
path: "resources/index.ts",
|
|
2487
|
+
content: renderRegistry(all),
|
|
2488
|
+
header: "// Registry of every resource descriptor (maintained by flare gen).\n"
|
|
2489
|
+
});
|
|
2490
|
+
if (stack === "next") {
|
|
2491
|
+
files.push(
|
|
2492
|
+
// Prisma keeps every model in one schema and writes its own migrations, so there
|
|
2493
|
+
// is no per-table module and no relations file to maintain.
|
|
2494
|
+
{
|
|
2495
|
+
path: "prisma/schema/resources.prisma",
|
|
2496
|
+
content: renderPrismaSchema(all),
|
|
2497
|
+
header: "// Generated from the descriptors in resources/. Edit a descriptor, not this.\n"
|
|
2498
|
+
},
|
|
2499
|
+
{
|
|
2500
|
+
path: "resources/server.ts",
|
|
2501
|
+
content: renderPrismaServerRegistry(all),
|
|
2502
|
+
header: "// Server-only: resource name \u2192 descriptor and Prisma delegate (maintained by flare gen).\n"
|
|
2503
|
+
}
|
|
2504
|
+
);
|
|
2505
|
+
} else {
|
|
2506
|
+
files.push(
|
|
2507
|
+
{ path: "db/relations.ts", content: renderRelations(all), header: "// Drizzle relations for every resource (maintained by flare gen).\n" },
|
|
2508
|
+
{
|
|
2509
|
+
path: "resources/server.ts",
|
|
2510
|
+
content: renderServerRegistry(all),
|
|
2511
|
+
header: "// Server-only: resource name \u2192 descriptor and table (maintained by flare gen).\n"
|
|
2512
|
+
},
|
|
2513
|
+
{ path: "db/schema.ts", content: renderSchemaIndex(all) }
|
|
2514
|
+
);
|
|
2515
|
+
}
|
|
2261
2516
|
if (policies) {
|
|
2262
2517
|
files.push({
|
|
2263
2518
|
path: "policies/index.ts",
|
|
@@ -2269,7 +2524,7 @@ function planFiles(all, policies) {
|
|
|
2269
2524
|
}
|
|
2270
2525
|
function applyPlan(appRoot, plan, options2 = {}) {
|
|
2271
2526
|
return plan.map(({ path, content, header }) => {
|
|
2272
|
-
const absolute =
|
|
2527
|
+
const absolute = join11(appRoot, path);
|
|
2273
2528
|
const status = inspectGenerated(absolute, content);
|
|
2274
2529
|
if (status === "unmarked") return { path, status, outcome: "skipped-unmarked" };
|
|
2275
2530
|
if (status === "drift" && !options2.force) return { path, status, outcome: "skipped-drift" };
|
|
@@ -2288,30 +2543,30 @@ function applyPlan(appRoot, plan, options2 = {}) {
|
|
|
2288
2543
|
function findOrphans(appRoot, all) {
|
|
2289
2544
|
const planned = new Set(planFiles(all).map((file) => file.path));
|
|
2290
2545
|
const candidates = [];
|
|
2291
|
-
const list2 = (dir, match) =>
|
|
2546
|
+
const list2 = (dir, match) => existsSync12(join11(appRoot, dir)) ? readdirSync6(join11(appRoot, dir)).filter(match).map((name) => `${dir}/${name}`) : [];
|
|
2292
2547
|
candidates.push(...list2("db/schema", (name) => name.endsWith(".ts")));
|
|
2293
2548
|
candidates.push(...list2("resources", (name) => /\.(client|validators)\.ts$/.test(name)));
|
|
2294
2549
|
for (const dir of list2("app/api", () => true)) {
|
|
2295
|
-
for (const file of [`${dir}/route.ts`, `${dir}/[id]/route.ts`]) if (
|
|
2550
|
+
for (const file of [`${dir}/route.ts`, `${dir}/[id]/route.ts`]) if (existsSync12(join11(appRoot, file))) candidates.push(file);
|
|
2296
2551
|
}
|
|
2297
2552
|
for (const dir of list2("app/dashboard", () => true)) {
|
|
2298
2553
|
for (const file of [`${dir}/page.tsx`, `${dir}/new/page.tsx`, `${dir}/[id]/page.tsx`, `${dir}/[id]/edit/page.tsx`]) {
|
|
2299
|
-
if (
|
|
2554
|
+
if (existsSync12(join11(appRoot, file))) candidates.push(file);
|
|
2300
2555
|
}
|
|
2301
2556
|
}
|
|
2302
2557
|
const orphans = [];
|
|
2303
2558
|
for (const path of candidates) {
|
|
2304
|
-
const match = HEADER_PATTERN.exec(
|
|
2559
|
+
const match = HEADER_PATTERN.exec(readFileSync8(join11(appRoot, path), "utf8"));
|
|
2305
2560
|
if (match && !planned.has(path)) orphans.push({ path, resource: match[1] });
|
|
2306
2561
|
}
|
|
2307
2562
|
return orphans;
|
|
2308
2563
|
}
|
|
2309
2564
|
function ensureSupportFiles(appRoot, log2) {
|
|
2310
2565
|
for (const path of ["lib/api.ts", "lib/cache.ts"]) {
|
|
2311
|
-
const target =
|
|
2312
|
-
if (
|
|
2566
|
+
const target = join11(appRoot, path);
|
|
2567
|
+
if (existsSync12(target)) continue;
|
|
2313
2568
|
mkdirSync4(dirname5(target), { recursive: true });
|
|
2314
|
-
copyFileSync2(
|
|
2569
|
+
copyFileSync2(join11(templatesDir, "app", path), target);
|
|
2315
2570
|
log2(`${pc5.green("create".padEnd(9))} ${path}`);
|
|
2316
2571
|
}
|
|
2317
2572
|
}
|
|
@@ -2339,15 +2594,15 @@ function billingFiles(appName) {
|
|
|
2339
2594
|
}
|
|
2340
2595
|
var RETIRED_FILES = ["app/api/billing/status/route.ts", "components/billing/subscribe-button.tsx"];
|
|
2341
2596
|
function appendVars(appRoot, target) {
|
|
2342
|
-
const path =
|
|
2343
|
-
const text =
|
|
2597
|
+
const path = join12(appRoot, target);
|
|
2598
|
+
const text = existsSync13(path) ? readFileSync9(path, "utf8") : "";
|
|
2344
2599
|
if (/STRIPE_SECRET_KEY/.test(text)) return false;
|
|
2345
2600
|
appendFileSync2(path, target === ".dev.vars" ? billingDevVarsEntries() : billingDevVarsExampleEntries());
|
|
2346
2601
|
return true;
|
|
2347
2602
|
}
|
|
2348
2603
|
function addStripeDependency(appRoot) {
|
|
2349
|
-
const path =
|
|
2350
|
-
const pkg = JSON.parse(
|
|
2604
|
+
const path = join12(appRoot, "package.json");
|
|
2605
|
+
const pkg = JSON.parse(readFileSync9(path, "utf8"));
|
|
2351
2606
|
pkg.dependencies ??= {};
|
|
2352
2607
|
if (pkg.dependencies.stripe) return false;
|
|
2353
2608
|
pkg.dependencies.stripe = STRIPE_DEP;
|
|
@@ -2356,14 +2611,14 @@ function addStripeDependency(appRoot) {
|
|
|
2356
2611
|
}
|
|
2357
2612
|
function writeBillingResource(appRoot, name, force, log2) {
|
|
2358
2613
|
const relative2 = descriptorPath(name);
|
|
2359
|
-
const path =
|
|
2360
|
-
if (!
|
|
2614
|
+
const path = join12(appRoot, relative2);
|
|
2615
|
+
if (!existsSync13(path)) {
|
|
2361
2616
|
mkdirSync5(dirname6(path), { recursive: true });
|
|
2362
|
-
|
|
2617
|
+
writeFileSync7(path, renderBillingDescriptor(name));
|
|
2363
2618
|
log2(`${pc6.green("create".padEnd(9))} ${relative2}`);
|
|
2364
2619
|
return;
|
|
2365
2620
|
}
|
|
2366
|
-
const parts = splitMarkers(
|
|
2621
|
+
const parts = splitMarkers(readFileSync9(path, "utf8"));
|
|
2367
2622
|
if (!parts) throw new Error(`${relative2} has no generated fields block, so the billing fields can't be merged. Add them by hand: ${BILLING_RESOURCES[name]}`);
|
|
2368
2623
|
const merged = mergeBillingFields(name, parts.block);
|
|
2369
2624
|
if (!merged) {
|
|
@@ -2387,26 +2642,26 @@ async function genBilling(options2 = {}) {
|
|
|
2387
2642
|
for (const name of ["Plan", "Customer", "Purchase"]) writeBillingResource(appRoot, name, options2.force, log2);
|
|
2388
2643
|
for (const [name, roles] of Object.entries(BILLING_POLICIES)) {
|
|
2389
2644
|
const relative2 = policyPath(name);
|
|
2390
|
-
if (
|
|
2645
|
+
if (existsSync13(join12(appRoot, relative2))) {
|
|
2391
2646
|
log2(`${pc6.dim("kept".padEnd(9))} ${relative2} (already exists)`);
|
|
2392
2647
|
continue;
|
|
2393
2648
|
}
|
|
2394
|
-
mkdirSync5(
|
|
2395
|
-
|
|
2649
|
+
mkdirSync5(join12(appRoot, "policies"), { recursive: true });
|
|
2650
|
+
writeFileSync7(join12(appRoot, relative2), renderPolicy(name, roles));
|
|
2396
2651
|
log2(`${pc6.green("create".padEnd(9))} ${relative2}`);
|
|
2397
2652
|
}
|
|
2398
2653
|
const files = [];
|
|
2399
|
-
const appName = JSON.parse(
|
|
2654
|
+
const appName = JSON.parse(readFileSync9(join12(appRoot, "package.json"), "utf8")).name ?? "app";
|
|
2400
2655
|
for (const { path, content } of billingFiles(appName)) {
|
|
2401
|
-
const outcome = writeGenerated(
|
|
2656
|
+
const outcome = writeGenerated(join12(appRoot, path), content, { header: billingHeader(), force: options2.force });
|
|
2402
2657
|
files.push({ path, status: "missing", outcome });
|
|
2403
2658
|
const label = outcome === "create" ? pc6.green("create") : outcome === "update" ? pc6.yellow("update") : pc6.dim("identical");
|
|
2404
2659
|
log2(`${label.padEnd(18)} ${path}`);
|
|
2405
2660
|
}
|
|
2406
2661
|
for (const path of RETIRED_FILES) {
|
|
2407
|
-
const absolute =
|
|
2408
|
-
if (!
|
|
2409
|
-
const parts = splitMarkers(
|
|
2662
|
+
const absolute = join12(appRoot, path);
|
|
2663
|
+
if (!existsSync13(absolute)) continue;
|
|
2664
|
+
const parts = splitMarkers(readFileSync9(absolute, "utf8"));
|
|
2410
2665
|
const untouched = parts && parts.before.includes("flare gen billing") && !parts.after.trim() && parts.hash !== void 0 && hashBlock(parts.block) === parts.hash;
|
|
2411
2666
|
if (untouched) {
|
|
2412
2667
|
unlinkSync(absolute);
|
|
@@ -2427,7 +2682,7 @@ async function genBilling(options2 = {}) {
|
|
|
2427
2682
|
}
|
|
2428
2683
|
const all = await loadResources(appRoot);
|
|
2429
2684
|
ensureSupportFiles(appRoot, log2);
|
|
2430
|
-
logResults(applyPlan(appRoot, planFiles(all, await loadPolicies(appRoot)), { force: options2.force }), log2);
|
|
2685
|
+
logResults(applyPlan(appRoot, planFiles(all, await loadPolicies(appRoot), readStack(appRoot)), { force: options2.force }), log2);
|
|
2431
2686
|
let migrations = [];
|
|
2432
2687
|
if (!options2.skipMigration) {
|
|
2433
2688
|
const result = await generateSchemaMigration(appRoot, "billing");
|
|
@@ -2450,8 +2705,8 @@ async function genBilling(options2 = {}) {
|
|
|
2450
2705
|
}
|
|
2451
2706
|
|
|
2452
2707
|
// src/commands/gen-security.ts
|
|
2453
|
-
import { existsSync as
|
|
2454
|
-
import { dirname as dirname7, join as
|
|
2708
|
+
import { existsSync as existsSync14, mkdirSync as mkdirSync6, readFileSync as readFileSync10, writeFileSync as writeFileSync8 } from "fs";
|
|
2709
|
+
import { dirname as dirname7, join as join13 } from "path";
|
|
2455
2710
|
import pc7 from "picocolors";
|
|
2456
2711
|
|
|
2457
2712
|
// src/generator/security.ts
|
|
@@ -2979,28 +3234,28 @@ function securityFiles(appName) {
|
|
|
2979
3234
|
}
|
|
2980
3235
|
var PASS_THROUGH_HEADER = /^\/\/ Request protection for worker\/index\.ts\.[\s\S]*?\n(?=\/\/ generated:start)/;
|
|
2981
3236
|
function writeOnce(appRoot, relative2, content, log2) {
|
|
2982
|
-
const path =
|
|
2983
|
-
if (
|
|
3237
|
+
const path = join13(appRoot, relative2);
|
|
3238
|
+
if (existsSync14(path)) {
|
|
2984
3239
|
log2(`${pc7.dim("kept".padEnd(9))} ${relative2} (already exists)`);
|
|
2985
3240
|
return;
|
|
2986
3241
|
}
|
|
2987
3242
|
mkdirSync6(dirname7(path), { recursive: true });
|
|
2988
|
-
|
|
3243
|
+
writeFileSync8(path, content);
|
|
2989
3244
|
log2(`${pc7.green("create".padEnd(9))} ${relative2}`);
|
|
2990
3245
|
}
|
|
2991
3246
|
async function genSecurity(options2 = {}) {
|
|
2992
3247
|
const log2 = options2.log ?? ((message) => console.log(message));
|
|
2993
3248
|
const appRoot = findAppRoot(options2.cwd ?? process.cwd());
|
|
2994
|
-
const appName = JSON.parse(
|
|
3249
|
+
const appName = JSON.parse(readFileSync10(join13(appRoot, "package.json"), "utf8")).name ?? "app";
|
|
2995
3250
|
writeOnce(appRoot, "security.config.ts", renderSecurityConfig(), log2);
|
|
2996
3251
|
writeOnce(appRoot, descriptorPath("SecurityEvent"), renderSecurityDescriptor(), log2);
|
|
2997
3252
|
writeOnce(appRoot, policyPath("SecurityEvent"), renderPolicy("SecurityEvent", SECURITY_EVENT_POLICY), log2);
|
|
2998
3253
|
const files = [];
|
|
2999
3254
|
const write = (path, content, header) => {
|
|
3000
|
-
const absolute =
|
|
3001
|
-
if (path === "lib/security.ts" &&
|
|
3002
|
-
const source =
|
|
3003
|
-
if (PASS_THROUGH_HEADER.test(source))
|
|
3255
|
+
const absolute = join13(appRoot, path);
|
|
3256
|
+
if (path === "lib/security.ts" && existsSync14(absolute)) {
|
|
3257
|
+
const source = readFileSync10(absolute, "utf8");
|
|
3258
|
+
if (PASS_THROUGH_HEADER.test(source)) writeFileSync8(absolute, source.replace(PASS_THROUGH_HEADER, securityHeader()));
|
|
3004
3259
|
}
|
|
3005
3260
|
const outcome = writeGenerated(absolute, content, { header, force: options2.force });
|
|
3006
3261
|
files.push({ path, status: "missing", outcome });
|
|
@@ -3008,16 +3263,16 @@ async function genSecurity(options2 = {}) {
|
|
|
3008
3263
|
log2(`${label.padEnd(18)} ${path}`);
|
|
3009
3264
|
};
|
|
3010
3265
|
for (const { path, content } of securityFiles(appName)) write(path, content, securityHeader());
|
|
3011
|
-
const navPath =
|
|
3012
|
-
if (
|
|
3266
|
+
const navPath = join13(appRoot, "lib/dashboard-nav.ts");
|
|
3267
|
+
if (existsSync14(navPath) && splitMarkers(readFileSync10(navPath, "utf8"))) write("lib/dashboard-nav.ts", renderAdminNavBlock());
|
|
3013
3268
|
else log2(pc7.yellow(`lib/dashboard-nav.ts not found: link to /dashboard/security from your dashboard sidebar yourself.`));
|
|
3014
|
-
const wranglerPath =
|
|
3269
|
+
const wranglerPath = join13(appRoot, "wrangler.jsonc");
|
|
3015
3270
|
let bindings = [];
|
|
3016
|
-
if (
|
|
3017
|
-
const { text, added } = addSecurityBindings(
|
|
3271
|
+
if (existsSync14(wranglerPath)) {
|
|
3272
|
+
const { text, added } = addSecurityBindings(readFileSync10(wranglerPath, "utf8"));
|
|
3018
3273
|
bindings = added;
|
|
3019
3274
|
if (added.length) {
|
|
3020
|
-
|
|
3275
|
+
writeFileSync8(wranglerPath, text);
|
|
3021
3276
|
for (const line of added) log2(`${pc7.yellow("update".padEnd(9))} wrangler.jsonc (${line})`);
|
|
3022
3277
|
}
|
|
3023
3278
|
} else {
|
|
@@ -3025,7 +3280,7 @@ async function genSecurity(options2 = {}) {
|
|
|
3025
3280
|
}
|
|
3026
3281
|
const all = await loadResources(appRoot);
|
|
3027
3282
|
ensureSupportFiles(appRoot, log2);
|
|
3028
|
-
logResults(applyPlan(appRoot, planFiles(all, await loadPolicies(appRoot)), { force: options2.force }), log2);
|
|
3283
|
+
logResults(applyPlan(appRoot, planFiles(all, await loadPolicies(appRoot), readStack(appRoot)), { force: options2.force }), log2);
|
|
3029
3284
|
let migrations = [];
|
|
3030
3285
|
if (!options2.skipMigration) {
|
|
3031
3286
|
const result = await generateSchemaMigration(appRoot, "security");
|
|
@@ -3048,15 +3303,15 @@ async function genSecurity(options2 = {}) {
|
|
|
3048
3303
|
}
|
|
3049
3304
|
|
|
3050
3305
|
// src/commands/theme.ts
|
|
3051
|
-
import { existsSync as
|
|
3052
|
-
import { join as
|
|
3306
|
+
import { existsSync as existsSync15, readFileSync as readFileSync11, writeFileSync as writeFileSync9 } from "fs";
|
|
3307
|
+
import { join as join14 } from "path";
|
|
3053
3308
|
import pc8 from "picocolors";
|
|
3054
3309
|
var THEME_LINE = /theme: "([a-z]+)" as ThemeName,/;
|
|
3055
3310
|
function setTheme(name, cwd = process.cwd(), log2 = console.log) {
|
|
3056
3311
|
const appRoot = findAppRoot(cwd);
|
|
3057
|
-
const path =
|
|
3058
|
-
if (!
|
|
3059
|
-
const source =
|
|
3312
|
+
const path = join14(appRoot, "lib/site.ts");
|
|
3313
|
+
if (!existsSync15(path)) throw new Error("lib/site.ts not found: this app was created before themes. Add it from the Flare app template.");
|
|
3314
|
+
const source = readFileSync11(path, "utf8");
|
|
3060
3315
|
const current = THEME_LINE.exec(source)?.[1];
|
|
3061
3316
|
if (!name) {
|
|
3062
3317
|
for (const [theme2, description] of Object.entries(THEMES)) {
|
|
@@ -3067,7 +3322,7 @@ function setTheme(name, cwd = process.cwd(), log2 = console.log) {
|
|
|
3067
3322
|
}
|
|
3068
3323
|
const theme = parseTheme(name);
|
|
3069
3324
|
if (!current) throw new Error('lib/site.ts has no `theme: "\u2026" as ThemeName,` line to change.');
|
|
3070
|
-
|
|
3325
|
+
writeFileSync9(path, source.replace(THEME_LINE, `theme: "${theme}" as ThemeName,`));
|
|
3071
3326
|
log2(`${pc8.green("\u2714")} Theme is now ${pc8.bold(theme)}. Restart \`flare dev\` (or rebuild) to see it.`);
|
|
3072
3327
|
}
|
|
3073
3328
|
|
|
@@ -3100,6 +3355,15 @@ function stopIfCancelled(value) {
|
|
|
3100
3355
|
async function askCreateQuestions(given, appName) {
|
|
3101
3356
|
prompts2.intro(`${pc9.bgRed(pc9.white(" Flare "))} ${pc9.dim(`v${FLARE_VERSION2}`)}${appName ? ` ${pc9.dim("\xB7")} ${pc9.bold(appName)}` : ""}`);
|
|
3102
3357
|
const answers = { ...given };
|
|
3358
|
+
if (given.stack === void 0) {
|
|
3359
|
+
answers.stack = stopIfCancelled(
|
|
3360
|
+
await prompts2.select({
|
|
3361
|
+
message: "Where will it run?",
|
|
3362
|
+
initialValue: "cloudflare",
|
|
3363
|
+
options: STACKS.map((value) => ({ value, label: value === "next" ? "Next.js" : "Cloudflare", hint: STACK_LABELS[value] }))
|
|
3364
|
+
})
|
|
3365
|
+
);
|
|
3366
|
+
}
|
|
3103
3367
|
if (given.theme === void 0) {
|
|
3104
3368
|
answers.theme = stopIfCancelled(
|
|
3105
3369
|
await prompts2.select({
|
|
@@ -3139,9 +3403,9 @@ async function askCreateQuestions(given, appName) {
|
|
|
3139
3403
|
}
|
|
3140
3404
|
|
|
3141
3405
|
// src/commands/gen.ts
|
|
3142
|
-
import { existsSync as
|
|
3143
|
-
import { dirname as dirname8, join as
|
|
3144
|
-
import { camelCase as
|
|
3406
|
+
import { existsSync as existsSync16, mkdirSync as mkdirSync7, writeFileSync as writeFileSync10 } from "fs";
|
|
3407
|
+
import { dirname as dirname8, join as join15 } from "path";
|
|
3408
|
+
import { camelCase as camelCase5, defineResource as defineResource2, relationGraph as relationGraph2 } from "@flaredev/core";
|
|
3145
3409
|
import pc10 from "picocolors";
|
|
3146
3410
|
function validateFields(name, fields, others, log2) {
|
|
3147
3411
|
const known = /* @__PURE__ */ new Set([name, ...others.map((resource) => resource.name)]);
|
|
@@ -3153,7 +3417,7 @@ function validateFields(name, fields, others, log2) {
|
|
|
3153
3417
|
const candidate = defineResource2({ name, fields: Object.fromEntries(fields.map((f) => [f.key, toField(f)])) });
|
|
3154
3418
|
const graph = relationGraph2([...others, candidate]);
|
|
3155
3419
|
for (const pending of graph.pending.filter((p) => p.resource === name)) {
|
|
3156
|
-
log2(pc10.dim(`note: ${name}.${pending.key} will link up once ${pending.target} exists (give it a ${
|
|
3420
|
+
log2(pc10.dim(`note: ${name}.${pending.key} will link up once ${pending.target} exists (give it a ${camelCase5(name)}Id: belongsTo(${name}) field).`));
|
|
3157
3421
|
}
|
|
3158
3422
|
}
|
|
3159
3423
|
async function genResource(rawName, options2) {
|
|
@@ -3161,8 +3425,8 @@ async function genResource(rawName, options2) {
|
|
|
3161
3425
|
const appRoot = findAppRoot(options2.cwd ?? process.cwd());
|
|
3162
3426
|
const name = resourceName(rawName);
|
|
3163
3427
|
const relativePath = descriptorPath(name);
|
|
3164
|
-
const path =
|
|
3165
|
-
const exists =
|
|
3428
|
+
const path = join15(appRoot, relativePath);
|
|
3429
|
+
const exists = existsSync16(path);
|
|
3166
3430
|
if (!exists && !options2.fields) {
|
|
3167
3431
|
throw new Error(`--fields is required, e.g. flare gen resource ${name} --fields "name:string, email:string"`);
|
|
3168
3432
|
}
|
|
@@ -3173,7 +3437,7 @@ async function genResource(rawName, options2) {
|
|
|
3173
3437
|
validateFields(name, fields, others, log2);
|
|
3174
3438
|
if (!exists) {
|
|
3175
3439
|
mkdirSync7(dirname8(path), { recursive: true });
|
|
3176
|
-
|
|
3440
|
+
writeFileSync10(path, renderDescriptor(name, fields, { group: options2.group, icon: options2.icon }));
|
|
3177
3441
|
log2(`${pc10.green("create".padEnd(9))} ${relativePath}`);
|
|
3178
3442
|
} else {
|
|
3179
3443
|
try {
|
|
@@ -3195,7 +3459,7 @@ async function genResource(rawName, options2) {
|
|
|
3195
3459
|
const all = await loadResources(appRoot);
|
|
3196
3460
|
table = all.find(({ resource }) => resource.name === name).resource.table;
|
|
3197
3461
|
ensureSupportFiles(appRoot, log2);
|
|
3198
|
-
written = applyPlan(appRoot, planFiles(all), { force: options2.force });
|
|
3462
|
+
written = applyPlan(appRoot, planFiles(all, void 0, readStack(appRoot)), { force: options2.force });
|
|
3199
3463
|
logResults(written, log2);
|
|
3200
3464
|
} catch (error) {
|
|
3201
3465
|
const hint = exists ? "" : `
|
|
@@ -3209,22 +3473,27 @@ Skipped ${skipped.length} hand-edited file(s); run \`flare sync-types\` for deta
|
|
|
3209
3473
|
}
|
|
3210
3474
|
let migrations = [];
|
|
3211
3475
|
if (!options2.skipMigration) {
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3476
|
+
if (readStack(appRoot) === "next") {
|
|
3477
|
+
log2(`
|
|
3478
|
+
Next: ${pc10.bold("npx prisma migrate dev")} to turn the schema into a migration and apply it.`);
|
|
3479
|
+
} else {
|
|
3480
|
+
const result = await generateSchemaMigration(appRoot, `${exists ? "update" : "create"}_${table}`);
|
|
3481
|
+
migrations = result.files;
|
|
3482
|
+
for (const file of migrations) log2(`${pc10.green("create".padEnd(9))} ${file}`);
|
|
3483
|
+
for (const repair of result.repairs) log2(pc10.dim(`repaired ${repair}`));
|
|
3484
|
+
if (migrations.length) log2(`
|
|
3217
3485
|
Next: ${pc10.bold("flare migrate")} to apply it locally.`);
|
|
3218
|
-
|
|
3486
|
+
else if (exists) log2(pc10.dim("\nNo table changes, so no migration."));
|
|
3487
|
+
}
|
|
3219
3488
|
}
|
|
3220
3489
|
const descriptor = { path: relativePath, status: exists ? "outdated" : "missing", outcome: exists ? "update" : "create" };
|
|
3221
3490
|
return { name, mode: exists ? "update" : "create", written: [descriptor, ...written], migrations };
|
|
3222
3491
|
}
|
|
3223
3492
|
|
|
3224
3493
|
// src/commands/gen-endpoint.ts
|
|
3225
|
-
import { existsSync as
|
|
3226
|
-
import { join as
|
|
3227
|
-
import { kebabCase as kebabCase3, pascalCase as
|
|
3494
|
+
import { existsSync as existsSync17, mkdirSync as mkdirSync8, writeFileSync as writeFileSync11 } from "fs";
|
|
3495
|
+
import { join as join16 } from "path";
|
|
3496
|
+
import { kebabCase as kebabCase3, pascalCase as pascalCase3 } from "@flaredev/core";
|
|
3228
3497
|
import pc11 from "picocolors";
|
|
3229
3498
|
var METHODS = ["GET", "POST", "PATCH", "PUT", "DELETE"];
|
|
3230
3499
|
var ACTIONS = ["read", "create", "update", "delete"];
|
|
@@ -3236,7 +3505,7 @@ async function genEndpoint(resourceName2, rawName, options2 = {}) {
|
|
|
3236
3505
|
const name = kebabCase3(rawName);
|
|
3237
3506
|
if (!name) throw new Error(`Invalid endpoint name "${rawName}".`);
|
|
3238
3507
|
const resources = await loadResources(appRoot);
|
|
3239
|
-
const wanted =
|
|
3508
|
+
const wanted = pascalCase3(resourceName2);
|
|
3240
3509
|
const entry = resources.find(({ resource: resource2 }) => resource2.name === wanted);
|
|
3241
3510
|
if (!entry) {
|
|
3242
3511
|
throw new Error(`No resource "${resourceName2}". Available: ${resources.map(({ resource: resource2 }) => resource2.name).join(", ") || "none"}.`);
|
|
@@ -3245,11 +3514,11 @@ async function genEndpoint(resourceName2, rawName, options2 = {}) {
|
|
|
3245
3514
|
if (!ACTIONS.includes(action)) throw new Error(`Unknown action "${action}". Use one of ${ACTIONS.join(", ")}.`);
|
|
3246
3515
|
const { resource, stem } = entry;
|
|
3247
3516
|
const relative2 = options2.record ? `app/api/${resource.slug}/[id]/${name}/route.ts` : `app/api/${resource.slug}/${name}/route.ts`;
|
|
3248
|
-
const path =
|
|
3249
|
-
if (
|
|
3250
|
-
mkdirSync8(
|
|
3517
|
+
const path = join16(appRoot, relative2);
|
|
3518
|
+
if (existsSync17(path)) throw new Error(`${relative2} already exists.`);
|
|
3519
|
+
mkdirSync8(join16(path, ".."), { recursive: true });
|
|
3251
3520
|
const route = options2.record ? `/api/${resource.slug}/[id]/${name}` : `/api/${resource.slug}/${name}`;
|
|
3252
|
-
|
|
3521
|
+
writeFileSync11(path, renderEndpoint({ resource: resource.name, stem, method, action, record: Boolean(options2.record), name, route }));
|
|
3253
3522
|
log2(`${pc11.green("create".padEnd(9))} ${relative2}`);
|
|
3254
3523
|
log2(`
|
|
3255
3524
|
It's an ordinary route handler \u2014 edit it freely; regeneration leaves it alone.`);
|
|
@@ -3285,8 +3554,8 @@ ${body}
|
|
|
3285
3554
|
}
|
|
3286
3555
|
|
|
3287
3556
|
// src/commands/gen-migration.ts
|
|
3288
|
-
import { existsSync as
|
|
3289
|
-
import { basename as basename2, join as
|
|
3557
|
+
import { existsSync as existsSync18, mkdirSync as mkdirSync9, writeFileSync as writeFileSync12 } from "fs";
|
|
3558
|
+
import { basename as basename2, join as join17 } from "path";
|
|
3290
3559
|
import { snakeCase } from "@flaredev/core";
|
|
3291
3560
|
import pc12 from "picocolors";
|
|
3292
3561
|
var DOWN_TEMPLATE = (up) => `-- Rollback for ${up}, run by \`flare migrate:rollback\`.
|
|
@@ -3307,12 +3576,12 @@ async function genMigration(rawName, options2 = {}) {
|
|
|
3307
3576
|
for (const file of files) log2(`${pc12.green("create".padEnd(9))} ${file}`);
|
|
3308
3577
|
for (const repair of repairs) log2(pc12.dim(`repaired ${repair}`));
|
|
3309
3578
|
if (!options2.fromSchema) {
|
|
3310
|
-
const downDir =
|
|
3579
|
+
const downDir = join17(appRoot, "migrations", "down");
|
|
3311
3580
|
mkdirSync9(downDir, { recursive: true });
|
|
3312
3581
|
for (const file of files) {
|
|
3313
|
-
const down =
|
|
3314
|
-
if (
|
|
3315
|
-
|
|
3582
|
+
const down = join17(downDir, basename2(file));
|
|
3583
|
+
if (existsSync18(down)) continue;
|
|
3584
|
+
writeFileSync12(down, DOWN_TEMPLATE(basename2(file)));
|
|
3316
3585
|
log2(`${pc12.green("create".padEnd(9))} migrations/down/${basename2(file)}`);
|
|
3317
3586
|
}
|
|
3318
3587
|
log2(`
|
|
@@ -3325,8 +3594,8 @@ Next: ${pc12.bold("flare migrate")}.`);
|
|
|
3325
3594
|
}
|
|
3326
3595
|
|
|
3327
3596
|
// src/commands/gen-policy.ts
|
|
3328
|
-
import { existsSync as
|
|
3329
|
-
import { dirname as dirname9, join as
|
|
3597
|
+
import { existsSync as existsSync19, mkdirSync as mkdirSync10, writeFileSync as writeFileSync13 } from "fs";
|
|
3598
|
+
import { dirname as dirname9, join as join18 } from "path";
|
|
3330
3599
|
import { definePolicy } from "@flaredev/core";
|
|
3331
3600
|
import pc13 from "picocolors";
|
|
3332
3601
|
function parseRoles(input, what = "--roles") {
|
|
@@ -3344,8 +3613,8 @@ async function genPolicy(rawName, options2) {
|
|
|
3344
3613
|
throw new Error(`No resource "${name}". Generate it first. Known resources: ${known}.`);
|
|
3345
3614
|
}
|
|
3346
3615
|
const relative2 = policyPath(name);
|
|
3347
|
-
const path =
|
|
3348
|
-
const exists =
|
|
3616
|
+
const path = join18(appRoot, relative2);
|
|
3617
|
+
const exists = existsSync19(path);
|
|
3349
3618
|
let roles;
|
|
3350
3619
|
let deleteRoles;
|
|
3351
3620
|
if (options2.roles || !exists) {
|
|
@@ -3357,7 +3626,7 @@ async function genPolicy(rawName, options2) {
|
|
|
3357
3626
|
const block = renderPolicyBlock({ read: roles, create: roles, update: roles, delete: deleteRoles });
|
|
3358
3627
|
if (!exists) {
|
|
3359
3628
|
mkdirSync10(dirname9(path), { recursive: true });
|
|
3360
|
-
|
|
3629
|
+
writeFileSync13(path, renderPolicy(name, { read: roles, create: roles, update: roles, delete: deleteRoles }));
|
|
3361
3630
|
log2(`${pc13.green("create".padEnd(9))} ${relative2}`);
|
|
3362
3631
|
} else {
|
|
3363
3632
|
try {
|
|
@@ -3372,16 +3641,16 @@ async function genPolicy(rawName, options2) {
|
|
|
3372
3641
|
}
|
|
3373
3642
|
}
|
|
3374
3643
|
const policies = await loadPolicies(appRoot);
|
|
3375
|
-
logResults(applyPlan(appRoot, planFiles(resources, policies)), log2);
|
|
3644
|
+
logResults(applyPlan(appRoot, planFiles(resources, policies, readStack(appRoot))), log2);
|
|
3376
3645
|
log2(`
|
|
3377
3646
|
${pc13.dim("Roles come from `flare role:add`; the API and the admin both enforce this policy.")}`);
|
|
3378
3647
|
return { name, path: relative2 };
|
|
3379
3648
|
}
|
|
3380
3649
|
|
|
3381
3650
|
// src/commands/migrate.ts
|
|
3382
|
-
import { existsSync as
|
|
3651
|
+
import { existsSync as existsSync20, mkdtempSync, readFileSync as readFileSync12, rmSync as rmSync3, writeFileSync as writeFileSync14 } from "fs";
|
|
3383
3652
|
import { tmpdir } from "os";
|
|
3384
|
-
import { join as
|
|
3653
|
+
import { join as join19 } from "path";
|
|
3385
3654
|
import pc14 from "picocolors";
|
|
3386
3655
|
|
|
3387
3656
|
// src/generator/down.ts
|
|
@@ -3475,7 +3744,17 @@ function context(options2) {
|
|
|
3475
3744
|
};
|
|
3476
3745
|
}
|
|
3477
3746
|
async function migrate(options2 = {}) {
|
|
3478
|
-
const
|
|
3747
|
+
const appRoot = findAppRoot(options2.cwd ?? process.cwd());
|
|
3748
|
+
const log2 = options2.log ?? ((message) => console.log(message));
|
|
3749
|
+
if (readStack(appRoot) === "next") {
|
|
3750
|
+
if (options2.remote) {
|
|
3751
|
+
log2(pc14.dim("On Next.js, `--remote` is whichever database DATABASE_URL points at."));
|
|
3752
|
+
}
|
|
3753
|
+
const prisma = resolveBin(appRoot, "prisma", "prisma");
|
|
3754
|
+
log2(`${pc14.cyan("prisma")} migrate deploy`);
|
|
3755
|
+
return runNode([prisma, "migrate", "deploy"], appRoot);
|
|
3756
|
+
}
|
|
3757
|
+
const { db, target, wrangler } = context(options2);
|
|
3479
3758
|
const result = await runWrangler(wrangler, ["d1", "migrations", "apply", db.binding, ...target], appRoot);
|
|
3480
3759
|
return result.code;
|
|
3481
3760
|
}
|
|
@@ -3499,7 +3778,7 @@ async function rollback(options2 = {}) {
|
|
|
3499
3778
|
const steps = options2.steps ?? 1;
|
|
3500
3779
|
if (!Number.isInteger(steps) || steps < 1) throw new Error("--steps must be a positive integer.");
|
|
3501
3780
|
const { appRoot, db, target, where, wrangler, log: log2 } = context(options2);
|
|
3502
|
-
const migrationsDir =
|
|
3781
|
+
const migrationsDir = join19(appRoot, db.migrations_dir ?? "migrations");
|
|
3503
3782
|
const applied = await appliedMigrations(options2, steps);
|
|
3504
3783
|
if (applied.length === 0) {
|
|
3505
3784
|
log2("Nothing to roll back.");
|
|
@@ -3508,18 +3787,18 @@ async function rollback(options2 = {}) {
|
|
|
3508
3787
|
const plan = [];
|
|
3509
3788
|
for (const { name } of applied) {
|
|
3510
3789
|
if (!/^[\w.-]+\.sql$/.test(name)) throw new Error(`Unexpected migration name "${name}" in d1_migrations.`);
|
|
3511
|
-
const handWritten =
|
|
3512
|
-
if (
|
|
3513
|
-
const sql2 =
|
|
3790
|
+
const handWritten = join19(migrationsDir, "down", name);
|
|
3791
|
+
if (existsSync20(handWritten)) {
|
|
3792
|
+
const sql2 = readFileSync12(handWritten, "utf8");
|
|
3514
3793
|
if (splitStatements(sql2).length === 0) {
|
|
3515
3794
|
throw new Error(`Can't roll back ${name}: migrations/down/${name} has no SQL statements yet. Write the rollback SQL, then run the rollback again. Nothing was changed.`);
|
|
3516
3795
|
}
|
|
3517
3796
|
plan.push({ name, sql: sql2, source: `migrations/down/${name}` });
|
|
3518
3797
|
continue;
|
|
3519
3798
|
}
|
|
3520
|
-
const upPath =
|
|
3521
|
-
if (!
|
|
3522
|
-
const down = deriveDown(
|
|
3799
|
+
const upPath = join19(migrationsDir, name);
|
|
3800
|
+
if (!existsSync20(upPath)) throw new Error(`${name} was applied but migrations/${name} is missing, so it can't be reversed.`);
|
|
3801
|
+
const down = deriveDown(readFileSync12(upPath, "utf8"));
|
|
3523
3802
|
if (!down.reversible) {
|
|
3524
3803
|
throw new Error(
|
|
3525
3804
|
`Can't roll back ${name}: ${down.reason}
|
|
@@ -3539,15 +3818,15 @@ Write the reverse SQL to migrations/down/${name} and run the rollback again. Not
|
|
|
3539
3818
|
}
|
|
3540
3819
|
const script = plan.map(({ name, sql: sql2 }) => `${sql2.trim()}
|
|
3541
3820
|
DELETE FROM d1_migrations WHERE name = '${name.replace(/'/g, "''")}';`).join("\n") + "\n";
|
|
3542
|
-
const dir = mkdtempSync(
|
|
3821
|
+
const dir = mkdtempSync(join19(tmpdir(), "flare-rollback-"));
|
|
3543
3822
|
try {
|
|
3544
|
-
const file =
|
|
3545
|
-
|
|
3823
|
+
const file = join19(dir, "rollback.sql");
|
|
3824
|
+
writeFileSync14(file, script);
|
|
3546
3825
|
const result = await runWrangler(wrangler, ["d1", "execute", db.binding, ...target, "--file", file], appRoot, { capture: true });
|
|
3547
3826
|
if (result.code !== 0) throw new Error(`Rollback failed:
|
|
3548
3827
|
${result.output.trim()}`);
|
|
3549
3828
|
} finally {
|
|
3550
|
-
|
|
3829
|
+
rmSync3(dir, { recursive: true, force: true });
|
|
3551
3830
|
}
|
|
3552
3831
|
log2(pc14.green(`
|
|
3553
3832
|
Rolled back ${plan.map((step) => step.name).join(", ")}.`));
|
|
@@ -3555,8 +3834,8 @@ Rolled back ${plan.map((step) => step.name).join(", ")}.`));
|
|
|
3555
3834
|
}
|
|
3556
3835
|
|
|
3557
3836
|
// src/commands/rm.ts
|
|
3558
|
-
import { existsSync as
|
|
3559
|
-
import { dirname as dirname10, join as
|
|
3837
|
+
import { existsSync as existsSync21, readdirSync as readdirSync7, readFileSync as readFileSync13, rmdirSync, unlinkSync as unlinkSync2 } from "fs";
|
|
3838
|
+
import { dirname as dirname10, join as join20 } from "path";
|
|
3560
3839
|
import { relationGraph as relationGraph3 } from "@flaredev/core";
|
|
3561
3840
|
import pc15 from "picocolors";
|
|
3562
3841
|
var squash = (text) => text.replace(/\r\n/g, "\n").replace(/\s+/g, " ").trim();
|
|
@@ -3575,10 +3854,10 @@ function handWrittenReason(source, header, options2 = {}) {
|
|
|
3575
3854
|
return null;
|
|
3576
3855
|
}
|
|
3577
3856
|
function removeEmptyDirs(appRoot, start) {
|
|
3578
|
-
let dir = dirname10(
|
|
3579
|
-
const roots = [
|
|
3857
|
+
let dir = dirname10(join20(appRoot, start));
|
|
3858
|
+
const roots = [join20(appRoot, "app", "api"), join20(appRoot, "app", "dashboard")];
|
|
3580
3859
|
const inside = () => roots.some((root) => dir.startsWith(root) && dir !== root);
|
|
3581
|
-
while (inside() &&
|
|
3860
|
+
while (inside() && existsSync21(dir) && readdirSync7(dir).length === 0) {
|
|
3582
3861
|
rmdirSync(dir);
|
|
3583
3862
|
dir = dirname10(dir);
|
|
3584
3863
|
}
|
|
@@ -3606,8 +3885,8 @@ async function rmResource(rawName, options2 = {}) {
|
|
|
3606
3885
|
const owned = /* @__PURE__ */ new Set();
|
|
3607
3886
|
if (entry) {
|
|
3608
3887
|
owned.add(entry.file);
|
|
3609
|
-
for (const file of planFiles(all)) {
|
|
3610
|
-
if (
|
|
3888
|
+
for (const file of planFiles(all, void 0, readStack(appRoot))) {
|
|
3889
|
+
if (existsSync21(join20(appRoot, file.path)) && file.header === resourceHeader(name)) owned.add(file.path);
|
|
3611
3890
|
}
|
|
3612
3891
|
}
|
|
3613
3892
|
for (const orphan of findOrphans(appRoot, remaining)) if (orphan.resource === name) owned.add(orphan.path);
|
|
@@ -3616,7 +3895,7 @@ async function rmResource(rawName, options2 = {}) {
|
|
|
3616
3895
|
if (!entry && owned.size === 0) throw new Error(`No resource "${name}" (no ${descriptorPath(name)} and no generated files).`);
|
|
3617
3896
|
const findings = [];
|
|
3618
3897
|
for (const path of owned) {
|
|
3619
|
-
const source =
|
|
3898
|
+
const source = readFileSync13(join20(appRoot, path), "utf8");
|
|
3620
3899
|
let wrapper;
|
|
3621
3900
|
if (entry && path === entry.file) {
|
|
3622
3901
|
const standard = splitMarkers(renderDescriptor(name, [{ key: "placeholder", kind: "string", required: true, unique: false }]));
|
|
@@ -3637,15 +3916,15 @@ Move what you want to keep, or pass --force to delete anyway.`);
|
|
|
3637
3916
|
}
|
|
3638
3917
|
const removed = [...owned].sort();
|
|
3639
3918
|
for (const path of removed) {
|
|
3640
|
-
unlinkSync2(
|
|
3919
|
+
unlinkSync2(join20(appRoot, path));
|
|
3641
3920
|
removeEmptyDirs(appRoot, path);
|
|
3642
3921
|
log2(`${pc15.red("remove".padEnd(9))} ${path}`);
|
|
3643
3922
|
}
|
|
3644
|
-
logResults(applyPlan(appRoot, planFiles(remaining, policy ? await loadPolicies(appRoot) : void 0)), log2);
|
|
3923
|
+
logResults(applyPlan(appRoot, planFiles(remaining, policy ? await loadPolicies(appRoot) : void 0, readStack(appRoot))), log2);
|
|
3645
3924
|
const table = entry ? tableExport(entry.resource) : void 0;
|
|
3646
|
-
if (table &&
|
|
3647
|
-
const seeds = readdirSync7(
|
|
3648
|
-
(file) => new RegExp(`\\b${table}\\b`).test(
|
|
3925
|
+
if (table && existsSync21(join20(appRoot, "seeds"))) {
|
|
3926
|
+
const seeds = readdirSync7(join20(appRoot, "seeds")).filter(
|
|
3927
|
+
(file) => new RegExp(`\\b${table}\\b`).test(readFileSync13(join20(appRoot, "seeds", file), "utf8"))
|
|
3649
3928
|
);
|
|
3650
3929
|
if (seeds.length) log2(pc15.yellow(`
|
|
3651
3930
|
These seeds still use ${table}: ${seeds.map((s) => `seeds/${s}`).join(", ")}.`));
|
|
@@ -3715,9 +3994,9 @@ ${result.output.trim()}`);
|
|
|
3715
3994
|
}
|
|
3716
3995
|
|
|
3717
3996
|
// src/commands/seed.ts
|
|
3718
|
-
import { existsSync as
|
|
3719
|
-
import { join as
|
|
3720
|
-
import { createFake, kebabCase as kebabCase4, storedFields as
|
|
3997
|
+
import { existsSync as existsSync22, mkdirSync as mkdirSync11, readdirSync as readdirSync8, rmSync as rmSync4, writeFileSync as writeFileSync15 } from "fs";
|
|
3998
|
+
import { join as join22 } from "path";
|
|
3999
|
+
import { camelCase as camelCase6, createFake, kebabCase as kebabCase4, storedFields as storedFields3 } from "@flaredev/core";
|
|
3721
4000
|
import { createJiti as createJiti4 } from "jiti";
|
|
3722
4001
|
import pc17 from "picocolors";
|
|
3723
4002
|
|
|
@@ -3814,16 +4093,16 @@ function createInsertMany(sink, drizzle, onProgress) {
|
|
|
3814
4093
|
|
|
3815
4094
|
// src/seed/local-d1.ts
|
|
3816
4095
|
import { createRequire } from "module";
|
|
3817
|
-
import { join as
|
|
4096
|
+
import { join as join21 } from "path";
|
|
3818
4097
|
import { pathToFileURL } from "url";
|
|
3819
4098
|
var LOCAL_STATE = ".wrangler/state/v3";
|
|
3820
4099
|
async function openLocalD1(appRoot, binding) {
|
|
3821
4100
|
process.env.WRANGLER_LOG ??= "error";
|
|
3822
|
-
const appRequire = createRequire(
|
|
4101
|
+
const appRequire = createRequire(join21(appRoot, "package.json"));
|
|
3823
4102
|
const wrangler = await import(pathToFileURL(appRequire.resolve("wrangler")).href);
|
|
3824
4103
|
const { env, dispose } = await wrangler.getPlatformProxy({
|
|
3825
|
-
configPath:
|
|
3826
|
-
persist: { path:
|
|
4104
|
+
configPath: join21(appRoot, "wrangler.jsonc"),
|
|
4105
|
+
persist: { path: join21(appRoot, LOCAL_STATE) }
|
|
3827
4106
|
});
|
|
3828
4107
|
const bindings = env;
|
|
3829
4108
|
const name = binding ?? "DB";
|
|
@@ -3838,8 +4117,8 @@ async function openLocalD1(appRoot, binding) {
|
|
|
3838
4117
|
// src/commands/seed.ts
|
|
3839
4118
|
var SEEDS_DIR = "seeds";
|
|
3840
4119
|
function seedFiles(appRoot, names) {
|
|
3841
|
-
const dir =
|
|
3842
|
-
const all =
|
|
4120
|
+
const dir = join22(appRoot, SEEDS_DIR);
|
|
4121
|
+
const all = existsSync22(dir) ? readdirSync8(dir).filter((file) => file.endsWith(".seed.ts")).sort() : [];
|
|
3843
4122
|
if (!names?.length) return all;
|
|
3844
4123
|
return names.map((name) => {
|
|
3845
4124
|
const file = `${name.replace(/\.seed\.ts$/, "")}.seed.ts`;
|
|
@@ -3857,16 +4136,20 @@ async function runSeeds(options2 = {}) {
|
|
|
3857
4136
|
log2(`No seeds found. Create one with ${pc17.bold("flare seed:make <name>")}.`);
|
|
3858
4137
|
return;
|
|
3859
4138
|
}
|
|
3860
|
-
|
|
4139
|
+
if (readStack(appRoot) === "next") {
|
|
4140
|
+
await runPrismaSeeds(appRoot, files, log2);
|
|
4141
|
+
return;
|
|
4142
|
+
}
|
|
4143
|
+
const jiti = createJiti4(join22(appRoot, "package.json"), { alias: { "@": appRoot }, moduleCache: false, fsCache: false });
|
|
3861
4144
|
const { drizzle } = await jiti.import("drizzle-orm/d1");
|
|
3862
4145
|
const drizzleHelpers = await jiti.import("drizzle-orm");
|
|
3863
|
-
const schema = await jiti.import(
|
|
4146
|
+
const schema = await jiti.import(join22(appRoot, "db/schema.ts"));
|
|
3864
4147
|
const local = await openLocalD1(appRoot);
|
|
3865
4148
|
try {
|
|
3866
4149
|
const db = drizzle(local.db, { schema });
|
|
3867
4150
|
const insertMany = createInsertMany(local.db, drizzleHelpers);
|
|
3868
4151
|
for (const file of files) {
|
|
3869
|
-
const mod = await jiti.import(
|
|
4152
|
+
const mod = await jiti.import(join22(appRoot, SEEDS_DIR, file));
|
|
3870
4153
|
if (typeof mod.default !== "function") throw new Error(`${SEEDS_DIR}/${file} must \`export default defineSeed(async ({ db }) => { ... })\`.`);
|
|
3871
4154
|
log2(`${pc17.cyan("seed")} ${file}`);
|
|
3872
4155
|
await mod.default({ db, env: local.env, insertMany, fake: createFake(), log: (message) => log2(` ${message}`) });
|
|
@@ -3916,9 +4199,17 @@ function sampleValue(key, def) {
|
|
|
3916
4199
|
return void 0;
|
|
3917
4200
|
}
|
|
3918
4201
|
}
|
|
3919
|
-
function renderSeed(resource) {
|
|
4202
|
+
function renderSeed(resource, stack = "cloudflare") {
|
|
3920
4203
|
if (!resource) {
|
|
3921
|
-
return `import { defineSeed } from "@flaredev/core";
|
|
4204
|
+
return stack === "next" ? `import { defineSeed } from "@flaredev/core";
|
|
4205
|
+
|
|
4206
|
+
export default defineSeed(async ({ db, insertMany, fake, log }) => {
|
|
4207
|
+
// \`db\` is this app's Prisma client, e.g.:
|
|
4208
|
+
// await db.contact.create({ data: { name: "Ada", email: "ada@example.com" } });
|
|
4209
|
+
// Or many at once: await insertMany("contact", [{ name: fake.fullName() }]);
|
|
4210
|
+
log("nothing to seed yet");
|
|
4211
|
+
});
|
|
4212
|
+
` : `import { defineSeed } from "@flaredev/core";
|
|
3922
4213
|
|
|
3923
4214
|
export default defineSeed(async ({ db, insertMany, fake, log }) => {
|
|
3924
4215
|
// Insert rows with Drizzle, e.g.:
|
|
@@ -3931,11 +4222,30 @@ export default defineSeed(async ({ db, insertMany, fake, log }) => {
|
|
|
3931
4222
|
const table = tableExport(resource);
|
|
3932
4223
|
const lines = [];
|
|
3933
4224
|
const todos = [];
|
|
3934
|
-
for (const [key, def] of
|
|
4225
|
+
for (const [key, def] of storedFields3(resource)) {
|
|
3935
4226
|
const value = sampleValue(key, def);
|
|
3936
4227
|
if (value !== void 0) lines.push(` ${key}: ${value},`);
|
|
3937
4228
|
else if (def.required) todos.push(` // ${key}: TODO (${def.kind === "belongsTo" ? `id of an existing ${def.target}` : "R2 object key"}),`);
|
|
3938
4229
|
}
|
|
4230
|
+
if (stack === "next") {
|
|
4231
|
+
const body = [...lines, ...todos].map((line) => ` ${line}`).join("\n");
|
|
4232
|
+
return `import { defineSeed } from "@flaredev/core";
|
|
4233
|
+
|
|
4234
|
+
/** How many to create. createMany sends them in one statement. */
|
|
4235
|
+
const COUNT = 50;
|
|
4236
|
+
|
|
4237
|
+
export default defineSeed(async ({ insertMany, fake, log }) => {
|
|
4238
|
+
await insertMany(
|
|
4239
|
+
${JSON.stringify(camelCase6(resource.name))},
|
|
4240
|
+
Array.from({ length: COUNT }, () => ({
|
|
4241
|
+
${body}
|
|
4242
|
+
updatedAt: new Date(),
|
|
4243
|
+
})),
|
|
4244
|
+
);
|
|
4245
|
+
log(\`inserted \${COUNT.toLocaleString()} ${resource.pluralLabel.toLowerCase()}\`);
|
|
4246
|
+
});
|
|
4247
|
+
`;
|
|
4248
|
+
}
|
|
3939
4249
|
return `import { defineSeed } from "@flaredev/core";
|
|
3940
4250
|
import { ${table} } from "@/db/schema";
|
|
3941
4251
|
|
|
@@ -3956,8 +4266,8 @@ async function makeSeed(rawName, options2 = {}) {
|
|
|
3956
4266
|
const name = kebabCase4(rawName);
|
|
3957
4267
|
if (!name) throw new Error(`Invalid seed name "${rawName}".`);
|
|
3958
4268
|
const relative2 = `${SEEDS_DIR}/${name}.seed.ts`;
|
|
3959
|
-
const path =
|
|
3960
|
-
if (
|
|
4269
|
+
const path = join22(appRoot, relative2);
|
|
4270
|
+
if (existsSync22(path)) throw new Error(`${relative2} already exists.`);
|
|
3961
4271
|
const resources = (await loadResources(appRoot)).map(({ resource: resource2 }) => resource2);
|
|
3962
4272
|
let resource;
|
|
3963
4273
|
if (options2.resource) {
|
|
@@ -3966,18 +4276,58 @@ async function makeSeed(rawName, options2 = {}) {
|
|
|
3966
4276
|
} else {
|
|
3967
4277
|
resource = resources.find((r) => [kebabCase4(r.name), r.slug].includes(name));
|
|
3968
4278
|
}
|
|
3969
|
-
mkdirSync11(
|
|
3970
|
-
|
|
4279
|
+
mkdirSync11(join22(appRoot, SEEDS_DIR), { recursive: true });
|
|
4280
|
+
writeFileSync15(path, renderSeed(resource, readStack(appRoot)));
|
|
3971
4281
|
log2(`${pc17.green("create".padEnd(9))} ${relative2}`);
|
|
3972
4282
|
log2(`
|
|
3973
4283
|
Run it with ${pc17.bold(`flare seed ${name}`)}.`);
|
|
3974
4284
|
return relative2;
|
|
3975
4285
|
}
|
|
4286
|
+
async function runPrismaSeeds(appRoot, files, log2) {
|
|
4287
|
+
const runner = join22(appRoot, ".flare-seed-runner.mts");
|
|
4288
|
+
const script = [
|
|
4289
|
+
`import "dotenv/config";`,
|
|
4290
|
+
`import { createFake } from "@flaredev/core";`,
|
|
4291
|
+
`import { prisma } from "./lib/db";`,
|
|
4292
|
+
...files.map((file, index) => `import seed${index} from "./${SEEDS_DIR}/${file.replace(/\.ts$/, "")}";`),
|
|
4293
|
+
"",
|
|
4294
|
+
`const files = ${JSON.stringify(files)};`,
|
|
4295
|
+
`const seeds = [${files.map((_, index) => `seed${index}`).join(", ")}];`,
|
|
4296
|
+
`const client = prisma as unknown as Record<string, { createMany(args: { data: unknown[] }): Promise<unknown> }>;`,
|
|
4297
|
+
"",
|
|
4298
|
+
`for (const [index, seed] of seeds.entries()) {`,
|
|
4299
|
+
` console.log("seed " + files[index]);`,
|
|
4300
|
+
` await seed({`,
|
|
4301
|
+
` db: prisma,`,
|
|
4302
|
+
` env: process.env,`,
|
|
4303
|
+
` insertMany: async (model: string, rows: Record<string, unknown>[]) => {`,
|
|
4304
|
+
` const delegate = client[model];`,
|
|
4305
|
+
` if (!delegate) throw new Error('No Prisma model "' + model + '". Use the camelCase name, e.g. "orderItem".');`,
|
|
4306
|
+
` await delegate.createMany({ data: rows });`,
|
|
4307
|
+
` },`,
|
|
4308
|
+
` fake: createFake(),`,
|
|
4309
|
+
` log: (message: string) => console.log(" " + message),`,
|
|
4310
|
+
` });`,
|
|
4311
|
+
`}`,
|
|
4312
|
+
`await (prisma as unknown as { $disconnect(): Promise<void> }).$disconnect();`,
|
|
4313
|
+
""
|
|
4314
|
+
].join("\n");
|
|
4315
|
+
writeFileSync15(runner, script);
|
|
4316
|
+
try {
|
|
4317
|
+
const tsx = resolveBin(appRoot, "tsx", "tsx");
|
|
4318
|
+
const code = await runNode([tsx, runner], appRoot);
|
|
4319
|
+
if (code !== 0) throw new Error("A seed failed; nothing further was run.");
|
|
4320
|
+
log2(pc17.green(`
|
|
4321
|
+
Ran ${files.length} seed(s).`));
|
|
4322
|
+
} finally {
|
|
4323
|
+
rmSync4(runner, { force: true });
|
|
4324
|
+
}
|
|
4325
|
+
}
|
|
3976
4326
|
|
|
3977
4327
|
// src/commands/db-push.ts
|
|
3978
|
-
import { mkdtempSync as mkdtempSync2, rmSync as
|
|
4328
|
+
import { mkdtempSync as mkdtempSync2, rmSync as rmSync5, writeFileSync as writeFileSync16 } from "fs";
|
|
3979
4329
|
import { tmpdir as tmpdir2 } from "os";
|
|
3980
|
-
import { join as
|
|
4330
|
+
import { join as join23 } from "path";
|
|
3981
4331
|
import pc18 from "picocolors";
|
|
3982
4332
|
|
|
3983
4333
|
// src/terminal.ts
|
|
@@ -4042,7 +4392,7 @@ async function dbPush(tables, options2 = {}) {
|
|
|
4042
4392
|
const wrangler = resolveBin(appRoot, "wrangler", "wrangler");
|
|
4043
4393
|
const started = Date.now();
|
|
4044
4394
|
const local = await openLocalD1(appRoot, options2.database);
|
|
4045
|
-
const dir = mkdtempSync2(
|
|
4395
|
+
const dir = mkdtempSync2(join23(tmpdir2(), "flare-push-"));
|
|
4046
4396
|
try {
|
|
4047
4397
|
const statements = [];
|
|
4048
4398
|
const counts = [];
|
|
@@ -4068,8 +4418,8 @@ async function dbPush(tables, options2 = {}) {
|
|
|
4068
4418
|
log2("Nothing to push: those tables are empty locally.");
|
|
4069
4419
|
return { tables: counts, ms: Date.now() - started };
|
|
4070
4420
|
}
|
|
4071
|
-
const file =
|
|
4072
|
-
|
|
4421
|
+
const file = join23(dir, "push.sql");
|
|
4422
|
+
writeFileSync16(file, statements.join("\n"));
|
|
4073
4423
|
const target = ["--remote", ...options2.env ? ["--env", options2.env] : []];
|
|
4074
4424
|
const result = await runWrangler(wrangler, ["d1", "execute", db.binding, ...target, "--yes", "--file", file], appRoot, { capture: true });
|
|
4075
4425
|
if (result.code !== 0) throw new Error(`Pushing to ${db.database_name} failed:
|
|
@@ -4078,19 +4428,19 @@ ${result.output.trim()}`);
|
|
|
4078
4428
|
log2(`${pc18.green("\u2714")} Pushed ${pc18.bold(formatCount(total))} rows to ${pc18.bold(db.database_name)} ${pc18.dim(`(${formatDuration(ms)})`)}`);
|
|
4079
4429
|
return { tables: counts, ms };
|
|
4080
4430
|
} finally {
|
|
4081
|
-
|
|
4431
|
+
rmSync5(dir, { recursive: true, force: true });
|
|
4082
4432
|
await local.dispose();
|
|
4083
4433
|
}
|
|
4084
4434
|
}
|
|
4085
4435
|
|
|
4086
4436
|
// src/commands/seed-resource.ts
|
|
4087
|
-
import { mkdtempSync as mkdtempSync3, rmSync as
|
|
4437
|
+
import { mkdtempSync as mkdtempSync3, rmSync as rmSync6, writeFileSync as writeFileSync17 } from "fs";
|
|
4088
4438
|
import { tmpdir as tmpdir3 } from "os";
|
|
4089
|
-
import { join as
|
|
4439
|
+
import { join as join24 } from "path";
|
|
4090
4440
|
import pc19 from "picocolors";
|
|
4091
4441
|
|
|
4092
4442
|
// src/seed/rows.ts
|
|
4093
|
-
import { columnName as
|
|
4443
|
+
import { columnName as columnName3, createFake as createFake2, storedFields as storedFields4 } from "@flaredev/core";
|
|
4094
4444
|
var has = (key, ...words) => words.some((word) => key.includes(word));
|
|
4095
4445
|
function stringValue(key, def, fake, resource, index) {
|
|
4096
4446
|
switch (def.format) {
|
|
@@ -4195,12 +4545,12 @@ function digitalish(resource) {
|
|
|
4195
4545
|
);
|
|
4196
4546
|
}
|
|
4197
4547
|
function seedColumns(resource) {
|
|
4198
|
-
return ["id", ...
|
|
4548
|
+
return ["id", ...storedFields4(resource).map(([key]) => columnName3(key)), "created_at", "updated_at"];
|
|
4199
4549
|
}
|
|
4200
4550
|
function* seedRows(resource, count, options2 = {}) {
|
|
4201
4551
|
const fake = createFake2(options2.seed ?? Math.floor(Math.random() * 2 ** 31));
|
|
4202
4552
|
const parentIds = options2.parentIds ?? /* @__PURE__ */ new Map();
|
|
4203
|
-
const fields =
|
|
4553
|
+
const fields = storedFields4(resource);
|
|
4204
4554
|
const now = options2.now ?? Date.now();
|
|
4205
4555
|
const start = options2.startIndex ?? 0;
|
|
4206
4556
|
for (let i = 0; i < count; i++) {
|
|
@@ -4209,7 +4559,7 @@ function* seedRows(resource, count, options2 = {}) {
|
|
|
4209
4559
|
for (const [key, def] of fields) {
|
|
4210
4560
|
let value = fieldValue(key, def, fake, resource, parentIds, number);
|
|
4211
4561
|
if (typeof value === "string" && "unique" in def && def.unique) value = uniquify(value, number + 1);
|
|
4212
|
-
row[
|
|
4562
|
+
row[columnName3(key)] = value;
|
|
4213
4563
|
}
|
|
4214
4564
|
const created = now - fake.int(0, 365) * 864e5 - fake.int(0, 86399999);
|
|
4215
4565
|
row.created_at = created;
|
|
@@ -4288,6 +4638,11 @@ async function seedResourceCommand(name, options2 = {}) {
|
|
|
4288
4638
|
async function seedResource(name, options2 = {}) {
|
|
4289
4639
|
const log2 = options2.log ?? ((message) => console.log(message));
|
|
4290
4640
|
const appRoot = findAppRoot(options2.cwd ?? process.cwd());
|
|
4641
|
+
if (readStack(appRoot) === "next") {
|
|
4642
|
+
throw new Error(
|
|
4643
|
+
"flare seed:resource is Cloudflare-only so far. On this stack, write a seed with `flare seed:make <name> --resource " + name + "` \u2014 it comes with rows already filled in \u2014 and run it with `flare seed`."
|
|
4644
|
+
);
|
|
4645
|
+
}
|
|
4291
4646
|
const resources = (await loadResources(appRoot)).map(({ resource: resource2 }) => resource2);
|
|
4292
4647
|
const resource = pickResource(resources, name);
|
|
4293
4648
|
const total = parseCount(options2.count);
|
|
@@ -4320,8 +4675,8 @@ ${result.output.trim()}`);
|
|
|
4320
4675
|
const [counted] = await query(`SELECT count(*) AS total FROM "${resource.table}"`);
|
|
4321
4676
|
startIndex = counted?.total ?? 0;
|
|
4322
4677
|
}
|
|
4323
|
-
const dir = mkdtempSync3(
|
|
4324
|
-
const file =
|
|
4678
|
+
const dir = mkdtempSync3(join24(tmpdir3(), "flare-seed-"));
|
|
4679
|
+
const file = join24(dir, `${resource.table}.sql`);
|
|
4325
4680
|
try {
|
|
4326
4681
|
const statements = [];
|
|
4327
4682
|
if (options2.truncate) statements.push(`DELETE FROM "${resource.table}";`);
|
|
@@ -4331,13 +4686,13 @@ ${result.output.trim()}`);
|
|
|
4331
4686
|
written += statement.rows;
|
|
4332
4687
|
options2.onProgress?.(written, total);
|
|
4333
4688
|
}
|
|
4334
|
-
|
|
4689
|
+
writeFileSync17(file, statements.join("\n"));
|
|
4335
4690
|
log2(`${pc19.cyan("\u25CF")} Uploading ${total.toLocaleString()} rows to ${pc19.bold(db.database_name)}`);
|
|
4336
4691
|
const result = await runWrangler(wrangler, ["d1", "execute", db.binding, ...target, "--yes", "--file", file], appRoot, { capture: true });
|
|
4337
4692
|
if (result.code !== 0) throw new Error(`Seeding the remote database failed:
|
|
4338
4693
|
${result.output.trim()}`);
|
|
4339
4694
|
} finally {
|
|
4340
|
-
|
|
4695
|
+
rmSync6(dir, { recursive: true, force: true });
|
|
4341
4696
|
}
|
|
4342
4697
|
return { resource, rows: total, ms: Date.now() - started, where };
|
|
4343
4698
|
}
|
|
@@ -4364,17 +4719,17 @@ ${result.output.trim()}`);
|
|
|
4364
4719
|
|
|
4365
4720
|
// src/commands/billing-sync.ts
|
|
4366
4721
|
import { createRequire as createRequire2 } from "module";
|
|
4367
|
-
import { existsSync as
|
|
4722
|
+
import { existsSync as existsSync23, mkdtempSync as mkdtempSync4, readFileSync as readFileSync14, rmSync as rmSync7, writeFileSync as writeFileSync18 } from "fs";
|
|
4368
4723
|
import { tmpdir as tmpdir4 } from "os";
|
|
4369
|
-
import { join as
|
|
4724
|
+
import { join as join25 } from "path";
|
|
4370
4725
|
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
4371
4726
|
import { kebabCase as kebabCase5 } from "@flaredev/core";
|
|
4372
4727
|
import pc20 from "picocolors";
|
|
4373
4728
|
function stripeKeys(appRoot) {
|
|
4374
4729
|
const map = {};
|
|
4375
|
-
const path =
|
|
4376
|
-
if (
|
|
4377
|
-
for (const line of
|
|
4730
|
+
const path = join25(appRoot, ".dev.vars");
|
|
4731
|
+
if (existsSync23(path)) {
|
|
4732
|
+
for (const line of readFileSync14(path, "utf8").split(/\r?\n/)) {
|
|
4378
4733
|
const match = /^\s*([A-Z][A-Z0-9_]*)=(.*)$/.exec(line);
|
|
4379
4734
|
if (match) map[match[1]] = match[2].trim();
|
|
4380
4735
|
}
|
|
@@ -4457,10 +4812,10 @@ async function syncPlans(options2 = {}) {
|
|
|
4457
4812
|
if (!key) throw new Error("STRIPE_SECRET_KEY is not set in .dev.vars or the environment.");
|
|
4458
4813
|
const db = readD1Databases(appRoot)[0];
|
|
4459
4814
|
if (!db) throw new Error("No d1_databases in wrangler.jsonc.");
|
|
4460
|
-
const appRequire = createRequire2(
|
|
4815
|
+
const appRequire = createRequire2(join25(appRoot, "package.json"));
|
|
4461
4816
|
const stripeModule = await import(pathToFileURL2(appRequire.resolve("stripe")).href);
|
|
4462
4817
|
const stripe = new stripeModule.default(key, { apiVersion: "2026-08-26.dahlia" });
|
|
4463
|
-
const appName = JSON.parse(
|
|
4818
|
+
const appName = JSON.parse(readFileSync14(join25(appRoot, "package.json"), "utf8")).name ?? "app";
|
|
4464
4819
|
const everything = await stripe.products.list({ active: true, limit: 100 }).autoPagingToArray({ limit: 1e4 });
|
|
4465
4820
|
const products = options2.all ? everything : everything.filter((product) => product.metadata?.flare_app === appName);
|
|
4466
4821
|
if (products.length === 0) {
|
|
@@ -4481,15 +4836,15 @@ async function syncPlans(options2 = {}) {
|
|
|
4481
4836
|
${existingResult.output.trim()}`);
|
|
4482
4837
|
}
|
|
4483
4838
|
const known = new Set(extractJson(existingResult.output)[0]?.results.map((row) => row.stripe_price_id));
|
|
4484
|
-
const dir = mkdtempSync4(
|
|
4839
|
+
const dir = mkdtempSync4(join25(tmpdir4(), "flare-sync-"));
|
|
4485
4840
|
try {
|
|
4486
|
-
const file =
|
|
4487
|
-
|
|
4841
|
+
const file = join25(dir, "sync-plans.sql");
|
|
4842
|
+
writeFileSync18(file, renderSyncSql(rows));
|
|
4488
4843
|
const result = await runWrangler(wrangler, ["d1", "execute", db.binding, ...target, "--file", file], appRoot, { capture: true });
|
|
4489
4844
|
if (result.code !== 0) throw new Error(`Could not write plans:
|
|
4490
4845
|
${result.output.trim()}`);
|
|
4491
4846
|
} finally {
|
|
4492
|
-
|
|
4847
|
+
rmSync7(dir, { recursive: true, force: true });
|
|
4493
4848
|
}
|
|
4494
4849
|
let created = 0;
|
|
4495
4850
|
for (const row of rows) {
|
|
@@ -4507,16 +4862,16 @@ ${rows.length} plan(s) synced to the ${options2.remote ? "remote" : "local"} dat
|
|
|
4507
4862
|
}
|
|
4508
4863
|
|
|
4509
4864
|
// src/commands/sync.ts
|
|
4510
|
-
import { existsSync as
|
|
4511
|
-
import { join as
|
|
4865
|
+
import { existsSync as existsSync24 } from "fs";
|
|
4866
|
+
import { join as join26 } from "path";
|
|
4512
4867
|
import pc21 from "picocolors";
|
|
4513
4868
|
async function syncTypes(options2 = {}) {
|
|
4514
4869
|
const log2 = options2.log ?? ((message) => console.log(message));
|
|
4515
4870
|
const appRoot = findAppRoot(options2.cwd ?? process.cwd());
|
|
4516
4871
|
const all = await loadResources(appRoot);
|
|
4517
4872
|
const policies = await loadPolicies(appRoot);
|
|
4518
|
-
const tracksPolicies = policies.length > 0 ||
|
|
4519
|
-
const plan = planFiles(all, tracksPolicies ? policies : void 0);
|
|
4873
|
+
const tracksPolicies = policies.length > 0 || existsSync24(join26(appRoot, "policies/index.ts"));
|
|
4874
|
+
const plan = planFiles(all, tracksPolicies ? policies : void 0, readStack(appRoot));
|
|
4520
4875
|
if (!options2.check) ensureSupportFiles(appRoot, log2);
|
|
4521
4876
|
const results = applyPlan(appRoot, plan, { force: options2.force, dryRun: options2.check });
|
|
4522
4877
|
logResults(results, log2);
|
|
@@ -4600,22 +4955,22 @@ ${result.output.trim()}`);
|
|
|
4600
4955
|
|
|
4601
4956
|
// src/utils/install.ts
|
|
4602
4957
|
import { spawn as spawn5 } from "child_process";
|
|
4603
|
-
import { existsSync as
|
|
4604
|
-
import { join as
|
|
4958
|
+
import { existsSync as existsSync25, readFileSync as readFileSync15, readdirSync as readdirSync9 } from "fs";
|
|
4959
|
+
import { join as join27 } from "path";
|
|
4605
4960
|
function expectedPackages(dir) {
|
|
4606
|
-
const npmLock =
|
|
4607
|
-
if (
|
|
4961
|
+
const npmLock = join27(dir, "package-lock.json");
|
|
4962
|
+
if (existsSync25(npmLock)) {
|
|
4608
4963
|
try {
|
|
4609
|
-
const lock = JSON.parse(
|
|
4964
|
+
const lock = JSON.parse(readFileSync15(npmLock, "utf8"));
|
|
4610
4965
|
return Math.max(0, Object.keys(lock.packages ?? {}).length - 1);
|
|
4611
4966
|
} catch {
|
|
4612
4967
|
return 0;
|
|
4613
4968
|
}
|
|
4614
4969
|
}
|
|
4615
|
-
const pnpmLock =
|
|
4616
|
-
if (
|
|
4970
|
+
const pnpmLock = join27(dir, "pnpm-lock.yaml");
|
|
4971
|
+
if (existsSync25(pnpmLock)) {
|
|
4617
4972
|
try {
|
|
4618
|
-
const text =
|
|
4973
|
+
const text = readFileSync15(pnpmLock, "utf8");
|
|
4619
4974
|
const packages = text.slice(text.indexOf("\npackages:"));
|
|
4620
4975
|
return (packages.match(/\n {2}[^\s:]+:/g) ?? []).length;
|
|
4621
4976
|
} catch {
|
|
@@ -4625,15 +4980,15 @@ function expectedPackages(dir) {
|
|
|
4625
4980
|
return 0;
|
|
4626
4981
|
}
|
|
4627
4982
|
function installedPackages(dir) {
|
|
4628
|
-
const root =
|
|
4629
|
-
if (!
|
|
4983
|
+
const root = join27(dir, "node_modules");
|
|
4984
|
+
if (!existsSync25(root)) return 0;
|
|
4630
4985
|
let total = 0;
|
|
4631
4986
|
try {
|
|
4632
4987
|
for (const entry of readdirSync9(root, { withFileTypes: true })) {
|
|
4633
4988
|
if (!entry.isDirectory() || entry.name === ".bin") continue;
|
|
4634
4989
|
if (entry.name.startsWith("@")) {
|
|
4635
4990
|
try {
|
|
4636
|
-
total += readdirSync9(
|
|
4991
|
+
total += readdirSync9(join27(root, entry.name)).length;
|
|
4637
4992
|
} catch {
|
|
4638
4993
|
}
|
|
4639
4994
|
} else if (entry.name !== ".pnpm" && entry.name !== ".vite") {
|
|
@@ -4677,31 +5032,33 @@ function install(packageManager, dir, onProgress) {
|
|
|
4677
5032
|
// src/cli.ts
|
|
4678
5033
|
function createCli() {
|
|
4679
5034
|
const cli = cac("flare");
|
|
4680
|
-
cli.command("create <dir>", "Scaffold a new Flare app").option("--pm <manager>", "Package manager: pnpm, npm, yarn, or bun (default: detected)").option("--theme <name>", "Look of the app: default, coral, amber, sky, mono or emerald").option("--auth <list>", "Sign-in methods: magic-link, email-otp, passkeys, 2fa-app, 2fa-email, all or none (default: all)").option("--auth-providers <list>", "Social sign-in, comma-separated: google, github, apple, microsoft").option("-y, --yes", "Don't ask: use the flags given and the defaults for the rest").option("--skip-install", "Write files without installing dependencies").example("flare create shop").example("flare create shop --theme mono --auth passkeys,2fa-app --auth-providers google,github --yes").action(
|
|
4681
|
-
|
|
4682
|
-
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
|
|
4686
|
-
|
|
4687
|
-
|
|
4688
|
-
|
|
4689
|
-
|
|
4690
|
-
|
|
4691
|
-
|
|
4692
|
-
|
|
4693
|
-
|
|
4694
|
-
|
|
4695
|
-
|
|
4696
|
-
|
|
4697
|
-
|
|
4698
|
-
|
|
4699
|
-
|
|
4700
|
-
|
|
5035
|
+
cli.command("create <dir>", "Scaffold a new Flare app").option("--pm <manager>", "Package manager: pnpm, npm, yarn, or bun (default: detected)").option("--stack <name>", "Where it runs: cloudflare (default) or next").option("--theme <name>", "Look of the app: default, coral, amber, sky, mono or emerald").option("--auth <list>", "Sign-in methods: magic-link, email-otp, passkeys, 2fa-app, 2fa-email, all or none (default: all)").option("--auth-providers <list>", "Social sign-in, comma-separated: google, github, apple, microsoft").option("-y, --yes", "Don't ask: use the flags given and the defaults for the rest").option("--skip-install", "Write files without installing dependencies").example("flare create shop").example("flare create shop --stack next").example("flare create shop --theme mono --auth passkeys,2fa-app --auth-providers google,github --yes").action(
|
|
5036
|
+
async (dir, options2) => {
|
|
5037
|
+
const wanted = !options2.skipInstall;
|
|
5038
|
+
let answers = { stack: options2.stack, theme: options2.theme, auth: options2.auth, authProviders: options2.authProviders };
|
|
5039
|
+
if (canPrompt(options2.yes)) answers = await askCreateQuestions(answers, toAppName(dir));
|
|
5040
|
+
const result = createApp(dir, { pm: options2.pm, install: false, ...answers, log: (message) => prompts3.log.message(message) });
|
|
5041
|
+
let installed = false;
|
|
5042
|
+
if (wanted) {
|
|
5043
|
+
const spin = prompts3.spinner({ indicator: "timer" });
|
|
5044
|
+
spin.start(`Installing dependencies with ${result.packageManager}`);
|
|
5045
|
+
const outcome = await install(result.packageManager, result.dir, ({ installed: done, expected, percent }) => {
|
|
5046
|
+
spin.message(
|
|
5047
|
+
percent === void 0 ? `Installing dependencies with ${result.packageManager} \xB7 ${formatCount(done)} packages` : `Installing dependencies with ${result.packageManager} \xB7 ${percent}% (${formatCount(done)} of ${formatCount(expected)})`
|
|
5048
|
+
);
|
|
5049
|
+
});
|
|
5050
|
+
installed = outcome.code === 0;
|
|
5051
|
+
if (installed) {
|
|
5052
|
+
spin.stop(`Installed ${formatCount(expectedPackages(result.dir) || 0)} packages with ${result.packageManager} ${pc23.dim(`(${formatDuration(outcome.ms)})`)}`);
|
|
5053
|
+
} else {
|
|
5054
|
+
spin.error(`${result.packageManager} install failed`);
|
|
5055
|
+
process.stderr.write(outcome.output);
|
|
5056
|
+
process.exitCode = 1;
|
|
5057
|
+
}
|
|
4701
5058
|
}
|
|
5059
|
+
printNextSteps(result, installed);
|
|
4702
5060
|
}
|
|
4703
|
-
|
|
4704
|
-
});
|
|
5061
|
+
);
|
|
4705
5062
|
cli.command("gen <generator> [name] [second]", "Generate code. Generators: resource, endpoint, migration, policy, billing, security").option("--fields <fields>", "resource: fields, e.g. 'name:string, email:string!, status:enum(lead,customer)' (single quotes: bash treats ! in double quotes as history)").option("--group <name>", "resource: sidebar heading to file it under, e.g. Sales").option("--icon <name>", "resource: lucide icon for the sidebar, e.g. users").option("--method <verb>", "endpoint: GET (default), POST, PATCH, PUT or DELETE").option("--action <action>", "endpoint: policy action to require (read, create, update, delete)").option("--record", "endpoint: put it under one record, /api/<resource>/[id]/<name>").option("--force", "resource/policy/billing/security: overwrite hand-edited generated blocks").option("--from-schema", "migration: diff the current tables instead of a blank migration").option("--roles <roles>", "policy: roles allowed to read, create and update, e.g. admin,staff").option("--delete-roles <roles>", "policy: roles allowed to delete (default: the first --roles entry)").option("--provider <provider>", "billing: payment provider (default: stripe)").option("--mode <mode>", "billing: subscriptions (the default; includes one-time checkout)").option("--skip-install", "billing: write files without installing the stripe dependency").option("--skip-migration", "billing/security: skip generating the schema migration").example("flare gen resource Contact --fields 'name:string, email:string!, company:belongsTo(Company)?'").example("flare gen endpoint Order publish --method POST --record").example("flare gen migration backfill_contact_status").example("flare gen migration add_phone_to_contacts --from-schema").example("flare gen policy Invoice --roles admin,staff --delete-roles admin").example("flare gen billing --provider stripe --mode subscriptions").example("flare gen security").action(
|
|
4706
5063
|
async (generator, name, second, options2) => {
|
|
4707
5064
|
if (generator === "billing")
|