@flaredev/cli 0.7.2 → 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,8 +4159,8 @@ ${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
4165
|
var TRACKED = ["lib", "components"];
|
|
4164
4166
|
var PER_APP = /__APP_NAME__|__PM__|__THEME__|__COMPAT_DATE__/;
|
|
@@ -4171,14 +4173,14 @@ function upstreamFile(path, stack) {
|
|
|
4171
4173
|
return found;
|
|
4172
4174
|
}
|
|
4173
4175
|
function locate(path, stack) {
|
|
4174
|
-
const candidates = stack === "next" ? [
|
|
4175
|
-
return candidates.find((candidate) =>
|
|
4176
|
+
const candidates = stack === "next" ? [join22(templatesDir, "next", path), join22(templatesDir, "app", path)] : [join22(templatesDir, "app", path)];
|
|
4177
|
+
return candidates.find((candidate) => existsSync23(candidate));
|
|
4176
4178
|
}
|
|
4177
4179
|
function trackedPaths(appRoot, stack) {
|
|
4178
4180
|
const paths = /* @__PURE__ */ new Set();
|
|
4179
4181
|
const walk = (base, dir) => {
|
|
4180
|
-
if (!
|
|
4181
|
-
for (const entry of readdirSync8(
|
|
4182
|
+
if (!existsSync23(join22(base, dir))) return;
|
|
4183
|
+
for (const entry of readdirSync8(join22(base, dir), { withFileTypes: true })) {
|
|
4182
4184
|
const next = `${dir}/${entry.name}`;
|
|
4183
4185
|
if (entry.isDirectory()) walk(base, next);
|
|
4184
4186
|
else if (entry.name.endsWith(".ts") || entry.name.endsWith(".tsx")) paths.add(next);
|
|
@@ -4186,8 +4188,8 @@ function trackedPaths(appRoot, stack) {
|
|
|
4186
4188
|
};
|
|
4187
4189
|
for (const root of TRACKED) {
|
|
4188
4190
|
walk(appRoot, root);
|
|
4189
|
-
walk(
|
|
4190
|
-
if (stack === "next") walk(
|
|
4191
|
+
walk(join22(templatesDir, "app"), root);
|
|
4192
|
+
if (stack === "next") walk(join22(templatesDir, "next"), root);
|
|
4191
4193
|
}
|
|
4192
4194
|
return [...paths].filter((path) => upstreamFile(path, stack) !== void 0).sort();
|
|
4193
4195
|
}
|
|
@@ -4195,8 +4197,8 @@ function compareTracked(appRoot, stack, filter) {
|
|
|
4195
4197
|
return trackedPaths(appRoot, stack).filter((path) => !filter || path.includes(filter)).map((path) => {
|
|
4196
4198
|
const source = upstreamFile(path, stack);
|
|
4197
4199
|
const theirs = readFileSync14(source, "utf8");
|
|
4198
|
-
const target =
|
|
4199
|
-
if (!
|
|
4200
|
+
const target = join22(appRoot, path);
|
|
4201
|
+
if (!existsSync23(target)) return { path, state: "missing", theirs };
|
|
4200
4202
|
const mine = readFileSync14(target, "utf8");
|
|
4201
4203
|
return { path, state: mine === theirs ? "same" : "changed", mine, theirs };
|
|
4202
4204
|
});
|
|
@@ -4259,7 +4261,7 @@ This discards your edits to those files. Re-run with ${pc17.bold("--yes")} to pr
|
|
|
4259
4261
|
return 1;
|
|
4260
4262
|
}
|
|
4261
4263
|
for (const entry of changed) {
|
|
4262
|
-
const target =
|
|
4264
|
+
const target = join22(appRoot, entry.path);
|
|
4263
4265
|
mkdirSync11(dirname11(target), { recursive: true });
|
|
4264
4266
|
writeFileSync15(target, entry.theirs);
|
|
4265
4267
|
log2(`${pc17.green("update")} ${relative2(process.cwd(), target) || entry.path}`);
|
|
@@ -4270,8 +4272,8 @@ Updated ${changed.length} file(s).`));
|
|
|
4270
4272
|
}
|
|
4271
4273
|
|
|
4272
4274
|
// src/commands/seed.ts
|
|
4273
|
-
import { existsSync as
|
|
4274
|
-
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";
|
|
4275
4277
|
import { camelCase as camelCase6, createFake, kebabCase as kebabCase4, storedFields as storedFields3 } from "@flaredev/core";
|
|
4276
4278
|
import { createJiti as createJiti4 } from "jiti";
|
|
4277
4279
|
import pc18 from "picocolors";
|
|
@@ -4370,18 +4372,18 @@ function createInsertMany(sink, drizzle, onProgress) {
|
|
|
4370
4372
|
// src/seed/local-d1.ts
|
|
4371
4373
|
import { readFileSync as readFileSync15, rmSync as rmSync4, writeFileSync as writeFileSync16 } from "fs";
|
|
4372
4374
|
import { createRequire } from "module";
|
|
4373
|
-
import { join as
|
|
4375
|
+
import { join as join23 } from "path";
|
|
4374
4376
|
import { pathToFileURL } from "url";
|
|
4375
4377
|
import { parse as parseJsonc3 } from "jsonc-parser";
|
|
4376
4378
|
var LOCAL_STATE = ".wrangler/state/v3";
|
|
4377
4379
|
async function openLocalD1(appRoot, binding) {
|
|
4378
4380
|
process.env.WRANGLER_LOG ??= "error";
|
|
4379
|
-
const appRequire = createRequire(
|
|
4381
|
+
const appRequire = createRequire(join23(appRoot, "package.json"));
|
|
4380
4382
|
const wrangler = await import(pathToFileURL(appRequire.resolve("wrangler")).href);
|
|
4381
4383
|
const configPath = seedConfig(appRoot);
|
|
4382
4384
|
const { env, dispose } = await wrangler.getPlatformProxy({
|
|
4383
4385
|
configPath,
|
|
4384
|
-
persist: { path:
|
|
4386
|
+
persist: { path: join23(appRoot, LOCAL_STATE) }
|
|
4385
4387
|
});
|
|
4386
4388
|
const bindings = env;
|
|
4387
4389
|
const name = binding ?? "DB";
|
|
@@ -4400,11 +4402,11 @@ async function openLocalD1(appRoot, binding) {
|
|
|
4400
4402
|
};
|
|
4401
4403
|
}
|
|
4402
4404
|
function seedConfig(appRoot) {
|
|
4403
|
-
const source =
|
|
4405
|
+
const source = join23(appRoot, "wrangler.jsonc");
|
|
4404
4406
|
const config = parseJsonc3(readFileSync15(source, "utf8")) ?? {};
|
|
4405
4407
|
delete config.durable_objects;
|
|
4406
4408
|
delete config.migrations;
|
|
4407
|
-
const path =
|
|
4409
|
+
const path = join23(appRoot, ".flare-seed.wrangler.json");
|
|
4408
4410
|
writeFileSync16(path, JSON.stringify(config, null, 2));
|
|
4409
4411
|
return path;
|
|
4410
4412
|
}
|
|
@@ -4412,8 +4414,8 @@ function seedConfig(appRoot) {
|
|
|
4412
4414
|
// src/commands/seed.ts
|
|
4413
4415
|
var SEEDS_DIR = "seeds";
|
|
4414
4416
|
function seedFiles(appRoot, names) {
|
|
4415
|
-
const dir =
|
|
4416
|
-
const all =
|
|
4417
|
+
const dir = join24(appRoot, SEEDS_DIR);
|
|
4418
|
+
const all = existsSync24(dir) ? readdirSync9(dir).filter((file) => file.endsWith(".seed.ts")).sort() : [];
|
|
4417
4419
|
if (!names?.length) return all;
|
|
4418
4420
|
return names.map((name) => {
|
|
4419
4421
|
const file = `${name.replace(/\.seed\.ts$/, "")}.seed.ts`;
|
|
@@ -4435,16 +4437,16 @@ async function runSeeds(options2 = {}) {
|
|
|
4435
4437
|
await runPrismaSeeds(appRoot, files, log2);
|
|
4436
4438
|
return;
|
|
4437
4439
|
}
|
|
4438
|
-
const jiti = createJiti4(
|
|
4440
|
+
const jiti = createJiti4(join24(appRoot, "package.json"), { alias: { "@": appRoot }, moduleCache: false, fsCache: false });
|
|
4439
4441
|
const { drizzle } = await jiti.import("drizzle-orm/d1");
|
|
4440
4442
|
const drizzleHelpers = await jiti.import("drizzle-orm");
|
|
4441
|
-
const schema = await jiti.import(
|
|
4443
|
+
const schema = await jiti.import(join24(appRoot, "db/schema.ts"));
|
|
4442
4444
|
const local = await openLocalD1(appRoot);
|
|
4443
4445
|
try {
|
|
4444
4446
|
const db = drizzle(local.db, { schema });
|
|
4445
4447
|
const insertMany = createInsertMany(local.db, drizzleHelpers);
|
|
4446
4448
|
for (const file of files) {
|
|
4447
|
-
const mod = await jiti.import(
|
|
4449
|
+
const mod = await jiti.import(join24(appRoot, SEEDS_DIR, file));
|
|
4448
4450
|
if (typeof mod.default !== "function") throw new Error(`${SEEDS_DIR}/${file} must \`export default defineSeed(async ({ db }) => { ... })\`.`);
|
|
4449
4451
|
log2(`${pc18.cyan("seed")} ${file}`);
|
|
4450
4452
|
await mod.default({ db, env: local.env, insertMany, fake: createFake(), log: (message) => log2(` ${message}`) });
|
|
@@ -4561,8 +4563,8 @@ async function makeSeed(rawName, options2 = {}) {
|
|
|
4561
4563
|
const name = kebabCase4(rawName);
|
|
4562
4564
|
if (!name) throw new Error(`Invalid seed name "${rawName}".`);
|
|
4563
4565
|
const relative3 = `${SEEDS_DIR}/${name}.seed.ts`;
|
|
4564
|
-
const path =
|
|
4565
|
-
if (
|
|
4566
|
+
const path = join24(appRoot, relative3);
|
|
4567
|
+
if (existsSync24(path)) throw new Error(`${relative3} already exists.`);
|
|
4566
4568
|
const resources = (await loadResources(appRoot)).map(({ resource: resource2 }) => resource2);
|
|
4567
4569
|
let resource;
|
|
4568
4570
|
if (options2.resource) {
|
|
@@ -4571,7 +4573,7 @@ async function makeSeed(rawName, options2 = {}) {
|
|
|
4571
4573
|
} else {
|
|
4572
4574
|
resource = resources.find((r) => [kebabCase4(r.name), r.slug].includes(name));
|
|
4573
4575
|
}
|
|
4574
|
-
mkdirSync12(
|
|
4576
|
+
mkdirSync12(join24(appRoot, SEEDS_DIR), { recursive: true });
|
|
4575
4577
|
writeFileSync17(path, renderSeed(resource, readStack(appRoot)));
|
|
4576
4578
|
log2(`${pc18.green("create".padEnd(9))} ${relative3}`);
|
|
4577
4579
|
log2(`
|
|
@@ -4579,7 +4581,7 @@ Run it with ${pc18.bold(`flare seed ${name}`)}.`);
|
|
|
4579
4581
|
return relative3;
|
|
4580
4582
|
}
|
|
4581
4583
|
async function runPrismaSeeds(appRoot, files, log2) {
|
|
4582
|
-
const runner =
|
|
4584
|
+
const runner = join24(appRoot, ".flare-seed-runner.mts");
|
|
4583
4585
|
const script = [
|
|
4584
4586
|
`import "dotenv/config";`,
|
|
4585
4587
|
`import { createFake } from "@flaredev/core";`,
|
|
@@ -4622,7 +4624,7 @@ Ran ${files.length} seed(s).`));
|
|
|
4622
4624
|
// src/commands/db-push.ts
|
|
4623
4625
|
import { mkdtempSync as mkdtempSync2, rmSync as rmSync6, writeFileSync as writeFileSync18 } from "fs";
|
|
4624
4626
|
import { tmpdir as tmpdir2 } from "os";
|
|
4625
|
-
import { join as
|
|
4627
|
+
import { join as join25 } from "path";
|
|
4626
4628
|
import pc19 from "picocolors";
|
|
4627
4629
|
|
|
4628
4630
|
// src/terminal.ts
|
|
@@ -4687,7 +4689,7 @@ async function dbPush(tables, options2 = {}) {
|
|
|
4687
4689
|
const wrangler = resolveBin(appRoot, "wrangler", "wrangler");
|
|
4688
4690
|
const started = Date.now();
|
|
4689
4691
|
const local = await openLocalD1(appRoot, options2.database);
|
|
4690
|
-
const dir = mkdtempSync2(
|
|
4692
|
+
const dir = mkdtempSync2(join25(tmpdir2(), "flare-push-"));
|
|
4691
4693
|
try {
|
|
4692
4694
|
const statements = [];
|
|
4693
4695
|
const counts = [];
|
|
@@ -4713,7 +4715,7 @@ async function dbPush(tables, options2 = {}) {
|
|
|
4713
4715
|
log2("Nothing to push: those tables are empty locally.");
|
|
4714
4716
|
return { tables: counts, ms: Date.now() - started };
|
|
4715
4717
|
}
|
|
4716
|
-
const file =
|
|
4718
|
+
const file = join25(dir, "push.sql");
|
|
4717
4719
|
writeFileSync18(file, statements.join("\n"));
|
|
4718
4720
|
const target = ["--remote", ...options2.env ? ["--env", options2.env] : []];
|
|
4719
4721
|
const result = await runWrangler(wrangler, ["d1", "execute", db.binding, ...target, "--yes", "--file", file], appRoot, { capture: true });
|
|
@@ -4731,7 +4733,7 @@ ${result.output.trim()}`);
|
|
|
4731
4733
|
// src/commands/seed-resource.ts
|
|
4732
4734
|
import { mkdtempSync as mkdtempSync3, rmSync as rmSync7, writeFileSync as writeFileSync19 } from "fs";
|
|
4733
4735
|
import { tmpdir as tmpdir3 } from "os";
|
|
4734
|
-
import { join as
|
|
4736
|
+
import { join as join26 } from "path";
|
|
4735
4737
|
import pc20 from "picocolors";
|
|
4736
4738
|
|
|
4737
4739
|
// src/seed/rows.ts
|
|
@@ -4970,8 +4972,8 @@ ${result.output.trim()}`);
|
|
|
4970
4972
|
const [counted] = await query(`SELECT count(*) AS total FROM "${resource.table}"`);
|
|
4971
4973
|
startIndex = counted?.total ?? 0;
|
|
4972
4974
|
}
|
|
4973
|
-
const dir = mkdtempSync3(
|
|
4974
|
-
const file =
|
|
4975
|
+
const dir = mkdtempSync3(join26(tmpdir3(), "flare-seed-"));
|
|
4976
|
+
const file = join26(dir, `${resource.table}.sql`);
|
|
4975
4977
|
try {
|
|
4976
4978
|
const statements = [];
|
|
4977
4979
|
if (options2.truncate) statements.push(`DELETE FROM "${resource.table}";`);
|
|
@@ -5014,16 +5016,16 @@ ${result.output.trim()}`);
|
|
|
5014
5016
|
|
|
5015
5017
|
// src/commands/billing-sync.ts
|
|
5016
5018
|
import { createRequire as createRequire2 } from "module";
|
|
5017
|
-
import { existsSync as
|
|
5019
|
+
import { existsSync as existsSync25, mkdtempSync as mkdtempSync4, readFileSync as readFileSync16, rmSync as rmSync8, writeFileSync as writeFileSync20 } from "fs";
|
|
5018
5020
|
import { tmpdir as tmpdir4 } from "os";
|
|
5019
|
-
import { join as
|
|
5021
|
+
import { join as join27 } from "path";
|
|
5020
5022
|
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
5021
5023
|
import { kebabCase as kebabCase5 } from "@flaredev/core";
|
|
5022
5024
|
import pc21 from "picocolors";
|
|
5023
5025
|
function stripeKeys(appRoot) {
|
|
5024
5026
|
const map = {};
|
|
5025
|
-
const path =
|
|
5026
|
-
if (
|
|
5027
|
+
const path = join27(appRoot, ".dev.vars");
|
|
5028
|
+
if (existsSync25(path)) {
|
|
5027
5029
|
for (const line of readFileSync16(path, "utf8").split(/\r?\n/)) {
|
|
5028
5030
|
const match = /^\s*([A-Z][A-Z0-9_]*)=(.*)$/.exec(line);
|
|
5029
5031
|
if (match) map[match[1]] = match[2].trim();
|
|
@@ -5107,10 +5109,10 @@ async function syncPlans(options2 = {}) {
|
|
|
5107
5109
|
if (!key) throw new Error("STRIPE_SECRET_KEY is not set in .dev.vars or the environment.");
|
|
5108
5110
|
const db = readD1Databases(appRoot)[0];
|
|
5109
5111
|
if (!db) throw new Error("No d1_databases in wrangler.jsonc.");
|
|
5110
|
-
const appRequire = createRequire2(
|
|
5112
|
+
const appRequire = createRequire2(join27(appRoot, "package.json"));
|
|
5111
5113
|
const stripeModule = await import(pathToFileURL2(appRequire.resolve("stripe")).href);
|
|
5112
5114
|
const stripe = new stripeModule.default(key, { apiVersion: "2026-08-26.dahlia" });
|
|
5113
|
-
const appName = JSON.parse(readFileSync16(
|
|
5115
|
+
const appName = JSON.parse(readFileSync16(join27(appRoot, "package.json"), "utf8")).name ?? "app";
|
|
5114
5116
|
const everything = await stripe.products.list({ active: true, limit: 100 }).autoPagingToArray({ limit: 1e4 });
|
|
5115
5117
|
const products = options2.all ? everything : everything.filter((product) => product.metadata?.flare_app === appName);
|
|
5116
5118
|
if (products.length === 0) {
|
|
@@ -5131,9 +5133,9 @@ async function syncPlans(options2 = {}) {
|
|
|
5131
5133
|
${existingResult.output.trim()}`);
|
|
5132
5134
|
}
|
|
5133
5135
|
const known = new Set(extractJson(existingResult.output)[0]?.results.map((row) => row.stripe_price_id));
|
|
5134
|
-
const dir = mkdtempSync4(
|
|
5136
|
+
const dir = mkdtempSync4(join27(tmpdir4(), "flare-sync-"));
|
|
5135
5137
|
try {
|
|
5136
|
-
const file =
|
|
5138
|
+
const file = join27(dir, "sync-plans.sql");
|
|
5137
5139
|
writeFileSync20(file, renderSyncSql(rows));
|
|
5138
5140
|
const result = await runWrangler(wrangler, ["d1", "execute", db.binding, ...target, "--file", file], appRoot, { capture: true });
|
|
5139
5141
|
if (result.code !== 0) throw new Error(`Could not write plans:
|
|
@@ -5157,15 +5159,15 @@ ${rows.length} plan(s) synced to the ${options2.remote ? "remote" : "local"} dat
|
|
|
5157
5159
|
}
|
|
5158
5160
|
|
|
5159
5161
|
// src/commands/sync.ts
|
|
5160
|
-
import { existsSync as
|
|
5161
|
-
import { join as
|
|
5162
|
+
import { existsSync as existsSync26 } from "fs";
|
|
5163
|
+
import { join as join28 } from "path";
|
|
5162
5164
|
import pc22 from "picocolors";
|
|
5163
5165
|
async function syncTypes(options2 = {}) {
|
|
5164
5166
|
const log2 = options2.log ?? ((message) => console.log(message));
|
|
5165
5167
|
const appRoot = findAppRoot(options2.cwd ?? process.cwd());
|
|
5166
5168
|
const all = await loadResources(appRoot);
|
|
5167
5169
|
const policies = await loadPolicies(appRoot);
|
|
5168
|
-
const tracksPolicies = policies.length > 0 ||
|
|
5170
|
+
const tracksPolicies = policies.length > 0 || existsSync26(join28(appRoot, "policies/index.ts"));
|
|
5169
5171
|
const plan = planFiles(all, tracksPolicies ? policies : void 0, readStack(appRoot));
|
|
5170
5172
|
if (!options2.check) ensureSupportFiles(appRoot, log2);
|
|
5171
5173
|
const results = applyPlan(appRoot, plan, { force: options2.force, dryRun: options2.check });
|
|
@@ -5213,7 +5215,7 @@ ${pc22.red("Out of sync:")} ${stale} file(s) would change. Run ${pc22.bold("flar
|
|
|
5213
5215
|
|
|
5214
5216
|
// src/commands/user-role.ts
|
|
5215
5217
|
import { rmSync as rmSync9, writeFileSync as writeFileSync21 } from "fs";
|
|
5216
|
-
import { join as
|
|
5218
|
+
import { join as join29 } from "path";
|
|
5217
5219
|
import pc23 from "picocolors";
|
|
5218
5220
|
var ROLE = /^[a-z][a-z0-9_-]{0,31}$/;
|
|
5219
5221
|
var EMAIL = /^[^\s@'"\\]+@[^\s@'"\\]+\.[^\s@'"\\]+$/;
|
|
@@ -5251,7 +5253,7 @@ ${result.output.trim()}`);
|
|
|
5251
5253
|
return 0;
|
|
5252
5254
|
}
|
|
5253
5255
|
async function setUserRoleOnPrisma(appRoot, email, role, log2) {
|
|
5254
|
-
const runner =
|
|
5256
|
+
const runner = join29(appRoot, ".flare-role-runner.mts");
|
|
5255
5257
|
const script = [
|
|
5256
5258
|
`import "dotenv/config";`,
|
|
5257
5259
|
`import { prisma } from "./lib/db";`,
|
|
@@ -5285,11 +5287,11 @@ async function setUserRoleOnPrisma(appRoot, email, role, log2) {
|
|
|
5285
5287
|
|
|
5286
5288
|
// src/utils/install.ts
|
|
5287
5289
|
import { spawn as spawn5 } from "child_process";
|
|
5288
|
-
import { existsSync as
|
|
5289
|
-
import { join as
|
|
5290
|
+
import { existsSync as existsSync27, readFileSync as readFileSync17, readdirSync as readdirSync10 } from "fs";
|
|
5291
|
+
import { join as join30 } from "path";
|
|
5290
5292
|
function expectedPackages(dir) {
|
|
5291
|
-
const npmLock =
|
|
5292
|
-
if (
|
|
5293
|
+
const npmLock = join30(dir, "package-lock.json");
|
|
5294
|
+
if (existsSync27(npmLock)) {
|
|
5293
5295
|
try {
|
|
5294
5296
|
const lock = JSON.parse(readFileSync17(npmLock, "utf8"));
|
|
5295
5297
|
return Math.max(0, Object.keys(lock.packages ?? {}).length - 1);
|
|
@@ -5297,8 +5299,8 @@ function expectedPackages(dir) {
|
|
|
5297
5299
|
return 0;
|
|
5298
5300
|
}
|
|
5299
5301
|
}
|
|
5300
|
-
const pnpmLock =
|
|
5301
|
-
if (
|
|
5302
|
+
const pnpmLock = join30(dir, "pnpm-lock.yaml");
|
|
5303
|
+
if (existsSync27(pnpmLock)) {
|
|
5302
5304
|
try {
|
|
5303
5305
|
const text = readFileSync17(pnpmLock, "utf8");
|
|
5304
5306
|
const packages = text.slice(text.indexOf("\npackages:"));
|
|
@@ -5310,15 +5312,15 @@ function expectedPackages(dir) {
|
|
|
5310
5312
|
return 0;
|
|
5311
5313
|
}
|
|
5312
5314
|
function installedPackages(dir) {
|
|
5313
|
-
const root =
|
|
5314
|
-
if (!
|
|
5315
|
+
const root = join30(dir, "node_modules");
|
|
5316
|
+
if (!existsSync27(root)) return 0;
|
|
5315
5317
|
let total = 0;
|
|
5316
5318
|
try {
|
|
5317
5319
|
for (const entry of readdirSync10(root, { withFileTypes: true })) {
|
|
5318
5320
|
if (!entry.isDirectory() || entry.name === ".bin") continue;
|
|
5319
5321
|
if (entry.name.startsWith("@")) {
|
|
5320
5322
|
try {
|
|
5321
|
-
total += readdirSync10(
|
|
5323
|
+
total += readdirSync10(join30(root, entry.name)).length;
|
|
5322
5324
|
} catch {
|
|
5323
5325
|
}
|
|
5324
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 } }] }];
|