@flaredev/cli 0.9.1 → 0.11.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 +360 -59
- package/package.json +2 -2
- package/templates/app/components/dashboard/dashboard-sidebar.tsx +8 -8
- package/templates/app/components/dashboard/resource-form.tsx +14 -3
- package/templates/app/lib/auth.ts +222 -154
- package/templates/app/lib/resource/drizzle-rows.ts +7 -2
- package/templates/app/lib/resource/rows.ts +5 -1
- package/templates/app/lib/resource/store.ts +106 -15
- package/templates/locks/{package-lock.json → cloudflare/package-lock.json} +1211 -1414
- package/templates/locks/{pnpm-lock.yaml → cloudflare/pnpm-lock.yaml} +1088 -1311
- package/templates/locks/{yarn.lock → cloudflare/yarn.lock} +1099 -1229
- package/templates/locks/next/package-lock.json +13673 -0
- package/templates/locks/next/pnpm-lock.yaml +8232 -0
- package/templates/locks/next/yarn.lock +5747 -0
- package/templates/next/.env.example +26 -10
- package/templates/next/docker-compose.yml +74 -0
- package/templates/next/lib/auth.ts +68 -2
- package/templates/next/lib/db.ts +5 -1
- package/templates/next/lib/resource/prisma-rows.ts +3 -2
package/dist/index.js
CHANGED
|
@@ -141,12 +141,12 @@ function parseTheme(value) {
|
|
|
141
141
|
// src/versions.ts
|
|
142
142
|
var COMPATIBILITY_DATE = "2026-09-15";
|
|
143
143
|
var APP_DEPENDENCIES = {
|
|
144
|
-
"@better-auth/drizzle-adapter": "1.7.
|
|
145
|
-
"@better-auth/passkey": "1.7.
|
|
144
|
+
"@better-auth/drizzle-adapter": "1.7.14",
|
|
145
|
+
"@better-auth/passkey": "1.7.14",
|
|
146
146
|
// Server components import Slot from here rather than the "radix-ui" umbrella (see components/ui/button.tsx).
|
|
147
147
|
"@radix-ui/react-slot": "1.3.3",
|
|
148
148
|
"@vinext/cloudflare": "1.0.0-beta.8",
|
|
149
|
-
"better-auth": "1.7.
|
|
149
|
+
"better-auth": "1.7.14",
|
|
150
150
|
// Admin UI (shadcn/ui primitives in components/ui).
|
|
151
151
|
"class-variance-authority": "^0.7.1",
|
|
152
152
|
clsx: "^2.1.1",
|
|
@@ -172,7 +172,7 @@ var APP_DEPENDENCIES = {
|
|
|
172
172
|
"react-server-dom-webpack": "19.3.0",
|
|
173
173
|
vinext: "1.0.0-beta.10"
|
|
174
174
|
};
|
|
175
|
-
var BETTER_AUTH_VERSION = "1.7.
|
|
175
|
+
var BETTER_AUTH_VERSION = "1.7.14";
|
|
176
176
|
var API_KEY_PACKAGE = "@better-auth/api-key";
|
|
177
177
|
var APP_DEV_DEPENDENCIES = {
|
|
178
178
|
"@cloudflare/vite-plugin": "^1.54.10",
|
|
@@ -189,12 +189,16 @@ var APP_DEV_DEPENDENCIES = {
|
|
|
189
189
|
wrangler: "^4.132.0"
|
|
190
190
|
};
|
|
191
191
|
var NEXT_DEPENDENCIES = {
|
|
192
|
-
|
|
192
|
+
// 16.0.4 was carrying 36 advisories, three of them critical: unauthenticated RCE in the
|
|
193
|
+
// image optimizer and on Windows hosts (both fixed in 16.3.3), and RCE in the flight
|
|
194
|
+
// protocol. Vercel refuses to build it. A pin is only worth having if it is moved when
|
|
195
|
+
// one of these lands, so treat a critical against this line as a release of its own.
|
|
196
|
+
next: "16.3.8",
|
|
193
197
|
"@prisma/client": "7.10.0",
|
|
194
198
|
"@prisma/adapter-pg": "7.10.0",
|
|
195
199
|
pg: "^8.16.3",
|
|
196
200
|
"@upstash/redis": "^1.36.0",
|
|
197
|
-
"@better-auth/prisma-adapter": "1.7.
|
|
201
|
+
"@better-auth/prisma-adapter": "1.7.14",
|
|
198
202
|
// Signs S3 requests in a few kilobytes, where the AWS SDK is megabytes.
|
|
199
203
|
aws4fetch: "^1.0.20",
|
|
200
204
|
dotenv: "^17.2.3"
|
|
@@ -229,7 +233,7 @@ var CLOUDFLARE_ONLY = [
|
|
|
229
233
|
// src/stack.ts
|
|
230
234
|
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
231
235
|
import { join } from "path";
|
|
232
|
-
var STACKS = ["
|
|
236
|
+
var STACKS = ["next", "cloudflare"];
|
|
233
237
|
var isStack = (value) => STACKS.includes(value);
|
|
234
238
|
var STACK_LABELS = {
|
|
235
239
|
cloudflare: "Cloudflare Workers, D1 and Drizzle \u2014 cheapest to run, deploys everywhere",
|
|
@@ -353,7 +357,7 @@ function createApp(target, options2 = {}) {
|
|
|
353
357
|
if (options2.stack !== void 0 && !isStack(options2.stack)) {
|
|
354
358
|
throw new Error(`Unknown stack "${options2.stack}". Use ${STACKS.join(" or ")}.`);
|
|
355
359
|
}
|
|
356
|
-
const stack = options2.stack ?? "
|
|
360
|
+
const stack = options2.stack ?? "next";
|
|
357
361
|
const authProviders = parseAuthProviders(options2.authProviders);
|
|
358
362
|
const authMethods = parseAuthMethods(options2.auth);
|
|
359
363
|
const theme = parseTheme(options2.theme);
|
|
@@ -372,9 +376,8 @@ function createApp(target, options2 = {}) {
|
|
|
372
376
|
writeFileSync3(join4(dir, "lib", "auth-config.ts"), renderAuthConfig(authMethods, authProviders));
|
|
373
377
|
appendFileSync(join4(dir, stack === "next" ? ".env.example" : ".dev.vars.example"), devVarsExampleEntries(authProviders));
|
|
374
378
|
const shared = { react: APP_DEPENDENCIES.react, "react-dom": APP_DEPENDENCIES["react-dom"] };
|
|
375
|
-
const
|
|
376
|
-
|
|
377
|
-
);
|
|
379
|
+
const cloudflareOnlyPackages = ["vinext", "@vinext/cloudflare", "drizzle-orm", "react-server-dom-webpack", "@better-auth/drizzle-adapter"];
|
|
380
|
+
const uiOnly = Object.fromEntries(Object.entries(APP_DEPENDENCIES).filter(([key]) => !cloudflareOnlyPackages.includes(key)));
|
|
378
381
|
writeJson(join4(dir, "package.json"), {
|
|
379
382
|
name,
|
|
380
383
|
version: "0.1.0",
|
|
@@ -413,7 +416,8 @@ function createApp(target, options2 = {}) {
|
|
|
413
416
|
`BETTER_AUTH_SECRET=${randomBytes(32).toString("base64")}
|
|
414
417
|
RESEND_API_KEY=
|
|
415
418
|
MAIL_FROM=
|
|
416
|
-
${devVarsEntries(authProviders)}`
|
|
419
|
+
${devVarsEntries(authProviders)}` + (stack === "next" ? `
|
|
420
|
+
${LOCAL_SERVICES}` : "")
|
|
417
421
|
);
|
|
418
422
|
if (packageManager === "pnpm" && !inWorkspace) {
|
|
419
423
|
writeFileSync3(join4(dir, "pnpm-workspace.yaml"), `allowBuilds:
|
|
@@ -421,16 +425,28 @@ ${ALLOW_BUILDS}
|
|
|
421
425
|
`);
|
|
422
426
|
}
|
|
423
427
|
const pinned = flarePackageSpec("core", dir, packageManager).startsWith("^");
|
|
424
|
-
if (pinned) copyLockfile(packageManager, dir);
|
|
428
|
+
if (pinned) copyLockfile(packageManager, dir, stack);
|
|
425
429
|
const log2 = options2.log ?? ((message) => console.log(message));
|
|
426
430
|
log2(`${pc.green("\u2714")} Wrote ${files} files to ${relative(process.cwd(), dir) || "."}`);
|
|
427
431
|
if (prefersPnpm) log2(pc.dim(`Installing with pnpm \u2014 far quicker for an app this size. Use ${pc.bold("--pm npm")} to change that.`));
|
|
428
432
|
return { dir, name, packageManager, inWorkspace };
|
|
429
433
|
}
|
|
430
|
-
|
|
434
|
+
var LOCAL_SERVICES = [
|
|
435
|
+
"# Local services (docker-compose.yml). Swap these for Neon, R2 and Upstash to deploy.",
|
|
436
|
+
"DATABASE_URL=postgresql://flare:flare@127.0.0.1:5432/app",
|
|
437
|
+
"BETTER_AUTH_URL=http://localhost:3000",
|
|
438
|
+
"R2_ENDPOINT=http://127.0.0.1:9000",
|
|
439
|
+
"R2_BUCKET=app-uploads",
|
|
440
|
+
"R2_ACCESS_KEY_ID=flare",
|
|
441
|
+
"R2_SECRET_ACCESS_KEY=flare-secret-123",
|
|
442
|
+
"UPSTASH_REDIS_REST_URL=http://127.0.0.1:8079",
|
|
443
|
+
"UPSTASH_REDIS_REST_TOKEN=flare-local-token",
|
|
444
|
+
""
|
|
445
|
+
].join("\n");
|
|
446
|
+
function copyLockfile(packageManager, dir, stack) {
|
|
431
447
|
const file = packageManager === "pnpm" ? "pnpm-lock.yaml" : packageManager === "yarn" ? "yarn.lock" : "package-lock.json";
|
|
432
448
|
if (!file) return false;
|
|
433
|
-
const source = join4(templatesDir, "locks", file);
|
|
449
|
+
const source = join4(templatesDir, "locks", stack, file);
|
|
434
450
|
if (!existsSync4(source)) return false;
|
|
435
451
|
copyFileSync(source, join4(dir, file));
|
|
436
452
|
return true;
|
|
@@ -1275,6 +1291,20 @@ function runNode(argv, cwd) {
|
|
|
1275
1291
|
child.on("exit", (code, signal) => resolvePromise(code ?? (signal ? 1 : 0)));
|
|
1276
1292
|
});
|
|
1277
1293
|
}
|
|
1294
|
+
function runNodeCapturing(argv, cwd) {
|
|
1295
|
+
return new Promise((resolvePromise, reject) => {
|
|
1296
|
+
const child = spawn3(process.execPath, argv, { cwd, stdio: ["inherit", "pipe", "pipe"] });
|
|
1297
|
+
let output = "";
|
|
1298
|
+
for (const stream of [child.stdout, child.stderr]) {
|
|
1299
|
+
stream?.on("data", (chunk) => {
|
|
1300
|
+
output += chunk.toString();
|
|
1301
|
+
process.stdout.write(chunk);
|
|
1302
|
+
});
|
|
1303
|
+
}
|
|
1304
|
+
child.on("error", reject);
|
|
1305
|
+
child.on("exit", (code, signal) => resolvePromise({ code: code ?? (signal ? 1 : 0), output }));
|
|
1306
|
+
});
|
|
1307
|
+
}
|
|
1278
1308
|
function runNodeWithTunnel(argv, cwd, opened) {
|
|
1279
1309
|
return new Promise((resolvePromise, reject) => {
|
|
1280
1310
|
const child = spawn3(process.execPath, argv, { cwd, stdio: ["inherit", "pipe", "pipe"], env: { ...process.env, FORCE_COLOR: process.env.FORCE_COLOR ?? "1" } });
|
|
@@ -1674,11 +1704,37 @@ function list(raw, what, spec) {
|
|
|
1674
1704
|
return items;
|
|
1675
1705
|
}
|
|
1676
1706
|
var RESOURCE_NAME = /^[A-Z][A-Za-z0-9]*$/;
|
|
1707
|
+
var CONDITION = /^(.*?)\s+if\s+(.+)$/s;
|
|
1708
|
+
var COMPARISON = /^([A-Za-z][A-Za-z0-9_]*)\s*(==|!=|\bin\b)\s*(.+)$/;
|
|
1709
|
+
function parseCondition(clause, spec) {
|
|
1710
|
+
const match = COMPARISON.exec(clause.trim());
|
|
1711
|
+
if (!match) {
|
|
1712
|
+
throw new FieldGrammarError(`"${spec}": "${clause.trim()}" isn't a condition. Write it like "if kind == recurring".`);
|
|
1713
|
+
}
|
|
1714
|
+
const [, dependsOn, operator, rawValue] = match;
|
|
1715
|
+
const op = operator === "==" ? "eq" : operator === "!=" ? "ne" : "in";
|
|
1716
|
+
const text = rawValue.trim();
|
|
1717
|
+
if (op === "in") {
|
|
1718
|
+
const inner = /^\((.*)\)$/.exec(text);
|
|
1719
|
+
if (!inner) throw new FieldGrammarError(`"${spec}": "in" takes a list in brackets, e.g. "if kind in (recurring, retainer)".`);
|
|
1720
|
+
return { field: dependsOn, op, value: list(inner[1], "condition value", spec) };
|
|
1721
|
+
}
|
|
1722
|
+
if (!text || /\s/.test(text)) {
|
|
1723
|
+
throw new FieldGrammarError(`"${spec}": "${text}" isn't a single value. Write it like "if kind == recurring".`);
|
|
1724
|
+
}
|
|
1725
|
+
return { field: dependsOn, op, value: text };
|
|
1726
|
+
}
|
|
1677
1727
|
function parseField(spec) {
|
|
1678
1728
|
const colon = spec.indexOf(":");
|
|
1679
1729
|
if (colon <= 0) throw new FieldGrammarError(`"${spec}": expected name:type, e.g. "title:string".`);
|
|
1680
1730
|
const rawName = spec.slice(0, colon).trim();
|
|
1681
1731
|
let type = spec.slice(colon + 1).trim();
|
|
1732
|
+
let showWhen;
|
|
1733
|
+
const conditioned = CONDITION.exec(type);
|
|
1734
|
+
if (conditioned) {
|
|
1735
|
+
type = conditioned[1].trim();
|
|
1736
|
+
showWhen = parseCondition(conditioned[2], spec);
|
|
1737
|
+
}
|
|
1682
1738
|
let required = true;
|
|
1683
1739
|
let unique = false;
|
|
1684
1740
|
while (/[?!]$/.test(type)) {
|
|
@@ -1691,6 +1747,7 @@ function parseField(spec) {
|
|
|
1691
1747
|
}
|
|
1692
1748
|
let key = camelCase(rawName);
|
|
1693
1749
|
const field = { key, kind: "string", required, unique };
|
|
1750
|
+
if (showWhen) field.showWhen = showWhen;
|
|
1694
1751
|
let match;
|
|
1695
1752
|
if (SIMPLE_KINDS.includes(type)) {
|
|
1696
1753
|
field.kind = type;
|
|
@@ -1768,6 +1825,40 @@ function parseFields(input) {
|
|
|
1768
1825
|
if (seen.has(field.key)) throw new FieldGrammarError(`Field "${field.key}" is declared more than once.`);
|
|
1769
1826
|
seen.add(field.key);
|
|
1770
1827
|
}
|
|
1828
|
+
const byKey = new Map(fields.map((field) => [field.key, field]));
|
|
1829
|
+
for (const field of fields) {
|
|
1830
|
+
const condition = field.showWhen;
|
|
1831
|
+
if (!condition) continue;
|
|
1832
|
+
if (condition.field === field.key) {
|
|
1833
|
+
throw new FieldGrammarError(`Field "${field.key}" cannot depend on itself.`);
|
|
1834
|
+
}
|
|
1835
|
+
const target = byKey.get(condition.field);
|
|
1836
|
+
if (!target) {
|
|
1837
|
+
const names = fields.map((other) => other.key).filter((name) => name !== field.key);
|
|
1838
|
+
throw new FieldGrammarError(
|
|
1839
|
+
`Field "${field.key}" depends on "${condition.field}", which isn't a field here.${suggest(condition.field, names)} Fields: ${names.join(", ")}.`
|
|
1840
|
+
);
|
|
1841
|
+
}
|
|
1842
|
+
if (target.showWhen) {
|
|
1843
|
+
throw new FieldGrammarError(`Field "${field.key}" depends on "${target.key}", which is itself conditional. Conditions only go one level deep.`);
|
|
1844
|
+
}
|
|
1845
|
+
const values = Array.isArray(condition.value) ? condition.value : [condition.value];
|
|
1846
|
+
if (target.options) {
|
|
1847
|
+
for (const value of values) {
|
|
1848
|
+
if (!target.options.includes(value)) {
|
|
1849
|
+
throw new FieldGrammarError(
|
|
1850
|
+
`Field "${field.key}" is shown when ${condition.field} is "${value}", which isn't one of its options.${suggest(value, target.options)} Options: ${target.options.join(", ")}.`
|
|
1851
|
+
);
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
} else if (target.kind === "boolean") {
|
|
1855
|
+
for (const value of values) {
|
|
1856
|
+
if (value !== "true" && value !== "false") {
|
|
1857
|
+
throw new FieldGrammarError(`Field "${field.key}" depends on the boolean "${condition.field}", so compare it with true or false, not "${value}".`);
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1771
1862
|
return fields;
|
|
1772
1863
|
}
|
|
1773
1864
|
|
|
@@ -1789,6 +1880,7 @@ function options(field) {
|
|
|
1789
1880
|
if (field.widget) opts.widget = field.widget;
|
|
1790
1881
|
if (field.kind === "belongsTo" && !field.required) opts.onDelete = "set null";
|
|
1791
1882
|
if (field.maxBytes !== void 0) opts.maxBytes = field.maxBytes;
|
|
1883
|
+
if (field.showWhen) opts.showWhen = field.showWhen;
|
|
1792
1884
|
return opts;
|
|
1793
1885
|
}
|
|
1794
1886
|
function toField(field) {
|
|
@@ -1812,7 +1904,13 @@ function toField(field) {
|
|
|
1812
1904
|
return builders[field.kind](opts);
|
|
1813
1905
|
}
|
|
1814
1906
|
}
|
|
1815
|
-
var literal = (value) =>
|
|
1907
|
+
var literal = (value) => {
|
|
1908
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
1909
|
+
const entries = Object.entries(value);
|
|
1910
|
+
return `{ ${entries.map(([key, inner]) => `${/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key) ? key : JSON.stringify(key)}: ${literal(inner)}`).join(", ")} }`;
|
|
1911
|
+
}
|
|
1912
|
+
return JSON.stringify(value);
|
|
1913
|
+
};
|
|
1816
1914
|
function renderOptions(opts) {
|
|
1817
1915
|
const entries = Object.entries(opts);
|
|
1818
1916
|
return entries.length ? `{ ${entries.map(([k, v]) => `${k}: ${literal(v)}`).join(", ")} }` : "";
|
|
@@ -2644,7 +2742,7 @@ function prismaType(resource, key, def) {
|
|
|
2644
2742
|
case "int":
|
|
2645
2743
|
return "Int";
|
|
2646
2744
|
case "float":
|
|
2647
|
-
return "Float";
|
|
2745
|
+
return def.format === "money" ? "BigInt" : "Float";
|
|
2648
2746
|
case "boolean":
|
|
2649
2747
|
return "Boolean";
|
|
2650
2748
|
case "date":
|
|
@@ -2713,7 +2811,7 @@ function renderPrismaModel(entry, all) {
|
|
|
2713
2811
|
if (def.kind === "belongsTo") {
|
|
2714
2812
|
const target = byName.get(def.target);
|
|
2715
2813
|
if (!target) throw new Error(`${resource.name}.${key} belongs to "${def.target}", but there is no ${def.target} resource. Generate it first.`);
|
|
2716
|
-
const optional2 = def.required ? "" : "?";
|
|
2814
|
+
const optional2 = def.required && !def.showWhen ? "" : "?";
|
|
2717
2815
|
const relationField = key.replace(/Id$/, "");
|
|
2718
2816
|
const named = backReferences(target, all).find((back) => back.from === resource.name && back.relationName?.endsWith(`_${key}`));
|
|
2719
2817
|
const relationArgs = [
|
|
@@ -2727,7 +2825,7 @@ function renderPrismaModel(entry, all) {
|
|
|
2727
2825
|
indexes.push(` @@index([${key}])`);
|
|
2728
2826
|
continue;
|
|
2729
2827
|
}
|
|
2730
|
-
const optional = def.required || def.kind === "multiselect" || def.kind === "tags" ? "" : "?";
|
|
2828
|
+
const optional = def.required && !def.showWhen || def.kind === "multiselect" || def.kind === "tags" ? "" : "?";
|
|
2731
2829
|
lines.push(` ${key} ${prismaType(resource, key, def)}${optional} ${attributes(key, def).join(" ")}`.trimEnd());
|
|
2732
2830
|
}
|
|
2733
2831
|
for (const back of backReferences(resource, all)) {
|
|
@@ -2773,7 +2871,7 @@ function columnExpression(key, def, resources, selfName) {
|
|
|
2773
2871
|
expr = `integer(${col})`;
|
|
2774
2872
|
break;
|
|
2775
2873
|
case "float":
|
|
2776
|
-
expr = `real(${col})`;
|
|
2874
|
+
expr = def.format === "money" ? `integer(${col})` : `real(${col})`;
|
|
2777
2875
|
break;
|
|
2778
2876
|
case "boolean":
|
|
2779
2877
|
expr = `integer(${col}, { mode: "boolean" })`;
|
|
@@ -2802,7 +2900,7 @@ function columnExpression(key, def, resources, selfName) {
|
|
|
2802
2900
|
default:
|
|
2803
2901
|
expr = `text(${col})`;
|
|
2804
2902
|
}
|
|
2805
|
-
if (def.required) expr += ".notNull()";
|
|
2903
|
+
if (def.required && !def.showWhen) expr += ".notNull()";
|
|
2806
2904
|
if ("unique" in def && def.unique) expr += ".unique()";
|
|
2807
2905
|
if ("default" in def && def.default !== void 0) expr += `.default(${q2(def.default)})`;
|
|
2808
2906
|
return expr;
|
|
@@ -4119,7 +4217,7 @@ async function askCreateQuestions(given, appName) {
|
|
|
4119
4217
|
answers.stack = stopIfCancelled(
|
|
4120
4218
|
await prompts2.select({
|
|
4121
4219
|
message: "Where will it run?",
|
|
4122
|
-
initialValue: "
|
|
4220
|
+
initialValue: "next",
|
|
4123
4221
|
options: STACKS.map((value) => ({ value, label: value === "next" ? "Next.js" : "Cloudflare", hint: STACK_LABELS[value] }))
|
|
4124
4222
|
})
|
|
4125
4223
|
);
|
|
@@ -4213,13 +4311,22 @@ async function genResource(rawName, options2) {
|
|
|
4213
4311
|
}
|
|
4214
4312
|
}
|
|
4215
4313
|
}
|
|
4314
|
+
const policyRelative = policyPath(name);
|
|
4315
|
+
const policyAbsolute = join17(appRoot, policyRelative);
|
|
4316
|
+
const wrotePolicy = !exists && !existsSync18(policyAbsolute);
|
|
4317
|
+
if (wrotePolicy) {
|
|
4318
|
+
mkdirSync8(dirname9(policyAbsolute), { recursive: true });
|
|
4319
|
+
writeFileSync11(policyAbsolute, renderPolicy(name, { read: ["admin"], create: ["admin"], update: ["admin"], delete: ["admin"] }));
|
|
4320
|
+
log2(`${pc11.green("create".padEnd(9))} ${policyRelative}`);
|
|
4321
|
+
}
|
|
4216
4322
|
let written;
|
|
4217
4323
|
let table;
|
|
4218
4324
|
try {
|
|
4219
4325
|
const all = await loadResources(appRoot);
|
|
4220
4326
|
table = all.find(({ resource }) => resource.name === name).resource.table;
|
|
4221
4327
|
ensureSupportFiles(appRoot, log2);
|
|
4222
|
-
|
|
4328
|
+
const policies = wrotePolicy ? await loadPolicies(appRoot) : void 0;
|
|
4329
|
+
written = applyPlan(appRoot, planFiles(all, policies, readStack(appRoot)), { force: options2.force });
|
|
4223
4330
|
logResults(written, log2);
|
|
4224
4331
|
} catch (error) {
|
|
4225
4332
|
const hint = exists ? "" : `
|
|
@@ -4247,7 +4354,8 @@ Next: ${pc11.bold("flare migrate")} to apply it locally.`);
|
|
|
4247
4354
|
}
|
|
4248
4355
|
}
|
|
4249
4356
|
const descriptor = { path: relativePath, status: exists ? "outdated" : "missing", outcome: exists ? "update" : "create" };
|
|
4250
|
-
|
|
4357
|
+
const policy = wrotePolicy ? [{ path: policyRelative, status: "missing", outcome: "create" }] : [];
|
|
4358
|
+
return { name, mode: exists ? "update" : "create", written: [descriptor, ...policy, ...written], migrations };
|
|
4251
4359
|
}
|
|
4252
4360
|
|
|
4253
4361
|
// src/commands/gen-endpoint.ts
|
|
@@ -4314,10 +4422,88 @@ ${body}
|
|
|
4314
4422
|
}
|
|
4315
4423
|
|
|
4316
4424
|
// src/commands/gen-migration.ts
|
|
4317
|
-
import { existsSync as existsSync20, mkdirSync as mkdirSync10, writeFileSync as writeFileSync13 } from "fs";
|
|
4425
|
+
import { existsSync as existsSync20, mkdirSync as mkdirSync10, readFileSync as readFileSync13, readdirSync as readdirSync7, writeFileSync as writeFileSync13 } from "fs";
|
|
4318
4426
|
import { basename as basename2, join as join19 } from "path";
|
|
4319
4427
|
import { snakeCase } from "@flaredev/core";
|
|
4320
4428
|
import pc13 from "picocolors";
|
|
4429
|
+
|
|
4430
|
+
// src/generator/destructive.ts
|
|
4431
|
+
function stripNoise(sql2) {
|
|
4432
|
+
let out = "";
|
|
4433
|
+
let index = 0;
|
|
4434
|
+
while (index < sql2.length) {
|
|
4435
|
+
const two = sql2.slice(index, index + 2);
|
|
4436
|
+
if (two === "--") {
|
|
4437
|
+
const end = sql2.indexOf("\n", index);
|
|
4438
|
+
const stop = end === -1 ? sql2.length : end;
|
|
4439
|
+
out += " ".repeat(stop - index);
|
|
4440
|
+
index = stop;
|
|
4441
|
+
continue;
|
|
4442
|
+
}
|
|
4443
|
+
if (two === "/*") {
|
|
4444
|
+
const end = sql2.indexOf("*/", index + 2);
|
|
4445
|
+
const skipped = sql2.slice(index, end === -1 ? sql2.length : end + 2);
|
|
4446
|
+
out += skipped.replaceAll(/[^\n]/g, " ");
|
|
4447
|
+
index = end === -1 ? sql2.length : end + 2;
|
|
4448
|
+
continue;
|
|
4449
|
+
}
|
|
4450
|
+
const char = sql2[index];
|
|
4451
|
+
if (char === "'" || char === '"' || char === "`") {
|
|
4452
|
+
let at = index + 1;
|
|
4453
|
+
while (at < sql2.length) {
|
|
4454
|
+
if (sql2[at] === char) {
|
|
4455
|
+
if (sql2[at + 1] === char) at += 2;
|
|
4456
|
+
else break;
|
|
4457
|
+
} else at += 1;
|
|
4458
|
+
}
|
|
4459
|
+
const literal2 = sql2.slice(index, Math.min(at + 1, sql2.length));
|
|
4460
|
+
out += literal2.replaceAll(/[^\n]/g, " ");
|
|
4461
|
+
index = at + 1;
|
|
4462
|
+
continue;
|
|
4463
|
+
}
|
|
4464
|
+
out += char;
|
|
4465
|
+
index += 1;
|
|
4466
|
+
}
|
|
4467
|
+
return out;
|
|
4468
|
+
}
|
|
4469
|
+
var PATTERNS = [
|
|
4470
|
+
[/\bDROP\s+COLUMN\b/i, "a column, and the values in it"],
|
|
4471
|
+
[/\bDROP\s+TABLE\b/i, "a table, and every row in it"],
|
|
4472
|
+
[/\bTRUNCATE\b/i, "every row in a table"],
|
|
4473
|
+
[/\bDROP\s+(?:MATERIALIZED\s+)?VIEW\b/i, "a view"],
|
|
4474
|
+
[/\bDROP\s+INDEX\b/i, "an index \u2014 if it was written by hand, Prisma cannot recreate it"],
|
|
4475
|
+
[/\bDROP\s+TRIGGER\b/i, "a trigger \u2014 Prisma does not know about it and will not put it back"],
|
|
4476
|
+
[/\bDROP\s+FUNCTION\b/i, "a function"],
|
|
4477
|
+
[/\bDROP\s+CONSTRAINT\b/i, "a constraint"],
|
|
4478
|
+
[/\bDROP\s+TYPE\b/i, "a type"],
|
|
4479
|
+
[/\bDROP\s+SEQUENCE\b/i, "a sequence"],
|
|
4480
|
+
[/\bDROP\s+SCHEMA\b/i, "a schema, and everything in it"],
|
|
4481
|
+
[/\bDROP\s+DATABASE\b/i, "the database"]
|
|
4482
|
+
];
|
|
4483
|
+
function destructiveStatements(sql2) {
|
|
4484
|
+
const cleaned = stripNoise(sql2);
|
|
4485
|
+
const found = [];
|
|
4486
|
+
let start = 0;
|
|
4487
|
+
for (let index = 0; index <= cleaned.length; index += 1) {
|
|
4488
|
+
if (index !== cleaned.length && cleaned[index] !== ";") continue;
|
|
4489
|
+
const statement = cleaned.slice(start, index);
|
|
4490
|
+
const trimmed = statement.trim();
|
|
4491
|
+
const from = start;
|
|
4492
|
+
start = index + 1;
|
|
4493
|
+
if (!trimmed) continue;
|
|
4494
|
+
const match = PATTERNS.find(([pattern]) => pattern.test(trimmed));
|
|
4495
|
+
if (!match) continue;
|
|
4496
|
+
const offset = from + statement.search(/\S/);
|
|
4497
|
+
found.push({
|
|
4498
|
+
statement: sql2.slice(offset, index).trim().replaceAll(/\s+/g, " ").slice(0, 160),
|
|
4499
|
+
kind: match[1],
|
|
4500
|
+
line: cleaned.slice(0, offset).split("\n").length
|
|
4501
|
+
});
|
|
4502
|
+
}
|
|
4503
|
+
return found;
|
|
4504
|
+
}
|
|
4505
|
+
|
|
4506
|
+
// src/commands/gen-migration.ts
|
|
4321
4507
|
var DOWN_TEMPLATE = (up) => `-- Rollback for ${up}, run by \`flare migrate:rollback\`.
|
|
4322
4508
|
-- Write SQL that undoes the up migration, e.g.:
|
|
4323
4509
|
-- DROP INDEX IF EXISTS \`contacts_phone_idx\`;
|
|
@@ -4328,6 +4514,7 @@ async function genMigration(rawName, options2 = {}) {
|
|
|
4328
4514
|
const appRoot = findAppRoot(options2.cwd ?? process.cwd());
|
|
4329
4515
|
const name = snakeCase(rawName);
|
|
4330
4516
|
if (!name) throw new Error(`Invalid migration name "${rawName}". Use letters, digits, and underscores.`);
|
|
4517
|
+
if (readStack(appRoot) === "next") return genPrismaMigration(appRoot, name, options2);
|
|
4331
4518
|
const { files, repairs } = await generateSchemaMigration(appRoot, name, { custom: !options2.fromSchema });
|
|
4332
4519
|
if (files.length === 0) {
|
|
4333
4520
|
log2(options2.fromSchema ? "No schema changes, so no migration was created." : "drizzle-kit didn't create a migration.");
|
|
@@ -4352,6 +4539,89 @@ Next: ${pc13.bold("flare migrate")}.`);
|
|
|
4352
4539
|
}
|
|
4353
4540
|
return files;
|
|
4354
4541
|
}
|
|
4542
|
+
async function genPrismaMigration(appRoot, name, options2) {
|
|
4543
|
+
const log2 = options2.log ?? ((message) => console.log(message));
|
|
4544
|
+
const prisma = resolveBin(appRoot, "prisma", "prisma");
|
|
4545
|
+
const migrations = join19(appRoot, "prisma", "migrations");
|
|
4546
|
+
const before = new Set(existsSync20(migrations) ? readdirSync7(migrations) : []);
|
|
4547
|
+
log2(`${pc13.cyan("prisma")} migrate dev --create-only --name ${name}`);
|
|
4548
|
+
const { code, output } = await runNodeCapturing([prisma, "migrate", "dev", "--create-only", "--name", name], appRoot);
|
|
4549
|
+
if (code !== 0) {
|
|
4550
|
+
if (/drift detected|we need to reset/i.test(output)) {
|
|
4551
|
+
log2("");
|
|
4552
|
+
log2(pc13.yellow("Prisma found something in the database that its schema does not describe."));
|
|
4553
|
+
log2(
|
|
4554
|
+
[
|
|
4555
|
+
"",
|
|
4556
|
+
`Nothing has been changed \u2014 ${pc13.bold("--create-only")} writes a migration, it does not apply one, and no`,
|
|
4557
|
+
"migration was written. Do not run `prisma migrate reset`: it drops the database, and what it",
|
|
4558
|
+
"would drop is the thing Prisma is complaining it cannot see.",
|
|
4559
|
+
"",
|
|
4560
|
+
"Drift above is reported as changes to get from your migrations to the database, so it lists",
|
|
4561
|
+
"what the database has and the migrations do not. Usually that is something added by hand:",
|
|
4562
|
+
"",
|
|
4563
|
+
` ${pc13.bold("It should be there")} \u2014 write it into a migration so the history matches. Add the SQL to a`,
|
|
4564
|
+
" new migration and mark it applied with `prisma migrate resolve --applied <name>`.",
|
|
4565
|
+
"",
|
|
4566
|
+
` ${pc13.bold("It should not be there")} \u2014 remove it from the database yourself, then run this again.`,
|
|
4567
|
+
"",
|
|
4568
|
+
"An index, a trigger or a function is not reported as drift at all; Prisma does not look at",
|
|
4569
|
+
"those. They survive a migration and are lost by a reset, which is the other reason not to."
|
|
4570
|
+
].join("\n")
|
|
4571
|
+
);
|
|
4572
|
+
}
|
|
4573
|
+
return [];
|
|
4574
|
+
}
|
|
4575
|
+
const added = (existsSync20(migrations) ? readdirSync7(migrations) : []).filter((entry) => !before.has(entry));
|
|
4576
|
+
if (added.length === 0) {
|
|
4577
|
+
log2("No schema changes, so no migration was created.");
|
|
4578
|
+
return [];
|
|
4579
|
+
}
|
|
4580
|
+
const written = [];
|
|
4581
|
+
const destructive = [];
|
|
4582
|
+
for (const entry of added) {
|
|
4583
|
+
const file = join19(migrations, entry, "migration.sql");
|
|
4584
|
+
if (!existsSync20(file)) continue;
|
|
4585
|
+
const relative3 = `prisma/migrations/${entry}/migration.sql`;
|
|
4586
|
+
written.push(relative3);
|
|
4587
|
+
log2(`${pc13.green("create".padEnd(9))} ${relative3}`);
|
|
4588
|
+
const found = destructiveStatements(readFileSync13(file, "utf8"));
|
|
4589
|
+
if (found.length > 0) destructive.push({ file: relative3, found });
|
|
4590
|
+
}
|
|
4591
|
+
if (destructive.length > 0) {
|
|
4592
|
+
log2("");
|
|
4593
|
+
log2(pc13.yellow(`This migration throws things away, so it has not been applied:`));
|
|
4594
|
+
for (const { file, found } of destructive) {
|
|
4595
|
+
log2(pc13.dim(`
|
|
4596
|
+
${file}`));
|
|
4597
|
+
for (const entry of found) {
|
|
4598
|
+
log2(` ${pc13.red(`line ${entry.line}`)} ${entry.statement}`);
|
|
4599
|
+
log2(` ${pc13.dim(`drops ${entry.kind}`)}`);
|
|
4600
|
+
}
|
|
4601
|
+
}
|
|
4602
|
+
log2(
|
|
4603
|
+
[
|
|
4604
|
+
"",
|
|
4605
|
+
"There are two reasons Prisma writes a drop, and only you can tell them apart:",
|
|
4606
|
+
"",
|
|
4607
|
+
` ${pc13.bold("You asked for it")} \u2014 a field left a descriptor, or a resource was removed. Then the drop is`,
|
|
4608
|
+
` the point, and ${pc13.bold("flare migrate")} applies it.`,
|
|
4609
|
+
"",
|
|
4610
|
+
` ${pc13.bold("Prisma cannot see it")} \u2014 the column, index or constraint was added by hand, so the schema`,
|
|
4611
|
+
" does not describe it and the diff reads it as something to remove. Then applying this",
|
|
4612
|
+
" loses it, and Prisma will not put it back.",
|
|
4613
|
+
"",
|
|
4614
|
+
`Read ${pc13.bold(destructive[0].file)} before deciding. To keep an object Prisma does not`,
|
|
4615
|
+
"know about, delete those statements from the migration \u2014 and consider writing the object into",
|
|
4616
|
+
"a migration of its own so the next diff stops proposing to drop it."
|
|
4617
|
+
].join("\n")
|
|
4618
|
+
);
|
|
4619
|
+
return written;
|
|
4620
|
+
}
|
|
4621
|
+
log2(`
|
|
4622
|
+
Nothing destructive in it. Next: ${pc13.bold("flare migrate")} to apply it.`);
|
|
4623
|
+
return written;
|
|
4624
|
+
}
|
|
4355
4625
|
|
|
4356
4626
|
// src/commands/gen-policy.ts
|
|
4357
4627
|
import { existsSync as existsSync21, mkdirSync as mkdirSync11, writeFileSync as writeFileSync14 } from "fs";
|
|
@@ -4429,7 +4699,7 @@ ${pc14.dim("Roles come from `flare role:add`; the API and the admin both enforce
|
|
|
4429
4699
|
}
|
|
4430
4700
|
|
|
4431
4701
|
// src/commands/migrate.ts
|
|
4432
|
-
import { existsSync as existsSync22, mkdtempSync, readFileSync as
|
|
4702
|
+
import { existsSync as existsSync22, mkdtempSync, readFileSync as readFileSync14, rmSync as rmSync3, writeFileSync as writeFileSync15 } from "fs";
|
|
4433
4703
|
import { tmpdir } from "os";
|
|
4434
4704
|
import { join as join21 } from "path";
|
|
4435
4705
|
import pc15 from "picocolors";
|
|
@@ -4570,7 +4840,7 @@ async function rollback(options2 = {}) {
|
|
|
4570
4840
|
if (!/^[\w.-]+\.sql$/.test(name)) throw new Error(`Unexpected migration name "${name}" in d1_migrations.`);
|
|
4571
4841
|
const handWritten = join21(migrationsDir, "down", name);
|
|
4572
4842
|
if (existsSync22(handWritten)) {
|
|
4573
|
-
const sql2 =
|
|
4843
|
+
const sql2 = readFileSync14(handWritten, "utf8");
|
|
4574
4844
|
if (splitStatements(sql2).length === 0) {
|
|
4575
4845
|
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.`);
|
|
4576
4846
|
}
|
|
@@ -4579,7 +4849,7 @@ async function rollback(options2 = {}) {
|
|
|
4579
4849
|
}
|
|
4580
4850
|
const upPath = join21(migrationsDir, name);
|
|
4581
4851
|
if (!existsSync22(upPath)) throw new Error(`${name} was applied but migrations/${name} is missing, so it can't be reversed.`);
|
|
4582
|
-
const down = deriveDown(
|
|
4852
|
+
const down = deriveDown(readFileSync14(upPath, "utf8"));
|
|
4583
4853
|
if (!down.reversible) {
|
|
4584
4854
|
throw new Error(
|
|
4585
4855
|
`Can't roll back ${name}: ${down.reason}
|
|
@@ -4615,7 +4885,7 @@ Rolled back ${plan.map((step) => step.name).join(", ")}.`));
|
|
|
4615
4885
|
}
|
|
4616
4886
|
|
|
4617
4887
|
// src/commands/rm.ts
|
|
4618
|
-
import { existsSync as existsSync23, readdirSync as
|
|
4888
|
+
import { existsSync as existsSync23, readdirSync as readdirSync8, readFileSync as readFileSync15, rmdirSync, unlinkSync as unlinkSync2 } from "fs";
|
|
4619
4889
|
import { dirname as dirname11, join as join22 } from "path";
|
|
4620
4890
|
import { relationGraph as relationGraph3 } from "@flaredev/core";
|
|
4621
4891
|
import pc16 from "picocolors";
|
|
@@ -4638,7 +4908,7 @@ function removeEmptyDirs(appRoot, start) {
|
|
|
4638
4908
|
let dir = dirname11(join22(appRoot, start));
|
|
4639
4909
|
const roots = [join22(appRoot, "app", "api"), join22(appRoot, "app", "dashboard")];
|
|
4640
4910
|
const inside = () => roots.some((root) => dir.startsWith(root) && dir !== root);
|
|
4641
|
-
while (inside() && existsSync23(dir) &&
|
|
4911
|
+
while (inside() && existsSync23(dir) && readdirSync8(dir).length === 0) {
|
|
4642
4912
|
rmdirSync(dir);
|
|
4643
4913
|
dir = dirname11(dir);
|
|
4644
4914
|
}
|
|
@@ -4676,7 +4946,7 @@ async function rmResource(rawName, options2 = {}) {
|
|
|
4676
4946
|
if (!entry && owned.size === 0) throw new Error(`No resource "${name}" (no ${descriptorPath(name)} and no generated files).`);
|
|
4677
4947
|
const findings = [];
|
|
4678
4948
|
for (const path of owned) {
|
|
4679
|
-
const source =
|
|
4949
|
+
const source = readFileSync15(join22(appRoot, path), "utf8");
|
|
4680
4950
|
let wrapper;
|
|
4681
4951
|
if (entry && path === entry.file) {
|
|
4682
4952
|
const standard = splitMarkers(renderDescriptor(name, [{ key: "placeholder", kind: "string", required: true, unique: false }]));
|
|
@@ -4704,8 +4974,8 @@ Move what you want to keep, or pass --force to delete anyway.`);
|
|
|
4704
4974
|
logResults(applyPlan(appRoot, planFiles(remaining, policy ? await loadPolicies(appRoot) : void 0, readStack(appRoot))), log2);
|
|
4705
4975
|
const table = entry ? tableExport(entry.resource) : void 0;
|
|
4706
4976
|
if (table && existsSync23(join22(appRoot, "seeds"))) {
|
|
4707
|
-
const seeds =
|
|
4708
|
-
(file) => new RegExp(`\\b${table}\\b`).test(
|
|
4977
|
+
const seeds = readdirSync8(join22(appRoot, "seeds")).filter(
|
|
4978
|
+
(file) => new RegExp(`\\b${table}\\b`).test(readFileSync15(join22(appRoot, "seeds", file), "utf8"))
|
|
4709
4979
|
);
|
|
4710
4980
|
if (seeds.length) log2(pc16.yellow(`
|
|
4711
4981
|
These seeds still use ${table}: ${seeds.map((s) => `seeds/${s}`).join(", ")}.`));
|
|
@@ -4780,7 +5050,7 @@ ${result.output.trim()}`);
|
|
|
4780
5050
|
}
|
|
4781
5051
|
|
|
4782
5052
|
// src/commands/eject.ts
|
|
4783
|
-
import { existsSync as existsSync24, mkdirSync as mkdirSync12, readFileSync as
|
|
5053
|
+
import { existsSync as existsSync24, mkdirSync as mkdirSync12, readFileSync as readFileSync16, readdirSync as readdirSync9, writeFileSync as writeFileSync16 } from "fs";
|
|
4784
5054
|
import { dirname as dirname12, join as join23, relative as relative2 } from "path";
|
|
4785
5055
|
import pc18 from "picocolors";
|
|
4786
5056
|
var TRACKED = ["app", "lib", "components"];
|
|
@@ -4790,7 +5060,7 @@ var prunedOn = (path, stack) => stack === "next" && CLOUDFLARE_ONLY.some((only)
|
|
|
4790
5060
|
function upstreamFile(path, stack) {
|
|
4791
5061
|
if (prunedOn(path, stack) || WRITTEN_PER_APP.has(path)) return void 0;
|
|
4792
5062
|
const found = locate(path, stack);
|
|
4793
|
-
if (found && PER_APP.test(
|
|
5063
|
+
if (found && PER_APP.test(readFileSync16(found, "utf8"))) return void 0;
|
|
4794
5064
|
return found;
|
|
4795
5065
|
}
|
|
4796
5066
|
function locate(path, stack) {
|
|
@@ -4801,7 +5071,7 @@ function trackedPaths(appRoot, stack) {
|
|
|
4801
5071
|
const paths = /* @__PURE__ */ new Set();
|
|
4802
5072
|
const walk = (base, dir) => {
|
|
4803
5073
|
if (!existsSync24(join23(base, dir))) return;
|
|
4804
|
-
for (const entry of
|
|
5074
|
+
for (const entry of readdirSync9(join23(base, dir), { withFileTypes: true })) {
|
|
4805
5075
|
const next = `${dir}/${entry.name}`;
|
|
4806
5076
|
if (entry.isDirectory()) walk(base, next);
|
|
4807
5077
|
else if (entry.name.endsWith(".ts") || entry.name.endsWith(".tsx")) paths.add(next);
|
|
@@ -4817,10 +5087,10 @@ function trackedPaths(appRoot, stack) {
|
|
|
4817
5087
|
function compareTracked(appRoot, stack, filter) {
|
|
4818
5088
|
return trackedPaths(appRoot, stack).filter((path) => !filter || path.includes(filter)).map((path) => {
|
|
4819
5089
|
const source = upstreamFile(path, stack);
|
|
4820
|
-
const theirs =
|
|
5090
|
+
const theirs = readFileSync16(source, "utf8");
|
|
4821
5091
|
const target = join23(appRoot, path);
|
|
4822
5092
|
if (!existsSync24(target)) return { path, state: "missing", theirs };
|
|
4823
|
-
const mine =
|
|
5093
|
+
const mine = readFileSync16(target, "utf8");
|
|
4824
5094
|
if (mine === theirs) return { path, state: "same", mine, theirs };
|
|
4825
5095
|
const merged = mergeGeneratedBlock(mine, theirs);
|
|
4826
5096
|
if (merged !== void 0) {
|
|
@@ -4912,9 +5182,9 @@ Updated ${changed.length} file(s).`));
|
|
|
4912
5182
|
}
|
|
4913
5183
|
|
|
4914
5184
|
// src/commands/seed.ts
|
|
4915
|
-
import { existsSync as existsSync25, mkdirSync as mkdirSync13, readdirSync as
|
|
5185
|
+
import { existsSync as existsSync25, mkdirSync as mkdirSync13, readdirSync as readdirSync10, rmSync as rmSync5, writeFileSync as writeFileSync18 } from "fs";
|
|
4916
5186
|
import { join as join25 } from "path";
|
|
4917
|
-
import { camelCase as camelCase6, createFake, kebabCase as kebabCase4, storedFields as storedFields3 } from "@flaredev/core";
|
|
5187
|
+
import { camelCase as camelCase6, createFake, credentialUserRows, kebabCase as kebabCase4, storedFields as storedFields3 } from "@flaredev/core";
|
|
4918
5188
|
import { createJiti as createJiti4 } from "jiti";
|
|
4919
5189
|
import pc19 from "picocolors";
|
|
4920
5190
|
|
|
@@ -5010,7 +5280,7 @@ function createInsertMany(sink, drizzle, onProgress) {
|
|
|
5010
5280
|
}
|
|
5011
5281
|
|
|
5012
5282
|
// src/seed/local-d1.ts
|
|
5013
|
-
import { readFileSync as
|
|
5283
|
+
import { readFileSync as readFileSync17, rmSync as rmSync4, writeFileSync as writeFileSync17 } from "fs";
|
|
5014
5284
|
import { createRequire } from "module";
|
|
5015
5285
|
import { join as join24 } from "path";
|
|
5016
5286
|
import { pathToFileURL } from "url";
|
|
@@ -5043,7 +5313,7 @@ async function openLocalD1(appRoot, binding) {
|
|
|
5043
5313
|
}
|
|
5044
5314
|
function seedConfig(appRoot) {
|
|
5045
5315
|
const source = join24(appRoot, "wrangler.jsonc");
|
|
5046
|
-
const config = parseJsonc3(
|
|
5316
|
+
const config = parseJsonc3(readFileSync17(source, "utf8")) ?? {};
|
|
5047
5317
|
delete config.durable_objects;
|
|
5048
5318
|
delete config.migrations;
|
|
5049
5319
|
const path = join24(appRoot, ".flare-seed.wrangler.json");
|
|
@@ -5055,7 +5325,7 @@ function seedConfig(appRoot) {
|
|
|
5055
5325
|
var SEEDS_DIR = "seeds";
|
|
5056
5326
|
function seedFiles(appRoot, names) {
|
|
5057
5327
|
const dir = join25(appRoot, SEEDS_DIR);
|
|
5058
|
-
const all = existsSync25(dir) ?
|
|
5328
|
+
const all = existsSync25(dir) ? readdirSync10(dir).filter((file) => file.endsWith(".seed.ts")).sort() : [];
|
|
5059
5329
|
if (!names?.length) return all;
|
|
5060
5330
|
return names.map((name) => {
|
|
5061
5331
|
const file = `${name.replace(/\.seed\.ts$/, "")}.seed.ts`;
|
|
@@ -5085,11 +5355,12 @@ async function runSeeds(options2 = {}) {
|
|
|
5085
5355
|
try {
|
|
5086
5356
|
const db = drizzle(local.db, { schema });
|
|
5087
5357
|
const insertMany = createInsertMany(local.db, drizzleHelpers);
|
|
5358
|
+
const seedUser = drizzleSeedUser(db, schema);
|
|
5088
5359
|
for (const file of files) {
|
|
5089
5360
|
const mod = await jiti.import(join25(appRoot, SEEDS_DIR, file));
|
|
5090
5361
|
if (typeof mod.default !== "function") throw new Error(`${SEEDS_DIR}/${file} must \`export default defineSeed(async ({ db }) => { ... })\`.`);
|
|
5091
5362
|
log2(`${pc19.cyan("seed")} ${file}`);
|
|
5092
|
-
await mod.default({ db, env: local.env, insertMany, fake: createFake(), log: (message) => log2(` ${message}`) });
|
|
5363
|
+
await mod.default({ db, env: local.env, insertMany, seedUser, fake: createFake(), log: (message) => log2(` ${message}`) });
|
|
5093
5364
|
}
|
|
5094
5365
|
log2(pc19.green(`
|
|
5095
5366
|
Ran ${files.length} seed(s) against the local database.`));
|
|
@@ -5097,6 +5368,17 @@ Ran ${files.length} seed(s) against the local database.`));
|
|
|
5097
5368
|
await local.dispose();
|
|
5098
5369
|
}
|
|
5099
5370
|
}
|
|
5371
|
+
function drizzleSeedUser(db, schema) {
|
|
5372
|
+
return async (input) => {
|
|
5373
|
+
const user = schema.user;
|
|
5374
|
+
const account = schema.account;
|
|
5375
|
+
if (!user || !account) throw new Error("db/schema.ts has no `user` and `account` tables, so there is nothing to sign in against.");
|
|
5376
|
+
const rows = await credentialUserRows(input);
|
|
5377
|
+
await db.insert(user).values(rows.user);
|
|
5378
|
+
await db.insert(account).values(rows.account);
|
|
5379
|
+
return rows.user;
|
|
5380
|
+
};
|
|
5381
|
+
}
|
|
5100
5382
|
function sampleValue(key, def) {
|
|
5101
5383
|
const name = key.toLowerCase();
|
|
5102
5384
|
switch (def.kind) {
|
|
@@ -5144,18 +5426,22 @@ function renderSeed(resource, stack = "cloudflare") {
|
|
|
5144
5426
|
if (!resource) {
|
|
5145
5427
|
return stack === "next" ? `import { defineSeed } from "@flaredev/core";
|
|
5146
5428
|
|
|
5147
|
-
export default defineSeed(async ({ db, insertMany, fake, log }) => {
|
|
5429
|
+
export default defineSeed(async ({ db, insertMany, seedUser, fake, log }) => {
|
|
5148
5430
|
// \`db\` is this app's Prisma client, e.g.:
|
|
5149
5431
|
// await db.contact.create({ data: { name: "Ada", email: "ada@example.com" } });
|
|
5150
5432
|
// Or many at once: await insertMany("contact", [{ name: fake.fullName() }]);
|
|
5433
|
+
// Somebody you can sign in as, with the credential account Better Auth looks for:
|
|
5434
|
+
// await seedUser({ email: "ada@example.com", password: "correct-horse-battery", role: "admin" });
|
|
5151
5435
|
log("nothing to seed yet");
|
|
5152
5436
|
});
|
|
5153
5437
|
` : `import { defineSeed } from "@flaredev/core";
|
|
5154
5438
|
|
|
5155
|
-
export default defineSeed(async ({ db, insertMany, fake, log }) => {
|
|
5439
|
+
export default defineSeed(async ({ db, insertMany, seedUser, fake, log }) => {
|
|
5156
5440
|
// Insert rows with Drizzle, e.g.:
|
|
5157
5441
|
// await db.insert(contacts).values([{ name: "Ada", email: "ada@example.com" }]);
|
|
5158
5442
|
// Or many at once, batched: insertMany(contacts, 10_000, () => ({ name: fake.fullName() }));
|
|
5443
|
+
// Somebody you can sign in as, with the credential account Better Auth looks for:
|
|
5444
|
+
// await seedUser({ email: "ada@example.com", password: "correct-horse-battery", role: "admin" });
|
|
5159
5445
|
log("nothing to seed yet");
|
|
5160
5446
|
});
|
|
5161
5447
|
`;
|
|
@@ -5225,10 +5511,17 @@ Run it with ${pc19.bold(`flare seed ${name}`)}.`);
|
|
|
5225
5511
|
return relative3;
|
|
5226
5512
|
}
|
|
5227
5513
|
async function runPrismaSeeds(appRoot, files, log2) {
|
|
5514
|
+
if (!existsSync25(join25(appRoot, "lib", "generated", "prisma"))) {
|
|
5515
|
+
log2(pc19.dim("Generating the Prisma client first (migrate dev no longer does)\u2026"));
|
|
5516
|
+
const prisma = resolveBin(appRoot, "prisma", "prisma");
|
|
5517
|
+
if (await runNode([prisma, "generate"], appRoot) !== 0) {
|
|
5518
|
+
throw new Error("`prisma generate` failed, so there is no client for the seeds to use.");
|
|
5519
|
+
}
|
|
5520
|
+
}
|
|
5228
5521
|
const runner = join25(appRoot, ".flare-seed-runner.mts");
|
|
5229
5522
|
const script = [
|
|
5230
5523
|
`import "dotenv/config";`,
|
|
5231
|
-
`import { createFake } from "@flaredev/core";`,
|
|
5524
|
+
`import { createFake, credentialUserRows } from "@flaredev/core";`,
|
|
5232
5525
|
`import { prisma } from "./lib/db";`,
|
|
5233
5526
|
...files.map((file, index) => `import seed${index} from "./${SEEDS_DIR}/${file.replace(/\.ts$/, "")}";`),
|
|
5234
5527
|
"",
|
|
@@ -5246,6 +5539,13 @@ async function runPrismaSeeds(appRoot, files, log2) {
|
|
|
5246
5539
|
` if (!delegate) throw new Error('No Prisma model "' + model + '". Use the camelCase name, e.g. "orderItem".');`,
|
|
5247
5540
|
` await delegate.createMany({ data: rows });`,
|
|
5248
5541
|
` },`,
|
|
5542
|
+
` seedUser: async (input: Parameters<typeof credentialUserRows>[0]) => {`,
|
|
5543
|
+
` const rows = await credentialUserRows(input);`,
|
|
5544
|
+
` const models = prisma as unknown as Record<string, { create(args: { data: unknown }): Promise<unknown> }>;`,
|
|
5545
|
+
` await models.user.create({ data: rows.user });`,
|
|
5546
|
+
` await models.account.create({ data: rows.account });`,
|
|
5547
|
+
` return rows.user;`,
|
|
5548
|
+
` },`,
|
|
5249
5549
|
` fake: createFake(),`,
|
|
5250
5550
|
` log: (message: string) => console.log(" " + message),`,
|
|
5251
5551
|
` });`,
|
|
@@ -5381,7 +5681,7 @@ import { join as join27 } from "path";
|
|
|
5381
5681
|
import pc21 from "picocolors";
|
|
5382
5682
|
|
|
5383
5683
|
// src/seed/rows.ts
|
|
5384
|
-
import { columnName as columnName3, createFake as createFake2, storedFields as storedFields4 } from "@flaredev/core";
|
|
5684
|
+
import { columnName as columnName3, createFake as createFake2, storedFields as storedFields4, toMinorUnits } from "@flaredev/core";
|
|
5385
5685
|
var has = (key, ...words) => words.some((word) => key.includes(word));
|
|
5386
5686
|
function stringValue(key, def, fake, resource, index) {
|
|
5387
5687
|
switch (def.format) {
|
|
@@ -5435,7 +5735,8 @@ function numberValue(key, def, fake) {
|
|
|
5435
5735
|
min = min ?? low;
|
|
5436
5736
|
max = max ?? high;
|
|
5437
5737
|
}
|
|
5438
|
-
|
|
5738
|
+
const amount = def.kind === "int" ? fake.int(Math.ceil(min), Math.floor(max)) : fake.float(min, max, 2);
|
|
5739
|
+
return def.format === "money" ? toMinorUnits(amount, def.currency) : amount;
|
|
5439
5740
|
}
|
|
5440
5741
|
function fieldValue(key, def, fake, resource, parentIds, index) {
|
|
5441
5742
|
const name = key.toLowerCase();
|
|
@@ -5665,7 +5966,7 @@ ${result.output.trim()}`);
|
|
|
5665
5966
|
|
|
5666
5967
|
// src/commands/billing-sync.ts
|
|
5667
5968
|
import { createRequire as createRequire2 } from "module";
|
|
5668
|
-
import { existsSync as existsSync26, mkdtempSync as mkdtempSync4, readFileSync as
|
|
5969
|
+
import { existsSync as existsSync26, mkdtempSync as mkdtempSync4, readFileSync as readFileSync18, rmSync as rmSync8, writeFileSync as writeFileSync21 } from "fs";
|
|
5669
5970
|
import { tmpdir as tmpdir4 } from "os";
|
|
5670
5971
|
import { join as join28 } from "path";
|
|
5671
5972
|
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
@@ -5675,7 +5976,7 @@ function stripeKeys(appRoot) {
|
|
|
5675
5976
|
const map = {};
|
|
5676
5977
|
const path = join28(appRoot, ".dev.vars");
|
|
5677
5978
|
if (existsSync26(path)) {
|
|
5678
|
-
for (const line of
|
|
5979
|
+
for (const line of readFileSync18(path, "utf8").split(/\r?\n/)) {
|
|
5679
5980
|
const match = /^\s*([A-Z][A-Z0-9_]*)=(.*)$/.exec(line);
|
|
5680
5981
|
if (match) map[match[1]] = match[2].trim();
|
|
5681
5982
|
}
|
|
@@ -5761,7 +6062,7 @@ async function syncPlans(options2 = {}) {
|
|
|
5761
6062
|
const appRequire = createRequire2(join28(appRoot, "package.json"));
|
|
5762
6063
|
const stripeModule = await import(pathToFileURL2(appRequire.resolve("stripe")).href);
|
|
5763
6064
|
const stripe = new stripeModule.default(key, { apiVersion: "2026-08-26.dahlia" });
|
|
5764
|
-
const appName = JSON.parse(
|
|
6065
|
+
const appName = JSON.parse(readFileSync18(join28(appRoot, "package.json"), "utf8")).name ?? "app";
|
|
5765
6066
|
const everything = await stripe.products.list({ active: true, limit: 100 }).autoPagingToArray({ limit: 1e4 });
|
|
5766
6067
|
const products = options2.all ? everything : everything.filter((product) => product.metadata?.flare_app === appName);
|
|
5767
6068
|
if (products.length === 0) {
|
|
@@ -5936,13 +6237,13 @@ async function setUserRoleOnPrisma(appRoot, email, role, log2) {
|
|
|
5936
6237
|
|
|
5937
6238
|
// src/utils/install.ts
|
|
5938
6239
|
import { spawn as spawn5 } from "child_process";
|
|
5939
|
-
import { existsSync as existsSync28, readFileSync as
|
|
6240
|
+
import { existsSync as existsSync28, readFileSync as readFileSync19, readdirSync as readdirSync11 } from "fs";
|
|
5940
6241
|
import { join as join31 } from "path";
|
|
5941
6242
|
function expectedPackages(dir) {
|
|
5942
6243
|
const npmLock = join31(dir, "package-lock.json");
|
|
5943
6244
|
if (existsSync28(npmLock)) {
|
|
5944
6245
|
try {
|
|
5945
|
-
const lock = JSON.parse(
|
|
6246
|
+
const lock = JSON.parse(readFileSync19(npmLock, "utf8"));
|
|
5946
6247
|
return Math.max(0, Object.keys(lock.packages ?? {}).length - 1);
|
|
5947
6248
|
} catch {
|
|
5948
6249
|
return 0;
|
|
@@ -5951,7 +6252,7 @@ function expectedPackages(dir) {
|
|
|
5951
6252
|
const pnpmLock = join31(dir, "pnpm-lock.yaml");
|
|
5952
6253
|
if (existsSync28(pnpmLock)) {
|
|
5953
6254
|
try {
|
|
5954
|
-
const text =
|
|
6255
|
+
const text = readFileSync19(pnpmLock, "utf8");
|
|
5955
6256
|
const packages = text.slice(text.indexOf("\npackages:"));
|
|
5956
6257
|
return (packages.match(/\n {2}[^\s:]+:/g) ?? []).length;
|
|
5957
6258
|
} catch {
|
|
@@ -5965,11 +6266,11 @@ function installedPackages(dir) {
|
|
|
5965
6266
|
if (!existsSync28(root)) return 0;
|
|
5966
6267
|
let total = 0;
|
|
5967
6268
|
try {
|
|
5968
|
-
for (const entry of
|
|
6269
|
+
for (const entry of readdirSync11(root, { withFileTypes: true })) {
|
|
5969
6270
|
if (!entry.isDirectory() || entry.name === ".bin") continue;
|
|
5970
6271
|
if (entry.name.startsWith("@")) {
|
|
5971
6272
|
try {
|
|
5972
|
-
total +=
|
|
6273
|
+
total += readdirSync11(join31(root, entry.name)).length;
|
|
5973
6274
|
} catch {
|
|
5974
6275
|
}
|
|
5975
6276
|
} else if (entry.name !== ".pnpm" && entry.name !== ".vite") {
|
|
@@ -6024,7 +6325,7 @@ function createCli() {
|
|
|
6024
6325
|
const stack = currentStack();
|
|
6025
6326
|
const perStack = (cloudflare, next, either) => stack === "next" ? next : stack === "cloudflare" ? cloudflare : either;
|
|
6026
6327
|
const cloudflareOnly = (text) => stack === void 0 ? `${text} (Cloudflare only)` : text;
|
|
6027
|
-
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:
|
|
6328
|
+
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: next (default) or cloudflare").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(
|
|
6028
6329
|
async (dir, options2) => {
|
|
6029
6330
|
const wanted = !options2.skipInstall;
|
|
6030
6331
|
let answers = { stack: options2.stack, theme: options2.theme, auth: options2.auth, authProviders: options2.authProviders };
|
|
@@ -6051,7 +6352,7 @@ function createCli() {
|
|
|
6051
6352
|
printNextSteps(result, installed);
|
|
6052
6353
|
}
|
|
6053
6354
|
);
|
|
6054
|
-
cli.command("gen <generator> [name] [second]", `Generate code. Generators: ${GENERATORS.join(", ")}`).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("--soft-delete", "resource: keep deleted rows and hide them, with a Trash view in the dashboard").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("--own <field>", "policy: confine each user to rows whose belongsTo(User) field holds their id, e.g. --own userId").option("--own-except <roles>", "policy: roles that see every row despite --own, e.g. admin").option("--provider <provider>", "billing: payment provider (default: stripe)").option("--mode <mode>", "billing: subscriptions (the default; includes one-time checkout)").option("--skip-install", "billing/apikeys: write files without adding the dependency").option("--skip-migration", "billing/security/apikeys: skip generating the schema migration").example("flare gen resource Contact --fields 'name:string, email:string!, company:belongsTo(Company)?'").example("flare gen resource Invoice --fields 'number:string!' --soft-delete # a delete goes to the Trash").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 policy Invoice --roles staff --own userId --own-except admin # each user sees only their own").example("flare gen billing --provider stripe --mode subscriptions").example("flare gen security").example("flare gen apikeys # API keys for cron jobs, scripts and mobile apps").example("flare gen apikeys # bearer keys for crons, scripts and mobile apps").action(
|
|
6355
|
+
cli.command("gen <generator> [name] [second]", `Generate code. Generators: ${GENERATORS.join(", ")}`).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("--soft-delete", "resource: keep deleted rows and hide them, with a Trash view in the dashboard").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 (Cloudflare only)").example("flare gen migration add_phone --from-schema # Cloudflare: diff the Drizzle tables").example("flare gen migration add_phone # Next.js: prisma migrate dev, refusing a drop").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("--own <field>", "policy: confine each user to rows whose belongsTo(User) field holds their id, e.g. --own userId").option("--own-except <roles>", "policy: roles that see every row despite --own, e.g. admin").option("--provider <provider>", "billing: payment provider (default: stripe)").option("--mode <mode>", "billing: subscriptions (the default; includes one-time checkout)").option("--skip-install", "billing/apikeys: write files without adding the dependency").option("--skip-migration", "billing/security/apikeys: skip generating the schema migration").example("flare gen resource Contact --fields 'name:string, email:string!, company:belongsTo(Company)?'").example("flare gen resource Invoice --fields 'number:string!' --soft-delete # a delete goes to the Trash").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 policy Invoice --roles staff --own userId --own-except admin # each user sees only their own").example("flare gen billing --provider stripe --mode subscriptions").example("flare gen security").example("flare gen apikeys # API keys for cron jobs, scripts and mobile apps").example("flare gen apikeys # bearer keys for crons, scripts and mobile apps").action(
|
|
6055
6356
|
async (generator, name, second, options2) => {
|
|
6056
6357
|
if (generator === "billing")
|
|
6057
6358
|
return genBilling({ provider: options2.provider, mode: options2.mode, force: options2.force, skipInstall: options2.skipInstall, skipMigration: options2.skipMigration });
|