@flaredev/cli 0.7.1 → 0.7.3
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
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 existsSync4, readdirSync as readdirSync2, rmSync, writeFileSync as writeFileSync3 } from "fs";
|
|
13
|
+
import { basename, dirname as dirname2, isAbsolute, join as join4, 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";
|
|
@@ -282,6 +282,8 @@ var templatesDir = join2(findUp("templates", dirname(fileURLToPath(import.meta.u
|
|
|
282
282
|
|
|
283
283
|
// src/utils/pm.ts
|
|
284
284
|
import { spawnSync } from "child_process";
|
|
285
|
+
import { existsSync as existsSync3 } from "fs";
|
|
286
|
+
import { delimiter, join as join3 } from "path";
|
|
285
287
|
var MANAGERS = ["pnpm", "npm", "yarn", "bun"];
|
|
286
288
|
function isPackageManager(value) {
|
|
287
289
|
return MANAGERS.includes(value);
|
|
@@ -291,9 +293,9 @@ function detectPackageManager(userAgent = process.env.npm_config_user_agent ?? "
|
|
|
291
293
|
return isPackageManager(name) ? name : "pnpm";
|
|
292
294
|
}
|
|
293
295
|
function isInstalled(packageManager) {
|
|
294
|
-
const
|
|
295
|
-
const
|
|
296
|
-
return
|
|
296
|
+
const dirs = (process.env.PATH ?? "").split(delimiter).filter(Boolean);
|
|
297
|
+
const suffixes = process.platform === "win32" ? (process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";") : [""];
|
|
298
|
+
return dirs.some((dir) => suffixes.some((suffix) => existsSync3(join3(dir, packageManager + suffix.toLowerCase()))));
|
|
297
299
|
}
|
|
298
300
|
function choosePackageManager(explicit) {
|
|
299
301
|
if (explicit !== void 0) {
|
|
@@ -332,7 +334,7 @@ function toAppName(dir) {
|
|
|
332
334
|
function flarePackageSpec(pkg, appDir, packageManager) {
|
|
333
335
|
const repoPackage = resolve2(templatesDir, "../..", pkg);
|
|
334
336
|
const repoRoot = resolve2(repoPackage, "../..");
|
|
335
|
-
const fromCheckout =
|
|
337
|
+
const fromCheckout = existsSync4(join4(repoPackage, "package.json")) && existsSync4(join4(repoRoot, "pnpm-workspace.yaml")) && !repoPackage.split(/[\\/]/).includes("node_modules");
|
|
336
338
|
if (!fromCheckout) return `^${FLARE_VERSION}`;
|
|
337
339
|
const rel = relative(repoRoot, appDir);
|
|
338
340
|
if (rel && !rel.startsWith("..") && !isAbsolute(rel)) return "workspace:*";
|
|
@@ -342,7 +344,7 @@ function flarePackageSpec(pkg, appDir, packageManager) {
|
|
|
342
344
|
function createApp(target, options2 = {}) {
|
|
343
345
|
const dir = resolve2(target);
|
|
344
346
|
const name = toAppName(dir);
|
|
345
|
-
if (
|
|
347
|
+
if (existsSync4(dir) && readdirSync2(dir).length > 0) {
|
|
346
348
|
throw new Error(`Directory ${dir} already exists and is not empty.`);
|
|
347
349
|
}
|
|
348
350
|
const { packageManager, preferred: prefersPnpm } = choosePackageManager(options2.pm);
|
|
@@ -355,23 +357,23 @@ function createApp(target, options2 = {}) {
|
|
|
355
357
|
const theme = parseTheme(options2.theme);
|
|
356
358
|
const inWorkspace = findUp("pnpm-workspace.yaml", dirname2(dir)) !== void 0;
|
|
357
359
|
const compatibilityDate = options2.compatibilityDate ?? COMPATIBILITY_DATE;
|
|
358
|
-
const files = copyTemplate(
|
|
360
|
+
const files = copyTemplate(join4(templatesDir, "app"), dir, {
|
|
359
361
|
APP_NAME: name,
|
|
360
362
|
COMPAT_DATE: compatibilityDate,
|
|
361
363
|
PM: packageManager,
|
|
362
364
|
THEME: theme
|
|
363
365
|
});
|
|
364
366
|
if (stack === "next") {
|
|
365
|
-
copyTemplate(
|
|
366
|
-
for (const path of CLOUDFLARE_ONLY) rmSync(
|
|
367
|
+
copyTemplate(join4(templatesDir, "next"), dir, { APP_NAME: name, PM: packageManager, THEME: theme });
|
|
368
|
+
for (const path of CLOUDFLARE_ONLY) rmSync(join4(dir, path), { recursive: true, force: true });
|
|
367
369
|
}
|
|
368
|
-
writeFileSync3(
|
|
369
|
-
appendFileSync(
|
|
370
|
+
writeFileSync3(join4(dir, "lib", "auth-config.ts"), renderAuthConfig(authMethods, authProviders));
|
|
371
|
+
appendFileSync(join4(dir, stack === "next" ? ".env.example" : ".dev.vars.example"), devVarsExampleEntries(authProviders));
|
|
370
372
|
const shared = { react: APP_DEPENDENCIES.react, "react-dom": APP_DEPENDENCIES["react-dom"] };
|
|
371
373
|
const uiOnly = Object.fromEntries(
|
|
372
374
|
Object.entries(APP_DEPENDENCIES).filter(([key]) => !["vinext", "@vinext/cloudflare", "drizzle-orm", "react-server-dom-webpack"].includes(key))
|
|
373
375
|
);
|
|
374
|
-
writeJson(
|
|
376
|
+
writeJson(join4(dir, "package.json"), {
|
|
375
377
|
name,
|
|
376
378
|
version: "0.1.0",
|
|
377
379
|
private: true,
|
|
@@ -405,14 +407,14 @@ function createApp(target, options2 = {}) {
|
|
|
405
407
|
}
|
|
406
408
|
});
|
|
407
409
|
writeFileSync3(
|
|
408
|
-
|
|
410
|
+
join4(dir, stack === "next" ? ".env" : ".dev.vars"),
|
|
409
411
|
`BETTER_AUTH_SECRET=${randomBytes(32).toString("base64")}
|
|
410
412
|
RESEND_API_KEY=
|
|
411
413
|
MAIL_FROM=
|
|
412
414
|
${devVarsEntries(authProviders)}`
|
|
413
415
|
);
|
|
414
416
|
if (packageManager === "pnpm" && !inWorkspace) {
|
|
415
|
-
writeFileSync3(
|
|
417
|
+
writeFileSync3(join4(dir, "pnpm-workspace.yaml"), `allowBuilds:
|
|
416
418
|
${ALLOW_BUILDS}
|
|
417
419
|
`);
|
|
418
420
|
}
|
|
@@ -426,9 +428,9 @@ ${ALLOW_BUILDS}
|
|
|
426
428
|
function copyLockfile(packageManager, dir) {
|
|
427
429
|
const file = packageManager === "pnpm" ? "pnpm-lock.yaml" : packageManager === "yarn" ? "yarn.lock" : "package-lock.json";
|
|
428
430
|
if (!file) return false;
|
|
429
|
-
const source =
|
|
430
|
-
if (!
|
|
431
|
-
copyFileSync(source,
|
|
431
|
+
const source = join4(templatesDir, "locks", file);
|
|
432
|
+
if (!existsSync4(source)) return false;
|
|
433
|
+
copyFileSync(source, join4(dir, file));
|
|
432
434
|
return true;
|
|
433
435
|
}
|
|
434
436
|
function printNextSteps(result, installed) {
|
|
@@ -448,8 +450,8 @@ function printNextSteps(result, installed) {
|
|
|
448
450
|
}
|
|
449
451
|
|
|
450
452
|
// src/commands/gen-billing.ts
|
|
451
|
-
import { appendFileSync as appendFileSync2, existsSync as
|
|
452
|
-
import { dirname as dirname6, join as
|
|
453
|
+
import { appendFileSync as appendFileSync2, existsSync as existsSync14, mkdirSync as mkdirSync5, readFileSync as readFileSync9, unlinkSync, writeFileSync as writeFileSync7 } from "fs";
|
|
454
|
+
import { dirname as dirname6, join as join13 } from "path";
|
|
453
455
|
import pc6 from "picocolors";
|
|
454
456
|
|
|
455
457
|
// src/generator/grammar.ts
|
|
@@ -613,7 +615,7 @@ import { defineResource, field as builders, kebabCase, pascalCase } from "@flare
|
|
|
613
615
|
|
|
614
616
|
// src/generator/markers.ts
|
|
615
617
|
import { createHash } from "crypto";
|
|
616
|
-
import { existsSync as
|
|
618
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
617
619
|
import { dirname as dirname3 } from "path";
|
|
618
620
|
var START = "// generated:start";
|
|
619
621
|
var END = "// generated:end";
|
|
@@ -663,7 +665,7 @@ ${indentBlock(body, indent)}${indent}${END}
|
|
|
663
665
|
${after}`;
|
|
664
666
|
}
|
|
665
667
|
function inspectGenerated(path, content) {
|
|
666
|
-
if (!
|
|
668
|
+
if (!existsSync5(path)) return "missing";
|
|
667
669
|
const parts = splitMarkers(readFileSync3(path, "utf8"));
|
|
668
670
|
if (!parts) return "unmarked";
|
|
669
671
|
if (parts.hash && hashBlock(parts.block) !== parts.hash) return "drift";
|
|
@@ -680,7 +682,7 @@ var DriftError = class extends Error {
|
|
|
680
682
|
};
|
|
681
683
|
function writeGenerated(path, content, options2 = {}) {
|
|
682
684
|
const hash = hashBlock(content);
|
|
683
|
-
if (!
|
|
685
|
+
if (!existsSync5(path)) {
|
|
684
686
|
mkdirSync2(dirname3(path), { recursive: true });
|
|
685
687
|
writeFileSync4(path, joinMarkers({ before: options2.header ?? "", block: content, after: "", indent: "", hash }));
|
|
686
688
|
return "create";
|
|
@@ -1446,17 +1448,17 @@ var billingDevVarsExampleEntries = () => [
|
|
|
1446
1448
|
].join("\n");
|
|
1447
1449
|
|
|
1448
1450
|
// src/generator/load.ts
|
|
1449
|
-
import { existsSync as
|
|
1450
|
-
import { join as
|
|
1451
|
+
import { existsSync as existsSync6, readdirSync as readdirSync3 } from "fs";
|
|
1452
|
+
import { join as join5, resolve as resolve3 } from "path";
|
|
1451
1453
|
import { createJiti } from "jiti";
|
|
1452
1454
|
async function loadResources(root) {
|
|
1453
1455
|
const appRoot = resolve3(root);
|
|
1454
|
-
const dir =
|
|
1455
|
-
if (!
|
|
1456
|
-
const jiti = createJiti(
|
|
1456
|
+
const dir = join5(appRoot, "resources");
|
|
1457
|
+
if (!existsSync6(dir)) return [];
|
|
1458
|
+
const jiti = createJiti(join5(appRoot, "package.json"), { alias: { "@": appRoot }, moduleCache: false, fsCache: false });
|
|
1457
1459
|
const loaded = [];
|
|
1458
1460
|
for (const file of readdirSync3(dir).filter((name) => name.endsWith(".resource.ts")).sort()) {
|
|
1459
|
-
const path =
|
|
1461
|
+
const path = join5(dir, file);
|
|
1460
1462
|
const mod = await jiti.import(path);
|
|
1461
1463
|
const resource = mod.default;
|
|
1462
1464
|
if (!resource || typeof resource !== "object" || !("fields" in resource) || !("table" in resource)) {
|
|
@@ -1474,20 +1476,20 @@ async function loadResources(root) {
|
|
|
1474
1476
|
|
|
1475
1477
|
// src/generator/migrations.ts
|
|
1476
1478
|
import { spawn as spawn4 } from "child_process";
|
|
1477
|
-
import { existsSync as
|
|
1478
|
-
import { join as
|
|
1479
|
+
import { existsSync as existsSync11, readdirSync as readdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "fs";
|
|
1480
|
+
import { join as join10 } from "path";
|
|
1479
1481
|
|
|
1480
1482
|
// src/commands/run.ts
|
|
1481
1483
|
import { spawn as spawn3 } from "child_process";
|
|
1482
|
-
import { existsSync as
|
|
1483
|
-
import { dirname as dirname4, join as
|
|
1484
|
+
import { existsSync as existsSync10, readFileSync as readFileSync6 } from "fs";
|
|
1485
|
+
import { dirname as dirname4, join as join9, resolve as resolve4 } from "path";
|
|
1484
1486
|
import pc4 from "picocolors";
|
|
1485
1487
|
|
|
1486
1488
|
// src/tunnel.ts
|
|
1487
1489
|
import { spawn, spawnSync as spawnSync2 } from "child_process";
|
|
1488
|
-
import { chmodSync, existsSync as
|
|
1490
|
+
import { chmodSync, existsSync as existsSync7, mkdirSync as mkdirSync3, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync5 } from "fs";
|
|
1489
1491
|
import { homedir } from "os";
|
|
1490
|
-
import { join as
|
|
1492
|
+
import { join as join6 } from "path";
|
|
1491
1493
|
import pc2 from "picocolors";
|
|
1492
1494
|
var TUNNEL_URL = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/;
|
|
1493
1495
|
var LOCAL_URL = /https?:\/\/(?:localhost|127\.0\.0\.1|\[::1\]|0\.0\.0\.0):\d+/;
|
|
@@ -1512,7 +1514,7 @@ function releaseAsset(platform = process.platform, arch = process.arch) {
|
|
|
1512
1514
|
throw new Error(`No cloudflared build for ${platform}/${arch}. Install it yourself: https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/`);
|
|
1513
1515
|
}
|
|
1514
1516
|
var binName = process.platform === "win32" ? "cloudflared.exe" : "cloudflared";
|
|
1515
|
-
var cacheDir = () =>
|
|
1517
|
+
var cacheDir = () => join6(homedir(), ".flare", "bin");
|
|
1516
1518
|
function onPath() {
|
|
1517
1519
|
const probe = spawnSync2(process.platform === "win32" ? "where" : "which", ["cloudflared"], { encoding: "utf8" });
|
|
1518
1520
|
return probe.status === 0 ? probe.stdout.split(/\r?\n/)[0]?.trim() ?? null : null;
|
|
@@ -1521,7 +1523,7 @@ async function download(log2) {
|
|
|
1521
1523
|
const asset = releaseAsset();
|
|
1522
1524
|
const dir = cacheDir();
|
|
1523
1525
|
mkdirSync3(dir, { recursive: true });
|
|
1524
|
-
const target =
|
|
1526
|
+
const target = join6(dir, binName);
|
|
1525
1527
|
log2(pc2.dim(`Downloading cloudflared (one time) into ${dir}\u2026`));
|
|
1526
1528
|
const response = await fetch(`https://github.com/cloudflare/cloudflared/releases/latest/download/${asset}`);
|
|
1527
1529
|
if (!response.ok) throw new Error(`Downloading cloudflared failed: HTTP ${response.status}.`);
|
|
@@ -1542,8 +1544,8 @@ async function download(log2) {
|
|
|
1542
1544
|
async function ensureCloudflared(log2 = console.log) {
|
|
1543
1545
|
const found = onPath();
|
|
1544
1546
|
if (found) return found;
|
|
1545
|
-
const cached =
|
|
1546
|
-
if (
|
|
1547
|
+
const cached = join6(cacheDir(), binName);
|
|
1548
|
+
if (existsSync7(cached)) return cached;
|
|
1547
1549
|
return download(log2);
|
|
1548
1550
|
}
|
|
1549
1551
|
async function openTunnel(localUrl, options2 = {}) {
|
|
@@ -1599,16 +1601,16 @@ function tunnelBanner(publicUrl, localUrl) {
|
|
|
1599
1601
|
|
|
1600
1602
|
// src/commands/deploy.ts
|
|
1601
1603
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
1602
|
-
import { existsSync as
|
|
1603
|
-
import { join as
|
|
1604
|
+
import { existsSync as existsSync9, readFileSync as readFileSync5 } from "fs";
|
|
1605
|
+
import { join as join8 } from "path";
|
|
1604
1606
|
import { createZoneClient } from "@flaredev/core/security";
|
|
1605
1607
|
import { createJiti as createJiti2 } from "jiti";
|
|
1606
1608
|
import pc3 from "picocolors";
|
|
1607
1609
|
|
|
1608
1610
|
// src/utils/wrangler.ts
|
|
1609
1611
|
import { spawn as spawn2 } from "child_process";
|
|
1610
|
-
import { existsSync as
|
|
1611
|
-
import { join as
|
|
1612
|
+
import { existsSync as existsSync8, readFileSync as readFileSync4 } from "fs";
|
|
1613
|
+
import { join as join7 } from "path";
|
|
1612
1614
|
import { parse as parseJsonc } from "jsonc-parser";
|
|
1613
1615
|
function runWrangler(wranglerBin, args, cwd, options2 = {}) {
|
|
1614
1616
|
return new Promise((resolve6, reject) => {
|
|
@@ -1633,8 +1635,8 @@ ${output.trim()}`);
|
|
|
1633
1635
|
return JSON.parse(output.slice(start, end + 1));
|
|
1634
1636
|
}
|
|
1635
1637
|
function readD1Databases(appRoot) {
|
|
1636
|
-
const path =
|
|
1637
|
-
if (!
|
|
1638
|
+
const path = join7(appRoot, "wrangler.jsonc");
|
|
1639
|
+
if (!existsSync8(path)) return [];
|
|
1638
1640
|
const config = parseJsonc(readFileSync4(path, "utf8"));
|
|
1639
1641
|
return (config?.d1_databases ?? []).filter((db) => db.binding && db.database_name);
|
|
1640
1642
|
}
|
|
@@ -1675,15 +1677,15 @@ function declaredSecretNames(devVarsExample) {
|
|
|
1675
1677
|
return devVarsExample.split(/\r?\n/).map((line) => /^([A-Z][A-Z0-9_]*)=/.exec(line.trim())?.[1]).filter((name) => Boolean(name));
|
|
1676
1678
|
}
|
|
1677
1679
|
async function loadSecurityConfig(appRoot) {
|
|
1678
|
-
const path =
|
|
1679
|
-
if (!
|
|
1680
|
-
const jiti = createJiti2(
|
|
1680
|
+
const path = join8(appRoot, "security.config.ts");
|
|
1681
|
+
if (!existsSync9(path)) return null;
|
|
1682
|
+
const jiti = createJiti2(join8(appRoot, "package.json"), { moduleCache: false, fsCache: false });
|
|
1681
1683
|
const mod = await jiti.import(path);
|
|
1682
1684
|
if (!mod.default) throw new Error("security.config.ts must `export default defineSecurity({ ... })`.");
|
|
1683
1685
|
return mod.default;
|
|
1684
1686
|
}
|
|
1685
1687
|
async function provisionZoneSecurity(appRoot, config, credentials, options2) {
|
|
1686
|
-
const app = JSON.parse(readFileSync5(
|
|
1688
|
+
const app = JSON.parse(readFileSync5(join8(appRoot, "package.json"), "utf8")).name ?? "app";
|
|
1687
1689
|
const zone = createZoneClient({ ...credentials, app, fetch: options2.fetch });
|
|
1688
1690
|
if (config.zone?.rateLimits) {
|
|
1689
1691
|
const result = await zone.syncRateLimits(config.zone.rateLimits);
|
|
@@ -1703,7 +1705,7 @@ async function runDeploy(args, context3) {
|
|
|
1703
1705
|
const envArgs = plan.env ? ["--env", plan.env] : [];
|
|
1704
1706
|
const step = (message) => log2(`
|
|
1705
1707
|
${pc3.bold(pc3.cyan("flare"))} ${message}`);
|
|
1706
|
-
const databases = plan.skipMigrations ? [] : readD1Databases(appRoot).filter((db) =>
|
|
1708
|
+
const databases = plan.skipMigrations ? [] : readD1Databases(appRoot).filter((db) => existsSync9(join8(appRoot, db.migrations_dir ?? "migrations")));
|
|
1707
1709
|
const migrateAfterDeploy = [];
|
|
1708
1710
|
const migrate2 = async (db) => {
|
|
1709
1711
|
step(`Applying migrations to ${db.database_name} (remote)`);
|
|
@@ -1762,8 +1764,8 @@ ${put.output.trim()}`);
|
|
|
1762
1764
|
existing.add(name);
|
|
1763
1765
|
}
|
|
1764
1766
|
}
|
|
1765
|
-
const examplePath =
|
|
1766
|
-
const optional =
|
|
1767
|
+
const examplePath = join8(appRoot, ".dev.vars.example");
|
|
1768
|
+
const optional = existsSync9(examplePath) ? declaredSecretNames(readFileSync5(examplePath, "utf8")).filter((name) => !existing.has(name)) : [];
|
|
1767
1769
|
if (optional.length > 0) {
|
|
1768
1770
|
step("Optional secrets not set in production (features using them stay off):");
|
|
1769
1771
|
for (const name of optional) log2(` wrangler secret put ${name}${plan.env ? ` --env ${plan.env}` : ""}`);
|
|
@@ -1798,7 +1800,7 @@ function findAppRoot(cwd) {
|
|
|
1798
1800
|
while (dir) {
|
|
1799
1801
|
const root = findUp("package.json", dir);
|
|
1800
1802
|
if (!root) break;
|
|
1801
|
-
const pkg = JSON.parse(readFileSync6(
|
|
1803
|
+
const pkg = JSON.parse(readFileSync6(join9(root, "package.json"), "utf8"));
|
|
1802
1804
|
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
1803
1805
|
if (deps["@flaredev/core"] || deps.vinext) return root;
|
|
1804
1806
|
const parent = dirname4(root);
|
|
@@ -1807,15 +1809,15 @@ function findAppRoot(cwd) {
|
|
|
1807
1809
|
throw new Error("Not inside a Flare app (no package.json depending on @flaredev/core found). Run this from your app directory.");
|
|
1808
1810
|
}
|
|
1809
1811
|
function resolveBin(appRoot, pkgName, binName2) {
|
|
1810
|
-
const pkgDir = findUp(
|
|
1812
|
+
const pkgDir = findUp(join9("node_modules", pkgName, "package.json"), appRoot);
|
|
1811
1813
|
if (!pkgDir) {
|
|
1812
1814
|
throw new Error(`Cannot find ${pkgName} in ${appRoot}. Install dependencies first (e.g. pnpm install).`);
|
|
1813
1815
|
}
|
|
1814
|
-
const manifestPath =
|
|
1816
|
+
const manifestPath = join9(pkgDir, "node_modules", pkgName, "package.json");
|
|
1815
1817
|
const { bin } = JSON.parse(readFileSync6(manifestPath, "utf8"));
|
|
1816
1818
|
const relativeBin = typeof bin === "string" ? bin : bin?.[binName2];
|
|
1817
1819
|
if (!relativeBin) throw new Error(`${pkgName} does not provide a "${binName2}" executable.`);
|
|
1818
|
-
return
|
|
1820
|
+
return join9(dirname4(manifestPath), relativeBin);
|
|
1819
1821
|
}
|
|
1820
1822
|
var NEXT_COMMANDS = {
|
|
1821
1823
|
dev: { pkg: "next", bin: "next", args: ["dev"] },
|
|
@@ -1888,7 +1890,7 @@ async function runDelegated(command, forwarded, cwd = process.cwd()) {
|
|
|
1888
1890
|
if (code !== 0) return code;
|
|
1889
1891
|
}
|
|
1890
1892
|
const built = next ? ".next" : "dist/server/wrangler.json";
|
|
1891
|
-
if (command === "start" && !wantsHelp && !
|
|
1893
|
+
if (command === "start" && !wantsHelp && !existsSync10(join9(appRoot, built))) {
|
|
1892
1894
|
console.log("No production build found; running `flare build` first.\n");
|
|
1893
1895
|
const code = await runDelegated("build", [], appRoot);
|
|
1894
1896
|
if (code !== 0) return code;
|
|
@@ -1939,9 +1941,9 @@ function snapshotColumns(snapshot) {
|
|
|
1939
1941
|
}
|
|
1940
1942
|
|
|
1941
1943
|
// src/generator/migrations.ts
|
|
1942
|
-
var listSql = (dir) =>
|
|
1944
|
+
var listSql = (dir) => existsSync11(dir) ? readdirSync4(dir).filter((file) => file.endsWith(".sql")) : [];
|
|
1943
1945
|
async function generateSchemaMigration(appRoot, name, options2 = {}) {
|
|
1944
|
-
const dir =
|
|
1946
|
+
const dir = join10(appRoot, "migrations");
|
|
1945
1947
|
const before = new Set(listSql(dir));
|
|
1946
1948
|
const bin = resolveBin(appRoot, "drizzle-kit", "drizzle-kit");
|
|
1947
1949
|
const { code, output } = await new Promise((resolve6, reject) => {
|
|
@@ -1969,7 +1971,7 @@ If it asked whether a column was renamed, run \`pnpm exec drizzle-kit generate\`
|
|
|
1969
1971
|
for (const file of created) {
|
|
1970
1972
|
const previous = previousSnapshot(dir, file);
|
|
1971
1973
|
if (!previous) continue;
|
|
1972
|
-
const path =
|
|
1974
|
+
const path = join10(dir, file);
|
|
1973
1975
|
const { sql: sql2, changes } = repairRebuildMigration(readFileSync7(path, "utf8"), snapshotColumns(previous));
|
|
1974
1976
|
if (changes.length) {
|
|
1975
1977
|
writeFileSync6(path, sql2);
|
|
@@ -1980,32 +1982,32 @@ If it asked whether a column was renamed, run \`pnpm exec drizzle-kit generate\`
|
|
|
1980
1982
|
}
|
|
1981
1983
|
function previousSnapshot(dir, migrationFile) {
|
|
1982
1984
|
const prefix = /^(\d+)_/.exec(migrationFile)?.[1];
|
|
1983
|
-
const metaDir =
|
|
1984
|
-
if (!prefix || !
|
|
1985
|
+
const metaDir = join10(dir, "meta");
|
|
1986
|
+
if (!prefix || !existsSync11(metaDir)) return void 0;
|
|
1985
1987
|
const earlier = readdirSync4(metaDir).filter((file) => /^\d+_snapshot\.json$/.test(file) && file < `${prefix}_snapshot.json`).sort().pop();
|
|
1986
|
-
return earlier ? JSON.parse(readFileSync7(
|
|
1988
|
+
return earlier ? JSON.parse(readFileSync7(join10(metaDir, earlier), "utf8")) : void 0;
|
|
1987
1989
|
}
|
|
1988
1990
|
|
|
1989
1991
|
// src/generator/plan.ts
|
|
1990
|
-
import { copyFileSync as copyFileSync2, existsSync as
|
|
1991
|
-
import { dirname as dirname5, join as
|
|
1992
|
+
import { copyFileSync as copyFileSync2, existsSync as existsSync13, mkdirSync as mkdirSync4, readdirSync as readdirSync6, readFileSync as readFileSync8 } from "fs";
|
|
1993
|
+
import { dirname as dirname5, join as join12, sep } from "path";
|
|
1992
1994
|
import pc5 from "picocolors";
|
|
1993
1995
|
|
|
1994
1996
|
// src/generator/policy.ts
|
|
1995
|
-
import { existsSync as
|
|
1996
|
-
import { join as
|
|
1997
|
+
import { existsSync as existsSync12, readdirSync as readdirSync5 } from "fs";
|
|
1998
|
+
import { join as join11, resolve as resolve5 } from "path";
|
|
1997
1999
|
import { camelCase as camelCase2, kebabCase as kebabCase2 } from "@flaredev/core";
|
|
1998
2000
|
import { createJiti as createJiti3 } from "jiti";
|
|
1999
2001
|
var policyPath = (name) => `policies/${kebabCase2(name)}.policy.ts`;
|
|
2000
2002
|
var policyLocal = (stem) => `${camelCase2(stem)}Policy`;
|
|
2001
2003
|
async function loadPolicies(root) {
|
|
2002
2004
|
const appRoot = resolve5(root);
|
|
2003
|
-
const dir =
|
|
2004
|
-
if (!
|
|
2005
|
-
const jiti = createJiti3(
|
|
2005
|
+
const dir = join11(appRoot, "policies");
|
|
2006
|
+
if (!existsSync12(dir)) return [];
|
|
2007
|
+
const jiti = createJiti3(join11(appRoot, "package.json"), { moduleCache: false, fsCache: false });
|
|
2006
2008
|
const loaded = [];
|
|
2007
2009
|
for (const file of readdirSync5(dir).filter((name) => name.endsWith(".policy.ts")).sort()) {
|
|
2008
|
-
const mod = await jiti.import(
|
|
2010
|
+
const mod = await jiti.import(join11(dir, file));
|
|
2009
2011
|
const policy = mod.default;
|
|
2010
2012
|
if (!policy || typeof policy !== "object" || !("resource" in policy) || !Array.isArray(policy.read)) {
|
|
2011
2013
|
throw new Error(`policies/${file} must \`export default definePolicy({ ... })\`.`);
|
|
@@ -2677,7 +2679,7 @@ function planFiles(all, policies, stack = "cloudflare") {
|
|
|
2677
2679
|
}
|
|
2678
2680
|
function applyPlan(appRoot, plan, options2 = {}) {
|
|
2679
2681
|
return plan.map(({ path, content, header }) => {
|
|
2680
|
-
const absolute =
|
|
2682
|
+
const absolute = join12(appRoot, path);
|
|
2681
2683
|
const status = inspectGenerated(absolute, content);
|
|
2682
2684
|
if (status === "unmarked") return { path, status, outcome: "skipped-unmarked" };
|
|
2683
2685
|
if (status === "drift" && !options2.force) return { path, status, outcome: "skipped-drift" };
|
|
@@ -2696,22 +2698,22 @@ function applyPlan(appRoot, plan, options2 = {}) {
|
|
|
2696
2698
|
function findOrphans(appRoot, all) {
|
|
2697
2699
|
const planned = new Set(planFiles(all).map((file) => file.path));
|
|
2698
2700
|
const candidates = [];
|
|
2699
|
-
const list2 = (dir, match) =>
|
|
2701
|
+
const list2 = (dir, match) => existsSync13(join12(appRoot, dir)) ? readdirSync6(join12(appRoot, dir)).filter(match).map((name) => `${dir}/${name}`) : [];
|
|
2700
2702
|
candidates.push(...list2("db/schema", (name) => name.endsWith(".ts")));
|
|
2701
2703
|
candidates.push(...list2("resources", (name) => /\.(client|validators)\.ts$/.test(name)));
|
|
2702
2704
|
for (const dir of list2("app/api", () => true)) {
|
|
2703
|
-
for (const file of [`${dir}/route.ts`, `${dir}/[id]/route.ts`]) if (
|
|
2705
|
+
for (const file of [`${dir}/route.ts`, `${dir}/[id]/route.ts`]) if (existsSync13(join12(appRoot, file))) candidates.push(file);
|
|
2704
2706
|
}
|
|
2705
2707
|
for (const dir of list2("app/dashboard", () => true)) {
|
|
2706
2708
|
const pages = [`${dir}/page.tsx`, `${dir}/new/page.tsx`, `${dir}/[id]/page.tsx`, `${dir}/[id]/edit/page.tsx`];
|
|
2707
2709
|
const skeletons = [`${dir}/loading.tsx`, `${dir}/new/loading.tsx`, `${dir}/[id]/loading.tsx`, `${dir}/[id]/edit/loading.tsx`];
|
|
2708
2710
|
for (const file of [...pages, ...skeletons]) {
|
|
2709
|
-
if (
|
|
2711
|
+
if (existsSync13(join12(appRoot, file))) candidates.push(file);
|
|
2710
2712
|
}
|
|
2711
2713
|
}
|
|
2712
2714
|
const orphans = [];
|
|
2713
2715
|
for (const path of candidates) {
|
|
2714
|
-
const match = HEADER_PATTERN.exec(readFileSync8(
|
|
2716
|
+
const match = HEADER_PATTERN.exec(readFileSync8(join12(appRoot, path), "utf8"));
|
|
2715
2717
|
if (match && !planned.has(path)) orphans.push({ path, resource: match[1] });
|
|
2716
2718
|
}
|
|
2717
2719
|
return orphans;
|
|
@@ -2722,10 +2724,10 @@ function ensureSupportFiles(appRoot, log2) {
|
|
|
2722
2724
|
(name) => `lib/resource/${name}`
|
|
2723
2725
|
);
|
|
2724
2726
|
for (const path of ["lib/api.ts", "lib/cache.ts", ...engine]) {
|
|
2725
|
-
const target =
|
|
2726
|
-
if (
|
|
2727
|
-
const source = [
|
|
2728
|
-
(candidate) => (stack === "next" || !candidate.includes(`${sep}next${sep}`)) &&
|
|
2727
|
+
const target = join12(appRoot, path);
|
|
2728
|
+
if (existsSync13(target)) continue;
|
|
2729
|
+
const source = [join12(templatesDir, "next", path), join12(templatesDir, "app", path)].find(
|
|
2730
|
+
(candidate) => (stack === "next" || !candidate.includes(`${sep}next${sep}`)) && existsSync13(candidate)
|
|
2729
2731
|
);
|
|
2730
2732
|
if (!source) continue;
|
|
2731
2733
|
mkdirSync4(dirname5(target), { recursive: true });
|
|
@@ -2757,14 +2759,14 @@ function billingFiles(appName) {
|
|
|
2757
2759
|
}
|
|
2758
2760
|
var RETIRED_FILES = ["app/api/billing/status/route.ts", "components/billing/subscribe-button.tsx"];
|
|
2759
2761
|
function appendVars(appRoot, target) {
|
|
2760
|
-
const path =
|
|
2761
|
-
const text =
|
|
2762
|
+
const path = join13(appRoot, target);
|
|
2763
|
+
const text = existsSync14(path) ? readFileSync9(path, "utf8") : "";
|
|
2762
2764
|
if (/STRIPE_SECRET_KEY/.test(text)) return false;
|
|
2763
2765
|
appendFileSync2(path, target === ".dev.vars" ? billingDevVarsEntries() : billingDevVarsExampleEntries());
|
|
2764
2766
|
return true;
|
|
2765
2767
|
}
|
|
2766
2768
|
function addStripeDependency(appRoot) {
|
|
2767
|
-
const path =
|
|
2769
|
+
const path = join13(appRoot, "package.json");
|
|
2768
2770
|
const pkg = JSON.parse(readFileSync9(path, "utf8"));
|
|
2769
2771
|
pkg.dependencies ??= {};
|
|
2770
2772
|
if (pkg.dependencies.stripe) return false;
|
|
@@ -2774,8 +2776,8 @@ function addStripeDependency(appRoot) {
|
|
|
2774
2776
|
}
|
|
2775
2777
|
function writeBillingResource(appRoot, name, force, log2) {
|
|
2776
2778
|
const relative3 = descriptorPath(name);
|
|
2777
|
-
const path =
|
|
2778
|
-
if (!
|
|
2779
|
+
const path = join13(appRoot, relative3);
|
|
2780
|
+
if (!existsSync14(path)) {
|
|
2779
2781
|
mkdirSync5(dirname6(path), { recursive: true });
|
|
2780
2782
|
writeFileSync7(path, renderBillingDescriptor(name));
|
|
2781
2783
|
log2(`${pc6.green("create".padEnd(9))} ${relative3}`);
|
|
@@ -2805,25 +2807,25 @@ async function genBilling(options2 = {}) {
|
|
|
2805
2807
|
for (const name of ["Plan", "Customer", "Purchase"]) writeBillingResource(appRoot, name, options2.force, log2);
|
|
2806
2808
|
for (const [name, roles] of Object.entries(BILLING_POLICIES)) {
|
|
2807
2809
|
const relative3 = policyPath(name);
|
|
2808
|
-
if (
|
|
2810
|
+
if (existsSync14(join13(appRoot, relative3))) {
|
|
2809
2811
|
log2(`${pc6.dim("kept".padEnd(9))} ${relative3} (already exists)`);
|
|
2810
2812
|
continue;
|
|
2811
2813
|
}
|
|
2812
|
-
mkdirSync5(
|
|
2813
|
-
writeFileSync7(
|
|
2814
|
+
mkdirSync5(join13(appRoot, "policies"), { recursive: true });
|
|
2815
|
+
writeFileSync7(join13(appRoot, relative3), renderPolicy(name, roles));
|
|
2814
2816
|
log2(`${pc6.green("create".padEnd(9))} ${relative3}`);
|
|
2815
2817
|
}
|
|
2816
2818
|
const files = [];
|
|
2817
|
-
const appName = JSON.parse(readFileSync9(
|
|
2819
|
+
const appName = JSON.parse(readFileSync9(join13(appRoot, "package.json"), "utf8")).name ?? "app";
|
|
2818
2820
|
for (const { path, content } of billingFiles(appName)) {
|
|
2819
|
-
const outcome = writeGenerated(
|
|
2821
|
+
const outcome = writeGenerated(join13(appRoot, path), content, { header: billingHeader(), force: options2.force });
|
|
2820
2822
|
files.push({ path, status: "missing", outcome });
|
|
2821
2823
|
const label = outcome === "create" ? pc6.green("create") : outcome === "update" ? pc6.yellow("update") : pc6.dim("identical");
|
|
2822
2824
|
log2(`${label.padEnd(18)} ${path}`);
|
|
2823
2825
|
}
|
|
2824
2826
|
for (const path of RETIRED_FILES) {
|
|
2825
|
-
const absolute =
|
|
2826
|
-
if (!
|
|
2827
|
+
const absolute = join13(appRoot, path);
|
|
2828
|
+
if (!existsSync14(absolute)) continue;
|
|
2827
2829
|
const parts = splitMarkers(readFileSync9(absolute, "utf8"));
|
|
2828
2830
|
const untouched = parts && parts.before.includes("flare gen billing") && !parts.after.trim() && parts.hash !== void 0 && hashBlock(parts.block) === parts.hash;
|
|
2829
2831
|
if (untouched) {
|
|
@@ -2868,8 +2870,8 @@ async function genBilling(options2 = {}) {
|
|
|
2868
2870
|
}
|
|
2869
2871
|
|
|
2870
2872
|
// src/commands/gen-security.ts
|
|
2871
|
-
import { existsSync as
|
|
2872
|
-
import { dirname as dirname7, join as
|
|
2873
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync6, readFileSync as readFileSync10, writeFileSync as writeFileSync8 } from "fs";
|
|
2874
|
+
import { dirname as dirname7, join as join14 } from "path";
|
|
2873
2875
|
import pc7 from "picocolors";
|
|
2874
2876
|
|
|
2875
2877
|
// src/generator/security.ts
|
|
@@ -3397,8 +3399,8 @@ function securityFiles(appName) {
|
|
|
3397
3399
|
}
|
|
3398
3400
|
var PASS_THROUGH_HEADER = /^\/\/ Request protection for worker\/index\.ts\.[\s\S]*?\n(?=\/\/ generated:start)/;
|
|
3399
3401
|
function writeOnce(appRoot, relative3, content, log2) {
|
|
3400
|
-
const path =
|
|
3401
|
-
if (
|
|
3402
|
+
const path = join14(appRoot, relative3);
|
|
3403
|
+
if (existsSync15(path)) {
|
|
3402
3404
|
log2(`${pc7.dim("kept".padEnd(9))} ${relative3} (already exists)`);
|
|
3403
3405
|
return;
|
|
3404
3406
|
}
|
|
@@ -3409,14 +3411,14 @@ function writeOnce(appRoot, relative3, content, log2) {
|
|
|
3409
3411
|
async function genSecurity(options2 = {}) {
|
|
3410
3412
|
const log2 = options2.log ?? ((message) => console.log(message));
|
|
3411
3413
|
const appRoot = findAppRoot(options2.cwd ?? process.cwd());
|
|
3412
|
-
const appName = JSON.parse(readFileSync10(
|
|
3414
|
+
const appName = JSON.parse(readFileSync10(join14(appRoot, "package.json"), "utf8")).name ?? "app";
|
|
3413
3415
|
writeOnce(appRoot, "security.config.ts", renderSecurityConfig(), log2);
|
|
3414
3416
|
writeOnce(appRoot, descriptorPath("SecurityEvent"), renderSecurityDescriptor(), log2);
|
|
3415
3417
|
writeOnce(appRoot, policyPath("SecurityEvent"), renderPolicy("SecurityEvent", SECURITY_EVENT_POLICY), log2);
|
|
3416
3418
|
const files = [];
|
|
3417
3419
|
const write = (path, content, header) => {
|
|
3418
|
-
const absolute =
|
|
3419
|
-
if (path === "lib/security.ts" &&
|
|
3420
|
+
const absolute = join14(appRoot, path);
|
|
3421
|
+
if (path === "lib/security.ts" && existsSync15(absolute)) {
|
|
3420
3422
|
const source = readFileSync10(absolute, "utf8");
|
|
3421
3423
|
if (PASS_THROUGH_HEADER.test(source)) writeFileSync8(absolute, source.replace(PASS_THROUGH_HEADER, securityHeader()));
|
|
3422
3424
|
}
|
|
@@ -3426,12 +3428,12 @@ async function genSecurity(options2 = {}) {
|
|
|
3426
3428
|
log2(`${label.padEnd(18)} ${path}`);
|
|
3427
3429
|
};
|
|
3428
3430
|
for (const { path, content } of securityFiles(appName)) write(path, content, securityHeader());
|
|
3429
|
-
const navPath =
|
|
3430
|
-
if (
|
|
3431
|
+
const navPath = join14(appRoot, "lib/dashboard-nav.ts");
|
|
3432
|
+
if (existsSync15(navPath) && splitMarkers(readFileSync10(navPath, "utf8"))) write("lib/dashboard-nav.ts", renderAdminNavBlock());
|
|
3431
3433
|
else log2(pc7.yellow(`lib/dashboard-nav.ts not found: link to /dashboard/security from your dashboard sidebar yourself.`));
|
|
3432
|
-
const wranglerPath =
|
|
3434
|
+
const wranglerPath = join14(appRoot, "wrangler.jsonc");
|
|
3433
3435
|
let bindings = [];
|
|
3434
|
-
if (
|
|
3436
|
+
if (existsSync15(wranglerPath)) {
|
|
3435
3437
|
const { text, added } = addSecurityBindings(readFileSync10(wranglerPath, "utf8"));
|
|
3436
3438
|
bindings = added;
|
|
3437
3439
|
if (added.length) {
|
|
@@ -3466,14 +3468,14 @@ async function genSecurity(options2 = {}) {
|
|
|
3466
3468
|
}
|
|
3467
3469
|
|
|
3468
3470
|
// src/commands/theme.ts
|
|
3469
|
-
import { existsSync as
|
|
3470
|
-
import { join as
|
|
3471
|
+
import { existsSync as existsSync16, readFileSync as readFileSync11, writeFileSync as writeFileSync9 } from "fs";
|
|
3472
|
+
import { join as join15 } from "path";
|
|
3471
3473
|
import pc8 from "picocolors";
|
|
3472
3474
|
var THEME_LINE = /theme: "([a-z]+)" as ThemeName,/;
|
|
3473
3475
|
function setTheme(name, cwd = process.cwd(), log2 = console.log) {
|
|
3474
3476
|
const appRoot = findAppRoot(cwd);
|
|
3475
|
-
const path =
|
|
3476
|
-
if (!
|
|
3477
|
+
const path = join15(appRoot, "lib/site.ts");
|
|
3478
|
+
if (!existsSync16(path)) throw new Error("lib/site.ts not found: this app was created before themes. Add it from the Flare app template.");
|
|
3477
3479
|
const source = readFileSync11(path, "utf8");
|
|
3478
3480
|
const current = THEME_LINE.exec(source)?.[1];
|
|
3479
3481
|
if (!name) {
|
|
@@ -3566,8 +3568,8 @@ async function askCreateQuestions(given, appName) {
|
|
|
3566
3568
|
}
|
|
3567
3569
|
|
|
3568
3570
|
// src/commands/gen.ts
|
|
3569
|
-
import { existsSync as
|
|
3570
|
-
import { dirname as dirname8, join as
|
|
3571
|
+
import { existsSync as existsSync17, mkdirSync as mkdirSync7, writeFileSync as writeFileSync10 } from "fs";
|
|
3572
|
+
import { dirname as dirname8, join as join16 } from "path";
|
|
3571
3573
|
import { camelCase as camelCase5, defineResource as defineResource2, relationGraph as relationGraph2 } from "@flaredev/core";
|
|
3572
3574
|
import pc10 from "picocolors";
|
|
3573
3575
|
function validateFields(name, fields, others, log2) {
|
|
@@ -3588,8 +3590,8 @@ async function genResource(rawName, options2) {
|
|
|
3588
3590
|
const appRoot = findAppRoot(options2.cwd ?? process.cwd());
|
|
3589
3591
|
const name = resourceName(rawName);
|
|
3590
3592
|
const relativePath = descriptorPath(name);
|
|
3591
|
-
const path =
|
|
3592
|
-
const exists =
|
|
3593
|
+
const path = join16(appRoot, relativePath);
|
|
3594
|
+
const exists = existsSync17(path);
|
|
3593
3595
|
if (!exists && !options2.fields) {
|
|
3594
3596
|
throw new Error(`--fields is required, e.g. flare gen resource ${name} --fields "name:string, email:string"`);
|
|
3595
3597
|
}
|
|
@@ -3654,8 +3656,8 @@ Next: ${pc10.bold("flare migrate")} to apply it locally.`);
|
|
|
3654
3656
|
}
|
|
3655
3657
|
|
|
3656
3658
|
// src/commands/gen-endpoint.ts
|
|
3657
|
-
import { existsSync as
|
|
3658
|
-
import { join as
|
|
3659
|
+
import { existsSync as existsSync18, mkdirSync as mkdirSync8, writeFileSync as writeFileSync11 } from "fs";
|
|
3660
|
+
import { join as join17 } from "path";
|
|
3659
3661
|
import { kebabCase as kebabCase3, pascalCase as pascalCase3 } from "@flaredev/core";
|
|
3660
3662
|
import pc11 from "picocolors";
|
|
3661
3663
|
var METHODS = ["GET", "POST", "PATCH", "PUT", "DELETE"];
|
|
@@ -3677,9 +3679,9 @@ async function genEndpoint(resourceName2, rawName, options2 = {}) {
|
|
|
3677
3679
|
if (!ACTIONS.includes(action)) throw new Error(`Unknown action "${action}". Use one of ${ACTIONS.join(", ")}.`);
|
|
3678
3680
|
const { resource, stem } = entry;
|
|
3679
3681
|
const relative3 = options2.record ? `app/api/${resource.slug}/[id]/${name}/route.ts` : `app/api/${resource.slug}/${name}/route.ts`;
|
|
3680
|
-
const path =
|
|
3681
|
-
if (
|
|
3682
|
-
mkdirSync8(
|
|
3682
|
+
const path = join17(appRoot, relative3);
|
|
3683
|
+
if (existsSync18(path)) throw new Error(`${relative3} already exists.`);
|
|
3684
|
+
mkdirSync8(join17(path, ".."), { recursive: true });
|
|
3683
3685
|
const route = options2.record ? `/api/${resource.slug}/[id]/${name}` : `/api/${resource.slug}/${name}`;
|
|
3684
3686
|
writeFileSync11(path, renderEndpoint({ resource: resource.name, stem, method, action, record: Boolean(options2.record), name, route }));
|
|
3685
3687
|
log2(`${pc11.green("create".padEnd(9))} ${relative3}`);
|
|
@@ -3717,8 +3719,8 @@ ${body}
|
|
|
3717
3719
|
}
|
|
3718
3720
|
|
|
3719
3721
|
// src/commands/gen-migration.ts
|
|
3720
|
-
import { existsSync as
|
|
3721
|
-
import { basename as basename2, join as
|
|
3722
|
+
import { existsSync as existsSync19, mkdirSync as mkdirSync9, writeFileSync as writeFileSync12 } from "fs";
|
|
3723
|
+
import { basename as basename2, join as join18 } from "path";
|
|
3722
3724
|
import { snakeCase } from "@flaredev/core";
|
|
3723
3725
|
import pc12 from "picocolors";
|
|
3724
3726
|
var DOWN_TEMPLATE = (up) => `-- Rollback for ${up}, run by \`flare migrate:rollback\`.
|
|
@@ -3739,11 +3741,11 @@ async function genMigration(rawName, options2 = {}) {
|
|
|
3739
3741
|
for (const file of files) log2(`${pc12.green("create".padEnd(9))} ${file}`);
|
|
3740
3742
|
for (const repair of repairs) log2(pc12.dim(`repaired ${repair}`));
|
|
3741
3743
|
if (!options2.fromSchema) {
|
|
3742
|
-
const downDir =
|
|
3744
|
+
const downDir = join18(appRoot, "migrations", "down");
|
|
3743
3745
|
mkdirSync9(downDir, { recursive: true });
|
|
3744
3746
|
for (const file of files) {
|
|
3745
|
-
const down =
|
|
3746
|
-
if (
|
|
3747
|
+
const down = join18(downDir, basename2(file));
|
|
3748
|
+
if (existsSync19(down)) continue;
|
|
3747
3749
|
writeFileSync12(down, DOWN_TEMPLATE(basename2(file)));
|
|
3748
3750
|
log2(`${pc12.green("create".padEnd(9))} migrations/down/${basename2(file)}`);
|
|
3749
3751
|
}
|
|
@@ -3757,8 +3759,8 @@ Next: ${pc12.bold("flare migrate")}.`);
|
|
|
3757
3759
|
}
|
|
3758
3760
|
|
|
3759
3761
|
// src/commands/gen-policy.ts
|
|
3760
|
-
import { existsSync as
|
|
3761
|
-
import { dirname as dirname9, join as
|
|
3762
|
+
import { existsSync as existsSync20, mkdirSync as mkdirSync10, writeFileSync as writeFileSync13 } from "fs";
|
|
3763
|
+
import { dirname as dirname9, join as join19 } from "path";
|
|
3762
3764
|
import { definePolicy } from "@flaredev/core";
|
|
3763
3765
|
import pc13 from "picocolors";
|
|
3764
3766
|
function parseRoles(input, what = "--roles") {
|
|
@@ -3776,8 +3778,8 @@ async function genPolicy(rawName, options2) {
|
|
|
3776
3778
|
throw new Error(`No resource "${name}". Generate it first. Known resources: ${known}.`);
|
|
3777
3779
|
}
|
|
3778
3780
|
const relative3 = policyPath(name);
|
|
3779
|
-
const path =
|
|
3780
|
-
const exists =
|
|
3781
|
+
const path = join19(appRoot, relative3);
|
|
3782
|
+
const exists = existsSync20(path);
|
|
3781
3783
|
let roles;
|
|
3782
3784
|
let deleteRoles;
|
|
3783
3785
|
if (options2.roles || !exists) {
|
|
@@ -3811,9 +3813,9 @@ ${pc13.dim("Roles come from `flare role:add`; the API and the admin both enforce
|
|
|
3811
3813
|
}
|
|
3812
3814
|
|
|
3813
3815
|
// src/commands/migrate.ts
|
|
3814
|
-
import { existsSync as
|
|
3816
|
+
import { existsSync as existsSync21, mkdtempSync, readFileSync as readFileSync12, rmSync as rmSync3, writeFileSync as writeFileSync14 } from "fs";
|
|
3815
3817
|
import { tmpdir } from "os";
|
|
3816
|
-
import { join as
|
|
3818
|
+
import { join as join20 } from "path";
|
|
3817
3819
|
import pc14 from "picocolors";
|
|
3818
3820
|
|
|
3819
3821
|
// src/generator/down.ts
|
|
@@ -3941,7 +3943,7 @@ async function rollback(options2 = {}) {
|
|
|
3941
3943
|
const steps = options2.steps ?? 1;
|
|
3942
3944
|
if (!Number.isInteger(steps) || steps < 1) throw new Error("--steps must be a positive integer.");
|
|
3943
3945
|
const { appRoot, db, target, where, wrangler, log: log2 } = context(options2);
|
|
3944
|
-
const migrationsDir =
|
|
3946
|
+
const migrationsDir = join20(appRoot, db.migrations_dir ?? "migrations");
|
|
3945
3947
|
const applied = await appliedMigrations(options2, steps);
|
|
3946
3948
|
if (applied.length === 0) {
|
|
3947
3949
|
log2("Nothing to roll back.");
|
|
@@ -3950,8 +3952,8 @@ async function rollback(options2 = {}) {
|
|
|
3950
3952
|
const plan = [];
|
|
3951
3953
|
for (const { name } of applied) {
|
|
3952
3954
|
if (!/^[\w.-]+\.sql$/.test(name)) throw new Error(`Unexpected migration name "${name}" in d1_migrations.`);
|
|
3953
|
-
const handWritten =
|
|
3954
|
-
if (
|
|
3955
|
+
const handWritten = join20(migrationsDir, "down", name);
|
|
3956
|
+
if (existsSync21(handWritten)) {
|
|
3955
3957
|
const sql2 = readFileSync12(handWritten, "utf8");
|
|
3956
3958
|
if (splitStatements(sql2).length === 0) {
|
|
3957
3959
|
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.`);
|
|
@@ -3959,8 +3961,8 @@ async function rollback(options2 = {}) {
|
|
|
3959
3961
|
plan.push({ name, sql: sql2, source: `migrations/down/${name}` });
|
|
3960
3962
|
continue;
|
|
3961
3963
|
}
|
|
3962
|
-
const upPath =
|
|
3963
|
-
if (!
|
|
3964
|
+
const upPath = join20(migrationsDir, name);
|
|
3965
|
+
if (!existsSync21(upPath)) throw new Error(`${name} was applied but migrations/${name} is missing, so it can't be reversed.`);
|
|
3964
3966
|
const down = deriveDown(readFileSync12(upPath, "utf8"));
|
|
3965
3967
|
if (!down.reversible) {
|
|
3966
3968
|
throw new Error(
|
|
@@ -3981,9 +3983,9 @@ Write the reverse SQL to migrations/down/${name} and run the rollback again. Not
|
|
|
3981
3983
|
}
|
|
3982
3984
|
const script = plan.map(({ name, sql: sql2 }) => `${sql2.trim()}
|
|
3983
3985
|
DELETE FROM d1_migrations WHERE name = '${name.replace(/'/g, "''")}';`).join("\n") + "\n";
|
|
3984
|
-
const dir = mkdtempSync(
|
|
3986
|
+
const dir = mkdtempSync(join20(tmpdir(), "flare-rollback-"));
|
|
3985
3987
|
try {
|
|
3986
|
-
const file =
|
|
3988
|
+
const file = join20(dir, "rollback.sql");
|
|
3987
3989
|
writeFileSync14(file, script);
|
|
3988
3990
|
const result = await runWrangler(wrangler, ["d1", "execute", db.binding, ...target, "--file", file], appRoot, { capture: true });
|
|
3989
3991
|
if (result.code !== 0) throw new Error(`Rollback failed:
|
|
@@ -3997,8 +3999,8 @@ Rolled back ${plan.map((step) => step.name).join(", ")}.`));
|
|
|
3997
3999
|
}
|
|
3998
4000
|
|
|
3999
4001
|
// src/commands/rm.ts
|
|
4000
|
-
import { existsSync as
|
|
4001
|
-
import { dirname as dirname10, join as
|
|
4002
|
+
import { existsSync as existsSync22, readdirSync as readdirSync7, readFileSync as readFileSync13, rmdirSync, unlinkSync as unlinkSync2 } from "fs";
|
|
4003
|
+
import { dirname as dirname10, join as join21 } from "path";
|
|
4002
4004
|
import { relationGraph as relationGraph3 } from "@flaredev/core";
|
|
4003
4005
|
import pc15 from "picocolors";
|
|
4004
4006
|
var squash = (text) => text.replace(/\r\n/g, "\n").replace(/\s+/g, " ").trim();
|
|
@@ -4017,10 +4019,10 @@ function handWrittenReason(source, header, options2 = {}) {
|
|
|
4017
4019
|
return null;
|
|
4018
4020
|
}
|
|
4019
4021
|
function removeEmptyDirs(appRoot, start) {
|
|
4020
|
-
let dir = dirname10(
|
|
4021
|
-
const roots = [
|
|
4022
|
+
let dir = dirname10(join21(appRoot, start));
|
|
4023
|
+
const roots = [join21(appRoot, "app", "api"), join21(appRoot, "app", "dashboard")];
|
|
4022
4024
|
const inside = () => roots.some((root) => dir.startsWith(root) && dir !== root);
|
|
4023
|
-
while (inside() &&
|
|
4025
|
+
while (inside() && existsSync22(dir) && readdirSync7(dir).length === 0) {
|
|
4024
4026
|
rmdirSync(dir);
|
|
4025
4027
|
dir = dirname10(dir);
|
|
4026
4028
|
}
|
|
@@ -4049,7 +4051,7 @@ async function rmResource(rawName, options2 = {}) {
|
|
|
4049
4051
|
if (entry) {
|
|
4050
4052
|
owned.add(entry.file);
|
|
4051
4053
|
for (const file of planFiles(all, void 0, readStack(appRoot))) {
|
|
4052
|
-
if (
|
|
4054
|
+
if (existsSync22(join21(appRoot, file.path)) && file.header === resourceHeader(name)) owned.add(file.path);
|
|
4053
4055
|
}
|
|
4054
4056
|
}
|
|
4055
4057
|
for (const orphan of findOrphans(appRoot, remaining)) if (orphan.resource === name) owned.add(orphan.path);
|
|
@@ -4058,7 +4060,7 @@ async function rmResource(rawName, options2 = {}) {
|
|
|
4058
4060
|
if (!entry && owned.size === 0) throw new Error(`No resource "${name}" (no ${descriptorPath(name)} and no generated files).`);
|
|
4059
4061
|
const findings = [];
|
|
4060
4062
|
for (const path of owned) {
|
|
4061
|
-
const source = readFileSync13(
|
|
4063
|
+
const source = readFileSync13(join21(appRoot, path), "utf8");
|
|
4062
4064
|
let wrapper;
|
|
4063
4065
|
if (entry && path === entry.file) {
|
|
4064
4066
|
const standard = splitMarkers(renderDescriptor(name, [{ key: "placeholder", kind: "string", required: true, unique: false }]));
|
|
@@ -4079,15 +4081,15 @@ Move what you want to keep, or pass --force to delete anyway.`);
|
|
|
4079
4081
|
}
|
|
4080
4082
|
const removed = [...owned].sort();
|
|
4081
4083
|
for (const path of removed) {
|
|
4082
|
-
unlinkSync2(
|
|
4084
|
+
unlinkSync2(join21(appRoot, path));
|
|
4083
4085
|
removeEmptyDirs(appRoot, path);
|
|
4084
4086
|
log2(`${pc15.red("remove".padEnd(9))} ${path}`);
|
|
4085
4087
|
}
|
|
4086
4088
|
logResults(applyPlan(appRoot, planFiles(remaining, policy ? await loadPolicies(appRoot) : void 0, readStack(appRoot))), log2);
|
|
4087
4089
|
const table = entry ? tableExport(entry.resource) : void 0;
|
|
4088
|
-
if (table &&
|
|
4089
|
-
const seeds = readdirSync7(
|
|
4090
|
-
(file) => new RegExp(`\\b${table}\\b`).test(readFileSync13(
|
|
4090
|
+
if (table && existsSync22(join21(appRoot, "seeds"))) {
|
|
4091
|
+
const seeds = readdirSync7(join21(appRoot, "seeds")).filter(
|
|
4092
|
+
(file) => new RegExp(`\\b${table}\\b`).test(readFileSync13(join21(appRoot, "seeds", file), "utf8"))
|
|
4091
4093
|
);
|
|
4092
4094
|
if (seeds.length) log2(pc15.yellow(`
|
|
4093
4095
|
These seeds still use ${table}: ${seeds.map((s) => `seeds/${s}`).join(", ")}.`));
|
|
@@ -4157,21 +4159,28 @@ ${result.output.trim()}`);
|
|
|
4157
4159
|
}
|
|
4158
4160
|
|
|
4159
4161
|
// src/commands/eject.ts
|
|
4160
|
-
import { existsSync as
|
|
4161
|
-
import { dirname as dirname11, join as
|
|
4162
|
+
import { existsSync as existsSync23, mkdirSync as mkdirSync11, readFileSync as readFileSync14, readdirSync as readdirSync8, writeFileSync as writeFileSync15 } from "fs";
|
|
4163
|
+
import { dirname as dirname11, join as join22, relative as relative2 } from "path";
|
|
4162
4164
|
import pc17 from "picocolors";
|
|
4163
|
-
var TRACKED = ["lib
|
|
4165
|
+
var TRACKED = ["lib", "components"];
|
|
4166
|
+
var PER_APP = /__APP_NAME__|__PM__|__THEME__|__COMPAT_DATE__/;
|
|
4167
|
+
var WRITTEN_PER_APP = /* @__PURE__ */ new Set(["lib/auth-config.ts"]);
|
|
4164
4168
|
var prunedOn = (path, stack) => stack === "next" && CLOUDFLARE_ONLY.some((only) => path === only || path.startsWith(`${only}/`));
|
|
4165
4169
|
function upstreamFile(path, stack) {
|
|
4166
|
-
if (prunedOn(path, stack)) return void 0;
|
|
4167
|
-
const
|
|
4168
|
-
|
|
4170
|
+
if (prunedOn(path, stack) || WRITTEN_PER_APP.has(path)) return void 0;
|
|
4171
|
+
const found = locate(path, stack);
|
|
4172
|
+
if (found && PER_APP.test(readFileSync14(found, "utf8"))) return void 0;
|
|
4173
|
+
return found;
|
|
4174
|
+
}
|
|
4175
|
+
function locate(path, stack) {
|
|
4176
|
+
const candidates = stack === "next" ? [join22(templatesDir, "next", path), join22(templatesDir, "app", path)] : [join22(templatesDir, "app", path)];
|
|
4177
|
+
return candidates.find((candidate) => existsSync23(candidate));
|
|
4169
4178
|
}
|
|
4170
4179
|
function trackedPaths(appRoot, stack) {
|
|
4171
4180
|
const paths = /* @__PURE__ */ new Set();
|
|
4172
4181
|
const walk = (base, dir) => {
|
|
4173
|
-
if (!
|
|
4174
|
-
for (const entry of readdirSync8(
|
|
4182
|
+
if (!existsSync23(join22(base, dir))) return;
|
|
4183
|
+
for (const entry of readdirSync8(join22(base, dir), { withFileTypes: true })) {
|
|
4175
4184
|
const next = `${dir}/${entry.name}`;
|
|
4176
4185
|
if (entry.isDirectory()) walk(base, next);
|
|
4177
4186
|
else if (entry.name.endsWith(".ts") || entry.name.endsWith(".tsx")) paths.add(next);
|
|
@@ -4179,8 +4188,8 @@ function trackedPaths(appRoot, stack) {
|
|
|
4179
4188
|
};
|
|
4180
4189
|
for (const root of TRACKED) {
|
|
4181
4190
|
walk(appRoot, root);
|
|
4182
|
-
walk(
|
|
4183
|
-
if (stack === "next") walk(
|
|
4191
|
+
walk(join22(templatesDir, "app"), root);
|
|
4192
|
+
if (stack === "next") walk(join22(templatesDir, "next"), root);
|
|
4184
4193
|
}
|
|
4185
4194
|
return [...paths].filter((path) => upstreamFile(path, stack) !== void 0).sort();
|
|
4186
4195
|
}
|
|
@@ -4188,8 +4197,8 @@ function compareTracked(appRoot, stack, filter) {
|
|
|
4188
4197
|
return trackedPaths(appRoot, stack).filter((path) => !filter || path.includes(filter)).map((path) => {
|
|
4189
4198
|
const source = upstreamFile(path, stack);
|
|
4190
4199
|
const theirs = readFileSync14(source, "utf8");
|
|
4191
|
-
const target =
|
|
4192
|
-
if (!
|
|
4200
|
+
const target = join22(appRoot, path);
|
|
4201
|
+
if (!existsSync23(target)) return { path, state: "missing", theirs };
|
|
4193
4202
|
const mine = readFileSync14(target, "utf8");
|
|
4194
4203
|
return { path, state: mine === theirs ? "same" : "changed", mine, theirs };
|
|
4195
4204
|
});
|
|
@@ -4252,7 +4261,7 @@ This discards your edits to those files. Re-run with ${pc17.bold("--yes")} to pr
|
|
|
4252
4261
|
return 1;
|
|
4253
4262
|
}
|
|
4254
4263
|
for (const entry of changed) {
|
|
4255
|
-
const target =
|
|
4264
|
+
const target = join22(appRoot, entry.path);
|
|
4256
4265
|
mkdirSync11(dirname11(target), { recursive: true });
|
|
4257
4266
|
writeFileSync15(target, entry.theirs);
|
|
4258
4267
|
log2(`${pc17.green("update")} ${relative2(process.cwd(), target) || entry.path}`);
|
|
@@ -4263,8 +4272,8 @@ Updated ${changed.length} file(s).`));
|
|
|
4263
4272
|
}
|
|
4264
4273
|
|
|
4265
4274
|
// src/commands/seed.ts
|
|
4266
|
-
import { existsSync as
|
|
4267
|
-
import { join as
|
|
4275
|
+
import { existsSync as existsSync24, mkdirSync as mkdirSync12, readdirSync as readdirSync9, rmSync as rmSync5, writeFileSync as writeFileSync17 } from "fs";
|
|
4276
|
+
import { join as join24 } from "path";
|
|
4268
4277
|
import { camelCase as camelCase6, createFake, kebabCase as kebabCase4, storedFields as storedFields3 } from "@flaredev/core";
|
|
4269
4278
|
import { createJiti as createJiti4 } from "jiti";
|
|
4270
4279
|
import pc18 from "picocolors";
|
|
@@ -4363,18 +4372,18 @@ function createInsertMany(sink, drizzle, onProgress) {
|
|
|
4363
4372
|
// src/seed/local-d1.ts
|
|
4364
4373
|
import { readFileSync as readFileSync15, rmSync as rmSync4, writeFileSync as writeFileSync16 } from "fs";
|
|
4365
4374
|
import { createRequire } from "module";
|
|
4366
|
-
import { join as
|
|
4375
|
+
import { join as join23 } from "path";
|
|
4367
4376
|
import { pathToFileURL } from "url";
|
|
4368
4377
|
import { parse as parseJsonc3 } from "jsonc-parser";
|
|
4369
4378
|
var LOCAL_STATE = ".wrangler/state/v3";
|
|
4370
4379
|
async function openLocalD1(appRoot, binding) {
|
|
4371
4380
|
process.env.WRANGLER_LOG ??= "error";
|
|
4372
|
-
const appRequire = createRequire(
|
|
4381
|
+
const appRequire = createRequire(join23(appRoot, "package.json"));
|
|
4373
4382
|
const wrangler = await import(pathToFileURL(appRequire.resolve("wrangler")).href);
|
|
4374
4383
|
const configPath = seedConfig(appRoot);
|
|
4375
4384
|
const { env, dispose } = await wrangler.getPlatformProxy({
|
|
4376
4385
|
configPath,
|
|
4377
|
-
persist: { path:
|
|
4386
|
+
persist: { path: join23(appRoot, LOCAL_STATE) }
|
|
4378
4387
|
});
|
|
4379
4388
|
const bindings = env;
|
|
4380
4389
|
const name = binding ?? "DB";
|
|
@@ -4393,11 +4402,11 @@ async function openLocalD1(appRoot, binding) {
|
|
|
4393
4402
|
};
|
|
4394
4403
|
}
|
|
4395
4404
|
function seedConfig(appRoot) {
|
|
4396
|
-
const source =
|
|
4405
|
+
const source = join23(appRoot, "wrangler.jsonc");
|
|
4397
4406
|
const config = parseJsonc3(readFileSync15(source, "utf8")) ?? {};
|
|
4398
4407
|
delete config.durable_objects;
|
|
4399
4408
|
delete config.migrations;
|
|
4400
|
-
const path =
|
|
4409
|
+
const path = join23(appRoot, ".flare-seed.wrangler.json");
|
|
4401
4410
|
writeFileSync16(path, JSON.stringify(config, null, 2));
|
|
4402
4411
|
return path;
|
|
4403
4412
|
}
|
|
@@ -4405,8 +4414,8 @@ function seedConfig(appRoot) {
|
|
|
4405
4414
|
// src/commands/seed.ts
|
|
4406
4415
|
var SEEDS_DIR = "seeds";
|
|
4407
4416
|
function seedFiles(appRoot, names) {
|
|
4408
|
-
const dir =
|
|
4409
|
-
const all =
|
|
4417
|
+
const dir = join24(appRoot, SEEDS_DIR);
|
|
4418
|
+
const all = existsSync24(dir) ? readdirSync9(dir).filter((file) => file.endsWith(".seed.ts")).sort() : [];
|
|
4410
4419
|
if (!names?.length) return all;
|
|
4411
4420
|
return names.map((name) => {
|
|
4412
4421
|
const file = `${name.replace(/\.seed\.ts$/, "")}.seed.ts`;
|
|
@@ -4428,16 +4437,16 @@ async function runSeeds(options2 = {}) {
|
|
|
4428
4437
|
await runPrismaSeeds(appRoot, files, log2);
|
|
4429
4438
|
return;
|
|
4430
4439
|
}
|
|
4431
|
-
const jiti = createJiti4(
|
|
4440
|
+
const jiti = createJiti4(join24(appRoot, "package.json"), { alias: { "@": appRoot }, moduleCache: false, fsCache: false });
|
|
4432
4441
|
const { drizzle } = await jiti.import("drizzle-orm/d1");
|
|
4433
4442
|
const drizzleHelpers = await jiti.import("drizzle-orm");
|
|
4434
|
-
const schema = await jiti.import(
|
|
4443
|
+
const schema = await jiti.import(join24(appRoot, "db/schema.ts"));
|
|
4435
4444
|
const local = await openLocalD1(appRoot);
|
|
4436
4445
|
try {
|
|
4437
4446
|
const db = drizzle(local.db, { schema });
|
|
4438
4447
|
const insertMany = createInsertMany(local.db, drizzleHelpers);
|
|
4439
4448
|
for (const file of files) {
|
|
4440
|
-
const mod = await jiti.import(
|
|
4449
|
+
const mod = await jiti.import(join24(appRoot, SEEDS_DIR, file));
|
|
4441
4450
|
if (typeof mod.default !== "function") throw new Error(`${SEEDS_DIR}/${file} must \`export default defineSeed(async ({ db }) => { ... })\`.`);
|
|
4442
4451
|
log2(`${pc18.cyan("seed")} ${file}`);
|
|
4443
4452
|
await mod.default({ db, env: local.env, insertMany, fake: createFake(), log: (message) => log2(` ${message}`) });
|
|
@@ -4554,8 +4563,8 @@ async function makeSeed(rawName, options2 = {}) {
|
|
|
4554
4563
|
const name = kebabCase4(rawName);
|
|
4555
4564
|
if (!name) throw new Error(`Invalid seed name "${rawName}".`);
|
|
4556
4565
|
const relative3 = `${SEEDS_DIR}/${name}.seed.ts`;
|
|
4557
|
-
const path =
|
|
4558
|
-
if (
|
|
4566
|
+
const path = join24(appRoot, relative3);
|
|
4567
|
+
if (existsSync24(path)) throw new Error(`${relative3} already exists.`);
|
|
4559
4568
|
const resources = (await loadResources(appRoot)).map(({ resource: resource2 }) => resource2);
|
|
4560
4569
|
let resource;
|
|
4561
4570
|
if (options2.resource) {
|
|
@@ -4564,7 +4573,7 @@ async function makeSeed(rawName, options2 = {}) {
|
|
|
4564
4573
|
} else {
|
|
4565
4574
|
resource = resources.find((r) => [kebabCase4(r.name), r.slug].includes(name));
|
|
4566
4575
|
}
|
|
4567
|
-
mkdirSync12(
|
|
4576
|
+
mkdirSync12(join24(appRoot, SEEDS_DIR), { recursive: true });
|
|
4568
4577
|
writeFileSync17(path, renderSeed(resource, readStack(appRoot)));
|
|
4569
4578
|
log2(`${pc18.green("create".padEnd(9))} ${relative3}`);
|
|
4570
4579
|
log2(`
|
|
@@ -4572,7 +4581,7 @@ Run it with ${pc18.bold(`flare seed ${name}`)}.`);
|
|
|
4572
4581
|
return relative3;
|
|
4573
4582
|
}
|
|
4574
4583
|
async function runPrismaSeeds(appRoot, files, log2) {
|
|
4575
|
-
const runner =
|
|
4584
|
+
const runner = join24(appRoot, ".flare-seed-runner.mts");
|
|
4576
4585
|
const script = [
|
|
4577
4586
|
`import "dotenv/config";`,
|
|
4578
4587
|
`import { createFake } from "@flaredev/core";`,
|
|
@@ -4615,7 +4624,7 @@ Ran ${files.length} seed(s).`));
|
|
|
4615
4624
|
// src/commands/db-push.ts
|
|
4616
4625
|
import { mkdtempSync as mkdtempSync2, rmSync as rmSync6, writeFileSync as writeFileSync18 } from "fs";
|
|
4617
4626
|
import { tmpdir as tmpdir2 } from "os";
|
|
4618
|
-
import { join as
|
|
4627
|
+
import { join as join25 } from "path";
|
|
4619
4628
|
import pc19 from "picocolors";
|
|
4620
4629
|
|
|
4621
4630
|
// src/terminal.ts
|
|
@@ -4680,7 +4689,7 @@ async function dbPush(tables, options2 = {}) {
|
|
|
4680
4689
|
const wrangler = resolveBin(appRoot, "wrangler", "wrangler");
|
|
4681
4690
|
const started = Date.now();
|
|
4682
4691
|
const local = await openLocalD1(appRoot, options2.database);
|
|
4683
|
-
const dir = mkdtempSync2(
|
|
4692
|
+
const dir = mkdtempSync2(join25(tmpdir2(), "flare-push-"));
|
|
4684
4693
|
try {
|
|
4685
4694
|
const statements = [];
|
|
4686
4695
|
const counts = [];
|
|
@@ -4706,7 +4715,7 @@ async function dbPush(tables, options2 = {}) {
|
|
|
4706
4715
|
log2("Nothing to push: those tables are empty locally.");
|
|
4707
4716
|
return { tables: counts, ms: Date.now() - started };
|
|
4708
4717
|
}
|
|
4709
|
-
const file =
|
|
4718
|
+
const file = join25(dir, "push.sql");
|
|
4710
4719
|
writeFileSync18(file, statements.join("\n"));
|
|
4711
4720
|
const target = ["--remote", ...options2.env ? ["--env", options2.env] : []];
|
|
4712
4721
|
const result = await runWrangler(wrangler, ["d1", "execute", db.binding, ...target, "--yes", "--file", file], appRoot, { capture: true });
|
|
@@ -4724,7 +4733,7 @@ ${result.output.trim()}`);
|
|
|
4724
4733
|
// src/commands/seed-resource.ts
|
|
4725
4734
|
import { mkdtempSync as mkdtempSync3, rmSync as rmSync7, writeFileSync as writeFileSync19 } from "fs";
|
|
4726
4735
|
import { tmpdir as tmpdir3 } from "os";
|
|
4727
|
-
import { join as
|
|
4736
|
+
import { join as join26 } from "path";
|
|
4728
4737
|
import pc20 from "picocolors";
|
|
4729
4738
|
|
|
4730
4739
|
// src/seed/rows.ts
|
|
@@ -4963,8 +4972,8 @@ ${result.output.trim()}`);
|
|
|
4963
4972
|
const [counted] = await query(`SELECT count(*) AS total FROM "${resource.table}"`);
|
|
4964
4973
|
startIndex = counted?.total ?? 0;
|
|
4965
4974
|
}
|
|
4966
|
-
const dir = mkdtempSync3(
|
|
4967
|
-
const file =
|
|
4975
|
+
const dir = mkdtempSync3(join26(tmpdir3(), "flare-seed-"));
|
|
4976
|
+
const file = join26(dir, `${resource.table}.sql`);
|
|
4968
4977
|
try {
|
|
4969
4978
|
const statements = [];
|
|
4970
4979
|
if (options2.truncate) statements.push(`DELETE FROM "${resource.table}";`);
|
|
@@ -5007,16 +5016,16 @@ ${result.output.trim()}`);
|
|
|
5007
5016
|
|
|
5008
5017
|
// src/commands/billing-sync.ts
|
|
5009
5018
|
import { createRequire as createRequire2 } from "module";
|
|
5010
|
-
import { existsSync as
|
|
5019
|
+
import { existsSync as existsSync25, mkdtempSync as mkdtempSync4, readFileSync as readFileSync16, rmSync as rmSync8, writeFileSync as writeFileSync20 } from "fs";
|
|
5011
5020
|
import { tmpdir as tmpdir4 } from "os";
|
|
5012
|
-
import { join as
|
|
5021
|
+
import { join as join27 } from "path";
|
|
5013
5022
|
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
5014
5023
|
import { kebabCase as kebabCase5 } from "@flaredev/core";
|
|
5015
5024
|
import pc21 from "picocolors";
|
|
5016
5025
|
function stripeKeys(appRoot) {
|
|
5017
5026
|
const map = {};
|
|
5018
|
-
const path =
|
|
5019
|
-
if (
|
|
5027
|
+
const path = join27(appRoot, ".dev.vars");
|
|
5028
|
+
if (existsSync25(path)) {
|
|
5020
5029
|
for (const line of readFileSync16(path, "utf8").split(/\r?\n/)) {
|
|
5021
5030
|
const match = /^\s*([A-Z][A-Z0-9_]*)=(.*)$/.exec(line);
|
|
5022
5031
|
if (match) map[match[1]] = match[2].trim();
|
|
@@ -5100,10 +5109,10 @@ async function syncPlans(options2 = {}) {
|
|
|
5100
5109
|
if (!key) throw new Error("STRIPE_SECRET_KEY is not set in .dev.vars or the environment.");
|
|
5101
5110
|
const db = readD1Databases(appRoot)[0];
|
|
5102
5111
|
if (!db) throw new Error("No d1_databases in wrangler.jsonc.");
|
|
5103
|
-
const appRequire = createRequire2(
|
|
5112
|
+
const appRequire = createRequire2(join27(appRoot, "package.json"));
|
|
5104
5113
|
const stripeModule = await import(pathToFileURL2(appRequire.resolve("stripe")).href);
|
|
5105
5114
|
const stripe = new stripeModule.default(key, { apiVersion: "2026-08-26.dahlia" });
|
|
5106
|
-
const appName = JSON.parse(readFileSync16(
|
|
5115
|
+
const appName = JSON.parse(readFileSync16(join27(appRoot, "package.json"), "utf8")).name ?? "app";
|
|
5107
5116
|
const everything = await stripe.products.list({ active: true, limit: 100 }).autoPagingToArray({ limit: 1e4 });
|
|
5108
5117
|
const products = options2.all ? everything : everything.filter((product) => product.metadata?.flare_app === appName);
|
|
5109
5118
|
if (products.length === 0) {
|
|
@@ -5124,9 +5133,9 @@ async function syncPlans(options2 = {}) {
|
|
|
5124
5133
|
${existingResult.output.trim()}`);
|
|
5125
5134
|
}
|
|
5126
5135
|
const known = new Set(extractJson(existingResult.output)[0]?.results.map((row) => row.stripe_price_id));
|
|
5127
|
-
const dir = mkdtempSync4(
|
|
5136
|
+
const dir = mkdtempSync4(join27(tmpdir4(), "flare-sync-"));
|
|
5128
5137
|
try {
|
|
5129
|
-
const file =
|
|
5138
|
+
const file = join27(dir, "sync-plans.sql");
|
|
5130
5139
|
writeFileSync20(file, renderSyncSql(rows));
|
|
5131
5140
|
const result = await runWrangler(wrangler, ["d1", "execute", db.binding, ...target, "--file", file], appRoot, { capture: true });
|
|
5132
5141
|
if (result.code !== 0) throw new Error(`Could not write plans:
|
|
@@ -5150,15 +5159,15 @@ ${rows.length} plan(s) synced to the ${options2.remote ? "remote" : "local"} dat
|
|
|
5150
5159
|
}
|
|
5151
5160
|
|
|
5152
5161
|
// src/commands/sync.ts
|
|
5153
|
-
import { existsSync as
|
|
5154
|
-
import { join as
|
|
5162
|
+
import { existsSync as existsSync26 } from "fs";
|
|
5163
|
+
import { join as join28 } from "path";
|
|
5155
5164
|
import pc22 from "picocolors";
|
|
5156
5165
|
async function syncTypes(options2 = {}) {
|
|
5157
5166
|
const log2 = options2.log ?? ((message) => console.log(message));
|
|
5158
5167
|
const appRoot = findAppRoot(options2.cwd ?? process.cwd());
|
|
5159
5168
|
const all = await loadResources(appRoot);
|
|
5160
5169
|
const policies = await loadPolicies(appRoot);
|
|
5161
|
-
const tracksPolicies = policies.length > 0 ||
|
|
5170
|
+
const tracksPolicies = policies.length > 0 || existsSync26(join28(appRoot, "policies/index.ts"));
|
|
5162
5171
|
const plan = planFiles(all, tracksPolicies ? policies : void 0, readStack(appRoot));
|
|
5163
5172
|
if (!options2.check) ensureSupportFiles(appRoot, log2);
|
|
5164
5173
|
const results = applyPlan(appRoot, plan, { force: options2.force, dryRun: options2.check });
|
|
@@ -5206,7 +5215,7 @@ ${pc22.red("Out of sync:")} ${stale} file(s) would change. Run ${pc22.bold("flar
|
|
|
5206
5215
|
|
|
5207
5216
|
// src/commands/user-role.ts
|
|
5208
5217
|
import { rmSync as rmSync9, writeFileSync as writeFileSync21 } from "fs";
|
|
5209
|
-
import { join as
|
|
5218
|
+
import { join as join29 } from "path";
|
|
5210
5219
|
import pc23 from "picocolors";
|
|
5211
5220
|
var ROLE = /^[a-z][a-z0-9_-]{0,31}$/;
|
|
5212
5221
|
var EMAIL = /^[^\s@'"\\]+@[^\s@'"\\]+\.[^\s@'"\\]+$/;
|
|
@@ -5244,7 +5253,7 @@ ${result.output.trim()}`);
|
|
|
5244
5253
|
return 0;
|
|
5245
5254
|
}
|
|
5246
5255
|
async function setUserRoleOnPrisma(appRoot, email, role, log2) {
|
|
5247
|
-
const runner =
|
|
5256
|
+
const runner = join29(appRoot, ".flare-role-runner.mts");
|
|
5248
5257
|
const script = [
|
|
5249
5258
|
`import "dotenv/config";`,
|
|
5250
5259
|
`import { prisma } from "./lib/db";`,
|
|
@@ -5278,11 +5287,11 @@ async function setUserRoleOnPrisma(appRoot, email, role, log2) {
|
|
|
5278
5287
|
|
|
5279
5288
|
// src/utils/install.ts
|
|
5280
5289
|
import { spawn as spawn5 } from "child_process";
|
|
5281
|
-
import { existsSync as
|
|
5282
|
-
import { join as
|
|
5290
|
+
import { existsSync as existsSync27, readFileSync as readFileSync17, readdirSync as readdirSync10 } from "fs";
|
|
5291
|
+
import { join as join30 } from "path";
|
|
5283
5292
|
function expectedPackages(dir) {
|
|
5284
|
-
const npmLock =
|
|
5285
|
-
if (
|
|
5293
|
+
const npmLock = join30(dir, "package-lock.json");
|
|
5294
|
+
if (existsSync27(npmLock)) {
|
|
5286
5295
|
try {
|
|
5287
5296
|
const lock = JSON.parse(readFileSync17(npmLock, "utf8"));
|
|
5288
5297
|
return Math.max(0, Object.keys(lock.packages ?? {}).length - 1);
|
|
@@ -5290,8 +5299,8 @@ function expectedPackages(dir) {
|
|
|
5290
5299
|
return 0;
|
|
5291
5300
|
}
|
|
5292
5301
|
}
|
|
5293
|
-
const pnpmLock =
|
|
5294
|
-
if (
|
|
5302
|
+
const pnpmLock = join30(dir, "pnpm-lock.yaml");
|
|
5303
|
+
if (existsSync27(pnpmLock)) {
|
|
5295
5304
|
try {
|
|
5296
5305
|
const text = readFileSync17(pnpmLock, "utf8");
|
|
5297
5306
|
const packages = text.slice(text.indexOf("\npackages:"));
|
|
@@ -5303,15 +5312,15 @@ function expectedPackages(dir) {
|
|
|
5303
5312
|
return 0;
|
|
5304
5313
|
}
|
|
5305
5314
|
function installedPackages(dir) {
|
|
5306
|
-
const root =
|
|
5307
|
-
if (!
|
|
5315
|
+
const root = join30(dir, "node_modules");
|
|
5316
|
+
if (!existsSync27(root)) return 0;
|
|
5308
5317
|
let total = 0;
|
|
5309
5318
|
try {
|
|
5310
5319
|
for (const entry of readdirSync10(root, { withFileTypes: true })) {
|
|
5311
5320
|
if (!entry.isDirectory() || entry.name === ".bin") continue;
|
|
5312
5321
|
if (entry.name.startsWith("@")) {
|
|
5313
5322
|
try {
|
|
5314
|
-
total += readdirSync10(
|
|
5323
|
+
total += readdirSync10(join30(root, entry.name)).length;
|
|
5315
5324
|
} catch {
|
|
5316
5325
|
}
|
|
5317
5326
|
} else if (entry.name !== ".pnpm" && entry.name !== ".vite") {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flaredev/cli",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.3",
|
|
4
4
|
"description": "The flare command-line tool: create, generate, migrate, and deploy Flare apps",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Muke Johnbaptist",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"jiti": "^2.7.0",
|
|
42
42
|
"jsonc-parser": "^3.3.1",
|
|
43
43
|
"picocolors": "^1.1.1",
|
|
44
|
-
"@flaredev/core": "0.7.
|
|
44
|
+
"@flaredev/core": "0.7.3"
|
|
45
45
|
},
|
|
46
46
|
"engines": {
|
|
47
47
|
"node": ">=22"
|
|
@@ -36,7 +36,20 @@ export interface RowsQuery {
|
|
|
36
36
|
* `(sort field, id)` together so rows sharing a sort value are neither skipped nor
|
|
37
37
|
* repeated.
|
|
38
38
|
*/
|
|
39
|
-
cursor?: {
|
|
39
|
+
cursor?: {
|
|
40
|
+
field: string;
|
|
41
|
+
value: unknown;
|
|
42
|
+
id: string;
|
|
43
|
+
greaterThan: boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Whether the sort column holds a date.
|
|
46
|
+
*
|
|
47
|
+
* A cursor travels through a URL, so a Date arrives back as a number. SQLite stores
|
|
48
|
+
* dates as milliseconds and wants that number; Postgres wants a Date. Neither
|
|
49
|
+
* adapter can tell which a column is, and the store can — so it says.
|
|
50
|
+
*/
|
|
51
|
+
isDate?: boolean;
|
|
52
|
+
};
|
|
40
53
|
limit: number;
|
|
41
54
|
/** Offset paging, for the page-number case. Never combined with a cursor. */
|
|
42
55
|
offset?: number;
|
|
@@ -42,7 +42,7 @@ export type Result<T> = { ok: true; data: T } | Failure;
|
|
|
42
42
|
*/
|
|
43
43
|
export const COUNT_LIMIT = 10_000;
|
|
44
44
|
|
|
45
|
-
/**
|
|
45
|
+
/** A cursor is JSON in a URL, so a Date has to cross it as a number. */
|
|
46
46
|
function toCursorValue(value: unknown): string | number | boolean | null {
|
|
47
47
|
if (value instanceof Date) return value.getTime();
|
|
48
48
|
if (value === null || value === undefined) return null;
|
|
@@ -211,6 +211,16 @@ export function createResourceStore(options: ResourceStoreOptions) {
|
|
|
211
211
|
if ("issues" in parsed) return fail(400, "Invalid query.", { queryIssues: parsed.issues });
|
|
212
212
|
const { page, perPage, sort, q, filters, cursor } = parsed.query;
|
|
213
213
|
|
|
214
|
+
/**
|
|
215
|
+
* Whether a sort column holds a date, so the adapter can shape the cursor for it.
|
|
216
|
+
* The timestamps every row has, plus any date or datetime field.
|
|
217
|
+
*/
|
|
218
|
+
const isDateField = (key: string) => {
|
|
219
|
+
if (key === "createdAt" || key === "updatedAt") return true;
|
|
220
|
+
const def = fields.find(([name]) => name === key)?.[1];
|
|
221
|
+
return def?.kind === "date" || def?.kind === "datetime";
|
|
222
|
+
};
|
|
223
|
+
|
|
214
224
|
const searchable = fields.filter(([, def]) => isSearchable(def)).map(([key]) => key);
|
|
215
225
|
const matching = { search: q ? { term: q, fields: searchable } : undefined, filters };
|
|
216
226
|
|
|
@@ -224,7 +234,9 @@ export function createResourceStore(options: ResourceStoreOptions) {
|
|
|
224
234
|
rows.find({
|
|
225
235
|
...matching,
|
|
226
236
|
sort: { field: sort.field, direction: descending ? "desc" : "asc" },
|
|
227
|
-
cursor: cursor
|
|
237
|
+
cursor: cursor
|
|
238
|
+
? { field: sort.field, value: cursor.value, id: cursor.id, greaterThan: !descending, isDate: isDateField(sort.field) }
|
|
239
|
+
: undefined,
|
|
228
240
|
limit: perPage + 1,
|
|
229
241
|
offset: cursor ? undefined : (page - 1) * perPage,
|
|
230
242
|
}),
|
|
@@ -97,8 +97,12 @@ export function prismaRows(delegate: PrismaDelegate, client: unknown): ResourceR
|
|
|
97
97
|
async find(query) {
|
|
98
98
|
const where = matching(query);
|
|
99
99
|
if (query.cursor) {
|
|
100
|
-
const { field,
|
|
100
|
+
const { field, id, greaterThan, isDate } = query.cursor;
|
|
101
101
|
const op = greaterThan ? "gt" : "lt";
|
|
102
|
+
// Postgres compares a timestamp against a Date, not the number the cursor
|
|
103
|
+
// carried. Without this, paging past the first page of a date-sorted list fails
|
|
104
|
+
// with "Expected DateTime, provided Int".
|
|
105
|
+
const value = isDate && (typeof query.cursor.value === "number" || typeof query.cursor.value === "string") ? new Date(query.cursor.value) : query.cursor.value;
|
|
102
106
|
// (sort, id) as one comparison, so rows sharing a sort value are neither skipped
|
|
103
107
|
// nor repeated. AND-ed with the filters rather than replacing them.
|
|
104
108
|
const keyset = [{ [field]: { [op]: value } }, { AND: [{ [field]: value }, { id: { [op]: id } }] }];
|