@flaredev/cli 0.4.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +685 -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,90 @@ 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
|
+
function readStack(appRoot) {
|
|
228
|
+
const file = join(appRoot, "package.json");
|
|
229
|
+
if (!existsSync(file)) return "cloudflare";
|
|
230
|
+
try {
|
|
231
|
+
const parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
232
|
+
return isStack(parsed.flare?.stack) ? parsed.flare.stack : "cloudflare";
|
|
233
|
+
} catch {
|
|
234
|
+
return "cloudflare";
|
|
235
|
+
}
|
|
236
|
+
}
|
|
189
237
|
|
|
190
238
|
// src/utils/fs.ts
|
|
191
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "fs";
|
|
192
|
-
import { dirname, join, resolve } from "path";
|
|
239
|
+
import { existsSync as existsSync2, mkdirSync, readdirSync, readFileSync as readFileSync2, statSync, writeFileSync as writeFileSync2 } from "fs";
|
|
240
|
+
import { dirname, join as join2, resolve } from "path";
|
|
193
241
|
import { fileURLToPath } from "url";
|
|
194
242
|
var RENAMES = { _gitignore: ".gitignore" };
|
|
195
243
|
function copyTemplate(from, to, tokens) {
|
|
196
244
|
mkdirSync(to, { recursive: true });
|
|
197
245
|
let files = 0;
|
|
198
246
|
for (const entry of readdirSync(from)) {
|
|
199
|
-
const src =
|
|
200
|
-
const dest =
|
|
247
|
+
const src = join2(from, entry);
|
|
248
|
+
const dest = join2(to, RENAMES[entry] ?? entry);
|
|
201
249
|
if (statSync(src).isDirectory()) {
|
|
202
250
|
files += copyTemplate(src, dest, tokens);
|
|
203
251
|
continue;
|
|
204
252
|
}
|
|
205
|
-
const content =
|
|
206
|
-
|
|
253
|
+
const content = readFileSync2(src, "utf8").replace(/__([A-Z_]+)__/g, (match, key) => tokens[key] ?? match);
|
|
254
|
+
writeFileSync2(dest, content);
|
|
207
255
|
files++;
|
|
208
256
|
}
|
|
209
257
|
return files;
|
|
210
258
|
}
|
|
211
259
|
function writeJson(path, value) {
|
|
212
260
|
mkdirSync(dirname(path), { recursive: true });
|
|
213
|
-
|
|
261
|
+
writeFileSync2(path, JSON.stringify(value, null, 2) + "\n");
|
|
214
262
|
}
|
|
215
263
|
function findUp(file, start) {
|
|
216
264
|
let dir = resolve(start);
|
|
217
265
|
while (true) {
|
|
218
|
-
if (
|
|
266
|
+
if (existsSync2(join2(dir, file))) return dir;
|
|
219
267
|
const parent = dirname(dir);
|
|
220
268
|
if (parent === dir) return void 0;
|
|
221
269
|
dir = parent;
|
|
222
270
|
}
|
|
223
271
|
}
|
|
224
|
-
var templatesDir =
|
|
272
|
+
var templatesDir = join2(findUp("templates", dirname(fileURLToPath(import.meta.url))), "templates");
|
|
225
273
|
|
|
226
274
|
// src/utils/pm.ts
|
|
227
275
|
import { spawnSync } from "child_process";
|
|
@@ -260,7 +308,7 @@ function toAppName(dir) {
|
|
|
260
308
|
function flarePackageSpec(pkg, appDir, packageManager) {
|
|
261
309
|
const repoPackage = resolve2(templatesDir, "../..", pkg);
|
|
262
310
|
const repoRoot = resolve2(repoPackage, "../..");
|
|
263
|
-
const fromCheckout =
|
|
311
|
+
const fromCheckout = existsSync3(join3(repoPackage, "package.json")) && existsSync3(join3(repoRoot, "pnpm-workspace.yaml")) && !repoPackage.split(/[\\/]/).includes("node_modules");
|
|
264
312
|
if (!fromCheckout) return `^${FLARE_VERSION}`;
|
|
265
313
|
const rel = relative(repoRoot, appDir);
|
|
266
314
|
if (rel && !rel.startsWith("..") && !isAbsolute(rel)) return "workspace:*";
|
|
@@ -270,32 +318,54 @@ function flarePackageSpec(pkg, appDir, packageManager) {
|
|
|
270
318
|
function createApp(target, options2 = {}) {
|
|
271
319
|
const dir = resolve2(target);
|
|
272
320
|
const name = toAppName(dir);
|
|
273
|
-
if (
|
|
321
|
+
if (existsSync3(dir) && readdirSync2(dir).length > 0) {
|
|
274
322
|
throw new Error(`Directory ${dir} already exists and is not empty.`);
|
|
275
323
|
}
|
|
276
324
|
if (options2.pm !== void 0 && !isPackageManager(options2.pm)) {
|
|
277
325
|
throw new Error(`Unknown package manager "${options2.pm}". Use pnpm, npm, yarn, or bun.`);
|
|
278
326
|
}
|
|
279
327
|
const packageManager = options2.pm ?? detectPackageManager();
|
|
328
|
+
if (options2.stack !== void 0 && !isStack(options2.stack)) {
|
|
329
|
+
throw new Error(`Unknown stack "${options2.stack}". Use ${STACKS.join(" or ")}.`);
|
|
330
|
+
}
|
|
331
|
+
const stack = options2.stack ?? "cloudflare";
|
|
280
332
|
const authProviders = parseAuthProviders(options2.authProviders);
|
|
281
333
|
const authMethods = parseAuthMethods(options2.auth);
|
|
282
334
|
const theme = parseTheme(options2.theme);
|
|
283
335
|
const inWorkspace = findUp("pnpm-workspace.yaml", dirname2(dir)) !== void 0;
|
|
284
336
|
const compatibilityDate = options2.compatibilityDate ?? COMPATIBILITY_DATE;
|
|
285
|
-
const files = copyTemplate(
|
|
337
|
+
const files = copyTemplate(join3(templatesDir, "app"), dir, {
|
|
286
338
|
APP_NAME: name,
|
|
287
339
|
COMPAT_DATE: compatibilityDate,
|
|
288
340
|
PM: packageManager,
|
|
289
341
|
THEME: theme
|
|
290
342
|
});
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
343
|
+
if (stack === "next") {
|
|
344
|
+
copyTemplate(join3(templatesDir, "next"), dir, { APP_NAME: name, PM: packageManager, THEME: theme });
|
|
345
|
+
for (const path of CLOUDFLARE_ONLY) rmSync(join3(dir, path), { recursive: true, force: true });
|
|
346
|
+
}
|
|
347
|
+
writeFileSync3(join3(dir, "lib", "auth-config.ts"), renderAuthConfig(authMethods, authProviders));
|
|
348
|
+
appendFileSync(join3(dir, stack === "next" ? ".env.example" : ".dev.vars.example"), devVarsExampleEntries(authProviders));
|
|
349
|
+
const shared = { react: APP_DEPENDENCIES.react, "react-dom": APP_DEPENDENCIES["react-dom"] };
|
|
350
|
+
const uiOnly = Object.fromEntries(
|
|
351
|
+
Object.entries(APP_DEPENDENCIES).filter(([key]) => !["vinext", "@vinext/cloudflare", "drizzle-orm", "react-server-dom-webpack"].includes(key))
|
|
352
|
+
);
|
|
353
|
+
writeJson(join3(dir, "package.json"), {
|
|
294
354
|
name,
|
|
295
355
|
version: "0.1.0",
|
|
296
356
|
private: true,
|
|
297
357
|
type: "module",
|
|
298
|
-
|
|
358
|
+
// Which stack this app targets. `flare gen` reads it; don't change it by hand.
|
|
359
|
+
flare: { stack },
|
|
360
|
+
scripts: stack === "next" ? {
|
|
361
|
+
dev: "next dev",
|
|
362
|
+
build: "prisma generate && next build",
|
|
363
|
+
start: "next start",
|
|
364
|
+
deploy: "vercel deploy --prod",
|
|
365
|
+
"db:generate": "prisma generate",
|
|
366
|
+
"db:migrate": "prisma migrate dev",
|
|
367
|
+
"db:studio": "prisma studio"
|
|
368
|
+
} : {
|
|
299
369
|
dev: "flare dev",
|
|
300
370
|
build: "flare build",
|
|
301
371
|
start: "flare start",
|
|
@@ -304,18 +374,25 @@ function createApp(target, options2 = {}) {
|
|
|
304
374
|
"db:generate": "drizzle-kit generate",
|
|
305
375
|
"db:migrate:local": "wrangler d1 migrations apply DB --local"
|
|
306
376
|
},
|
|
307
|
-
dependencies: {
|
|
308
|
-
|
|
377
|
+
dependencies: {
|
|
378
|
+
"@flaredev/core": flarePackageSpec("core", dir, packageManager),
|
|
379
|
+
...stack === "next" ? { ...uiOnly, ...shared, ...NEXT_DEPENDENCIES } : APP_DEPENDENCIES
|
|
380
|
+
},
|
|
381
|
+
devDependencies: {
|
|
382
|
+
"@flaredev/cli": flarePackageSpec("cli", dir, packageManager),
|
|
383
|
+
...stack === "next" ? NEXT_DEV_DEPENDENCIES : APP_DEV_DEPENDENCIES
|
|
384
|
+
}
|
|
309
385
|
});
|
|
310
|
-
|
|
311
|
-
|
|
386
|
+
writeFileSync3(
|
|
387
|
+
join3(dir, stack === "next" ? ".env" : ".dev.vars"),
|
|
312
388
|
`BETTER_AUTH_SECRET=${randomBytes(32).toString("base64")}
|
|
313
389
|
RESEND_API_KEY=
|
|
314
390
|
MAIL_FROM=
|
|
315
391
|
${devVarsEntries(authProviders)}`
|
|
316
392
|
);
|
|
317
393
|
if (packageManager === "pnpm" && !inWorkspace) {
|
|
318
|
-
|
|
394
|
+
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";
|
|
395
|
+
writeFileSync3(join3(dir, "pnpm-workspace.yaml"), allowBuilds);
|
|
319
396
|
}
|
|
320
397
|
const pinned = flarePackageSpec("core", dir, packageManager).startsWith("^");
|
|
321
398
|
if (pinned) copyLockfile(packageManager, dir);
|
|
@@ -326,9 +403,9 @@ ${devVarsEntries(authProviders)}`
|
|
|
326
403
|
function copyLockfile(packageManager, dir) {
|
|
327
404
|
const file = packageManager === "pnpm" ? "pnpm-lock.yaml" : packageManager === "yarn" ? "yarn.lock" : "package-lock.json";
|
|
328
405
|
if (!file) return false;
|
|
329
|
-
const source =
|
|
330
|
-
if (!
|
|
331
|
-
copyFileSync(source,
|
|
406
|
+
const source = join3(templatesDir, "locks", file);
|
|
407
|
+
if (!existsSync3(source)) return false;
|
|
408
|
+
copyFileSync(source, join3(dir, file));
|
|
332
409
|
return true;
|
|
333
410
|
}
|
|
334
411
|
function printNextSteps(result, installed) {
|
|
@@ -348,8 +425,8 @@ function printNextSteps(result, installed) {
|
|
|
348
425
|
}
|
|
349
426
|
|
|
350
427
|
// src/commands/gen-billing.ts
|
|
351
|
-
import { appendFileSync as appendFileSync2, existsSync as
|
|
352
|
-
import { dirname as dirname6, join as
|
|
428
|
+
import { appendFileSync as appendFileSync2, existsSync as existsSync13, mkdirSync as mkdirSync5, readFileSync as readFileSync9, unlinkSync, writeFileSync as writeFileSync7 } from "fs";
|
|
429
|
+
import { dirname as dirname6, join as join12 } from "path";
|
|
353
430
|
import pc6 from "picocolors";
|
|
354
431
|
|
|
355
432
|
// src/generator/grammar.ts
|
|
@@ -513,7 +590,7 @@ import { defineResource, field as builders, kebabCase, pascalCase } from "@flare
|
|
|
513
590
|
|
|
514
591
|
// src/generator/markers.ts
|
|
515
592
|
import { createHash } from "crypto";
|
|
516
|
-
import { existsSync as
|
|
593
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
517
594
|
import { dirname as dirname3 } from "path";
|
|
518
595
|
var START = "// generated:start";
|
|
519
596
|
var END = "// generated:end";
|
|
@@ -563,8 +640,8 @@ ${indentBlock(body, indent)}${indent}${END}
|
|
|
563
640
|
${after}`;
|
|
564
641
|
}
|
|
565
642
|
function inspectGenerated(path, content) {
|
|
566
|
-
if (!
|
|
567
|
-
const parts = splitMarkers(
|
|
643
|
+
if (!existsSync4(path)) return "missing";
|
|
644
|
+
const parts = splitMarkers(readFileSync3(path, "utf8"));
|
|
568
645
|
if (!parts) return "unmarked";
|
|
569
646
|
if (parts.hash && hashBlock(parts.block) !== parts.hash) return "drift";
|
|
570
647
|
if (normalize(parts.block) !== normalize(content)) return "outdated";
|
|
@@ -580,18 +657,18 @@ var DriftError = class extends Error {
|
|
|
580
657
|
};
|
|
581
658
|
function writeGenerated(path, content, options2 = {}) {
|
|
582
659
|
const hash = hashBlock(content);
|
|
583
|
-
if (!
|
|
660
|
+
if (!existsSync4(path)) {
|
|
584
661
|
mkdirSync2(dirname3(path), { recursive: true });
|
|
585
|
-
|
|
662
|
+
writeFileSync4(path, joinMarkers({ before: options2.header ?? "", block: content, after: "", indent: "", hash }));
|
|
586
663
|
return "create";
|
|
587
664
|
}
|
|
588
|
-
const source =
|
|
665
|
+
const source = readFileSync3(path, "utf8");
|
|
589
666
|
const parts = splitMarkers(source);
|
|
590
667
|
if (!parts) throw new Error(`${path} exists but has no generated block; refusing to overwrite hand-written code.`);
|
|
591
668
|
if (parts.hash && hashBlock(parts.block) !== parts.hash && !options2.force) throw new DriftError(path);
|
|
592
669
|
const next = joinMarkers({ ...parts, block: content, hash });
|
|
593
670
|
if (next === source) return "identical";
|
|
594
|
-
|
|
671
|
+
writeFileSync4(path, next);
|
|
595
672
|
return "update";
|
|
596
673
|
}
|
|
597
674
|
|
|
@@ -1346,17 +1423,17 @@ var billingDevVarsExampleEntries = () => [
|
|
|
1346
1423
|
].join("\n");
|
|
1347
1424
|
|
|
1348
1425
|
// src/generator/load.ts
|
|
1349
|
-
import { existsSync as
|
|
1350
|
-
import { join as
|
|
1426
|
+
import { existsSync as existsSync5, readdirSync as readdirSync3 } from "fs";
|
|
1427
|
+
import { join as join4, resolve as resolve3 } from "path";
|
|
1351
1428
|
import { createJiti } from "jiti";
|
|
1352
1429
|
async function loadResources(root) {
|
|
1353
1430
|
const appRoot = resolve3(root);
|
|
1354
|
-
const dir =
|
|
1355
|
-
if (!
|
|
1356
|
-
const jiti = createJiti(
|
|
1431
|
+
const dir = join4(appRoot, "resources");
|
|
1432
|
+
if (!existsSync5(dir)) return [];
|
|
1433
|
+
const jiti = createJiti(join4(appRoot, "package.json"), { alias: { "@": appRoot }, moduleCache: false, fsCache: false });
|
|
1357
1434
|
const loaded = [];
|
|
1358
1435
|
for (const file of readdirSync3(dir).filter((name) => name.endsWith(".resource.ts")).sort()) {
|
|
1359
|
-
const path =
|
|
1436
|
+
const path = join4(dir, file);
|
|
1360
1437
|
const mod = await jiti.import(path);
|
|
1361
1438
|
const resource = mod.default;
|
|
1362
1439
|
if (!resource || typeof resource !== "object" || !("fields" in resource) || !("table" in resource)) {
|
|
@@ -1374,20 +1451,20 @@ async function loadResources(root) {
|
|
|
1374
1451
|
|
|
1375
1452
|
// src/generator/migrations.ts
|
|
1376
1453
|
import { spawn as spawn4 } from "child_process";
|
|
1377
|
-
import { existsSync as
|
|
1378
|
-
import { join as
|
|
1454
|
+
import { existsSync as existsSync10, readdirSync as readdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "fs";
|
|
1455
|
+
import { join as join9 } from "path";
|
|
1379
1456
|
|
|
1380
1457
|
// src/commands/run.ts
|
|
1381
1458
|
import { spawn as spawn3 } from "child_process";
|
|
1382
|
-
import { existsSync as
|
|
1383
|
-
import { dirname as dirname4, join as
|
|
1459
|
+
import { existsSync as existsSync9, readFileSync as readFileSync6 } from "fs";
|
|
1460
|
+
import { dirname as dirname4, join as join8, resolve as resolve4 } from "path";
|
|
1384
1461
|
import pc4 from "picocolors";
|
|
1385
1462
|
|
|
1386
1463
|
// src/tunnel.ts
|
|
1387
1464
|
import { spawn, spawnSync as spawnSync2 } from "child_process";
|
|
1388
|
-
import { chmodSync, existsSync as
|
|
1465
|
+
import { chmodSync, existsSync as existsSync6, mkdirSync as mkdirSync3, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync5 } from "fs";
|
|
1389
1466
|
import { homedir } from "os";
|
|
1390
|
-
import { join as
|
|
1467
|
+
import { join as join5 } from "path";
|
|
1391
1468
|
import pc2 from "picocolors";
|
|
1392
1469
|
var TUNNEL_URL = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/;
|
|
1393
1470
|
var LOCAL_URL = /https?:\/\/(?:localhost|127\.0\.0\.1|\[::1\]|0\.0\.0\.0):\d+/;
|
|
@@ -1412,7 +1489,7 @@ function releaseAsset(platform = process.platform, arch = process.arch) {
|
|
|
1412
1489
|
throw new Error(`No cloudflared build for ${platform}/${arch}. Install it yourself: https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/`);
|
|
1413
1490
|
}
|
|
1414
1491
|
var binName = process.platform === "win32" ? "cloudflared.exe" : "cloudflared";
|
|
1415
|
-
var cacheDir = () =>
|
|
1492
|
+
var cacheDir = () => join5(homedir(), ".flare", "bin");
|
|
1416
1493
|
function onPath() {
|
|
1417
1494
|
const probe = spawnSync2(process.platform === "win32" ? "where" : "which", ["cloudflared"], { encoding: "utf8" });
|
|
1418
1495
|
return probe.status === 0 ? probe.stdout.split(/\r?\n/)[0]?.trim() ?? null : null;
|
|
@@ -1421,19 +1498,19 @@ async function download(log2) {
|
|
|
1421
1498
|
const asset = releaseAsset();
|
|
1422
1499
|
const dir = cacheDir();
|
|
1423
1500
|
mkdirSync3(dir, { recursive: true });
|
|
1424
|
-
const target =
|
|
1501
|
+
const target = join5(dir, binName);
|
|
1425
1502
|
log2(pc2.dim(`Downloading cloudflared (one time) into ${dir}\u2026`));
|
|
1426
1503
|
const response = await fetch(`https://github.com/cloudflare/cloudflared/releases/latest/download/${asset}`);
|
|
1427
1504
|
if (!response.ok) throw new Error(`Downloading cloudflared failed: HTTP ${response.status}.`);
|
|
1428
1505
|
const bytes = Buffer.from(await response.arrayBuffer());
|
|
1429
1506
|
const partial = `${target}.download`;
|
|
1430
1507
|
if (asset.endsWith(".tgz")) {
|
|
1431
|
-
|
|
1508
|
+
writeFileSync5(`${partial}.tgz`, bytes);
|
|
1432
1509
|
const untar = spawnSync2("tar", ["-xzf", `${partial}.tgz`, "-C", dir], { encoding: "utf8" });
|
|
1433
|
-
|
|
1510
|
+
rmSync2(`${partial}.tgz`, { force: true });
|
|
1434
1511
|
if (untar.status !== 0) throw new Error(`Unpacking cloudflared failed: ${untar.stderr}`);
|
|
1435
1512
|
} else {
|
|
1436
|
-
|
|
1513
|
+
writeFileSync5(partial, bytes);
|
|
1437
1514
|
renameSync(partial, target);
|
|
1438
1515
|
}
|
|
1439
1516
|
if (process.platform !== "win32") chmodSync(target, 493);
|
|
@@ -1442,8 +1519,8 @@ async function download(log2) {
|
|
|
1442
1519
|
async function ensureCloudflared(log2 = console.log) {
|
|
1443
1520
|
const found = onPath();
|
|
1444
1521
|
if (found) return found;
|
|
1445
|
-
const cached =
|
|
1446
|
-
if (
|
|
1522
|
+
const cached = join5(cacheDir(), binName);
|
|
1523
|
+
if (existsSync6(cached)) return cached;
|
|
1447
1524
|
return download(log2);
|
|
1448
1525
|
}
|
|
1449
1526
|
async function openTunnel(localUrl, options2 = {}) {
|
|
@@ -1499,16 +1576,16 @@ function tunnelBanner(publicUrl, localUrl) {
|
|
|
1499
1576
|
|
|
1500
1577
|
// src/commands/deploy.ts
|
|
1501
1578
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
1502
|
-
import { existsSync as
|
|
1503
|
-
import { join as
|
|
1579
|
+
import { existsSync as existsSync8, readFileSync as readFileSync5 } from "fs";
|
|
1580
|
+
import { join as join7 } from "path";
|
|
1504
1581
|
import { createZoneClient } from "@flaredev/core/security";
|
|
1505
1582
|
import { createJiti as createJiti2 } from "jiti";
|
|
1506
1583
|
import pc3 from "picocolors";
|
|
1507
1584
|
|
|
1508
1585
|
// src/utils/wrangler.ts
|
|
1509
1586
|
import { spawn as spawn2 } from "child_process";
|
|
1510
|
-
import { existsSync as
|
|
1511
|
-
import { join as
|
|
1587
|
+
import { existsSync as existsSync7, readFileSync as readFileSync4 } from "fs";
|
|
1588
|
+
import { join as join6 } from "path";
|
|
1512
1589
|
import { parse as parseJsonc } from "jsonc-parser";
|
|
1513
1590
|
function runWrangler(wranglerBin, args, cwd, options2 = {}) {
|
|
1514
1591
|
return new Promise((resolve6, reject) => {
|
|
@@ -1533,9 +1610,9 @@ ${output.trim()}`);
|
|
|
1533
1610
|
return JSON.parse(output.slice(start, end + 1));
|
|
1534
1611
|
}
|
|
1535
1612
|
function readD1Databases(appRoot) {
|
|
1536
|
-
const path =
|
|
1537
|
-
if (!
|
|
1538
|
-
const config = parseJsonc(
|
|
1613
|
+
const path = join6(appRoot, "wrangler.jsonc");
|
|
1614
|
+
if (!existsSync7(path)) return [];
|
|
1615
|
+
const config = parseJsonc(readFileSync4(path, "utf8"));
|
|
1539
1616
|
return (config?.d1_databases ?? []).filter((db) => db.binding && db.database_name);
|
|
1540
1617
|
}
|
|
1541
1618
|
|
|
@@ -1575,15 +1652,15 @@ function declaredSecretNames(devVarsExample) {
|
|
|
1575
1652
|
return devVarsExample.split(/\r?\n/).map((line) => /^([A-Z][A-Z0-9_]*)=/.exec(line.trim())?.[1]).filter((name) => Boolean(name));
|
|
1576
1653
|
}
|
|
1577
1654
|
async function loadSecurityConfig(appRoot) {
|
|
1578
|
-
const path =
|
|
1579
|
-
if (!
|
|
1580
|
-
const jiti = createJiti2(
|
|
1655
|
+
const path = join7(appRoot, "security.config.ts");
|
|
1656
|
+
if (!existsSync8(path)) return null;
|
|
1657
|
+
const jiti = createJiti2(join7(appRoot, "package.json"), { moduleCache: false, fsCache: false });
|
|
1581
1658
|
const mod = await jiti.import(path);
|
|
1582
1659
|
if (!mod.default) throw new Error("security.config.ts must `export default defineSecurity({ ... })`.");
|
|
1583
1660
|
return mod.default;
|
|
1584
1661
|
}
|
|
1585
1662
|
async function provisionZoneSecurity(appRoot, config, credentials, options2) {
|
|
1586
|
-
const app = JSON.parse(
|
|
1663
|
+
const app = JSON.parse(readFileSync5(join7(appRoot, "package.json"), "utf8")).name ?? "app";
|
|
1587
1664
|
const zone = createZoneClient({ ...credentials, app, fetch: options2.fetch });
|
|
1588
1665
|
if (config.zone?.rateLimits) {
|
|
1589
1666
|
const result = await zone.syncRateLimits(config.zone.rateLimits);
|
|
@@ -1603,7 +1680,7 @@ async function runDeploy(args, context3) {
|
|
|
1603
1680
|
const envArgs = plan.env ? ["--env", plan.env] : [];
|
|
1604
1681
|
const step = (message) => log2(`
|
|
1605
1682
|
${pc3.bold(pc3.cyan("flare"))} ${message}`);
|
|
1606
|
-
const databases = plan.skipMigrations ? [] : readD1Databases(appRoot).filter((db) =>
|
|
1683
|
+
const databases = plan.skipMigrations ? [] : readD1Databases(appRoot).filter((db) => existsSync8(join7(appRoot, db.migrations_dir ?? "migrations")));
|
|
1607
1684
|
const migrateAfterDeploy = [];
|
|
1608
1685
|
const migrate2 = async (db) => {
|
|
1609
1686
|
step(`Applying migrations to ${db.database_name} (remote)`);
|
|
@@ -1662,8 +1739,8 @@ ${put.output.trim()}`);
|
|
|
1662
1739
|
existing.add(name);
|
|
1663
1740
|
}
|
|
1664
1741
|
}
|
|
1665
|
-
const examplePath =
|
|
1666
|
-
const optional =
|
|
1742
|
+
const examplePath = join7(appRoot, ".dev.vars.example");
|
|
1743
|
+
const optional = existsSync8(examplePath) ? declaredSecretNames(readFileSync5(examplePath, "utf8")).filter((name) => !existing.has(name)) : [];
|
|
1667
1744
|
if (optional.length > 0) {
|
|
1668
1745
|
step("Optional secrets not set in production (features using them stay off):");
|
|
1669
1746
|
for (const name of optional) log2(` wrangler secret put ${name}${plan.env ? ` --env ${plan.env}` : ""}`);
|
|
@@ -1698,25 +1775,30 @@ function findAppRoot(cwd) {
|
|
|
1698
1775
|
while (dir) {
|
|
1699
1776
|
const root = findUp("package.json", dir);
|
|
1700
1777
|
if (!root) break;
|
|
1701
|
-
const pkg = JSON.parse(
|
|
1702
|
-
|
|
1778
|
+
const pkg = JSON.parse(readFileSync6(join8(root, "package.json"), "utf8"));
|
|
1779
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
1780
|
+
if (deps["@flaredev/core"] || deps.vinext) return root;
|
|
1703
1781
|
const parent = dirname4(root);
|
|
1704
1782
|
dir = parent === root ? void 0 : parent;
|
|
1705
1783
|
}
|
|
1706
|
-
throw new Error("Not inside a Flare app (no package.json depending on
|
|
1784
|
+
throw new Error("Not inside a Flare app (no package.json depending on @flaredev/core found). Run this from your app directory.");
|
|
1707
1785
|
}
|
|
1708
1786
|
function resolveBin(appRoot, pkgName, binName2) {
|
|
1709
|
-
const pkgDir = findUp(
|
|
1787
|
+
const pkgDir = findUp(join8("node_modules", pkgName, "package.json"), appRoot);
|
|
1710
1788
|
if (!pkgDir) {
|
|
1711
1789
|
throw new Error(`Cannot find ${pkgName} in ${appRoot}. Install dependencies first (e.g. pnpm install).`);
|
|
1712
1790
|
}
|
|
1713
|
-
const manifestPath =
|
|
1714
|
-
const { bin } = JSON.parse(
|
|
1791
|
+
const manifestPath = join8(pkgDir, "node_modules", pkgName, "package.json");
|
|
1792
|
+
const { bin } = JSON.parse(readFileSync6(manifestPath, "utf8"));
|
|
1715
1793
|
const relativeBin = typeof bin === "string" ? bin : bin?.[binName2];
|
|
1716
1794
|
if (!relativeBin) throw new Error(`${pkgName} does not provide a "${binName2}" executable.`);
|
|
1717
|
-
return
|
|
1795
|
+
return join8(dirname4(manifestPath), relativeBin);
|
|
1718
1796
|
}
|
|
1719
1797
|
function delegatedArgv(appRoot, command, forwarded) {
|
|
1798
|
+
if (readStack(appRoot) === "next") {
|
|
1799
|
+
const script = { dev: "npm run dev", build: "npm run build", start: "npm start", deploy: "npm run deploy" }[command];
|
|
1800
|
+
throw new Error(`\`flare ${command}\` is for the Cloudflare stack. This app targets Next.js \u2014 run \`${script}\` instead.`);
|
|
1801
|
+
}
|
|
1720
1802
|
const spec = DELEGATED_COMMANDS[command];
|
|
1721
1803
|
return [resolveBin(appRoot, spec.pkg, spec.bin), ...spec.args, ...forwarded];
|
|
1722
1804
|
}
|
|
@@ -1775,7 +1857,7 @@ async function runDelegated(command, forwarded, cwd = process.cwd()) {
|
|
|
1775
1857
|
const tunnel = (command === "dev" || command === "start") && forwarded.includes("--tunnel");
|
|
1776
1858
|
if (tunnel) forwarded = forwarded.filter((arg) => arg !== "--tunnel");
|
|
1777
1859
|
const wantsHelp = forwarded.includes("--help") || forwarded.includes("-h");
|
|
1778
|
-
if (command === "start" && !wantsHelp && !
|
|
1860
|
+
if (command === "start" && !wantsHelp && !existsSync9(join8(appRoot, "dist/server/wrangler.json"))) {
|
|
1779
1861
|
console.log("No production build found; running `flare build` first.\n");
|
|
1780
1862
|
const code = await runNode(delegatedArgv(appRoot, "build", []), appRoot);
|
|
1781
1863
|
if (code !== 0) return code;
|
|
@@ -1825,9 +1907,9 @@ function snapshotColumns(snapshot) {
|
|
|
1825
1907
|
}
|
|
1826
1908
|
|
|
1827
1909
|
// src/generator/migrations.ts
|
|
1828
|
-
var listSql = (dir) =>
|
|
1910
|
+
var listSql = (dir) => existsSync10(dir) ? readdirSync4(dir).filter((file) => file.endsWith(".sql")) : [];
|
|
1829
1911
|
async function generateSchemaMigration(appRoot, name, options2 = {}) {
|
|
1830
|
-
const dir =
|
|
1912
|
+
const dir = join9(appRoot, "migrations");
|
|
1831
1913
|
const before = new Set(listSql(dir));
|
|
1832
1914
|
const bin = resolveBin(appRoot, "drizzle-kit", "drizzle-kit");
|
|
1833
1915
|
const { code, output } = await new Promise((resolve6, reject) => {
|
|
@@ -1855,10 +1937,10 @@ If it asked whether a column was renamed, run \`pnpm exec drizzle-kit generate\`
|
|
|
1855
1937
|
for (const file of created) {
|
|
1856
1938
|
const previous = previousSnapshot(dir, file);
|
|
1857
1939
|
if (!previous) continue;
|
|
1858
|
-
const path =
|
|
1859
|
-
const { sql: sql2, changes } = repairRebuildMigration(
|
|
1940
|
+
const path = join9(dir, file);
|
|
1941
|
+
const { sql: sql2, changes } = repairRebuildMigration(readFileSync7(path, "utf8"), snapshotColumns(previous));
|
|
1860
1942
|
if (changes.length) {
|
|
1861
|
-
|
|
1943
|
+
writeFileSync6(path, sql2);
|
|
1862
1944
|
repairs.push(...changes.map((change) => `${file}: ${change}`));
|
|
1863
1945
|
}
|
|
1864
1946
|
}
|
|
@@ -1866,32 +1948,32 @@ If it asked whether a column was renamed, run \`pnpm exec drizzle-kit generate\`
|
|
|
1866
1948
|
}
|
|
1867
1949
|
function previousSnapshot(dir, migrationFile) {
|
|
1868
1950
|
const prefix = /^(\d+)_/.exec(migrationFile)?.[1];
|
|
1869
|
-
const metaDir =
|
|
1870
|
-
if (!prefix || !
|
|
1951
|
+
const metaDir = join9(dir, "meta");
|
|
1952
|
+
if (!prefix || !existsSync10(metaDir)) return void 0;
|
|
1871
1953
|
const earlier = readdirSync4(metaDir).filter((file) => /^\d+_snapshot\.json$/.test(file) && file < `${prefix}_snapshot.json`).sort().pop();
|
|
1872
|
-
return earlier ? JSON.parse(
|
|
1954
|
+
return earlier ? JSON.parse(readFileSync7(join9(metaDir, earlier), "utf8")) : void 0;
|
|
1873
1955
|
}
|
|
1874
1956
|
|
|
1875
1957
|
// src/generator/plan.ts
|
|
1876
|
-
import { copyFileSync as copyFileSync2, existsSync as
|
|
1877
|
-
import { dirname as dirname5, join as
|
|
1958
|
+
import { copyFileSync as copyFileSync2, existsSync as existsSync12, mkdirSync as mkdirSync4, readdirSync as readdirSync6, readFileSync as readFileSync8 } from "fs";
|
|
1959
|
+
import { dirname as dirname5, join as join11 } from "path";
|
|
1878
1960
|
import pc5 from "picocolors";
|
|
1879
1961
|
|
|
1880
1962
|
// src/generator/policy.ts
|
|
1881
|
-
import { existsSync as
|
|
1882
|
-
import { join as
|
|
1963
|
+
import { existsSync as existsSync11, readdirSync as readdirSync5 } from "fs";
|
|
1964
|
+
import { join as join10, resolve as resolve5 } from "path";
|
|
1883
1965
|
import { camelCase as camelCase2, kebabCase as kebabCase2 } from "@flaredev/core";
|
|
1884
1966
|
import { createJiti as createJiti3 } from "jiti";
|
|
1885
1967
|
var policyPath = (name) => `policies/${kebabCase2(name)}.policy.ts`;
|
|
1886
1968
|
var policyLocal = (stem) => `${camelCase2(stem)}Policy`;
|
|
1887
1969
|
async function loadPolicies(root) {
|
|
1888
1970
|
const appRoot = resolve5(root);
|
|
1889
|
-
const dir =
|
|
1890
|
-
if (!
|
|
1891
|
-
const jiti = createJiti3(
|
|
1971
|
+
const dir = join10(appRoot, "policies");
|
|
1972
|
+
if (!existsSync11(dir)) return [];
|
|
1973
|
+
const jiti = createJiti3(join10(appRoot, "package.json"), { moduleCache: false, fsCache: false });
|
|
1892
1974
|
const loaded = [];
|
|
1893
1975
|
for (const file of readdirSync5(dir).filter((name) => name.endsWith(".policy.ts")).sort()) {
|
|
1894
|
-
const mod = await jiti.import(
|
|
1976
|
+
const mod = await jiti.import(join10(dir, file));
|
|
1895
1977
|
const policy = mod.default;
|
|
1896
1978
|
if (!policy || typeof policy !== "object" || !("resource" in policy) || !Array.isArray(policy.read)) {
|
|
1897
1979
|
throw new Error(`policies/${file} must \`export default definePolicy({ ... })\`.`);
|
|
@@ -1936,15 +2018,136 @@ function renderPolicyRegistry(all) {
|
|
|
1936
2018
|
].join("\n");
|
|
1937
2019
|
}
|
|
1938
2020
|
|
|
1939
|
-
// src/generator/
|
|
1940
|
-
import { camelCase as camelCase3, columnName,
|
|
1941
|
-
var resourceLocal = (stem) => `${camelCase3(stem)}Resource`;
|
|
1942
|
-
var tableExport = (resource) => camelCase3(resource.table);
|
|
2021
|
+
// src/generator/prisma.ts
|
|
2022
|
+
import { camelCase as camelCase3, columnName, pascalCase as pascalCase2, pluralize, storedFields } from "@flaredev/core";
|
|
1943
2023
|
var q = (value) => JSON.stringify(value);
|
|
2024
|
+
var enumTypeName = (resource, key) => `${pascalCase2(resource)}${pascalCase2(key)}`;
|
|
2025
|
+
var enumMember = (option) => {
|
|
2026
|
+
const safe = option.replace(/[^A-Za-z0-9_]/g, "_").replace(/^(?=\d)/, "v_");
|
|
2027
|
+
return safe === option ? ` ${safe}` : ` ${safe} @map(${q(option)})`;
|
|
2028
|
+
};
|
|
2029
|
+
function prismaType(resource, key, def) {
|
|
2030
|
+
switch (def.kind) {
|
|
2031
|
+
case "int":
|
|
2032
|
+
return "Int";
|
|
2033
|
+
case "float":
|
|
2034
|
+
return "Float";
|
|
2035
|
+
case "boolean":
|
|
2036
|
+
return "Boolean";
|
|
2037
|
+
case "date":
|
|
2038
|
+
case "datetime":
|
|
2039
|
+
return "DateTime";
|
|
2040
|
+
case "enum":
|
|
2041
|
+
return enumTypeName(resource.name, key);
|
|
2042
|
+
case "multiselect":
|
|
2043
|
+
return "String[]";
|
|
2044
|
+
default:
|
|
2045
|
+
return "String";
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
function attributes(key, def) {
|
|
2049
|
+
const parts = [];
|
|
2050
|
+
const column = columnName(key);
|
|
2051
|
+
if (column !== key) parts.push(`@map(${q(column)})`);
|
|
2052
|
+
if ("unique" in def && def.unique) parts.push("@unique");
|
|
2053
|
+
if (def.kind === "text") parts.push("@db.Text");
|
|
2054
|
+
if (def.kind === "date") parts.push("@db.Date");
|
|
2055
|
+
if ("default" in def && def.default !== void 0) {
|
|
2056
|
+
const value = def.default;
|
|
2057
|
+
if (def.kind === "enum") parts.push(`@default(${String(value).replace(/[^A-Za-z0-9_]/g, "_")})`);
|
|
2058
|
+
else if (def.kind === "multiselect") parts.push(`@default([${value.map(q).join(", ")}])`);
|
|
2059
|
+
else parts.push(`@default(${typeof value === "string" ? q(value) : String(value)})`);
|
|
2060
|
+
}
|
|
2061
|
+
return parts;
|
|
2062
|
+
}
|
|
2063
|
+
var onDeleteAction = (onDelete) => onDelete === "set null" ? "SetNull" : onDelete === "cascade" ? "Cascade" : "Restrict";
|
|
2064
|
+
function renderPrismaEnums(resource) {
|
|
2065
|
+
return storedFields(resource).filter(([, def]) => def.kind === "enum").map(([key, def]) => {
|
|
2066
|
+
if (def.kind !== "enum") return "";
|
|
2067
|
+
return [`enum ${enumTypeName(resource.name, key)} {`, ...def.options.map(enumMember), "}"].join("\n");
|
|
2068
|
+
}).filter(Boolean);
|
|
2069
|
+
}
|
|
2070
|
+
function backReferences(target, all) {
|
|
2071
|
+
const found = [];
|
|
2072
|
+
for (const { resource: source } of all) {
|
|
2073
|
+
const pointing = storedFields(source).filter(([, def]) => def.kind === "belongsTo" && def.target === target.name);
|
|
2074
|
+
const ambiguous = pointing.length > 1;
|
|
2075
|
+
for (const [key, def] of pointing) {
|
|
2076
|
+
if (def.kind !== "belongsTo") continue;
|
|
2077
|
+
const declared = Object.entries(target.fields).find(([, other]) => other.kind === "hasMany" && other.target === source.name)?.[0];
|
|
2078
|
+
const base = ambiguous ? camelCase3(`${key.replace(/Id$/, "")} ${pluralize(source.name)}`) : declared ?? camelCase3(pluralize(source.name));
|
|
2079
|
+
found.push({ field: base, from: source.name, relationName: ambiguous ? `${source.name}_${key}` : void 0 });
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
2082
|
+
return found;
|
|
2083
|
+
}
|
|
2084
|
+
function renderPrismaModel(entry, all) {
|
|
2085
|
+
const resource = entry.resource;
|
|
2086
|
+
const byName = new Map(all.map((item) => [item.resource.name, item.resource]));
|
|
2087
|
+
const lines = [`model ${resource.name} {`, ` id String @id @default(uuid())`];
|
|
2088
|
+
const indexes = [];
|
|
2089
|
+
for (const [key, def] of Object.entries(resource.fields)) {
|
|
2090
|
+
if (def.kind === "hasMany") {
|
|
2091
|
+
if (!byName.get(def.target)) {
|
|
2092
|
+
throw new Error(`${resource.name}.${key} has many "${def.target}", but there is no ${def.target} resource. Generate it first.`);
|
|
2093
|
+
}
|
|
2094
|
+
continue;
|
|
2095
|
+
}
|
|
2096
|
+
if (def.kind === "belongsTo") {
|
|
2097
|
+
const target = byName.get(def.target);
|
|
2098
|
+
if (!target) throw new Error(`${resource.name}.${key} belongs to "${def.target}", but there is no ${def.target} resource. Generate it first.`);
|
|
2099
|
+
const optional2 = def.required ? "" : "?";
|
|
2100
|
+
const relationField = key.replace(/Id$/, "");
|
|
2101
|
+
const named = backReferences(target, all).find((back) => back.from === resource.name && back.relationName?.endsWith(`_${key}`));
|
|
2102
|
+
const relationArgs = [
|
|
2103
|
+
...named?.relationName ? [q(named.relationName)] : [],
|
|
2104
|
+
`fields: [${key}]`,
|
|
2105
|
+
"references: [id]",
|
|
2106
|
+
`onDelete: ${onDeleteAction(def.onDelete)}`
|
|
2107
|
+
];
|
|
2108
|
+
lines.push(` ${key} String${optional2} @map(${q(columnName(key))})`);
|
|
2109
|
+
lines.push(` ${relationField} ${target.name}${optional2} @relation(${relationArgs.join(", ")})`);
|
|
2110
|
+
indexes.push(` @@index([${key}])`);
|
|
2111
|
+
continue;
|
|
2112
|
+
}
|
|
2113
|
+
const optional = def.required || def.kind === "multiselect" ? "" : "?";
|
|
2114
|
+
lines.push(` ${key} ${prismaType(resource, key, def)}${optional} ${attributes(key, def).join(" ")}`.trimEnd());
|
|
2115
|
+
}
|
|
2116
|
+
for (const back of backReferences(resource, all)) {
|
|
2117
|
+
const args = back.relationName ? `@relation(${q(back.relationName)})` : "";
|
|
2118
|
+
lines.push(` ${back.field} ${back.from}[] ${args}`.trimEnd());
|
|
2119
|
+
}
|
|
2120
|
+
lines.push(` createdAt DateTime @default(now()) @map("created_at")`);
|
|
2121
|
+
lines.push(` updatedAt DateTime @updatedAt @map("updated_at")`);
|
|
2122
|
+
indexes.push(` @@index([createdAt])`);
|
|
2123
|
+
const sortField = resource.defaultSort.field;
|
|
2124
|
+
if (!["createdAt", "updatedAt"].includes(sortField) && resource.fields[sortField]) indexes.push(` @@index([${sortField}])`);
|
|
2125
|
+
for (const [key, def] of storedFields(resource)) {
|
|
2126
|
+
if (def.kind === "enum" && def.filterable !== false) indexes.push(` @@index([${key}])`);
|
|
2127
|
+
}
|
|
2128
|
+
lines.push("", ...[...new Set(indexes)], ` @@map(${q(resource.table)})`, "}");
|
|
2129
|
+
return lines.join("\n");
|
|
2130
|
+
}
|
|
2131
|
+
function renderPrismaSchema(all) {
|
|
2132
|
+
const ordered = [...all].sort((a, b) => a.resource.name.localeCompare(b.resource.name));
|
|
2133
|
+
const blocks = [];
|
|
2134
|
+
for (const entry of ordered) {
|
|
2135
|
+
blocks.push(...renderPrismaEnums(entry.resource));
|
|
2136
|
+
blocks.push(renderPrismaModel(entry, all));
|
|
2137
|
+
}
|
|
2138
|
+
return `${blocks.join("\n\n")}
|
|
2139
|
+
`;
|
|
2140
|
+
}
|
|
2141
|
+
|
|
2142
|
+
// src/generator/render.ts
|
|
2143
|
+
import { camelCase as camelCase4, columnName as columnName2, relationGraph, storedFields as storedFields2 } from "@flaredev/core";
|
|
2144
|
+
var resourceLocal = (stem) => `${camelCase4(stem)}Resource`;
|
|
2145
|
+
var tableExport = (resource) => camelCase4(resource.table);
|
|
2146
|
+
var q2 = (value) => JSON.stringify(value);
|
|
1944
2147
|
var sqlString = (value) => `'${value.replace(/'/g, "''")}'`;
|
|
1945
2148
|
var TIMESTAMP_DEFAULT = "sql`(cast(unixepoch('subsecond') * 1000 as integer))`";
|
|
1946
2149
|
function columnExpression(key, def, resources, selfName) {
|
|
1947
|
-
const col =
|
|
2150
|
+
const col = q2(columnName2(key));
|
|
1948
2151
|
let expr;
|
|
1949
2152
|
switch (def.kind) {
|
|
1950
2153
|
case "int":
|
|
@@ -1957,10 +2160,10 @@ function columnExpression(key, def, resources, selfName) {
|
|
|
1957
2160
|
expr = `integer(${col}, { mode: "boolean" })`;
|
|
1958
2161
|
break;
|
|
1959
2162
|
case "enum":
|
|
1960
|
-
expr = `text(${col}, { enum: ${
|
|
2163
|
+
expr = `text(${col}, { enum: ${q2(def.options)} })`;
|
|
1961
2164
|
break;
|
|
1962
2165
|
case "multiselect":
|
|
1963
|
-
expr = `text(${col}, { mode: "json" }).$type<(${def.options.map((option) =>
|
|
2166
|
+
expr = `text(${col}, { mode: "json" }).$type<(${def.options.map((option) => q2(option)).join(" | ")})[]>()`;
|
|
1964
2167
|
break;
|
|
1965
2168
|
case "belongsTo": {
|
|
1966
2169
|
const target = resources.get(def.target);
|
|
@@ -1968,7 +2171,7 @@ function columnExpression(key, def, resources, selfName) {
|
|
|
1968
2171
|
throw new Error(`${selfName}.${key} belongs to "${def.target}", but there is no ${def.target} resource. Generate it first.`);
|
|
1969
2172
|
}
|
|
1970
2173
|
const onDelete = def.onDelete ?? "restrict";
|
|
1971
|
-
expr = `text(${col}).references(() => ${tableExport(target)}.id, { onDelete: ${
|
|
2174
|
+
expr = `text(${col}).references(() => ${tableExport(target)}.id, { onDelete: ${q2(onDelete)} })`;
|
|
1972
2175
|
break;
|
|
1973
2176
|
}
|
|
1974
2177
|
default:
|
|
@@ -1976,13 +2179,13 @@ function columnExpression(key, def, resources, selfName) {
|
|
|
1976
2179
|
}
|
|
1977
2180
|
if (def.required) expr += ".notNull()";
|
|
1978
2181
|
if ("unique" in def && def.unique) expr += ".unique()";
|
|
1979
|
-
if ("default" in def && def.default !== void 0) expr += `.default(${
|
|
2182
|
+
if ("default" in def && def.default !== void 0) expr += `.default(${q2(def.default)})`;
|
|
1980
2183
|
return expr;
|
|
1981
2184
|
}
|
|
1982
2185
|
function renderTableModule(entry, all) {
|
|
1983
2186
|
const { resource } = entry;
|
|
1984
2187
|
const resources = new Map(all.map((item) => [item.resource.name, item.resource]));
|
|
1985
|
-
const fields =
|
|
2188
|
+
const fields = storedFields2(resource);
|
|
1986
2189
|
const name = tableExport(resource);
|
|
1987
2190
|
const columns = fields.map(([key, def]) => ` ${key}: ${columnExpression(key, def, resources, resource.name)},`);
|
|
1988
2191
|
const extras = [];
|
|
@@ -1990,12 +2193,12 @@ function renderTableModule(entry, all) {
|
|
|
1990
2193
|
const addIndex = (key, column) => {
|
|
1991
2194
|
if (indexed.has(key)) return;
|
|
1992
2195
|
indexed.add(key);
|
|
1993
|
-
extras.push(` index(${
|
|
2196
|
+
extras.push(` index(${q2(`${resource.table}_${columnName2(column)}_idx`)}).on(table.${key}),`);
|
|
1994
2197
|
};
|
|
1995
2198
|
for (const [key, def] of fields) {
|
|
1996
2199
|
if (def.kind === "enum") {
|
|
1997
2200
|
const options2 = def.options.map(sqlString).join(", ");
|
|
1998
|
-
extras.push(` check(${
|
|
2201
|
+
extras.push(` check(${q2(`${resource.table}_${columnName2(key)}_check`)}, sql\`\${table.${key}} in (${options2})\`),`);
|
|
1999
2202
|
if (def.filterable !== false) addIndex(key, key);
|
|
2000
2203
|
}
|
|
2001
2204
|
if (def.kind === "belongsTo") addIndex(key, key);
|
|
@@ -2017,7 +2220,7 @@ function renderTableModule(entry, all) {
|
|
|
2017
2220
|
...targets.map((target) => `import { ${tableExport(target)} } from "./${target.table}";`),
|
|
2018
2221
|
"",
|
|
2019
2222
|
`export const ${name} = sqliteTable(`,
|
|
2020
|
-
` ${
|
|
2223
|
+
` ${q2(resource.table)},`,
|
|
2021
2224
|
" {",
|
|
2022
2225
|
` id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),`,
|
|
2023
2226
|
...columns.map(
|
|
@@ -2057,12 +2260,12 @@ function renderRelations(all) {
|
|
|
2057
2260
|
const target = byName.get(relation.target);
|
|
2058
2261
|
used.add(target);
|
|
2059
2262
|
const other = tableExport(target);
|
|
2060
|
-
return ` ${relation.name}: one(${other}, { fields: [${self}.${relation.key}], references: [${other}.id], relationName: ${
|
|
2263
|
+
return ` ${relation.name}: one(${other}, { fields: [${self}.${relation.key}], references: [${other}.id], relationName: ${q2(relation.relationName)} }),`;
|
|
2061
2264
|
}),
|
|
2062
2265
|
...hasMany.map((relation) => {
|
|
2063
2266
|
const target = byName.get(relation.target);
|
|
2064
2267
|
used.add(target);
|
|
2065
|
-
return ` ${relation.key}: many(${tableExport(target)}, { relationName: ${
|
|
2268
|
+
return ` ${relation.key}: many(${tableExport(target)}, { relationName: ${q2(relation.relationName)} }),`;
|
|
2066
2269
|
})
|
|
2067
2270
|
];
|
|
2068
2271
|
const helpers = [belongsTo.length ? "one" : "", hasMany.length ? "many" : ""].filter(Boolean).join(", ");
|
|
@@ -2075,32 +2278,40 @@ ${lines.join("\n")}
|
|
|
2075
2278
|
const imports = [...used].sort((a, b) => a.table.localeCompare(b.table)).map((resource) => `import { ${tableExport(resource)} } from "./schema/${resource.table}";`);
|
|
2076
2279
|
return [`import { relations } from "drizzle-orm";`, ...imports, "", blocks.join("\n")].join("\n");
|
|
2077
2280
|
}
|
|
2078
|
-
var handlerImports = (entry) =>
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2281
|
+
var handlerImports = (entry, stack = "cloudflare") => {
|
|
2282
|
+
const local = resourceLocal(entry.stem);
|
|
2283
|
+
const source = stack === "next" ? {
|
|
2284
|
+
imports: [`import { prismaRows } from "@flaredev/core/server";`, `import { prisma } from "@/lib/db";`],
|
|
2285
|
+
wiring: [` rows: prismaRows(prisma.${camelCase4(entry.resource.name)}, prisma),`]
|
|
2286
|
+
} : {
|
|
2287
|
+
imports: [`import { getDb } from "@/db";`, `import { ${tableExport(entry.resource)} } from "@/db/schema";`],
|
|
2288
|
+
wiring: [` table: ${tableExport(entry.resource)},`, ` getDb,`]
|
|
2289
|
+
};
|
|
2290
|
+
return [
|
|
2291
|
+
`import { createResourceHandlers } from "@flaredev/core/server";`,
|
|
2292
|
+
...source.imports,
|
|
2293
|
+
`import { authorize, currentUser } from "@/lib/api";`,
|
|
2294
|
+
`import { revalidateResource } from "@/lib/cache";`,
|
|
2295
|
+
`import ${local} from "@/resources/${entry.stem}.resource";`,
|
|
2296
|
+
"",
|
|
2297
|
+
`const handlers = createResourceHandlers({`,
|
|
2298
|
+
` resource: ${local},`,
|
|
2299
|
+
...source.wiring,
|
|
2300
|
+
` authorize,`,
|
|
2301
|
+
` currentUser,`,
|
|
2302
|
+
` onChange: revalidateResource,`,
|
|
2303
|
+
`});`,
|
|
2304
|
+
""
|
|
2305
|
+
];
|
|
2306
|
+
};
|
|
2307
|
+
function renderCollectionRoute(entry, stack = "cloudflare") {
|
|
2308
|
+
return [...handlerImports(entry, stack), "export const GET = handlers.collection.GET;", "export const POST = handlers.collection.POST;", ""].join(
|
|
2098
2309
|
"\n"
|
|
2099
2310
|
);
|
|
2100
2311
|
}
|
|
2101
|
-
function renderItemRoute(entry) {
|
|
2312
|
+
function renderItemRoute(entry, stack = "cloudflare") {
|
|
2102
2313
|
return [
|
|
2103
|
-
...handlerImports(entry),
|
|
2314
|
+
...handlerImports(entry, stack),
|
|
2104
2315
|
"export const GET = handlers.item.GET;",
|
|
2105
2316
|
"export const PATCH = handlers.item.PATCH;",
|
|
2106
2317
|
"export const PUT = handlers.item.PUT;",
|
|
@@ -2179,7 +2390,7 @@ function renderClient(entry) {
|
|
|
2179
2390
|
`import { createResourceClient } from "@flaredev/core/client";`,
|
|
2180
2391
|
`import type ${local} from "./${stem}.resource";`,
|
|
2181
2392
|
"",
|
|
2182
|
-
`export const ${
|
|
2393
|
+
`export const ${camelCase4(stem)}Client = createResourceClient<typeof ${local}>(${q2(`/api/${resource.slug}`)});`,
|
|
2183
2394
|
"",
|
|
2184
2395
|
`export type ${resource.name} = typeof ${local}.$types.record;`,
|
|
2185
2396
|
`export type ${resource.name}Create = typeof ${local}.$types.create;`,
|
|
@@ -2193,7 +2404,7 @@ function renderValidators(entry) {
|
|
|
2193
2404
|
`import { createValidators } from "@flaredev/core";`,
|
|
2194
2405
|
`import ${local} from "./${entry.stem}.resource";`,
|
|
2195
2406
|
"",
|
|
2196
|
-
`export const ${
|
|
2407
|
+
`export const ${camelCase4(entry.stem)}Validators = createValidators(${local});`,
|
|
2197
2408
|
""
|
|
2198
2409
|
].join("\n");
|
|
2199
2410
|
}
|
|
@@ -2223,12 +2434,31 @@ function renderServerRegistry(all) {
|
|
|
2223
2434
|
""
|
|
2224
2435
|
].join("\n");
|
|
2225
2436
|
}
|
|
2226
|
-
function
|
|
2437
|
+
function renderPrismaServerRegistry(all) {
|
|
2438
|
+
if (all.length === 0) return "export const resourceTables = {} as const;\n";
|
|
2439
|
+
const sorted = [...all].sort((a, b) => a.resource.name.localeCompare(b.resource.name));
|
|
2440
|
+
return [
|
|
2441
|
+
`import { prisma } from "@/lib/db";`,
|
|
2442
|
+
`import { ${sorted.map(({ stem }) => resourceLocal(stem)).join(", ")} } from "./index";`,
|
|
2443
|
+
"",
|
|
2444
|
+
"export const resourceTables = {",
|
|
2445
|
+
...sorted.map(
|
|
2446
|
+
({ resource, stem }) => ` ${resource.name}: { resource: ${resourceLocal(stem)}, delegate: prisma.${camelCase4(resource.name)} },`
|
|
2447
|
+
),
|
|
2448
|
+
"} as const;",
|
|
2449
|
+
"",
|
|
2450
|
+
"export type ResourceName = keyof typeof resourceTables;",
|
|
2451
|
+
""
|
|
2452
|
+
].join("\n");
|
|
2453
|
+
}
|
|
2454
|
+
function resourceFiles(entry, all, stack = "cloudflare") {
|
|
2227
2455
|
const { resource, stem } = entry;
|
|
2228
2456
|
return [
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
{ path: `
|
|
2457
|
+
// The schema is the one file that belongs to a stack. On Next.js every model lives
|
|
2458
|
+
// in one prisma/schema.prisma, written by planFiles rather than per resource.
|
|
2459
|
+
...stack === "cloudflare" ? [{ path: `db/schema/${resource.table}.ts`, content: renderTableModule(entry, all) }] : [],
|
|
2460
|
+
{ path: `app/api/${resource.slug}/route.ts`, content: renderCollectionRoute(entry, stack) },
|
|
2461
|
+
{ path: `app/api/${resource.slug}/[id]/route.ts`, content: renderItemRoute(entry, stack) },
|
|
2232
2462
|
{ path: `resources/${stem}.client.ts`, content: renderClient(entry) },
|
|
2233
2463
|
{ path: `resources/${stem}.validators.ts`, content: renderValidators(entry) },
|
|
2234
2464
|
{ path: `app/dashboard/${resource.slug}/page.tsx`, content: renderAdminListPage(entry) },
|
|
@@ -2243,21 +2473,42 @@ var resourceHeader = (name) => `// Generated by \`flare gen resource ${name}\`.
|
|
|
2243
2473
|
// rewritten on regeneration; add your own code outside it.
|
|
2244
2474
|
`;
|
|
2245
2475
|
var HEADER_PATTERN = /^\/\/ Generated by `flare gen resource (\w+)`/;
|
|
2246
|
-
function planFiles(all, policies) {
|
|
2476
|
+
function planFiles(all, policies, stack = "cloudflare") {
|
|
2247
2477
|
const files = [];
|
|
2248
2478
|
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
|
-
|
|
2479
|
+
for (const file of resourceFiles(entry, all, stack)) files.push({ ...file, header: resourceHeader(entry.resource.name) });
|
|
2480
|
+
}
|
|
2481
|
+
files.push({
|
|
2482
|
+
path: "resources/index.ts",
|
|
2483
|
+
content: renderRegistry(all),
|
|
2484
|
+
header: "// Registry of every resource descriptor (maintained by flare gen).\n"
|
|
2485
|
+
});
|
|
2486
|
+
if (stack === "next") {
|
|
2487
|
+
files.push(
|
|
2488
|
+
// Prisma keeps every model in one schema and writes its own migrations, so there
|
|
2489
|
+
// is no per-table module and no relations file to maintain.
|
|
2490
|
+
{
|
|
2491
|
+
path: "prisma/schema/resources.prisma",
|
|
2492
|
+
content: renderPrismaSchema(all),
|
|
2493
|
+
header: "// Generated from the descriptors in resources/. Edit a descriptor, not this.\n"
|
|
2494
|
+
},
|
|
2495
|
+
{
|
|
2496
|
+
path: "resources/server.ts",
|
|
2497
|
+
content: renderPrismaServerRegistry(all),
|
|
2498
|
+
header: "// Server-only: resource name \u2192 descriptor and Prisma delegate (maintained by flare gen).\n"
|
|
2499
|
+
}
|
|
2500
|
+
);
|
|
2501
|
+
} else {
|
|
2502
|
+
files.push(
|
|
2503
|
+
{ path: "db/relations.ts", content: renderRelations(all), header: "// Drizzle relations for every resource (maintained by flare gen).\n" },
|
|
2504
|
+
{
|
|
2505
|
+
path: "resources/server.ts",
|
|
2506
|
+
content: renderServerRegistry(all),
|
|
2507
|
+
header: "// Server-only: resource name \u2192 descriptor and table (maintained by flare gen).\n"
|
|
2508
|
+
},
|
|
2509
|
+
{ path: "db/schema.ts", content: renderSchemaIndex(all) }
|
|
2510
|
+
);
|
|
2511
|
+
}
|
|
2261
2512
|
if (policies) {
|
|
2262
2513
|
files.push({
|
|
2263
2514
|
path: "policies/index.ts",
|
|
@@ -2269,7 +2520,7 @@ function planFiles(all, policies) {
|
|
|
2269
2520
|
}
|
|
2270
2521
|
function applyPlan(appRoot, plan, options2 = {}) {
|
|
2271
2522
|
return plan.map(({ path, content, header }) => {
|
|
2272
|
-
const absolute =
|
|
2523
|
+
const absolute = join11(appRoot, path);
|
|
2273
2524
|
const status = inspectGenerated(absolute, content);
|
|
2274
2525
|
if (status === "unmarked") return { path, status, outcome: "skipped-unmarked" };
|
|
2275
2526
|
if (status === "drift" && !options2.force) return { path, status, outcome: "skipped-drift" };
|
|
@@ -2288,30 +2539,30 @@ function applyPlan(appRoot, plan, options2 = {}) {
|
|
|
2288
2539
|
function findOrphans(appRoot, all) {
|
|
2289
2540
|
const planned = new Set(planFiles(all).map((file) => file.path));
|
|
2290
2541
|
const candidates = [];
|
|
2291
|
-
const list2 = (dir, match) =>
|
|
2542
|
+
const list2 = (dir, match) => existsSync12(join11(appRoot, dir)) ? readdirSync6(join11(appRoot, dir)).filter(match).map((name) => `${dir}/${name}`) : [];
|
|
2292
2543
|
candidates.push(...list2("db/schema", (name) => name.endsWith(".ts")));
|
|
2293
2544
|
candidates.push(...list2("resources", (name) => /\.(client|validators)\.ts$/.test(name)));
|
|
2294
2545
|
for (const dir of list2("app/api", () => true)) {
|
|
2295
|
-
for (const file of [`${dir}/route.ts`, `${dir}/[id]/route.ts`]) if (
|
|
2546
|
+
for (const file of [`${dir}/route.ts`, `${dir}/[id]/route.ts`]) if (existsSync12(join11(appRoot, file))) candidates.push(file);
|
|
2296
2547
|
}
|
|
2297
2548
|
for (const dir of list2("app/dashboard", () => true)) {
|
|
2298
2549
|
for (const file of [`${dir}/page.tsx`, `${dir}/new/page.tsx`, `${dir}/[id]/page.tsx`, `${dir}/[id]/edit/page.tsx`]) {
|
|
2299
|
-
if (
|
|
2550
|
+
if (existsSync12(join11(appRoot, file))) candidates.push(file);
|
|
2300
2551
|
}
|
|
2301
2552
|
}
|
|
2302
2553
|
const orphans = [];
|
|
2303
2554
|
for (const path of candidates) {
|
|
2304
|
-
const match = HEADER_PATTERN.exec(
|
|
2555
|
+
const match = HEADER_PATTERN.exec(readFileSync8(join11(appRoot, path), "utf8"));
|
|
2305
2556
|
if (match && !planned.has(path)) orphans.push({ path, resource: match[1] });
|
|
2306
2557
|
}
|
|
2307
2558
|
return orphans;
|
|
2308
2559
|
}
|
|
2309
2560
|
function ensureSupportFiles(appRoot, log2) {
|
|
2310
2561
|
for (const path of ["lib/api.ts", "lib/cache.ts"]) {
|
|
2311
|
-
const target =
|
|
2312
|
-
if (
|
|
2562
|
+
const target = join11(appRoot, path);
|
|
2563
|
+
if (existsSync12(target)) continue;
|
|
2313
2564
|
mkdirSync4(dirname5(target), { recursive: true });
|
|
2314
|
-
copyFileSync2(
|
|
2565
|
+
copyFileSync2(join11(templatesDir, "app", path), target);
|
|
2315
2566
|
log2(`${pc5.green("create".padEnd(9))} ${path}`);
|
|
2316
2567
|
}
|
|
2317
2568
|
}
|
|
@@ -2339,15 +2590,15 @@ function billingFiles(appName) {
|
|
|
2339
2590
|
}
|
|
2340
2591
|
var RETIRED_FILES = ["app/api/billing/status/route.ts", "components/billing/subscribe-button.tsx"];
|
|
2341
2592
|
function appendVars(appRoot, target) {
|
|
2342
|
-
const path =
|
|
2343
|
-
const text =
|
|
2593
|
+
const path = join12(appRoot, target);
|
|
2594
|
+
const text = existsSync13(path) ? readFileSync9(path, "utf8") : "";
|
|
2344
2595
|
if (/STRIPE_SECRET_KEY/.test(text)) return false;
|
|
2345
2596
|
appendFileSync2(path, target === ".dev.vars" ? billingDevVarsEntries() : billingDevVarsExampleEntries());
|
|
2346
2597
|
return true;
|
|
2347
2598
|
}
|
|
2348
2599
|
function addStripeDependency(appRoot) {
|
|
2349
|
-
const path =
|
|
2350
|
-
const pkg = JSON.parse(
|
|
2600
|
+
const path = join12(appRoot, "package.json");
|
|
2601
|
+
const pkg = JSON.parse(readFileSync9(path, "utf8"));
|
|
2351
2602
|
pkg.dependencies ??= {};
|
|
2352
2603
|
if (pkg.dependencies.stripe) return false;
|
|
2353
2604
|
pkg.dependencies.stripe = STRIPE_DEP;
|
|
@@ -2356,14 +2607,14 @@ function addStripeDependency(appRoot) {
|
|
|
2356
2607
|
}
|
|
2357
2608
|
function writeBillingResource(appRoot, name, force, log2) {
|
|
2358
2609
|
const relative2 = descriptorPath(name);
|
|
2359
|
-
const path =
|
|
2360
|
-
if (!
|
|
2610
|
+
const path = join12(appRoot, relative2);
|
|
2611
|
+
if (!existsSync13(path)) {
|
|
2361
2612
|
mkdirSync5(dirname6(path), { recursive: true });
|
|
2362
|
-
|
|
2613
|
+
writeFileSync7(path, renderBillingDescriptor(name));
|
|
2363
2614
|
log2(`${pc6.green("create".padEnd(9))} ${relative2}`);
|
|
2364
2615
|
return;
|
|
2365
2616
|
}
|
|
2366
|
-
const parts = splitMarkers(
|
|
2617
|
+
const parts = splitMarkers(readFileSync9(path, "utf8"));
|
|
2367
2618
|
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
2619
|
const merged = mergeBillingFields(name, parts.block);
|
|
2369
2620
|
if (!merged) {
|
|
@@ -2387,26 +2638,26 @@ async function genBilling(options2 = {}) {
|
|
|
2387
2638
|
for (const name of ["Plan", "Customer", "Purchase"]) writeBillingResource(appRoot, name, options2.force, log2);
|
|
2388
2639
|
for (const [name, roles] of Object.entries(BILLING_POLICIES)) {
|
|
2389
2640
|
const relative2 = policyPath(name);
|
|
2390
|
-
if (
|
|
2641
|
+
if (existsSync13(join12(appRoot, relative2))) {
|
|
2391
2642
|
log2(`${pc6.dim("kept".padEnd(9))} ${relative2} (already exists)`);
|
|
2392
2643
|
continue;
|
|
2393
2644
|
}
|
|
2394
|
-
mkdirSync5(
|
|
2395
|
-
|
|
2645
|
+
mkdirSync5(join12(appRoot, "policies"), { recursive: true });
|
|
2646
|
+
writeFileSync7(join12(appRoot, relative2), renderPolicy(name, roles));
|
|
2396
2647
|
log2(`${pc6.green("create".padEnd(9))} ${relative2}`);
|
|
2397
2648
|
}
|
|
2398
2649
|
const files = [];
|
|
2399
|
-
const appName = JSON.parse(
|
|
2650
|
+
const appName = JSON.parse(readFileSync9(join12(appRoot, "package.json"), "utf8")).name ?? "app";
|
|
2400
2651
|
for (const { path, content } of billingFiles(appName)) {
|
|
2401
|
-
const outcome = writeGenerated(
|
|
2652
|
+
const outcome = writeGenerated(join12(appRoot, path), content, { header: billingHeader(), force: options2.force });
|
|
2402
2653
|
files.push({ path, status: "missing", outcome });
|
|
2403
2654
|
const label = outcome === "create" ? pc6.green("create") : outcome === "update" ? pc6.yellow("update") : pc6.dim("identical");
|
|
2404
2655
|
log2(`${label.padEnd(18)} ${path}`);
|
|
2405
2656
|
}
|
|
2406
2657
|
for (const path of RETIRED_FILES) {
|
|
2407
|
-
const absolute =
|
|
2408
|
-
if (!
|
|
2409
|
-
const parts = splitMarkers(
|
|
2658
|
+
const absolute = join12(appRoot, path);
|
|
2659
|
+
if (!existsSync13(absolute)) continue;
|
|
2660
|
+
const parts = splitMarkers(readFileSync9(absolute, "utf8"));
|
|
2410
2661
|
const untouched = parts && parts.before.includes("flare gen billing") && !parts.after.trim() && parts.hash !== void 0 && hashBlock(parts.block) === parts.hash;
|
|
2411
2662
|
if (untouched) {
|
|
2412
2663
|
unlinkSync(absolute);
|
|
@@ -2427,7 +2678,7 @@ async function genBilling(options2 = {}) {
|
|
|
2427
2678
|
}
|
|
2428
2679
|
const all = await loadResources(appRoot);
|
|
2429
2680
|
ensureSupportFiles(appRoot, log2);
|
|
2430
|
-
logResults(applyPlan(appRoot, planFiles(all, await loadPolicies(appRoot)), { force: options2.force }), log2);
|
|
2681
|
+
logResults(applyPlan(appRoot, planFiles(all, await loadPolicies(appRoot), readStack(appRoot)), { force: options2.force }), log2);
|
|
2431
2682
|
let migrations = [];
|
|
2432
2683
|
if (!options2.skipMigration) {
|
|
2433
2684
|
const result = await generateSchemaMigration(appRoot, "billing");
|
|
@@ -2450,8 +2701,8 @@ async function genBilling(options2 = {}) {
|
|
|
2450
2701
|
}
|
|
2451
2702
|
|
|
2452
2703
|
// src/commands/gen-security.ts
|
|
2453
|
-
import { existsSync as
|
|
2454
|
-
import { dirname as dirname7, join as
|
|
2704
|
+
import { existsSync as existsSync14, mkdirSync as mkdirSync6, readFileSync as readFileSync10, writeFileSync as writeFileSync8 } from "fs";
|
|
2705
|
+
import { dirname as dirname7, join as join13 } from "path";
|
|
2455
2706
|
import pc7 from "picocolors";
|
|
2456
2707
|
|
|
2457
2708
|
// src/generator/security.ts
|
|
@@ -2979,28 +3230,28 @@ function securityFiles(appName) {
|
|
|
2979
3230
|
}
|
|
2980
3231
|
var PASS_THROUGH_HEADER = /^\/\/ Request protection for worker\/index\.ts\.[\s\S]*?\n(?=\/\/ generated:start)/;
|
|
2981
3232
|
function writeOnce(appRoot, relative2, content, log2) {
|
|
2982
|
-
const path =
|
|
2983
|
-
if (
|
|
3233
|
+
const path = join13(appRoot, relative2);
|
|
3234
|
+
if (existsSync14(path)) {
|
|
2984
3235
|
log2(`${pc7.dim("kept".padEnd(9))} ${relative2} (already exists)`);
|
|
2985
3236
|
return;
|
|
2986
3237
|
}
|
|
2987
3238
|
mkdirSync6(dirname7(path), { recursive: true });
|
|
2988
|
-
|
|
3239
|
+
writeFileSync8(path, content);
|
|
2989
3240
|
log2(`${pc7.green("create".padEnd(9))} ${relative2}`);
|
|
2990
3241
|
}
|
|
2991
3242
|
async function genSecurity(options2 = {}) {
|
|
2992
3243
|
const log2 = options2.log ?? ((message) => console.log(message));
|
|
2993
3244
|
const appRoot = findAppRoot(options2.cwd ?? process.cwd());
|
|
2994
|
-
const appName = JSON.parse(
|
|
3245
|
+
const appName = JSON.parse(readFileSync10(join13(appRoot, "package.json"), "utf8")).name ?? "app";
|
|
2995
3246
|
writeOnce(appRoot, "security.config.ts", renderSecurityConfig(), log2);
|
|
2996
3247
|
writeOnce(appRoot, descriptorPath("SecurityEvent"), renderSecurityDescriptor(), log2);
|
|
2997
3248
|
writeOnce(appRoot, policyPath("SecurityEvent"), renderPolicy("SecurityEvent", SECURITY_EVENT_POLICY), log2);
|
|
2998
3249
|
const files = [];
|
|
2999
3250
|
const write = (path, content, header) => {
|
|
3000
|
-
const absolute =
|
|
3001
|
-
if (path === "lib/security.ts" &&
|
|
3002
|
-
const source =
|
|
3003
|
-
if (PASS_THROUGH_HEADER.test(source))
|
|
3251
|
+
const absolute = join13(appRoot, path);
|
|
3252
|
+
if (path === "lib/security.ts" && existsSync14(absolute)) {
|
|
3253
|
+
const source = readFileSync10(absolute, "utf8");
|
|
3254
|
+
if (PASS_THROUGH_HEADER.test(source)) writeFileSync8(absolute, source.replace(PASS_THROUGH_HEADER, securityHeader()));
|
|
3004
3255
|
}
|
|
3005
3256
|
const outcome = writeGenerated(absolute, content, { header, force: options2.force });
|
|
3006
3257
|
files.push({ path, status: "missing", outcome });
|
|
@@ -3008,16 +3259,16 @@ async function genSecurity(options2 = {}) {
|
|
|
3008
3259
|
log2(`${label.padEnd(18)} ${path}`);
|
|
3009
3260
|
};
|
|
3010
3261
|
for (const { path, content } of securityFiles(appName)) write(path, content, securityHeader());
|
|
3011
|
-
const navPath =
|
|
3012
|
-
if (
|
|
3262
|
+
const navPath = join13(appRoot, "lib/dashboard-nav.ts");
|
|
3263
|
+
if (existsSync14(navPath) && splitMarkers(readFileSync10(navPath, "utf8"))) write("lib/dashboard-nav.ts", renderAdminNavBlock());
|
|
3013
3264
|
else log2(pc7.yellow(`lib/dashboard-nav.ts not found: link to /dashboard/security from your dashboard sidebar yourself.`));
|
|
3014
|
-
const wranglerPath =
|
|
3265
|
+
const wranglerPath = join13(appRoot, "wrangler.jsonc");
|
|
3015
3266
|
let bindings = [];
|
|
3016
|
-
if (
|
|
3017
|
-
const { text, added } = addSecurityBindings(
|
|
3267
|
+
if (existsSync14(wranglerPath)) {
|
|
3268
|
+
const { text, added } = addSecurityBindings(readFileSync10(wranglerPath, "utf8"));
|
|
3018
3269
|
bindings = added;
|
|
3019
3270
|
if (added.length) {
|
|
3020
|
-
|
|
3271
|
+
writeFileSync8(wranglerPath, text);
|
|
3021
3272
|
for (const line of added) log2(`${pc7.yellow("update".padEnd(9))} wrangler.jsonc (${line})`);
|
|
3022
3273
|
}
|
|
3023
3274
|
} else {
|
|
@@ -3025,7 +3276,7 @@ async function genSecurity(options2 = {}) {
|
|
|
3025
3276
|
}
|
|
3026
3277
|
const all = await loadResources(appRoot);
|
|
3027
3278
|
ensureSupportFiles(appRoot, log2);
|
|
3028
|
-
logResults(applyPlan(appRoot, planFiles(all, await loadPolicies(appRoot)), { force: options2.force }), log2);
|
|
3279
|
+
logResults(applyPlan(appRoot, planFiles(all, await loadPolicies(appRoot), readStack(appRoot)), { force: options2.force }), log2);
|
|
3029
3280
|
let migrations = [];
|
|
3030
3281
|
if (!options2.skipMigration) {
|
|
3031
3282
|
const result = await generateSchemaMigration(appRoot, "security");
|
|
@@ -3048,15 +3299,15 @@ async function genSecurity(options2 = {}) {
|
|
|
3048
3299
|
}
|
|
3049
3300
|
|
|
3050
3301
|
// src/commands/theme.ts
|
|
3051
|
-
import { existsSync as
|
|
3052
|
-
import { join as
|
|
3302
|
+
import { existsSync as existsSync15, readFileSync as readFileSync11, writeFileSync as writeFileSync9 } from "fs";
|
|
3303
|
+
import { join as join14 } from "path";
|
|
3053
3304
|
import pc8 from "picocolors";
|
|
3054
3305
|
var THEME_LINE = /theme: "([a-z]+)" as ThemeName,/;
|
|
3055
3306
|
function setTheme(name, cwd = process.cwd(), log2 = console.log) {
|
|
3056
3307
|
const appRoot = findAppRoot(cwd);
|
|
3057
|
-
const path =
|
|
3058
|
-
if (!
|
|
3059
|
-
const source =
|
|
3308
|
+
const path = join14(appRoot, "lib/site.ts");
|
|
3309
|
+
if (!existsSync15(path)) throw new Error("lib/site.ts not found: this app was created before themes. Add it from the Flare app template.");
|
|
3310
|
+
const source = readFileSync11(path, "utf8");
|
|
3060
3311
|
const current = THEME_LINE.exec(source)?.[1];
|
|
3061
3312
|
if (!name) {
|
|
3062
3313
|
for (const [theme2, description] of Object.entries(THEMES)) {
|
|
@@ -3067,7 +3318,7 @@ function setTheme(name, cwd = process.cwd(), log2 = console.log) {
|
|
|
3067
3318
|
}
|
|
3068
3319
|
const theme = parseTheme(name);
|
|
3069
3320
|
if (!current) throw new Error('lib/site.ts has no `theme: "\u2026" as ThemeName,` line to change.');
|
|
3070
|
-
|
|
3321
|
+
writeFileSync9(path, source.replace(THEME_LINE, `theme: "${theme}" as ThemeName,`));
|
|
3071
3322
|
log2(`${pc8.green("\u2714")} Theme is now ${pc8.bold(theme)}. Restart \`flare dev\` (or rebuild) to see it.`);
|
|
3072
3323
|
}
|
|
3073
3324
|
|
|
@@ -3139,9 +3390,9 @@ async function askCreateQuestions(given, appName) {
|
|
|
3139
3390
|
}
|
|
3140
3391
|
|
|
3141
3392
|
// src/commands/gen.ts
|
|
3142
|
-
import { existsSync as
|
|
3143
|
-
import { dirname as dirname8, join as
|
|
3144
|
-
import { camelCase as
|
|
3393
|
+
import { existsSync as existsSync16, mkdirSync as mkdirSync7, writeFileSync as writeFileSync10 } from "fs";
|
|
3394
|
+
import { dirname as dirname8, join as join15 } from "path";
|
|
3395
|
+
import { camelCase as camelCase5, defineResource as defineResource2, relationGraph as relationGraph2 } from "@flaredev/core";
|
|
3145
3396
|
import pc10 from "picocolors";
|
|
3146
3397
|
function validateFields(name, fields, others, log2) {
|
|
3147
3398
|
const known = /* @__PURE__ */ new Set([name, ...others.map((resource) => resource.name)]);
|
|
@@ -3153,7 +3404,7 @@ function validateFields(name, fields, others, log2) {
|
|
|
3153
3404
|
const candidate = defineResource2({ name, fields: Object.fromEntries(fields.map((f) => [f.key, toField(f)])) });
|
|
3154
3405
|
const graph = relationGraph2([...others, candidate]);
|
|
3155
3406
|
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 ${
|
|
3407
|
+
log2(pc10.dim(`note: ${name}.${pending.key} will link up once ${pending.target} exists (give it a ${camelCase5(name)}Id: belongsTo(${name}) field).`));
|
|
3157
3408
|
}
|
|
3158
3409
|
}
|
|
3159
3410
|
async function genResource(rawName, options2) {
|
|
@@ -3161,8 +3412,8 @@ async function genResource(rawName, options2) {
|
|
|
3161
3412
|
const appRoot = findAppRoot(options2.cwd ?? process.cwd());
|
|
3162
3413
|
const name = resourceName(rawName);
|
|
3163
3414
|
const relativePath = descriptorPath(name);
|
|
3164
|
-
const path =
|
|
3165
|
-
const exists =
|
|
3415
|
+
const path = join15(appRoot, relativePath);
|
|
3416
|
+
const exists = existsSync16(path);
|
|
3166
3417
|
if (!exists && !options2.fields) {
|
|
3167
3418
|
throw new Error(`--fields is required, e.g. flare gen resource ${name} --fields "name:string, email:string"`);
|
|
3168
3419
|
}
|
|
@@ -3173,7 +3424,7 @@ async function genResource(rawName, options2) {
|
|
|
3173
3424
|
validateFields(name, fields, others, log2);
|
|
3174
3425
|
if (!exists) {
|
|
3175
3426
|
mkdirSync7(dirname8(path), { recursive: true });
|
|
3176
|
-
|
|
3427
|
+
writeFileSync10(path, renderDescriptor(name, fields, { group: options2.group, icon: options2.icon }));
|
|
3177
3428
|
log2(`${pc10.green("create".padEnd(9))} ${relativePath}`);
|
|
3178
3429
|
} else {
|
|
3179
3430
|
try {
|
|
@@ -3195,7 +3446,7 @@ async function genResource(rawName, options2) {
|
|
|
3195
3446
|
const all = await loadResources(appRoot);
|
|
3196
3447
|
table = all.find(({ resource }) => resource.name === name).resource.table;
|
|
3197
3448
|
ensureSupportFiles(appRoot, log2);
|
|
3198
|
-
written = applyPlan(appRoot, planFiles(all), { force: options2.force });
|
|
3449
|
+
written = applyPlan(appRoot, planFiles(all, void 0, readStack(appRoot)), { force: options2.force });
|
|
3199
3450
|
logResults(written, log2);
|
|
3200
3451
|
} catch (error) {
|
|
3201
3452
|
const hint = exists ? "" : `
|
|
@@ -3209,22 +3460,27 @@ Skipped ${skipped.length} hand-edited file(s); run \`flare sync-types\` for deta
|
|
|
3209
3460
|
}
|
|
3210
3461
|
let migrations = [];
|
|
3211
3462
|
if (!options2.skipMigration) {
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3463
|
+
if (readStack(appRoot) === "next") {
|
|
3464
|
+
log2(`
|
|
3465
|
+
Next: ${pc10.bold("npx prisma migrate dev")} to turn the schema into a migration and apply it.`);
|
|
3466
|
+
} else {
|
|
3467
|
+
const result = await generateSchemaMigration(appRoot, `${exists ? "update" : "create"}_${table}`);
|
|
3468
|
+
migrations = result.files;
|
|
3469
|
+
for (const file of migrations) log2(`${pc10.green("create".padEnd(9))} ${file}`);
|
|
3470
|
+
for (const repair of result.repairs) log2(pc10.dim(`repaired ${repair}`));
|
|
3471
|
+
if (migrations.length) log2(`
|
|
3217
3472
|
Next: ${pc10.bold("flare migrate")} to apply it locally.`);
|
|
3218
|
-
|
|
3473
|
+
else if (exists) log2(pc10.dim("\nNo table changes, so no migration."));
|
|
3474
|
+
}
|
|
3219
3475
|
}
|
|
3220
3476
|
const descriptor = { path: relativePath, status: exists ? "outdated" : "missing", outcome: exists ? "update" : "create" };
|
|
3221
3477
|
return { name, mode: exists ? "update" : "create", written: [descriptor, ...written], migrations };
|
|
3222
3478
|
}
|
|
3223
3479
|
|
|
3224
3480
|
// src/commands/gen-endpoint.ts
|
|
3225
|
-
import { existsSync as
|
|
3226
|
-
import { join as
|
|
3227
|
-
import { kebabCase as kebabCase3, pascalCase as
|
|
3481
|
+
import { existsSync as existsSync17, mkdirSync as mkdirSync8, writeFileSync as writeFileSync11 } from "fs";
|
|
3482
|
+
import { join as join16 } from "path";
|
|
3483
|
+
import { kebabCase as kebabCase3, pascalCase as pascalCase3 } from "@flaredev/core";
|
|
3228
3484
|
import pc11 from "picocolors";
|
|
3229
3485
|
var METHODS = ["GET", "POST", "PATCH", "PUT", "DELETE"];
|
|
3230
3486
|
var ACTIONS = ["read", "create", "update", "delete"];
|
|
@@ -3236,7 +3492,7 @@ async function genEndpoint(resourceName2, rawName, options2 = {}) {
|
|
|
3236
3492
|
const name = kebabCase3(rawName);
|
|
3237
3493
|
if (!name) throw new Error(`Invalid endpoint name "${rawName}".`);
|
|
3238
3494
|
const resources = await loadResources(appRoot);
|
|
3239
|
-
const wanted =
|
|
3495
|
+
const wanted = pascalCase3(resourceName2);
|
|
3240
3496
|
const entry = resources.find(({ resource: resource2 }) => resource2.name === wanted);
|
|
3241
3497
|
if (!entry) {
|
|
3242
3498
|
throw new Error(`No resource "${resourceName2}". Available: ${resources.map(({ resource: resource2 }) => resource2.name).join(", ") || "none"}.`);
|
|
@@ -3245,11 +3501,11 @@ async function genEndpoint(resourceName2, rawName, options2 = {}) {
|
|
|
3245
3501
|
if (!ACTIONS.includes(action)) throw new Error(`Unknown action "${action}". Use one of ${ACTIONS.join(", ")}.`);
|
|
3246
3502
|
const { resource, stem } = entry;
|
|
3247
3503
|
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(
|
|
3504
|
+
const path = join16(appRoot, relative2);
|
|
3505
|
+
if (existsSync17(path)) throw new Error(`${relative2} already exists.`);
|
|
3506
|
+
mkdirSync8(join16(path, ".."), { recursive: true });
|
|
3251
3507
|
const route = options2.record ? `/api/${resource.slug}/[id]/${name}` : `/api/${resource.slug}/${name}`;
|
|
3252
|
-
|
|
3508
|
+
writeFileSync11(path, renderEndpoint({ resource: resource.name, stem, method, action, record: Boolean(options2.record), name, route }));
|
|
3253
3509
|
log2(`${pc11.green("create".padEnd(9))} ${relative2}`);
|
|
3254
3510
|
log2(`
|
|
3255
3511
|
It's an ordinary route handler \u2014 edit it freely; regeneration leaves it alone.`);
|
|
@@ -3285,8 +3541,8 @@ ${body}
|
|
|
3285
3541
|
}
|
|
3286
3542
|
|
|
3287
3543
|
// src/commands/gen-migration.ts
|
|
3288
|
-
import { existsSync as
|
|
3289
|
-
import { basename as basename2, join as
|
|
3544
|
+
import { existsSync as existsSync18, mkdirSync as mkdirSync9, writeFileSync as writeFileSync12 } from "fs";
|
|
3545
|
+
import { basename as basename2, join as join17 } from "path";
|
|
3290
3546
|
import { snakeCase } from "@flaredev/core";
|
|
3291
3547
|
import pc12 from "picocolors";
|
|
3292
3548
|
var DOWN_TEMPLATE = (up) => `-- Rollback for ${up}, run by \`flare migrate:rollback\`.
|
|
@@ -3307,12 +3563,12 @@ async function genMigration(rawName, options2 = {}) {
|
|
|
3307
3563
|
for (const file of files) log2(`${pc12.green("create".padEnd(9))} ${file}`);
|
|
3308
3564
|
for (const repair of repairs) log2(pc12.dim(`repaired ${repair}`));
|
|
3309
3565
|
if (!options2.fromSchema) {
|
|
3310
|
-
const downDir =
|
|
3566
|
+
const downDir = join17(appRoot, "migrations", "down");
|
|
3311
3567
|
mkdirSync9(downDir, { recursive: true });
|
|
3312
3568
|
for (const file of files) {
|
|
3313
|
-
const down =
|
|
3314
|
-
if (
|
|
3315
|
-
|
|
3569
|
+
const down = join17(downDir, basename2(file));
|
|
3570
|
+
if (existsSync18(down)) continue;
|
|
3571
|
+
writeFileSync12(down, DOWN_TEMPLATE(basename2(file)));
|
|
3316
3572
|
log2(`${pc12.green("create".padEnd(9))} migrations/down/${basename2(file)}`);
|
|
3317
3573
|
}
|
|
3318
3574
|
log2(`
|
|
@@ -3325,8 +3581,8 @@ Next: ${pc12.bold("flare migrate")}.`);
|
|
|
3325
3581
|
}
|
|
3326
3582
|
|
|
3327
3583
|
// src/commands/gen-policy.ts
|
|
3328
|
-
import { existsSync as
|
|
3329
|
-
import { dirname as dirname9, join as
|
|
3584
|
+
import { existsSync as existsSync19, mkdirSync as mkdirSync10, writeFileSync as writeFileSync13 } from "fs";
|
|
3585
|
+
import { dirname as dirname9, join as join18 } from "path";
|
|
3330
3586
|
import { definePolicy } from "@flaredev/core";
|
|
3331
3587
|
import pc13 from "picocolors";
|
|
3332
3588
|
function parseRoles(input, what = "--roles") {
|
|
@@ -3344,8 +3600,8 @@ async function genPolicy(rawName, options2) {
|
|
|
3344
3600
|
throw new Error(`No resource "${name}". Generate it first. Known resources: ${known}.`);
|
|
3345
3601
|
}
|
|
3346
3602
|
const relative2 = policyPath(name);
|
|
3347
|
-
const path =
|
|
3348
|
-
const exists =
|
|
3603
|
+
const path = join18(appRoot, relative2);
|
|
3604
|
+
const exists = existsSync19(path);
|
|
3349
3605
|
let roles;
|
|
3350
3606
|
let deleteRoles;
|
|
3351
3607
|
if (options2.roles || !exists) {
|
|
@@ -3357,7 +3613,7 @@ async function genPolicy(rawName, options2) {
|
|
|
3357
3613
|
const block = renderPolicyBlock({ read: roles, create: roles, update: roles, delete: deleteRoles });
|
|
3358
3614
|
if (!exists) {
|
|
3359
3615
|
mkdirSync10(dirname9(path), { recursive: true });
|
|
3360
|
-
|
|
3616
|
+
writeFileSync13(path, renderPolicy(name, { read: roles, create: roles, update: roles, delete: deleteRoles }));
|
|
3361
3617
|
log2(`${pc13.green("create".padEnd(9))} ${relative2}`);
|
|
3362
3618
|
} else {
|
|
3363
3619
|
try {
|
|
@@ -3372,16 +3628,16 @@ async function genPolicy(rawName, options2) {
|
|
|
3372
3628
|
}
|
|
3373
3629
|
}
|
|
3374
3630
|
const policies = await loadPolicies(appRoot);
|
|
3375
|
-
logResults(applyPlan(appRoot, planFiles(resources, policies)), log2);
|
|
3631
|
+
logResults(applyPlan(appRoot, planFiles(resources, policies, readStack(appRoot))), log2);
|
|
3376
3632
|
log2(`
|
|
3377
3633
|
${pc13.dim("Roles come from `flare role:add`; the API and the admin both enforce this policy.")}`);
|
|
3378
3634
|
return { name, path: relative2 };
|
|
3379
3635
|
}
|
|
3380
3636
|
|
|
3381
3637
|
// src/commands/migrate.ts
|
|
3382
|
-
import { existsSync as
|
|
3638
|
+
import { existsSync as existsSync20, mkdtempSync, readFileSync as readFileSync12, rmSync as rmSync3, writeFileSync as writeFileSync14 } from "fs";
|
|
3383
3639
|
import { tmpdir } from "os";
|
|
3384
|
-
import { join as
|
|
3640
|
+
import { join as join19 } from "path";
|
|
3385
3641
|
import pc14 from "picocolors";
|
|
3386
3642
|
|
|
3387
3643
|
// src/generator/down.ts
|
|
@@ -3475,7 +3731,17 @@ function context(options2) {
|
|
|
3475
3731
|
};
|
|
3476
3732
|
}
|
|
3477
3733
|
async function migrate(options2 = {}) {
|
|
3478
|
-
const
|
|
3734
|
+
const appRoot = findAppRoot(options2.cwd ?? process.cwd());
|
|
3735
|
+
const log2 = options2.log ?? ((message) => console.log(message));
|
|
3736
|
+
if (readStack(appRoot) === "next") {
|
|
3737
|
+
if (options2.remote) {
|
|
3738
|
+
log2(pc14.dim("On Next.js, `--remote` is whichever database DATABASE_URL points at."));
|
|
3739
|
+
}
|
|
3740
|
+
const prisma = resolveBin(appRoot, "prisma", "prisma");
|
|
3741
|
+
log2(`${pc14.cyan("prisma")} migrate deploy`);
|
|
3742
|
+
return runNode([prisma, "migrate", "deploy"], appRoot);
|
|
3743
|
+
}
|
|
3744
|
+
const { db, target, wrangler } = context(options2);
|
|
3479
3745
|
const result = await runWrangler(wrangler, ["d1", "migrations", "apply", db.binding, ...target], appRoot);
|
|
3480
3746
|
return result.code;
|
|
3481
3747
|
}
|
|
@@ -3499,7 +3765,7 @@ async function rollback(options2 = {}) {
|
|
|
3499
3765
|
const steps = options2.steps ?? 1;
|
|
3500
3766
|
if (!Number.isInteger(steps) || steps < 1) throw new Error("--steps must be a positive integer.");
|
|
3501
3767
|
const { appRoot, db, target, where, wrangler, log: log2 } = context(options2);
|
|
3502
|
-
const migrationsDir =
|
|
3768
|
+
const migrationsDir = join19(appRoot, db.migrations_dir ?? "migrations");
|
|
3503
3769
|
const applied = await appliedMigrations(options2, steps);
|
|
3504
3770
|
if (applied.length === 0) {
|
|
3505
3771
|
log2("Nothing to roll back.");
|
|
@@ -3508,18 +3774,18 @@ async function rollback(options2 = {}) {
|
|
|
3508
3774
|
const plan = [];
|
|
3509
3775
|
for (const { name } of applied) {
|
|
3510
3776
|
if (!/^[\w.-]+\.sql$/.test(name)) throw new Error(`Unexpected migration name "${name}" in d1_migrations.`);
|
|
3511
|
-
const handWritten =
|
|
3512
|
-
if (
|
|
3513
|
-
const sql2 =
|
|
3777
|
+
const handWritten = join19(migrationsDir, "down", name);
|
|
3778
|
+
if (existsSync20(handWritten)) {
|
|
3779
|
+
const sql2 = readFileSync12(handWritten, "utf8");
|
|
3514
3780
|
if (splitStatements(sql2).length === 0) {
|
|
3515
3781
|
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
3782
|
}
|
|
3517
3783
|
plan.push({ name, sql: sql2, source: `migrations/down/${name}` });
|
|
3518
3784
|
continue;
|
|
3519
3785
|
}
|
|
3520
|
-
const upPath =
|
|
3521
|
-
if (!
|
|
3522
|
-
const down = deriveDown(
|
|
3786
|
+
const upPath = join19(migrationsDir, name);
|
|
3787
|
+
if (!existsSync20(upPath)) throw new Error(`${name} was applied but migrations/${name} is missing, so it can't be reversed.`);
|
|
3788
|
+
const down = deriveDown(readFileSync12(upPath, "utf8"));
|
|
3523
3789
|
if (!down.reversible) {
|
|
3524
3790
|
throw new Error(
|
|
3525
3791
|
`Can't roll back ${name}: ${down.reason}
|
|
@@ -3539,15 +3805,15 @@ Write the reverse SQL to migrations/down/${name} and run the rollback again. Not
|
|
|
3539
3805
|
}
|
|
3540
3806
|
const script = plan.map(({ name, sql: sql2 }) => `${sql2.trim()}
|
|
3541
3807
|
DELETE FROM d1_migrations WHERE name = '${name.replace(/'/g, "''")}';`).join("\n") + "\n";
|
|
3542
|
-
const dir = mkdtempSync(
|
|
3808
|
+
const dir = mkdtempSync(join19(tmpdir(), "flare-rollback-"));
|
|
3543
3809
|
try {
|
|
3544
|
-
const file =
|
|
3545
|
-
|
|
3810
|
+
const file = join19(dir, "rollback.sql");
|
|
3811
|
+
writeFileSync14(file, script);
|
|
3546
3812
|
const result = await runWrangler(wrangler, ["d1", "execute", db.binding, ...target, "--file", file], appRoot, { capture: true });
|
|
3547
3813
|
if (result.code !== 0) throw new Error(`Rollback failed:
|
|
3548
3814
|
${result.output.trim()}`);
|
|
3549
3815
|
} finally {
|
|
3550
|
-
|
|
3816
|
+
rmSync3(dir, { recursive: true, force: true });
|
|
3551
3817
|
}
|
|
3552
3818
|
log2(pc14.green(`
|
|
3553
3819
|
Rolled back ${plan.map((step) => step.name).join(", ")}.`));
|
|
@@ -3555,8 +3821,8 @@ Rolled back ${plan.map((step) => step.name).join(", ")}.`));
|
|
|
3555
3821
|
}
|
|
3556
3822
|
|
|
3557
3823
|
// src/commands/rm.ts
|
|
3558
|
-
import { existsSync as
|
|
3559
|
-
import { dirname as dirname10, join as
|
|
3824
|
+
import { existsSync as existsSync21, readdirSync as readdirSync7, readFileSync as readFileSync13, rmdirSync, unlinkSync as unlinkSync2 } from "fs";
|
|
3825
|
+
import { dirname as dirname10, join as join20 } from "path";
|
|
3560
3826
|
import { relationGraph as relationGraph3 } from "@flaredev/core";
|
|
3561
3827
|
import pc15 from "picocolors";
|
|
3562
3828
|
var squash = (text) => text.replace(/\r\n/g, "\n").replace(/\s+/g, " ").trim();
|
|
@@ -3575,10 +3841,10 @@ function handWrittenReason(source, header, options2 = {}) {
|
|
|
3575
3841
|
return null;
|
|
3576
3842
|
}
|
|
3577
3843
|
function removeEmptyDirs(appRoot, start) {
|
|
3578
|
-
let dir = dirname10(
|
|
3579
|
-
const roots = [
|
|
3844
|
+
let dir = dirname10(join20(appRoot, start));
|
|
3845
|
+
const roots = [join20(appRoot, "app", "api"), join20(appRoot, "app", "dashboard")];
|
|
3580
3846
|
const inside = () => roots.some((root) => dir.startsWith(root) && dir !== root);
|
|
3581
|
-
while (inside() &&
|
|
3847
|
+
while (inside() && existsSync21(dir) && readdirSync7(dir).length === 0) {
|
|
3582
3848
|
rmdirSync(dir);
|
|
3583
3849
|
dir = dirname10(dir);
|
|
3584
3850
|
}
|
|
@@ -3606,8 +3872,8 @@ async function rmResource(rawName, options2 = {}) {
|
|
|
3606
3872
|
const owned = /* @__PURE__ */ new Set();
|
|
3607
3873
|
if (entry) {
|
|
3608
3874
|
owned.add(entry.file);
|
|
3609
|
-
for (const file of planFiles(all)) {
|
|
3610
|
-
if (
|
|
3875
|
+
for (const file of planFiles(all, void 0, readStack(appRoot))) {
|
|
3876
|
+
if (existsSync21(join20(appRoot, file.path)) && file.header === resourceHeader(name)) owned.add(file.path);
|
|
3611
3877
|
}
|
|
3612
3878
|
}
|
|
3613
3879
|
for (const orphan of findOrphans(appRoot, remaining)) if (orphan.resource === name) owned.add(orphan.path);
|
|
@@ -3616,7 +3882,7 @@ async function rmResource(rawName, options2 = {}) {
|
|
|
3616
3882
|
if (!entry && owned.size === 0) throw new Error(`No resource "${name}" (no ${descriptorPath(name)} and no generated files).`);
|
|
3617
3883
|
const findings = [];
|
|
3618
3884
|
for (const path of owned) {
|
|
3619
|
-
const source =
|
|
3885
|
+
const source = readFileSync13(join20(appRoot, path), "utf8");
|
|
3620
3886
|
let wrapper;
|
|
3621
3887
|
if (entry && path === entry.file) {
|
|
3622
3888
|
const standard = splitMarkers(renderDescriptor(name, [{ key: "placeholder", kind: "string", required: true, unique: false }]));
|
|
@@ -3637,15 +3903,15 @@ Move what you want to keep, or pass --force to delete anyway.`);
|
|
|
3637
3903
|
}
|
|
3638
3904
|
const removed = [...owned].sort();
|
|
3639
3905
|
for (const path of removed) {
|
|
3640
|
-
unlinkSync2(
|
|
3906
|
+
unlinkSync2(join20(appRoot, path));
|
|
3641
3907
|
removeEmptyDirs(appRoot, path);
|
|
3642
3908
|
log2(`${pc15.red("remove".padEnd(9))} ${path}`);
|
|
3643
3909
|
}
|
|
3644
|
-
logResults(applyPlan(appRoot, planFiles(remaining, policy ? await loadPolicies(appRoot) : void 0)), log2);
|
|
3910
|
+
logResults(applyPlan(appRoot, planFiles(remaining, policy ? await loadPolicies(appRoot) : void 0, readStack(appRoot))), log2);
|
|
3645
3911
|
const table = entry ? tableExport(entry.resource) : void 0;
|
|
3646
|
-
if (table &&
|
|
3647
|
-
const seeds = readdirSync7(
|
|
3648
|
-
(file) => new RegExp(`\\b${table}\\b`).test(
|
|
3912
|
+
if (table && existsSync21(join20(appRoot, "seeds"))) {
|
|
3913
|
+
const seeds = readdirSync7(join20(appRoot, "seeds")).filter(
|
|
3914
|
+
(file) => new RegExp(`\\b${table}\\b`).test(readFileSync13(join20(appRoot, "seeds", file), "utf8"))
|
|
3649
3915
|
);
|
|
3650
3916
|
if (seeds.length) log2(pc15.yellow(`
|
|
3651
3917
|
These seeds still use ${table}: ${seeds.map((s) => `seeds/${s}`).join(", ")}.`));
|
|
@@ -3715,9 +3981,9 @@ ${result.output.trim()}`);
|
|
|
3715
3981
|
}
|
|
3716
3982
|
|
|
3717
3983
|
// src/commands/seed.ts
|
|
3718
|
-
import { existsSync as
|
|
3719
|
-
import { join as
|
|
3720
|
-
import { createFake, kebabCase as kebabCase4, storedFields as
|
|
3984
|
+
import { existsSync as existsSync22, mkdirSync as mkdirSync11, readdirSync as readdirSync8, rmSync as rmSync4, writeFileSync as writeFileSync15 } from "fs";
|
|
3985
|
+
import { join as join22 } from "path";
|
|
3986
|
+
import { camelCase as camelCase6, createFake, kebabCase as kebabCase4, storedFields as storedFields3 } from "@flaredev/core";
|
|
3721
3987
|
import { createJiti as createJiti4 } from "jiti";
|
|
3722
3988
|
import pc17 from "picocolors";
|
|
3723
3989
|
|
|
@@ -3814,16 +4080,16 @@ function createInsertMany(sink, drizzle, onProgress) {
|
|
|
3814
4080
|
|
|
3815
4081
|
// src/seed/local-d1.ts
|
|
3816
4082
|
import { createRequire } from "module";
|
|
3817
|
-
import { join as
|
|
4083
|
+
import { join as join21 } from "path";
|
|
3818
4084
|
import { pathToFileURL } from "url";
|
|
3819
4085
|
var LOCAL_STATE = ".wrangler/state/v3";
|
|
3820
4086
|
async function openLocalD1(appRoot, binding) {
|
|
3821
4087
|
process.env.WRANGLER_LOG ??= "error";
|
|
3822
|
-
const appRequire = createRequire(
|
|
4088
|
+
const appRequire = createRequire(join21(appRoot, "package.json"));
|
|
3823
4089
|
const wrangler = await import(pathToFileURL(appRequire.resolve("wrangler")).href);
|
|
3824
4090
|
const { env, dispose } = await wrangler.getPlatformProxy({
|
|
3825
|
-
configPath:
|
|
3826
|
-
persist: { path:
|
|
4091
|
+
configPath: join21(appRoot, "wrangler.jsonc"),
|
|
4092
|
+
persist: { path: join21(appRoot, LOCAL_STATE) }
|
|
3827
4093
|
});
|
|
3828
4094
|
const bindings = env;
|
|
3829
4095
|
const name = binding ?? "DB";
|
|
@@ -3838,8 +4104,8 @@ async function openLocalD1(appRoot, binding) {
|
|
|
3838
4104
|
// src/commands/seed.ts
|
|
3839
4105
|
var SEEDS_DIR = "seeds";
|
|
3840
4106
|
function seedFiles(appRoot, names) {
|
|
3841
|
-
const dir =
|
|
3842
|
-
const all =
|
|
4107
|
+
const dir = join22(appRoot, SEEDS_DIR);
|
|
4108
|
+
const all = existsSync22(dir) ? readdirSync8(dir).filter((file) => file.endsWith(".seed.ts")).sort() : [];
|
|
3843
4109
|
if (!names?.length) return all;
|
|
3844
4110
|
return names.map((name) => {
|
|
3845
4111
|
const file = `${name.replace(/\.seed\.ts$/, "")}.seed.ts`;
|
|
@@ -3857,16 +4123,20 @@ async function runSeeds(options2 = {}) {
|
|
|
3857
4123
|
log2(`No seeds found. Create one with ${pc17.bold("flare seed:make <name>")}.`);
|
|
3858
4124
|
return;
|
|
3859
4125
|
}
|
|
3860
|
-
|
|
4126
|
+
if (readStack(appRoot) === "next") {
|
|
4127
|
+
await runPrismaSeeds(appRoot, files, log2);
|
|
4128
|
+
return;
|
|
4129
|
+
}
|
|
4130
|
+
const jiti = createJiti4(join22(appRoot, "package.json"), { alias: { "@": appRoot }, moduleCache: false, fsCache: false });
|
|
3861
4131
|
const { drizzle } = await jiti.import("drizzle-orm/d1");
|
|
3862
4132
|
const drizzleHelpers = await jiti.import("drizzle-orm");
|
|
3863
|
-
const schema = await jiti.import(
|
|
4133
|
+
const schema = await jiti.import(join22(appRoot, "db/schema.ts"));
|
|
3864
4134
|
const local = await openLocalD1(appRoot);
|
|
3865
4135
|
try {
|
|
3866
4136
|
const db = drizzle(local.db, { schema });
|
|
3867
4137
|
const insertMany = createInsertMany(local.db, drizzleHelpers);
|
|
3868
4138
|
for (const file of files) {
|
|
3869
|
-
const mod = await jiti.import(
|
|
4139
|
+
const mod = await jiti.import(join22(appRoot, SEEDS_DIR, file));
|
|
3870
4140
|
if (typeof mod.default !== "function") throw new Error(`${SEEDS_DIR}/${file} must \`export default defineSeed(async ({ db }) => { ... })\`.`);
|
|
3871
4141
|
log2(`${pc17.cyan("seed")} ${file}`);
|
|
3872
4142
|
await mod.default({ db, env: local.env, insertMany, fake: createFake(), log: (message) => log2(` ${message}`) });
|
|
@@ -3916,9 +4186,17 @@ function sampleValue(key, def) {
|
|
|
3916
4186
|
return void 0;
|
|
3917
4187
|
}
|
|
3918
4188
|
}
|
|
3919
|
-
function renderSeed(resource) {
|
|
4189
|
+
function renderSeed(resource, stack = "cloudflare") {
|
|
3920
4190
|
if (!resource) {
|
|
3921
|
-
return `import { defineSeed } from "@flaredev/core";
|
|
4191
|
+
return stack === "next" ? `import { defineSeed } from "@flaredev/core";
|
|
4192
|
+
|
|
4193
|
+
export default defineSeed(async ({ db, insertMany, fake, log }) => {
|
|
4194
|
+
// \`db\` is this app's Prisma client, e.g.:
|
|
4195
|
+
// await db.contact.create({ data: { name: "Ada", email: "ada@example.com" } });
|
|
4196
|
+
// Or many at once: await insertMany("contact", [{ name: fake.fullName() }]);
|
|
4197
|
+
log("nothing to seed yet");
|
|
4198
|
+
});
|
|
4199
|
+
` : `import { defineSeed } from "@flaredev/core";
|
|
3922
4200
|
|
|
3923
4201
|
export default defineSeed(async ({ db, insertMany, fake, log }) => {
|
|
3924
4202
|
// Insert rows with Drizzle, e.g.:
|
|
@@ -3931,11 +4209,30 @@ export default defineSeed(async ({ db, insertMany, fake, log }) => {
|
|
|
3931
4209
|
const table = tableExport(resource);
|
|
3932
4210
|
const lines = [];
|
|
3933
4211
|
const todos = [];
|
|
3934
|
-
for (const [key, def] of
|
|
4212
|
+
for (const [key, def] of storedFields3(resource)) {
|
|
3935
4213
|
const value = sampleValue(key, def);
|
|
3936
4214
|
if (value !== void 0) lines.push(` ${key}: ${value},`);
|
|
3937
4215
|
else if (def.required) todos.push(` // ${key}: TODO (${def.kind === "belongsTo" ? `id of an existing ${def.target}` : "R2 object key"}),`);
|
|
3938
4216
|
}
|
|
4217
|
+
if (stack === "next") {
|
|
4218
|
+
const body = [...lines, ...todos].map((line) => ` ${line}`).join("\n");
|
|
4219
|
+
return `import { defineSeed } from "@flaredev/core";
|
|
4220
|
+
|
|
4221
|
+
/** How many to create. createMany sends them in one statement. */
|
|
4222
|
+
const COUNT = 50;
|
|
4223
|
+
|
|
4224
|
+
export default defineSeed(async ({ insertMany, fake, log }) => {
|
|
4225
|
+
await insertMany(
|
|
4226
|
+
${JSON.stringify(camelCase6(resource.name))},
|
|
4227
|
+
Array.from({ length: COUNT }, () => ({
|
|
4228
|
+
${body}
|
|
4229
|
+
updatedAt: new Date(),
|
|
4230
|
+
})),
|
|
4231
|
+
);
|
|
4232
|
+
log(\`inserted \${COUNT.toLocaleString()} ${resource.pluralLabel.toLowerCase()}\`);
|
|
4233
|
+
});
|
|
4234
|
+
`;
|
|
4235
|
+
}
|
|
3939
4236
|
return `import { defineSeed } from "@flaredev/core";
|
|
3940
4237
|
import { ${table} } from "@/db/schema";
|
|
3941
4238
|
|
|
@@ -3956,8 +4253,8 @@ async function makeSeed(rawName, options2 = {}) {
|
|
|
3956
4253
|
const name = kebabCase4(rawName);
|
|
3957
4254
|
if (!name) throw new Error(`Invalid seed name "${rawName}".`);
|
|
3958
4255
|
const relative2 = `${SEEDS_DIR}/${name}.seed.ts`;
|
|
3959
|
-
const path =
|
|
3960
|
-
if (
|
|
4256
|
+
const path = join22(appRoot, relative2);
|
|
4257
|
+
if (existsSync22(path)) throw new Error(`${relative2} already exists.`);
|
|
3961
4258
|
const resources = (await loadResources(appRoot)).map(({ resource: resource2 }) => resource2);
|
|
3962
4259
|
let resource;
|
|
3963
4260
|
if (options2.resource) {
|
|
@@ -3966,18 +4263,58 @@ async function makeSeed(rawName, options2 = {}) {
|
|
|
3966
4263
|
} else {
|
|
3967
4264
|
resource = resources.find((r) => [kebabCase4(r.name), r.slug].includes(name));
|
|
3968
4265
|
}
|
|
3969
|
-
mkdirSync11(
|
|
3970
|
-
|
|
4266
|
+
mkdirSync11(join22(appRoot, SEEDS_DIR), { recursive: true });
|
|
4267
|
+
writeFileSync15(path, renderSeed(resource, readStack(appRoot)));
|
|
3971
4268
|
log2(`${pc17.green("create".padEnd(9))} ${relative2}`);
|
|
3972
4269
|
log2(`
|
|
3973
4270
|
Run it with ${pc17.bold(`flare seed ${name}`)}.`);
|
|
3974
4271
|
return relative2;
|
|
3975
4272
|
}
|
|
4273
|
+
async function runPrismaSeeds(appRoot, files, log2) {
|
|
4274
|
+
const runner = join22(appRoot, ".flare-seed-runner.mts");
|
|
4275
|
+
const script = [
|
|
4276
|
+
`import "dotenv/config";`,
|
|
4277
|
+
`import { createFake } from "@flaredev/core";`,
|
|
4278
|
+
`import { prisma } from "./lib/db";`,
|
|
4279
|
+
...files.map((file, index) => `import seed${index} from "./${SEEDS_DIR}/${file.replace(/\.ts$/, "")}";`),
|
|
4280
|
+
"",
|
|
4281
|
+
`const files = ${JSON.stringify(files)};`,
|
|
4282
|
+
`const seeds = [${files.map((_, index) => `seed${index}`).join(", ")}];`,
|
|
4283
|
+
`const client = prisma as unknown as Record<string, { createMany(args: { data: unknown[] }): Promise<unknown> }>;`,
|
|
4284
|
+
"",
|
|
4285
|
+
`for (const [index, seed] of seeds.entries()) {`,
|
|
4286
|
+
` console.log("seed " + files[index]);`,
|
|
4287
|
+
` await seed({`,
|
|
4288
|
+
` db: prisma,`,
|
|
4289
|
+
` env: process.env,`,
|
|
4290
|
+
` insertMany: async (model: string, rows: Record<string, unknown>[]) => {`,
|
|
4291
|
+
` const delegate = client[model];`,
|
|
4292
|
+
` if (!delegate) throw new Error('No Prisma model "' + model + '". Use the camelCase name, e.g. "orderItem".');`,
|
|
4293
|
+
` await delegate.createMany({ data: rows });`,
|
|
4294
|
+
` },`,
|
|
4295
|
+
` fake: createFake(),`,
|
|
4296
|
+
` log: (message: string) => console.log(" " + message),`,
|
|
4297
|
+
` });`,
|
|
4298
|
+
`}`,
|
|
4299
|
+
`await (prisma as unknown as { $disconnect(): Promise<void> }).$disconnect();`,
|
|
4300
|
+
""
|
|
4301
|
+
].join("\n");
|
|
4302
|
+
writeFileSync15(runner, script);
|
|
4303
|
+
try {
|
|
4304
|
+
const tsx = resolveBin(appRoot, "tsx", "tsx");
|
|
4305
|
+
const code = await runNode([tsx, runner], appRoot);
|
|
4306
|
+
if (code !== 0) throw new Error("A seed failed; nothing further was run.");
|
|
4307
|
+
log2(pc17.green(`
|
|
4308
|
+
Ran ${files.length} seed(s).`));
|
|
4309
|
+
} finally {
|
|
4310
|
+
rmSync4(runner, { force: true });
|
|
4311
|
+
}
|
|
4312
|
+
}
|
|
3976
4313
|
|
|
3977
4314
|
// src/commands/db-push.ts
|
|
3978
|
-
import { mkdtempSync as mkdtempSync2, rmSync as
|
|
4315
|
+
import { mkdtempSync as mkdtempSync2, rmSync as rmSync5, writeFileSync as writeFileSync16 } from "fs";
|
|
3979
4316
|
import { tmpdir as tmpdir2 } from "os";
|
|
3980
|
-
import { join as
|
|
4317
|
+
import { join as join23 } from "path";
|
|
3981
4318
|
import pc18 from "picocolors";
|
|
3982
4319
|
|
|
3983
4320
|
// src/terminal.ts
|
|
@@ -4042,7 +4379,7 @@ async function dbPush(tables, options2 = {}) {
|
|
|
4042
4379
|
const wrangler = resolveBin(appRoot, "wrangler", "wrangler");
|
|
4043
4380
|
const started = Date.now();
|
|
4044
4381
|
const local = await openLocalD1(appRoot, options2.database);
|
|
4045
|
-
const dir = mkdtempSync2(
|
|
4382
|
+
const dir = mkdtempSync2(join23(tmpdir2(), "flare-push-"));
|
|
4046
4383
|
try {
|
|
4047
4384
|
const statements = [];
|
|
4048
4385
|
const counts = [];
|
|
@@ -4068,8 +4405,8 @@ async function dbPush(tables, options2 = {}) {
|
|
|
4068
4405
|
log2("Nothing to push: those tables are empty locally.");
|
|
4069
4406
|
return { tables: counts, ms: Date.now() - started };
|
|
4070
4407
|
}
|
|
4071
|
-
const file =
|
|
4072
|
-
|
|
4408
|
+
const file = join23(dir, "push.sql");
|
|
4409
|
+
writeFileSync16(file, statements.join("\n"));
|
|
4073
4410
|
const target = ["--remote", ...options2.env ? ["--env", options2.env] : []];
|
|
4074
4411
|
const result = await runWrangler(wrangler, ["d1", "execute", db.binding, ...target, "--yes", "--file", file], appRoot, { capture: true });
|
|
4075
4412
|
if (result.code !== 0) throw new Error(`Pushing to ${db.database_name} failed:
|
|
@@ -4078,19 +4415,19 @@ ${result.output.trim()}`);
|
|
|
4078
4415
|
log2(`${pc18.green("\u2714")} Pushed ${pc18.bold(formatCount(total))} rows to ${pc18.bold(db.database_name)} ${pc18.dim(`(${formatDuration(ms)})`)}`);
|
|
4079
4416
|
return { tables: counts, ms };
|
|
4080
4417
|
} finally {
|
|
4081
|
-
|
|
4418
|
+
rmSync5(dir, { recursive: true, force: true });
|
|
4082
4419
|
await local.dispose();
|
|
4083
4420
|
}
|
|
4084
4421
|
}
|
|
4085
4422
|
|
|
4086
4423
|
// src/commands/seed-resource.ts
|
|
4087
|
-
import { mkdtempSync as mkdtempSync3, rmSync as
|
|
4424
|
+
import { mkdtempSync as mkdtempSync3, rmSync as rmSync6, writeFileSync as writeFileSync17 } from "fs";
|
|
4088
4425
|
import { tmpdir as tmpdir3 } from "os";
|
|
4089
|
-
import { join as
|
|
4426
|
+
import { join as join24 } from "path";
|
|
4090
4427
|
import pc19 from "picocolors";
|
|
4091
4428
|
|
|
4092
4429
|
// src/seed/rows.ts
|
|
4093
|
-
import { columnName as
|
|
4430
|
+
import { columnName as columnName3, createFake as createFake2, storedFields as storedFields4 } from "@flaredev/core";
|
|
4094
4431
|
var has = (key, ...words) => words.some((word) => key.includes(word));
|
|
4095
4432
|
function stringValue(key, def, fake, resource, index) {
|
|
4096
4433
|
switch (def.format) {
|
|
@@ -4195,12 +4532,12 @@ function digitalish(resource) {
|
|
|
4195
4532
|
);
|
|
4196
4533
|
}
|
|
4197
4534
|
function seedColumns(resource) {
|
|
4198
|
-
return ["id", ...
|
|
4535
|
+
return ["id", ...storedFields4(resource).map(([key]) => columnName3(key)), "created_at", "updated_at"];
|
|
4199
4536
|
}
|
|
4200
4537
|
function* seedRows(resource, count, options2 = {}) {
|
|
4201
4538
|
const fake = createFake2(options2.seed ?? Math.floor(Math.random() * 2 ** 31));
|
|
4202
4539
|
const parentIds = options2.parentIds ?? /* @__PURE__ */ new Map();
|
|
4203
|
-
const fields =
|
|
4540
|
+
const fields = storedFields4(resource);
|
|
4204
4541
|
const now = options2.now ?? Date.now();
|
|
4205
4542
|
const start = options2.startIndex ?? 0;
|
|
4206
4543
|
for (let i = 0; i < count; i++) {
|
|
@@ -4209,7 +4546,7 @@ function* seedRows(resource, count, options2 = {}) {
|
|
|
4209
4546
|
for (const [key, def] of fields) {
|
|
4210
4547
|
let value = fieldValue(key, def, fake, resource, parentIds, number);
|
|
4211
4548
|
if (typeof value === "string" && "unique" in def && def.unique) value = uniquify(value, number + 1);
|
|
4212
|
-
row[
|
|
4549
|
+
row[columnName3(key)] = value;
|
|
4213
4550
|
}
|
|
4214
4551
|
const created = now - fake.int(0, 365) * 864e5 - fake.int(0, 86399999);
|
|
4215
4552
|
row.created_at = created;
|
|
@@ -4288,6 +4625,11 @@ async function seedResourceCommand(name, options2 = {}) {
|
|
|
4288
4625
|
async function seedResource(name, options2 = {}) {
|
|
4289
4626
|
const log2 = options2.log ?? ((message) => console.log(message));
|
|
4290
4627
|
const appRoot = findAppRoot(options2.cwd ?? process.cwd());
|
|
4628
|
+
if (readStack(appRoot) === "next") {
|
|
4629
|
+
throw new Error(
|
|
4630
|
+
"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`."
|
|
4631
|
+
);
|
|
4632
|
+
}
|
|
4291
4633
|
const resources = (await loadResources(appRoot)).map(({ resource: resource2 }) => resource2);
|
|
4292
4634
|
const resource = pickResource(resources, name);
|
|
4293
4635
|
const total = parseCount(options2.count);
|
|
@@ -4320,8 +4662,8 @@ ${result.output.trim()}`);
|
|
|
4320
4662
|
const [counted] = await query(`SELECT count(*) AS total FROM "${resource.table}"`);
|
|
4321
4663
|
startIndex = counted?.total ?? 0;
|
|
4322
4664
|
}
|
|
4323
|
-
const dir = mkdtempSync3(
|
|
4324
|
-
const file =
|
|
4665
|
+
const dir = mkdtempSync3(join24(tmpdir3(), "flare-seed-"));
|
|
4666
|
+
const file = join24(dir, `${resource.table}.sql`);
|
|
4325
4667
|
try {
|
|
4326
4668
|
const statements = [];
|
|
4327
4669
|
if (options2.truncate) statements.push(`DELETE FROM "${resource.table}";`);
|
|
@@ -4331,13 +4673,13 @@ ${result.output.trim()}`);
|
|
|
4331
4673
|
written += statement.rows;
|
|
4332
4674
|
options2.onProgress?.(written, total);
|
|
4333
4675
|
}
|
|
4334
|
-
|
|
4676
|
+
writeFileSync17(file, statements.join("\n"));
|
|
4335
4677
|
log2(`${pc19.cyan("\u25CF")} Uploading ${total.toLocaleString()} rows to ${pc19.bold(db.database_name)}`);
|
|
4336
4678
|
const result = await runWrangler(wrangler, ["d1", "execute", db.binding, ...target, "--yes", "--file", file], appRoot, { capture: true });
|
|
4337
4679
|
if (result.code !== 0) throw new Error(`Seeding the remote database failed:
|
|
4338
4680
|
${result.output.trim()}`);
|
|
4339
4681
|
} finally {
|
|
4340
|
-
|
|
4682
|
+
rmSync6(dir, { recursive: true, force: true });
|
|
4341
4683
|
}
|
|
4342
4684
|
return { resource, rows: total, ms: Date.now() - started, where };
|
|
4343
4685
|
}
|
|
@@ -4364,17 +4706,17 @@ ${result.output.trim()}`);
|
|
|
4364
4706
|
|
|
4365
4707
|
// src/commands/billing-sync.ts
|
|
4366
4708
|
import { createRequire as createRequire2 } from "module";
|
|
4367
|
-
import { existsSync as
|
|
4709
|
+
import { existsSync as existsSync23, mkdtempSync as mkdtempSync4, readFileSync as readFileSync14, rmSync as rmSync7, writeFileSync as writeFileSync18 } from "fs";
|
|
4368
4710
|
import { tmpdir as tmpdir4 } from "os";
|
|
4369
|
-
import { join as
|
|
4711
|
+
import { join as join25 } from "path";
|
|
4370
4712
|
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
4371
4713
|
import { kebabCase as kebabCase5 } from "@flaredev/core";
|
|
4372
4714
|
import pc20 from "picocolors";
|
|
4373
4715
|
function stripeKeys(appRoot) {
|
|
4374
4716
|
const map = {};
|
|
4375
|
-
const path =
|
|
4376
|
-
if (
|
|
4377
|
-
for (const line of
|
|
4717
|
+
const path = join25(appRoot, ".dev.vars");
|
|
4718
|
+
if (existsSync23(path)) {
|
|
4719
|
+
for (const line of readFileSync14(path, "utf8").split(/\r?\n/)) {
|
|
4378
4720
|
const match = /^\s*([A-Z][A-Z0-9_]*)=(.*)$/.exec(line);
|
|
4379
4721
|
if (match) map[match[1]] = match[2].trim();
|
|
4380
4722
|
}
|
|
@@ -4457,10 +4799,10 @@ async function syncPlans(options2 = {}) {
|
|
|
4457
4799
|
if (!key) throw new Error("STRIPE_SECRET_KEY is not set in .dev.vars or the environment.");
|
|
4458
4800
|
const db = readD1Databases(appRoot)[0];
|
|
4459
4801
|
if (!db) throw new Error("No d1_databases in wrangler.jsonc.");
|
|
4460
|
-
const appRequire = createRequire2(
|
|
4802
|
+
const appRequire = createRequire2(join25(appRoot, "package.json"));
|
|
4461
4803
|
const stripeModule = await import(pathToFileURL2(appRequire.resolve("stripe")).href);
|
|
4462
4804
|
const stripe = new stripeModule.default(key, { apiVersion: "2026-08-26.dahlia" });
|
|
4463
|
-
const appName = JSON.parse(
|
|
4805
|
+
const appName = JSON.parse(readFileSync14(join25(appRoot, "package.json"), "utf8")).name ?? "app";
|
|
4464
4806
|
const everything = await stripe.products.list({ active: true, limit: 100 }).autoPagingToArray({ limit: 1e4 });
|
|
4465
4807
|
const products = options2.all ? everything : everything.filter((product) => product.metadata?.flare_app === appName);
|
|
4466
4808
|
if (products.length === 0) {
|
|
@@ -4481,15 +4823,15 @@ async function syncPlans(options2 = {}) {
|
|
|
4481
4823
|
${existingResult.output.trim()}`);
|
|
4482
4824
|
}
|
|
4483
4825
|
const known = new Set(extractJson(existingResult.output)[0]?.results.map((row) => row.stripe_price_id));
|
|
4484
|
-
const dir = mkdtempSync4(
|
|
4826
|
+
const dir = mkdtempSync4(join25(tmpdir4(), "flare-sync-"));
|
|
4485
4827
|
try {
|
|
4486
|
-
const file =
|
|
4487
|
-
|
|
4828
|
+
const file = join25(dir, "sync-plans.sql");
|
|
4829
|
+
writeFileSync18(file, renderSyncSql(rows));
|
|
4488
4830
|
const result = await runWrangler(wrangler, ["d1", "execute", db.binding, ...target, "--file", file], appRoot, { capture: true });
|
|
4489
4831
|
if (result.code !== 0) throw new Error(`Could not write plans:
|
|
4490
4832
|
${result.output.trim()}`);
|
|
4491
4833
|
} finally {
|
|
4492
|
-
|
|
4834
|
+
rmSync7(dir, { recursive: true, force: true });
|
|
4493
4835
|
}
|
|
4494
4836
|
let created = 0;
|
|
4495
4837
|
for (const row of rows) {
|
|
@@ -4507,16 +4849,16 @@ ${rows.length} plan(s) synced to the ${options2.remote ? "remote" : "local"} dat
|
|
|
4507
4849
|
}
|
|
4508
4850
|
|
|
4509
4851
|
// src/commands/sync.ts
|
|
4510
|
-
import { existsSync as
|
|
4511
|
-
import { join as
|
|
4852
|
+
import { existsSync as existsSync24 } from "fs";
|
|
4853
|
+
import { join as join26 } from "path";
|
|
4512
4854
|
import pc21 from "picocolors";
|
|
4513
4855
|
async function syncTypes(options2 = {}) {
|
|
4514
4856
|
const log2 = options2.log ?? ((message) => console.log(message));
|
|
4515
4857
|
const appRoot = findAppRoot(options2.cwd ?? process.cwd());
|
|
4516
4858
|
const all = await loadResources(appRoot);
|
|
4517
4859
|
const policies = await loadPolicies(appRoot);
|
|
4518
|
-
const tracksPolicies = policies.length > 0 ||
|
|
4519
|
-
const plan = planFiles(all, tracksPolicies ? policies : void 0);
|
|
4860
|
+
const tracksPolicies = policies.length > 0 || existsSync24(join26(appRoot, "policies/index.ts"));
|
|
4861
|
+
const plan = planFiles(all, tracksPolicies ? policies : void 0, readStack(appRoot));
|
|
4520
4862
|
if (!options2.check) ensureSupportFiles(appRoot, log2);
|
|
4521
4863
|
const results = applyPlan(appRoot, plan, { force: options2.force, dryRun: options2.check });
|
|
4522
4864
|
logResults(results, log2);
|
|
@@ -4600,22 +4942,22 @@ ${result.output.trim()}`);
|
|
|
4600
4942
|
|
|
4601
4943
|
// src/utils/install.ts
|
|
4602
4944
|
import { spawn as spawn5 } from "child_process";
|
|
4603
|
-
import { existsSync as
|
|
4604
|
-
import { join as
|
|
4945
|
+
import { existsSync as existsSync25, readFileSync as readFileSync15, readdirSync as readdirSync9 } from "fs";
|
|
4946
|
+
import { join as join27 } from "path";
|
|
4605
4947
|
function expectedPackages(dir) {
|
|
4606
|
-
const npmLock =
|
|
4607
|
-
if (
|
|
4948
|
+
const npmLock = join27(dir, "package-lock.json");
|
|
4949
|
+
if (existsSync25(npmLock)) {
|
|
4608
4950
|
try {
|
|
4609
|
-
const lock = JSON.parse(
|
|
4951
|
+
const lock = JSON.parse(readFileSync15(npmLock, "utf8"));
|
|
4610
4952
|
return Math.max(0, Object.keys(lock.packages ?? {}).length - 1);
|
|
4611
4953
|
} catch {
|
|
4612
4954
|
return 0;
|
|
4613
4955
|
}
|
|
4614
4956
|
}
|
|
4615
|
-
const pnpmLock =
|
|
4616
|
-
if (
|
|
4957
|
+
const pnpmLock = join27(dir, "pnpm-lock.yaml");
|
|
4958
|
+
if (existsSync25(pnpmLock)) {
|
|
4617
4959
|
try {
|
|
4618
|
-
const text =
|
|
4960
|
+
const text = readFileSync15(pnpmLock, "utf8");
|
|
4619
4961
|
const packages = text.slice(text.indexOf("\npackages:"));
|
|
4620
4962
|
return (packages.match(/\n {2}[^\s:]+:/g) ?? []).length;
|
|
4621
4963
|
} catch {
|
|
@@ -4625,15 +4967,15 @@ function expectedPackages(dir) {
|
|
|
4625
4967
|
return 0;
|
|
4626
4968
|
}
|
|
4627
4969
|
function installedPackages(dir) {
|
|
4628
|
-
const root =
|
|
4629
|
-
if (!
|
|
4970
|
+
const root = join27(dir, "node_modules");
|
|
4971
|
+
if (!existsSync25(root)) return 0;
|
|
4630
4972
|
let total = 0;
|
|
4631
4973
|
try {
|
|
4632
4974
|
for (const entry of readdirSync9(root, { withFileTypes: true })) {
|
|
4633
4975
|
if (!entry.isDirectory() || entry.name === ".bin") continue;
|
|
4634
4976
|
if (entry.name.startsWith("@")) {
|
|
4635
4977
|
try {
|
|
4636
|
-
total += readdirSync9(
|
|
4978
|
+
total += readdirSync9(join27(root, entry.name)).length;
|
|
4637
4979
|
} catch {
|
|
4638
4980
|
}
|
|
4639
4981
|
} else if (entry.name !== ".pnpm" && entry.name !== ".vite") {
|
|
@@ -4677,31 +5019,33 @@ function install(packageManager, dir, onProgress) {
|
|
|
4677
5019
|
// src/cli.ts
|
|
4678
5020
|
function createCli() {
|
|
4679
5021
|
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
|
-
|
|
5022
|
+
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(
|
|
5023
|
+
async (dir, options2) => {
|
|
5024
|
+
const wanted = !options2.skipInstall;
|
|
5025
|
+
let answers = { theme: options2.theme, auth: options2.auth, authProviders: options2.authProviders };
|
|
5026
|
+
if (canPrompt(options2.yes)) answers = await askCreateQuestions(answers, toAppName(dir));
|
|
5027
|
+
const result = createApp(dir, { pm: options2.pm, stack: options2.stack, install: false, ...answers, log: (message) => prompts3.log.message(message) });
|
|
5028
|
+
let installed = false;
|
|
5029
|
+
if (wanted) {
|
|
5030
|
+
const spin = prompts3.spinner({ indicator: "timer" });
|
|
5031
|
+
spin.start(`Installing dependencies with ${result.packageManager}`);
|
|
5032
|
+
const outcome = await install(result.packageManager, result.dir, ({ installed: done, expected, percent }) => {
|
|
5033
|
+
spin.message(
|
|
5034
|
+
percent === void 0 ? `Installing dependencies with ${result.packageManager} \xB7 ${formatCount(done)} packages` : `Installing dependencies with ${result.packageManager} \xB7 ${percent}% (${formatCount(done)} of ${formatCount(expected)})`
|
|
5035
|
+
);
|
|
5036
|
+
});
|
|
5037
|
+
installed = outcome.code === 0;
|
|
5038
|
+
if (installed) {
|
|
5039
|
+
spin.stop(`Installed ${formatCount(expectedPackages(result.dir) || 0)} packages with ${result.packageManager} ${pc23.dim(`(${formatDuration(outcome.ms)})`)}`);
|
|
5040
|
+
} else {
|
|
5041
|
+
spin.error(`${result.packageManager} install failed`);
|
|
5042
|
+
process.stderr.write(outcome.output);
|
|
5043
|
+
process.exitCode = 1;
|
|
5044
|
+
}
|
|
4701
5045
|
}
|
|
5046
|
+
printNextSteps(result, installed);
|
|
4702
5047
|
}
|
|
4703
|
-
|
|
4704
|
-
});
|
|
5048
|
+
);
|
|
4705
5049
|
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
5050
|
async (generator, name, second, options2) => {
|
|
4707
5051
|
if (generator === "billing")
|