@cedarjs/cli 6.0.0-rc.189 → 6.0.0-rc.221
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/commands/experimental/setupOpentelemetryHandler.js +1 -1
- package/dist/commands/generate/scaffold/scaffoldHandler.js +73 -4
- package/dist/commands/serve.js +19 -9
- package/dist/commands/serveBothHandler.js +2 -2
- package/dist/commands/setup/database/database.js +15 -0
- package/dist/commands/setup/database/postgres.js +19 -0
- package/dist/commands/setup/database/postgresHandler.js +194 -0
- package/dist/commands/setup/deploy/providers/renderHandler.js +4 -20
- package/dist/commands/setup/deploy/templates/render.js +24 -14
- package/dist/commands/setup/docker/templates/Dockerfile.yarn +2 -5
- package/dist/commands/setup/docker/templates/docker-compose.dev.yml +1 -1
- package/dist/commands/setup/docker/templates/docker-compose.prod.yml +2 -5
- package/dist/commands/setup/neon/neonHandler.js +96 -254
- package/dist/commands/setup.js +2 -1
- package/dist/lib/index.js +22 -6
- package/dist/telemetry/resource.js +3 -6
- package/package.json +16 -16
- /package/dist/commands/setup/{neon → database}/templates/db.ts.template +0 -0
|
@@ -114,7 +114,7 @@ const handler = async ({
|
|
|
114
114
|
},
|
|
115
115
|
task: (_ctx, task) => {
|
|
116
116
|
task.output = [
|
|
117
|
-
"Please add the following to your '
|
|
117
|
+
"Please add the following to your 'cedarFastifyGraphQLServer' plugin options to enable OTel for your graphql",
|
|
118
118
|
"openTelemetryOptions: {",
|
|
119
119
|
" resolvers: true,",
|
|
120
120
|
" result: true,",
|
|
@@ -42,6 +42,7 @@ import { files as serviceFiles } from "../service/serviceHandler.js";
|
|
|
42
42
|
import { customOrDefaultTemplatePath } from "../yargsHandlerHelpers.js";
|
|
43
43
|
const SKIPPABLE_ASSETS = ["scaffold.css"];
|
|
44
44
|
const PACKAGE_SET = "Set";
|
|
45
|
+
const PACKAGE_PRIVATE_SET = "PrivateSet";
|
|
45
46
|
const getIdType = (model) => {
|
|
46
47
|
return model.fields.find((field) => field.isId)?.type;
|
|
47
48
|
};
|
|
@@ -512,6 +513,65 @@ const addHelperPackages = async (task) => {
|
|
|
512
513
|
await removeWorkspacePackages("web", ["humanize-string"]);
|
|
513
514
|
});
|
|
514
515
|
};
|
|
516
|
+
const isAuthSetup = () => {
|
|
517
|
+
const extensions = ["ts", "js", "tsx", "jsx"];
|
|
518
|
+
return extensions.some(
|
|
519
|
+
(ext) => fs.existsSync(path.join(getPaths().web.src, "auth." + ext))
|
|
520
|
+
);
|
|
521
|
+
};
|
|
522
|
+
const ROUTE_TAG_RE = /<Route\s+([^>]*?)\/?>/g;
|
|
523
|
+
const extractRouteAttr = (tagAttrs, attrName) => tagAttrs.match(new RegExp(`\\b${attrName}=["']([^"']+)["']`))?.[1];
|
|
524
|
+
const getRoutesFileContent = () => {
|
|
525
|
+
const routesPath = getPaths().web.routes;
|
|
526
|
+
if (!fs.existsSync(routesPath)) {
|
|
527
|
+
return void 0;
|
|
528
|
+
}
|
|
529
|
+
return readFile(routesPath).toString();
|
|
530
|
+
};
|
|
531
|
+
const hasLoginRoute = () => {
|
|
532
|
+
const content = getRoutesFileContent();
|
|
533
|
+
if (!content) {
|
|
534
|
+
return false;
|
|
535
|
+
}
|
|
536
|
+
return Array.from(content.matchAll(ROUTE_TAG_RE)).some(
|
|
537
|
+
([, attrs]) => extractRouteAttr(attrs, "name") === "login"
|
|
538
|
+
);
|
|
539
|
+
};
|
|
540
|
+
const findUnprotectedLandingPageRouteName = () => {
|
|
541
|
+
const content = getRoutesFileContent();
|
|
542
|
+
if (!content) {
|
|
543
|
+
return void 0;
|
|
544
|
+
}
|
|
545
|
+
const privateSetRanges = Array.from(
|
|
546
|
+
content.matchAll(/<PrivateSet\b[^>]*>([\s\S]*?)<\/PrivateSet>/g)
|
|
547
|
+
).map((match) => ({
|
|
548
|
+
start: match.index ?? 0,
|
|
549
|
+
end: (match.index ?? 0) + match[0].length
|
|
550
|
+
}));
|
|
551
|
+
for (const match of content.matchAll(ROUTE_TAG_RE)) {
|
|
552
|
+
const [, attrs] = match;
|
|
553
|
+
if (extractRouteAttr(attrs, "path") !== "/") {
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
const tagStart = match.index ?? 0;
|
|
557
|
+
const isProtected = privateSetRanges.some(
|
|
558
|
+
(range) => tagStart >= range.start && tagStart < range.end
|
|
559
|
+
);
|
|
560
|
+
if (!isProtected) {
|
|
561
|
+
return extractRouteAttr(attrs, "name");
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
return void 0;
|
|
565
|
+
};
|
|
566
|
+
const getUnauthenticatedRedirectRoute = () => {
|
|
567
|
+
if (!isAuthSetup()) {
|
|
568
|
+
return void 0;
|
|
569
|
+
}
|
|
570
|
+
if (hasLoginRoute()) {
|
|
571
|
+
return "login";
|
|
572
|
+
}
|
|
573
|
+
return findUnprotectedLandingPageRouteName();
|
|
574
|
+
};
|
|
515
575
|
const addSetImport = (task) => {
|
|
516
576
|
const routesPath = getPaths().web.routes;
|
|
517
577
|
const routesContent = readFile(routesPath).toString();
|
|
@@ -525,15 +585,19 @@ const addSetImport = (task) => {
|
|
|
525
585
|
return void 0;
|
|
526
586
|
}
|
|
527
587
|
const routerImports = importContent.replace(/\s/g, "").split(",");
|
|
528
|
-
|
|
588
|
+
const namesToImport = [
|
|
589
|
+
PACKAGE_SET,
|
|
590
|
+
...getUnauthenticatedRedirectRoute() ? [PACKAGE_PRIVATE_SET] : []
|
|
591
|
+
].filter((name) => !routerImports.includes(name));
|
|
592
|
+
if (!namesToImport.length) {
|
|
529
593
|
return "Skipping Set import";
|
|
530
594
|
}
|
|
531
595
|
const newRoutesContent = routesContent.replace(
|
|
532
596
|
cedarRouterImport,
|
|
533
|
-
importStart + spacing +
|
|
597
|
+
importStart + spacing + namesToImport.join("," + spacing) + `,` + spacing + importContent + importEnd
|
|
534
598
|
);
|
|
535
599
|
writeFile(routesPath, newRoutesContent, { overwriteExisting: true });
|
|
536
|
-
return
|
|
600
|
+
return `Added ${namesToImport.join(", ")} import to Routes.{jsx,tsx}`;
|
|
537
601
|
};
|
|
538
602
|
const addScaffoldSetToRouter = async (model, scaffoldPath) => {
|
|
539
603
|
const templateNames = getTemplateStrings(model, scaffoldPath);
|
|
@@ -542,10 +606,12 @@ const addScaffoldSetToRouter = async (model, scaffoldPath) => {
|
|
|
542
606
|
const titleTo = templateNames.pluralRouteName;
|
|
543
607
|
const buttonLabel = `New ${nameVars.singularPascalName}`;
|
|
544
608
|
const buttonTo = templateNames.newRouteName;
|
|
609
|
+
const unauthenticatedRoute = getUnauthenticatedRedirectRoute();
|
|
545
610
|
return addRoutesToRouterTask(
|
|
546
611
|
await routes({ model, path: scaffoldPath }),
|
|
547
612
|
"ScaffoldLayout",
|
|
548
|
-
{ title, titleTo, buttonLabel, buttonTo }
|
|
613
|
+
{ title, titleTo, buttonLabel, buttonTo },
|
|
614
|
+
unauthenticatedRoute ? { unauthenticated: unauthenticatedRoute } : void 0
|
|
549
615
|
);
|
|
550
616
|
};
|
|
551
617
|
const tasks = ({
|
|
@@ -664,7 +730,10 @@ const splitPathAndModel = (pathSlashModel) => {
|
|
|
664
730
|
};
|
|
665
731
|
export {
|
|
666
732
|
files,
|
|
733
|
+
getUnauthenticatedRedirectRoute,
|
|
667
734
|
handler,
|
|
735
|
+
hasLoginRoute,
|
|
736
|
+
isAuthSetup,
|
|
668
737
|
routes,
|
|
669
738
|
shouldUseTailwindCSS,
|
|
670
739
|
splitPathAndModel,
|
package/dist/commands/serve.js
CHANGED
|
@@ -10,6 +10,14 @@ import * as webServerCLIConfig from "@cedarjs/web-server";
|
|
|
10
10
|
import { getPaths, getConfig } from "../lib/index.js";
|
|
11
11
|
import { serverFileExists } from "../lib/project.js";
|
|
12
12
|
import { webSsrServerHandler } from "./serveWebHandler.js";
|
|
13
|
+
function refuseServerFileUnderUD() {
|
|
14
|
+
console.error(
|
|
15
|
+
c.error(
|
|
16
|
+
"\n api/src/server.ts was detected, but a custom server file is not supported with --ud. It is a Fastify concept \u2014 anything registered there (Realtime, custom plugins, custom middleware) would silently be skipped if serving continued.\n"
|
|
17
|
+
)
|
|
18
|
+
);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
13
21
|
function resolveUDEntryPath() {
|
|
14
22
|
const base = path.join(getPaths().api.dist, "ud", "index");
|
|
15
23
|
for (const ext of [".mjs", ".js"]) {
|
|
@@ -90,17 +98,13 @@ const builder = async (yargs) => {
|
|
|
90
98
|
process.exit(1);
|
|
91
99
|
}
|
|
92
100
|
if (serverFileExists()) {
|
|
93
|
-
|
|
94
|
-
c.warning(
|
|
95
|
-
"\n Note: api/src/server.ts was detected. This file is a Fastify concept and will be ignored when using --ud. You are testing the experimental UD support, so the behavior will not match your production Fastify setup.\n"
|
|
96
|
-
)
|
|
97
|
-
);
|
|
101
|
+
refuseServerFileUnderUD();
|
|
98
102
|
}
|
|
99
103
|
const { getAPIHost, getAPIPort, getWebHost, getWebPort } = await import("@cedarjs/api-server/cliHelpers");
|
|
100
104
|
const apiPort = argv.apiPort ?? getAPIPort();
|
|
101
105
|
const apiHost = argv.apiHost ?? getAPIHost();
|
|
102
|
-
const webPort = argv.webPort ?? getWebPort();
|
|
103
|
-
const webHost = argv.webHost ?? getWebHost();
|
|
106
|
+
const webPort = argv.webPort ?? getWebPort({ isPublicSide: true });
|
|
107
|
+
const webHost = argv.webHost ?? getWebHost({ isPublicSide: true });
|
|
104
108
|
const apiRootPath = argv.apiRootPath ?? "/";
|
|
105
109
|
const apiTarget = `http://${apiHost.includes(":") ? `[${apiHost}]` : apiHost}:${apiPort}`;
|
|
106
110
|
const { serveStatic } = await import("srvx/static");
|
|
@@ -187,7 +191,15 @@ const builder = async (yargs) => {
|
|
|
187
191
|
socket: argv.socket,
|
|
188
192
|
apiRootPath: argv.apiRootPath
|
|
189
193
|
});
|
|
194
|
+
const { getAPIHost, getAPIPort } = await import("@cedarjs/api-server/cliHelpers");
|
|
195
|
+
const apiPort = argv.port ?? getAPIPort({ isPublicSide: true });
|
|
196
|
+
const apiHost = argv.host ?? getAPIHost({ isPublicSide: true });
|
|
197
|
+
argv.port = apiPort;
|
|
198
|
+
argv.host = apiHost;
|
|
190
199
|
if (argv.ud) {
|
|
200
|
+
if (serverFileExists()) {
|
|
201
|
+
refuseServerFileUnderUD();
|
|
202
|
+
}
|
|
191
203
|
const udEntryPath = resolveUDEntryPath();
|
|
192
204
|
if (!udEntryPath) {
|
|
193
205
|
console.error(
|
|
@@ -197,8 +209,6 @@ const builder = async (yargs) => {
|
|
|
197
209
|
);
|
|
198
210
|
process.exit(1);
|
|
199
211
|
}
|
|
200
|
-
const apiPort = argv.port ?? parseInt(process.env.PORT ?? "8911", 10);
|
|
201
|
-
const apiHost = argv.host ?? process.env.HOST ?? "localhost";
|
|
202
212
|
process.stdout.write(
|
|
203
213
|
`API server starting at http://${apiHost}:${apiPort}...`
|
|
204
214
|
);
|
|
@@ -27,8 +27,8 @@ const bothServerFileHandler = async (argv) => {
|
|
|
27
27
|
} else {
|
|
28
28
|
argv.apiPort ??= getAPIPort();
|
|
29
29
|
argv.apiHost ??= getAPIHost();
|
|
30
|
-
argv.webPort ??= getWebPort();
|
|
31
|
-
argv.webHost ??= getWebHost();
|
|
30
|
+
argv.webPort ??= getWebPort({ isPublicSide: true });
|
|
31
|
+
argv.webHost ??= getWebHost({ isPublicSide: true });
|
|
32
32
|
const apiRootPath = argv.apiRootPath ?? getAPIRootPath();
|
|
33
33
|
const apiProxyTarget = [
|
|
34
34
|
"http://",
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { terminalLink } from "termi-link";
|
|
2
|
+
import * as setupDatabasePostgres from "./postgres.js";
|
|
3
|
+
const command = "database <command>";
|
|
4
|
+
const description = "Switch your project's database";
|
|
5
|
+
const builder = (yargs) => yargs.command(setupDatabasePostgres).demandCommand().epilogue(
|
|
6
|
+
`Also see the ${terminalLink(
|
|
7
|
+
"CedarJS CLI Reference",
|
|
8
|
+
"https://cedarjs.com/docs/cli-commands#setup"
|
|
9
|
+
)}`
|
|
10
|
+
);
|
|
11
|
+
export {
|
|
12
|
+
builder,
|
|
13
|
+
command,
|
|
14
|
+
description
|
|
15
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { recordTelemetryAttributes } from "@cedarjs/cli-helpers";
|
|
2
|
+
const command = "postgres";
|
|
3
|
+
const description = "Switch your project from SQLite to PostgreSQL (schema, dependencies, and database adapter)";
|
|
4
|
+
function builder(yargs) {
|
|
5
|
+
return yargs;
|
|
6
|
+
}
|
|
7
|
+
async function handler() {
|
|
8
|
+
recordTelemetryAttributes({
|
|
9
|
+
command: "setup database postgres"
|
|
10
|
+
});
|
|
11
|
+
const { handler: handler2 } = await import("./postgresHandler.js");
|
|
12
|
+
return handler2();
|
|
13
|
+
}
|
|
14
|
+
export {
|
|
15
|
+
builder,
|
|
16
|
+
command,
|
|
17
|
+
description,
|
|
18
|
+
handler
|
|
19
|
+
};
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import execa from "execa";
|
|
4
|
+
import { Listr } from "listr2";
|
|
5
|
+
import { colors, getPaths, installPackages } from "@cedarjs/cli-helpers";
|
|
6
|
+
import { prettyPrintCedarCommand } from "@cedarjs/cli-helpers/packageManager";
|
|
7
|
+
import { addWorkspacePackages } from "@cedarjs/cli-helpers/packageManager/packages";
|
|
8
|
+
import { resolveFile } from "@cedarjs/project-config";
|
|
9
|
+
import { errorTelemetry } from "@cedarjs/telemetry";
|
|
10
|
+
function checkProjectShape(cedarPaths) {
|
|
11
|
+
const schemaPath = path.join(cedarPaths.api.base, "db", "schema.prisma");
|
|
12
|
+
const dbPath = resolveFile(path.join(cedarPaths.api.lib, "db"));
|
|
13
|
+
if (!fs.existsSync(schemaPath)) {
|
|
14
|
+
return blocked(`Could not find ${schemaPath}.`);
|
|
15
|
+
}
|
|
16
|
+
if (!dbPath) {
|
|
17
|
+
return blocked(`No ${path.join(cedarPaths.api.lib, "db")} file found`);
|
|
18
|
+
}
|
|
19
|
+
const schemaContent = fs.readFileSync(schemaPath, "utf-8");
|
|
20
|
+
const hasPgAdapter = fs.readFileSync(dbPath, "utf-8").includes("PrismaPg");
|
|
21
|
+
if (schemaContent.includes('provider = "postgresql"') && hasPgAdapter) {
|
|
22
|
+
return {
|
|
23
|
+
ok: false,
|
|
24
|
+
alreadyConverted: true,
|
|
25
|
+
message: "This project is already configured for PostgreSQL."
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
if (!schemaContent.includes('provider = "sqlite"') || hasPgAdapter) {
|
|
29
|
+
return blocked(
|
|
30
|
+
"This command only converts a project that is still on SQLite, with the default adapter in api/src/lib/db.ts (or db.js) untouched. This project doesn't match that shape (a different provider, or a partial previous conversion). Please switch it over to PostgreSQL manually."
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
return { ok: true, dbPath };
|
|
34
|
+
}
|
|
35
|
+
function blocked(message) {
|
|
36
|
+
return { ok: false, alreadyConverted: false, message };
|
|
37
|
+
}
|
|
38
|
+
function getSqliteToPostgresTasks({
|
|
39
|
+
dbPath
|
|
40
|
+
}) {
|
|
41
|
+
const cedarPaths = getPaths();
|
|
42
|
+
const schemaPath = path.join(cedarPaths.api.base, "db", "schema.prisma");
|
|
43
|
+
const rootPkgPath = path.join(cedarPaths.base, "package.json");
|
|
44
|
+
const apiPkgPath = path.join(cedarPaths.api.base, "package.json");
|
|
45
|
+
const dbTsTemplatePath = path.join(
|
|
46
|
+
import.meta.dirname,
|
|
47
|
+
"templates",
|
|
48
|
+
"db.ts.template"
|
|
49
|
+
);
|
|
50
|
+
return [
|
|
51
|
+
{
|
|
52
|
+
title: "Removing SQLite dependencies from api/package.json",
|
|
53
|
+
task: () => {
|
|
54
|
+
const pkg = JSON.parse(fs.readFileSync(apiPkgPath, "utf-8"));
|
|
55
|
+
if (pkg.dependencies) {
|
|
56
|
+
delete pkg.dependencies["better-sqlite3"];
|
|
57
|
+
delete pkg.dependencies["@prisma/adapter-better-sqlite3"];
|
|
58
|
+
}
|
|
59
|
+
fs.writeFileSync(apiPkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
title: "Removing better-sqlite3 dependenciesMeta",
|
|
64
|
+
task: () => {
|
|
65
|
+
if (!fs.existsSync(rootPkgPath)) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const pkg = JSON.parse(fs.readFileSync(rootPkgPath, "utf-8"));
|
|
69
|
+
if (pkg.dependenciesMeta?.["better-sqlite3"]) {
|
|
70
|
+
delete pkg.dependenciesMeta["better-sqlite3"];
|
|
71
|
+
if (Object.keys(pkg.dependenciesMeta).length === 0) {
|
|
72
|
+
delete pkg.dependenciesMeta;
|
|
73
|
+
}
|
|
74
|
+
fs.writeFileSync(rootPkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
title: "Switching Prisma schema to PostgreSQL",
|
|
80
|
+
task: () => {
|
|
81
|
+
const schemaContent = fs.readFileSync(schemaPath, "utf-8");
|
|
82
|
+
fs.writeFileSync(
|
|
83
|
+
schemaPath,
|
|
84
|
+
schemaContent.replace(
|
|
85
|
+
'provider = "sqlite"',
|
|
86
|
+
'provider = "postgresql"'
|
|
87
|
+
)
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
title: "Updating database adapter",
|
|
93
|
+
task: () => {
|
|
94
|
+
const pgDbTs = fs.readFileSync(dbTsTemplatePath, "utf-8");
|
|
95
|
+
fs.writeFileSync(dbPath, pgDbTs);
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
title: "Adding required api packages...",
|
|
100
|
+
task: async () => {
|
|
101
|
+
await addWorkspacePackages("api", ["@prisma/adapter-pg@7.8.0"], {
|
|
102
|
+
cwd: cedarPaths.api.base
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
];
|
|
107
|
+
}
|
|
108
|
+
function readEnvVar(envContent, name) {
|
|
109
|
+
return envContent.match(new RegExp(`^${name}=(.*)$`, "m"))?.[1] || void 0;
|
|
110
|
+
}
|
|
111
|
+
async function handler() {
|
|
112
|
+
const cedarPaths = getPaths();
|
|
113
|
+
const shape = checkProjectShape(cedarPaths);
|
|
114
|
+
if (!shape.ok) {
|
|
115
|
+
if (shape.alreadyConverted) {
|
|
116
|
+
console.log(colors.note(shape.message));
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
console.error(colors.error(shape.message));
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
const envPath = path.join(cedarPaths.base, ".env");
|
|
123
|
+
const envContent = fs.existsSync(envPath) ? fs.readFileSync(envPath, "utf-8") : "";
|
|
124
|
+
const databaseUrl = readEnvVar(envContent, "DATABASE_URL");
|
|
125
|
+
const tasks = new Listr(
|
|
126
|
+
[
|
|
127
|
+
...getSqliteToPostgresTasks({ dbPath: shape.dbPath }),
|
|
128
|
+
installPackages,
|
|
129
|
+
{
|
|
130
|
+
title: "Running Prisma migrations",
|
|
131
|
+
skip: () => {
|
|
132
|
+
if (!databaseUrl) {
|
|
133
|
+
return `No DATABASE_URL found in \`.env\`. Set it to your PostgreSQL connection string, then run \`${prettyPrintCedarCommand(["prisma", "migrate", "dev"])}\``;
|
|
134
|
+
}
|
|
135
|
+
return false;
|
|
136
|
+
},
|
|
137
|
+
task: () => {
|
|
138
|
+
const result = execa.commandSync(
|
|
139
|
+
"yarn cedar prisma migrate dev --name init-postgres",
|
|
140
|
+
{
|
|
141
|
+
cwd: cedarPaths.base,
|
|
142
|
+
stdio: ["inherit", "inherit", "pipe"],
|
|
143
|
+
reject: false
|
|
144
|
+
}
|
|
145
|
+
);
|
|
146
|
+
if (result.exitCode !== 0) {
|
|
147
|
+
throw new Error(
|
|
148
|
+
"Prisma migration failed:\n\n" + result.stderr + `
|
|
149
|
+
|
|
150
|
+
You can try running it manually:
|
|
151
|
+
${prettyPrintCedarCommand(["prisma", "migrate", "dev", "--name", "init-postgres"])}`
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
],
|
|
157
|
+
{
|
|
158
|
+
exitOnError: false,
|
|
159
|
+
collectErrors: "minimal"
|
|
160
|
+
}
|
|
161
|
+
);
|
|
162
|
+
try {
|
|
163
|
+
await tasks.run();
|
|
164
|
+
if (tasks.errors.length > 0) {
|
|
165
|
+
for (const error of tasks.errors) {
|
|
166
|
+
if (isErrorWithMessage(error)) {
|
|
167
|
+
errorTelemetry(process.argv, error.message);
|
|
168
|
+
console.error(colors.error(error.message));
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
process.exit(1);
|
|
172
|
+
}
|
|
173
|
+
} catch (e) {
|
|
174
|
+
if (isErrorWithMessage(e)) {
|
|
175
|
+
errorTelemetry(process.argv, e.message);
|
|
176
|
+
console.error(colors.error(e.message));
|
|
177
|
+
}
|
|
178
|
+
if (isErrorWithExitCode(e)) {
|
|
179
|
+
process.exit(e.exitCode);
|
|
180
|
+
}
|
|
181
|
+
process.exit(1);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
function isErrorWithMessage(e) {
|
|
185
|
+
return !!e && typeof e === "object" && "message" in e;
|
|
186
|
+
}
|
|
187
|
+
function isErrorWithExitCode(e) {
|
|
188
|
+
return !!e && typeof e === "object" && "exitCode" in e && typeof e.exitCode === "number";
|
|
189
|
+
}
|
|
190
|
+
export {
|
|
191
|
+
checkProjectShape,
|
|
192
|
+
getSqliteToPostgresTasks,
|
|
193
|
+
handler
|
|
194
|
+
};
|
|
@@ -5,13 +5,7 @@ import { recordTelemetryAttributes, colors as c } from "@cedarjs/cli-helpers";
|
|
|
5
5
|
import { getPaths, getPrismaSchemas } from "@cedarjs/project-config";
|
|
6
6
|
import { errorTelemetry } from "@cedarjs/telemetry";
|
|
7
7
|
import { writeFilesTask, printSetupNotes } from "../../../../lib/index.js";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
POSTGRES_YAML,
|
|
11
|
-
RENDER_HEALTH_CHECK,
|
|
12
|
-
RENDER_YAML,
|
|
13
|
-
SQLITE_YAML
|
|
14
|
-
} from "../templates/render.js";
|
|
8
|
+
import { POSTGRES_YAML, RENDER_YAML, SQLITE_YAML } from "../templates/render.js";
|
|
15
9
|
const { getConfig } = prismaInternals;
|
|
16
10
|
const getRenderYamlContent = async (database) => {
|
|
17
11
|
if (database === "none") {
|
|
@@ -54,14 +48,9 @@ const getRenderYamlContent = async (database) => {
|
|
|
54
48
|
const notes = [
|
|
55
49
|
"You are ready to deploy to Render!\n",
|
|
56
50
|
"Go to https://dashboard.render.com/iacs to create your account and deploy to Render",
|
|
57
|
-
"Check out the deployment docs at https://
|
|
58
|
-
"Note: After first deployment to Render update the rewrite rule destination in `./render.yaml`"
|
|
59
|
-
|
|
60
|
-
const additionalFiles = [
|
|
61
|
-
{
|
|
62
|
-
path: path.join(getPaths().base, "api/src/functions/healthz.js"),
|
|
63
|
-
content: RENDER_HEALTH_CHECK
|
|
64
|
-
}
|
|
51
|
+
"Check out the deployment docs at https://cedarjs.com/docs/deploy/render for detailed instructions",
|
|
52
|
+
"Note: After first deployment to Render update the rewrite rule destination in `./render.yaml`",
|
|
53
|
+
"Note: The api service now health checks `/graphql/health`. If a previous setup left an unused `api/src/functions/healthz.js` behind, you can delete it"
|
|
65
54
|
];
|
|
66
55
|
const handler = async ({
|
|
67
56
|
force,
|
|
@@ -83,11 +72,6 @@ const handler = async ({
|
|
|
83
72
|
return writeFilesTask(files, { overwriteExisting: force });
|
|
84
73
|
}
|
|
85
74
|
},
|
|
86
|
-
// Add health check api function
|
|
87
|
-
addFilesTask({
|
|
88
|
-
files: additionalFiles,
|
|
89
|
-
force
|
|
90
|
-
}),
|
|
91
75
|
printSetupNotes(notes)
|
|
92
76
|
],
|
|
93
77
|
{ rendererOptions: { collapseSubtasks: false } }
|
|
@@ -5,13 +5,15 @@ const PROJECT_NAME = path.basename(getPaths().base);
|
|
|
5
5
|
const RENDER_YAML = (database) => {
|
|
6
6
|
const apiUrl = getUserApiUrl().replace(/\/$/, "");
|
|
7
7
|
return `# Quick links to the docs:
|
|
8
|
-
# -
|
|
9
|
-
# - Render's
|
|
8
|
+
# - Deploying Cedar: https://cedarjs.com/docs/deploy/render
|
|
9
|
+
# - Render's own walkthrough (uses \`yarn rw\`, but just swap in \`yarn cedar\`):
|
|
10
|
+
# https://render.com/docs/deploy-redwood
|
|
11
|
+
# - Render's Blueprint spec: https://render.com/docs/blueprint-spec
|
|
10
12
|
|
|
11
13
|
services:
|
|
12
14
|
- name: ${PROJECT_NAME}-web
|
|
13
15
|
type: web
|
|
14
|
-
|
|
16
|
+
runtime: static
|
|
15
17
|
buildCommand: npm install --global corepack && yarn install && yarn cedar deploy render web
|
|
16
18
|
staticPublishPath: ./web/dist
|
|
17
19
|
|
|
@@ -22,11 +24,16 @@ services:
|
|
|
22
24
|
routes:
|
|
23
25
|
- type: rewrite
|
|
24
26
|
source: ${apiUrl}/*
|
|
25
|
-
# Replace \`destination\`
|
|
27
|
+
# Replace \`destination\` after your first deploy, with the api service's
|
|
28
|
+
# URL from the Render dashboard:
|
|
26
29
|
#
|
|
27
30
|
# \`\`\`
|
|
28
|
-
# destination: https
|
|
31
|
+
# destination: https://${PROJECT_NAME}-api.onrender.com/*
|
|
29
32
|
# \`\`\`
|
|
33
|
+
#
|
|
34
|
+
# This can't be filled in automatically \u2014 Render's \`fromService\` only
|
|
35
|
+
# resolves service hosts into \`envVars\`, not into a static site's route
|
|
36
|
+
# destination.
|
|
30
37
|
destination: replace_with_api_url/*
|
|
31
38
|
- type: rewrite
|
|
32
39
|
source: /*
|
|
@@ -35,11 +42,22 @@ services:
|
|
|
35
42
|
- name: ${PROJECT_NAME}-api
|
|
36
43
|
type: web
|
|
37
44
|
plan: free
|
|
38
|
-
|
|
45
|
+
runtime: node
|
|
39
46
|
region: oregon
|
|
40
47
|
buildCommand: npm install --global corepack && yarn install && yarn cedar build api
|
|
41
48
|
startCommand: yarn cedar deploy render api
|
|
42
49
|
|
|
50
|
+
# Proves the GraphQL server is actually serving, not just that the process
|
|
51
|
+
# is alive. Returns 200 with an \`x-yoga-id\` response header.
|
|
52
|
+
#
|
|
53
|
+
# The route is \`<apiRootPath><graphiQLEndpoint>/health\`, and the value below
|
|
54
|
+
# assumes both defaults (\`/\` and \`/graphql\`). Update it to match if you
|
|
55
|
+
# customize either \u2014 by setting \`CEDAR_API_ROOT_PATH\` in the envVars below,
|
|
56
|
+
# by passing \`apiRootPath\` to \`createServer\` in \`api/src/server.ts\`, or by
|
|
57
|
+
# setting \`graphiQLEndpoint\` in \`api/src/functions/graphql.ts\`. A mismatch
|
|
58
|
+
# here 404s, and Render will not promote the deploy.
|
|
59
|
+
healthCheckPath: /graphql/health
|
|
60
|
+
|
|
43
61
|
envVars:
|
|
44
62
|
${database}
|
|
45
63
|
`;
|
|
@@ -58,17 +76,9 @@ const SQLITE_YAML = ` - key: DATABASE_URL
|
|
|
58
76
|
name: sqlite-data
|
|
59
77
|
mountPath: /opt/render/project/src/api/db/data
|
|
60
78
|
sizeGB: 1`;
|
|
61
|
-
const RENDER_HEALTH_CHECK = `// render-health-check
|
|
62
|
-
export const handler = async () => {
|
|
63
|
-
return {
|
|
64
|
-
statusCode: 200,
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
`;
|
|
68
79
|
export {
|
|
69
80
|
POSTGRES_YAML,
|
|
70
81
|
PROJECT_NAME,
|
|
71
|
-
RENDER_HEALTH_CHECK,
|
|
72
82
|
RENDER_YAML,
|
|
73
83
|
SQLITE_YAML
|
|
74
84
|
};
|
|
@@ -94,11 +94,8 @@ ENV NODE_ENV=production
|
|
|
94
94
|
|
|
95
95
|
# default api serve command
|
|
96
96
|
# ---------
|
|
97
|
-
#
|
|
98
|
-
#
|
|
99
|
-
# This is important if you intend to configure GraphQL to use Realtime.
|
|
100
|
-
#
|
|
101
|
-
# CMD [ "./api/dist/server.js" ]
|
|
97
|
+
# See https://cedarjs.com/docs/server-file for customizing the api server
|
|
98
|
+
# (Realtime, custom Fastify plugins, etc).
|
|
102
99
|
CMD ["node_modules/.bin/cedarjs-server", "api"]
|
|
103
100
|
|
|
104
101
|
# web serve
|
|
@@ -4,11 +4,8 @@ services:
|
|
|
4
4
|
context: .
|
|
5
5
|
dockerfile: ./Dockerfile
|
|
6
6
|
target: api_serve
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
# command to launch your server or update the Dockerfile to do so.
|
|
10
|
-
# This is important if you intend to configure GraphQL to use Realtime.
|
|
11
|
-
# command: "./api/dist/server.js"
|
|
7
|
+
# See https://cedarjs.com/docs/server-file for customizing the api
|
|
8
|
+
# server (Realtime, custom Fastify plugins, etc).
|
|
12
9
|
ports:
|
|
13
10
|
- '8911:8911'
|
|
14
11
|
depends_on:
|
|
@@ -3,213 +3,76 @@ import path from "node:path";
|
|
|
3
3
|
import execa from "execa";
|
|
4
4
|
import { Listr } from "listr2";
|
|
5
5
|
import { colors, getPaths, installPackages } from "@cedarjs/cli-helpers";
|
|
6
|
-
import {
|
|
6
|
+
import { prettyPrintCedarCommand } from "@cedarjs/cli-helpers/packageManager";
|
|
7
7
|
import { errorTelemetry } from "@cedarjs/telemetry";
|
|
8
|
-
|
|
8
|
+
import {
|
|
9
|
+
checkProjectShape,
|
|
10
|
+
getSqliteToPostgresTasks
|
|
11
|
+
} from "../database/postgresHandler.js";
|
|
9
12
|
async function handler({ force }) {
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
13
|
+
const cedarPaths = getPaths();
|
|
14
|
+
const shape = checkProjectShape(cedarPaths);
|
|
15
|
+
if (!shape.ok) {
|
|
16
|
+
if (shape.alreadyConverted) {
|
|
17
|
+
console.log(colors.note(shape.message));
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
console.error(colors.error(shape.message));
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
20
23
|
const envPath = path.join(cedarPaths.base, ".env");
|
|
21
|
-
|
|
22
|
-
const apiPkgPath = path.join(cedarPaths.api.base, "package.json");
|
|
23
|
-
const dbTsTemplatePath = path.join(
|
|
24
|
-
import.meta.dirname,
|
|
25
|
-
"templates",
|
|
26
|
-
"db.ts.template"
|
|
27
|
-
);
|
|
28
|
-
let hasDirectDatabaseUrl = false;
|
|
24
|
+
let hasExistingDatabaseUrl = false;
|
|
29
25
|
if (fs.existsSync(envPath)) {
|
|
30
|
-
|
|
26
|
+
hasExistingDatabaseUrl = /^DATABASE_URL=/m.test(
|
|
31
27
|
fs.readFileSync(envPath, "utf-8")
|
|
32
28
|
);
|
|
33
29
|
}
|
|
30
|
+
const skipProvisioning = hasExistingDatabaseUrl && !force;
|
|
34
31
|
const notes = [];
|
|
32
|
+
if (skipProvisioning) {
|
|
33
|
+
notes.push(
|
|
34
|
+
colors.note(
|
|
35
|
+
"DATABASE_URL is already set in .env. Use --force to overwrite."
|
|
36
|
+
)
|
|
37
|
+
);
|
|
38
|
+
}
|
|
35
39
|
const tasks = new Listr(
|
|
36
40
|
[
|
|
41
|
+
...getSqliteToPostgresTasks({ dbPath: shape.dbPath }),
|
|
37
42
|
{
|
|
38
|
-
title: "
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
ctx.isNeon = false;
|
|
49
|
-
}
|
|
50
|
-
if (!ctx.isSqlite && !ctx.isPostgres) {
|
|
51
|
-
ctx.unsupportedProvider = true;
|
|
52
|
-
notes.push(
|
|
53
|
-
colors.note(
|
|
54
|
-
"setup neon only supports migrating from SQLite to PostgreSQL. Your project uses a different database provider."
|
|
55
|
-
)
|
|
56
|
-
);
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
59
|
-
if (!ctx.isPostgres) {
|
|
60
|
-
ctx.hasSqliteUsageOutsideDb = hasSqliteUsageOutsideDb(
|
|
61
|
-
cedarPaths.api.src,
|
|
62
|
-
dbTsPath
|
|
63
|
-
);
|
|
64
|
-
}
|
|
65
|
-
if (hasDirectDatabaseUrl && !force) {
|
|
66
|
-
ctx.skipWithNote = true;
|
|
67
|
-
notes.push(
|
|
68
|
-
colors.note(
|
|
69
|
-
"DATABASE_URL is already set in .env. Use --force to overwrite."
|
|
70
|
-
)
|
|
71
|
-
);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
},
|
|
75
|
-
{
|
|
76
|
-
title: "Removing SQLite dependencies from api/package.json",
|
|
77
|
-
skip: (ctx) => {
|
|
78
|
-
if (ctx.unsupportedProvider) {
|
|
79
|
-
return "Unsupported database provider";
|
|
80
|
-
}
|
|
81
|
-
if (ctx.isPostgres) {
|
|
82
|
-
return "Already configured for PostgreSQL";
|
|
83
|
-
}
|
|
84
|
-
if (ctx.hasSqliteUsageOutsideDb) {
|
|
85
|
-
return "SQLite is in use outside db.ts \u2014 keeping packages";
|
|
86
|
-
}
|
|
87
|
-
return false;
|
|
88
|
-
},
|
|
89
|
-
task: () => {
|
|
90
|
-
const pkg = JSON.parse(fs.readFileSync(apiPkgPath, "utf-8"));
|
|
91
|
-
if (pkg.dependencies) {
|
|
92
|
-
delete pkg.dependencies["better-sqlite3"];
|
|
93
|
-
delete pkg.dependencies["@prisma/adapter-better-sqlite3"];
|
|
94
|
-
}
|
|
95
|
-
fs.writeFileSync(apiPkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
96
|
-
}
|
|
97
|
-
},
|
|
98
|
-
{
|
|
99
|
-
title: "Removing better-sqlite3 dependenciesMeta",
|
|
100
|
-
skip: (ctx) => {
|
|
101
|
-
if (ctx.unsupportedProvider) {
|
|
102
|
-
return "Unsupported database provider";
|
|
103
|
-
}
|
|
104
|
-
if (ctx.isPostgres) {
|
|
105
|
-
return "Already configured for PostgreSQL";
|
|
106
|
-
}
|
|
107
|
-
if (ctx.hasSqliteUsageOutsideDb) {
|
|
108
|
-
return "SQLite is in use outside db.ts so we're keeping it installed";
|
|
109
|
-
}
|
|
110
|
-
return false;
|
|
111
|
-
},
|
|
112
|
-
task: () => {
|
|
113
|
-
if (!fs.existsSync(rootPkgPath)) {
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
const pkg = JSON.parse(fs.readFileSync(rootPkgPath, "utf-8"));
|
|
117
|
-
if (pkg.dependenciesMeta?.["better-sqlite3"]) {
|
|
118
|
-
delete pkg.dependenciesMeta["better-sqlite3"];
|
|
119
|
-
if (Object.keys(pkg.dependenciesMeta).length === 0) {
|
|
120
|
-
delete pkg.dependenciesMeta;
|
|
121
|
-
}
|
|
122
|
-
fs.writeFileSync(rootPkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
},
|
|
126
|
-
{
|
|
127
|
-
title: "Switching Prisma schema to PostgreSQL",
|
|
128
|
-
skip: (ctx) => {
|
|
129
|
-
if (ctx.unsupportedProvider) {
|
|
130
|
-
return "Unsupported database provider";
|
|
131
|
-
}
|
|
132
|
-
if (ctx.isPostgres) {
|
|
133
|
-
return "Schema is already configured for PostgreSQL";
|
|
134
|
-
}
|
|
135
|
-
return false;
|
|
136
|
-
},
|
|
137
|
-
task: (ctx) => {
|
|
138
|
-
const updated = ctx.schemaContent.replace(
|
|
139
|
-
'provider = "sqlite"',
|
|
140
|
-
'provider = "postgresql"'
|
|
43
|
+
title: "Setting DIRECT_DATABASE_URL in Prisma config",
|
|
44
|
+
skip: () => skipProvisioning,
|
|
45
|
+
task: (ctx, task) => {
|
|
46
|
+
const prismaConfigPathCjs = path.join(
|
|
47
|
+
cedarPaths.api.base,
|
|
48
|
+
"prisma.config.cjs"
|
|
49
|
+
);
|
|
50
|
+
const prismaConfigPathMts = path.join(
|
|
51
|
+
cedarPaths.api.base,
|
|
52
|
+
"prisma.config.mts"
|
|
141
53
|
);
|
|
142
|
-
fs.writeFileSync(schemaPath, updated);
|
|
143
|
-
}
|
|
144
|
-
},
|
|
145
|
-
{
|
|
146
|
-
title: "Updating database adapter",
|
|
147
|
-
skip: (ctx) => {
|
|
148
|
-
if (ctx.unsupportedProvider) {
|
|
149
|
-
return "Unsupported database provider";
|
|
150
|
-
}
|
|
151
|
-
if (ctx.isNeon) {
|
|
152
|
-
return "Database adapter is already configured for Neon (PrismaPg)";
|
|
153
|
-
}
|
|
154
|
-
if (ctx.skipWithNote) {
|
|
155
|
-
return "DATABASE_URL already configured \u2014 skipping adapter update";
|
|
156
|
-
}
|
|
157
|
-
return false;
|
|
158
|
-
},
|
|
159
|
-
task: () => {
|
|
160
|
-
const neonDbTs = fs.readFileSync(dbTsTemplatePath, "utf-8");
|
|
161
|
-
fs.writeFileSync(dbTsPath, neonDbTs);
|
|
162
|
-
}
|
|
163
|
-
},
|
|
164
|
-
{
|
|
165
|
-
title: "Updating Prisma config",
|
|
166
|
-
skip: (ctx) => {
|
|
167
|
-
if (ctx.unsupportedProvider) {
|
|
168
|
-
return "Unsupported database provider";
|
|
169
|
-
}
|
|
170
|
-
if (ctx.isNeon) {
|
|
171
|
-
return "Prisma config is already configured for Neon";
|
|
172
|
-
}
|
|
173
|
-
if (ctx.skipWithNote) {
|
|
174
|
-
return "DATABASE_URL already configured \u2014 skipping config update";
|
|
175
|
-
}
|
|
176
|
-
return false;
|
|
177
|
-
},
|
|
178
|
-
task: () => {
|
|
179
|
-
if (!fs.existsSync(prismaConfigPathCjs) && !fs.existsSync(prismaConfigPathMts)) {
|
|
180
|
-
throw new Error(
|
|
181
|
-
"No Prisma config file found. Expected prisma.config.cjs or prisma.config.mts in the api directory."
|
|
182
|
-
);
|
|
183
|
-
}
|
|
184
54
|
const configPath = fs.existsSync(prismaConfigPathCjs) ? prismaConfigPathCjs : prismaConfigPathMts;
|
|
185
55
|
const configContent = fs.readFileSync(configPath, "utf-8");
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
56
|
+
const datasourceUrlRegex = /(\burl\s*:\s*)env\(["'][^"']*?["']\)/;
|
|
57
|
+
if (!datasourceUrlRegex.test(configContent)) {
|
|
58
|
+
ctx.directDatabaseUrlNotSet = true;
|
|
59
|
+
task.skip(
|
|
60
|
+
"Could not set DIRECT_DATABASE_URL. Please manually set datasource.url in " + configPath
|
|
61
|
+
);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
fs.writeFileSync(
|
|
65
|
+
configPath,
|
|
66
|
+
configContent.replace(
|
|
67
|
+
datasourceUrlRegex,
|
|
68
|
+
"$1env('DIRECT_DATABASE_URL')"
|
|
69
|
+
)
|
|
189
70
|
);
|
|
190
|
-
fs.writeFileSync(configPath, updated);
|
|
191
|
-
}
|
|
192
|
-
},
|
|
193
|
-
{
|
|
194
|
-
title: "Adding required api packages...",
|
|
195
|
-
skip: (ctx) => ctx.unsupportedProvider,
|
|
196
|
-
task: async () => {
|
|
197
|
-
await addWorkspacePackages("api", ["@prisma/adapter-pg@7.8.0"], {
|
|
198
|
-
cwd: cedarPaths.api.base
|
|
199
|
-
});
|
|
200
71
|
}
|
|
201
72
|
},
|
|
202
73
|
{
|
|
203
74
|
title: "Provisioning Neon database",
|
|
204
|
-
skip: (
|
|
205
|
-
if (ctx.unsupportedProvider) {
|
|
206
|
-
return true;
|
|
207
|
-
}
|
|
208
|
-
if (hasDirectDatabaseUrl && !force) {
|
|
209
|
-
return true;
|
|
210
|
-
}
|
|
211
|
-
return false;
|
|
212
|
-
},
|
|
75
|
+
skip: () => skipProvisioning,
|
|
213
76
|
task: async (ctx) => {
|
|
214
77
|
const res = await fetch("https://neon.new/api/v1/database", {
|
|
215
78
|
method: "POST",
|
|
@@ -239,20 +102,52 @@ async function handler({ force }) {
|
|
|
239
102
|
ctx.neonClaimExpiry = new Date(data.expires_at).toUTCString();
|
|
240
103
|
}
|
|
241
104
|
},
|
|
105
|
+
installPackages,
|
|
242
106
|
{
|
|
243
|
-
title: "
|
|
107
|
+
title: "Running Prisma migrations",
|
|
244
108
|
skip: (ctx) => {
|
|
245
|
-
if (
|
|
109
|
+
if (skipProvisioning) {
|
|
246
110
|
return true;
|
|
247
111
|
}
|
|
248
|
-
if (
|
|
249
|
-
return
|
|
250
|
-
}
|
|
251
|
-
if (!ctx.databaseUrl) {
|
|
252
|
-
return "No database URL to write (Neon provisioning skipped)";
|
|
112
|
+
if (ctx.directDatabaseUrlNotSet) {
|
|
113
|
+
return `Skipping migrations \u2014 could not confirm prisma.config is reading DIRECT_DATABASE_URL, so migrations could target the wrong database. Fix datasource.url, then run \`${prettyPrintCedarCommand(["prisma", "migrate", "dev"])}\` manually.`;
|
|
253
114
|
}
|
|
254
115
|
return false;
|
|
255
116
|
},
|
|
117
|
+
task: (ctx) => {
|
|
118
|
+
const result = execa.commandSync(
|
|
119
|
+
"yarn cedar prisma migrate dev --name init-neon",
|
|
120
|
+
{
|
|
121
|
+
cwd: cedarPaths.base,
|
|
122
|
+
stdio: ["inherit", "inherit", "pipe"],
|
|
123
|
+
reject: false,
|
|
124
|
+
env: {
|
|
125
|
+
...process.env,
|
|
126
|
+
DIRECT_DATABASE_URL: ctx.databaseUrlDirect
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
);
|
|
130
|
+
if (result.exitCode !== 0) {
|
|
131
|
+
throw new Error(
|
|
132
|
+
"Prisma migration failed:\n\n" + result.stderr + `
|
|
133
|
+
|
|
134
|
+
You can try running it manually:
|
|
135
|
+
${prettyPrintCedarCommand(["prisma", "migrate", "dev", "--name", "init-neon"])}`
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
title: "Writing database connection to .env",
|
|
142
|
+
// Deliberately runs after migrations, not before — with
|
|
143
|
+
// `exitOnError: true`, a migration failure stops the list here and
|
|
144
|
+
// this task never runs. That means a project whose migrations
|
|
145
|
+
// failed never has DATABASE_URL/DIRECT_DATABASE_URL written to
|
|
146
|
+
// .env, so it's never left pointing at a Neon database it doesn't
|
|
147
|
+
// know it needs to claim. Provisioning that database anyway (and
|
|
148
|
+
// letting it expire unclaimed) is fine — it's exactly as if the
|
|
149
|
+
// command were re-run from scratch, which is safe.
|
|
150
|
+
skip: () => skipProvisioning,
|
|
256
151
|
task: (ctx) => {
|
|
257
152
|
let envContent = "";
|
|
258
153
|
if (fs.existsSync(envPath)) {
|
|
@@ -275,49 +170,10 @@ async function handler({ force }) {
|
|
|
275
170
|
fs.writeFileSync(envPath, envContent);
|
|
276
171
|
}
|
|
277
172
|
},
|
|
278
|
-
installPackages,
|
|
279
|
-
{
|
|
280
|
-
title: "Running Prisma migrations",
|
|
281
|
-
skip: (ctx) => {
|
|
282
|
-
if (ctx.unsupportedProvider) {
|
|
283
|
-
return true;
|
|
284
|
-
}
|
|
285
|
-
if (ctx.skipWithNote) {
|
|
286
|
-
return "DATABASE_URL already configured \u2014 skipping migration";
|
|
287
|
-
}
|
|
288
|
-
if (!ctx.databaseUrl) {
|
|
289
|
-
return "No database provisioned \u2014 skipping migration";
|
|
290
|
-
}
|
|
291
|
-
return false;
|
|
292
|
-
},
|
|
293
|
-
task: (ctx) => {
|
|
294
|
-
const result = execa.commandSync(
|
|
295
|
-
"yarn cedar prisma migrate dev --name init-neon",
|
|
296
|
-
{
|
|
297
|
-
cwd: cedarPaths.base,
|
|
298
|
-
stdio: ["inherit", "inherit", "pipe"],
|
|
299
|
-
reject: false,
|
|
300
|
-
env: {
|
|
301
|
-
...process.env,
|
|
302
|
-
DIRECT_DATABASE_URL: ctx.databaseUrlDirect
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
);
|
|
306
|
-
if (result.exitCode !== 0) {
|
|
307
|
-
throw new Error(
|
|
308
|
-
"Prisma migration failed:\n\n" + result.stderr + "\n\nYou can try running it manually:\n yarn cedar prisma migrate dev --name init-neon"
|
|
309
|
-
);
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
},
|
|
313
173
|
{
|
|
314
174
|
title: "One more thing...",
|
|
315
175
|
task: (ctx, task) => {
|
|
316
|
-
if (
|
|
317
|
-
task.output = "Skipped \u2014 unsupported database provider";
|
|
318
|
-
return;
|
|
319
|
-
}
|
|
320
|
-
if (ctx.skipWithNote) {
|
|
176
|
+
if (skipProvisioning) {
|
|
321
177
|
task.output = "Skipped \u2014 DATABASE_URL already configured";
|
|
322
178
|
return;
|
|
323
179
|
}
|
|
@@ -336,7 +192,11 @@ async function handler({ force }) {
|
|
|
336
192
|
}
|
|
337
193
|
],
|
|
338
194
|
{
|
|
339
|
-
|
|
195
|
+
// Migrations run before .env is written (see above) specifically so
|
|
196
|
+
// that a failure here — the one step that shouldn't be allowed to
|
|
197
|
+
// continue — stops the whole list via the default exitOnError
|
|
198
|
+
// behavior, rather than needing every later task to know to skip.
|
|
199
|
+
exitOnError: true
|
|
340
200
|
}
|
|
341
201
|
);
|
|
342
202
|
try {
|
|
@@ -362,24 +222,6 @@ function isErrorWithMessage(e) {
|
|
|
362
222
|
function isErrorWithExitCode(e) {
|
|
363
223
|
return !!e && typeof e === "object" && "exitCode" in e && typeof e.exitCode === "number";
|
|
364
224
|
}
|
|
365
|
-
function hasSqliteUsageOutsideDb(srcPath, dbTsPath) {
|
|
366
|
-
const sqlitePattern = /better-sqlite3|@prisma\/adapter-better-sqlite3/;
|
|
367
|
-
const files = fs.globSync("**/*.{ts,tsx,js,jsx}", { cwd: srcPath });
|
|
368
|
-
for (const file of files) {
|
|
369
|
-
const fullPath = path.join(srcPath, file);
|
|
370
|
-
if (fullPath === dbTsPath) {
|
|
371
|
-
continue;
|
|
372
|
-
}
|
|
373
|
-
try {
|
|
374
|
-
const content = fs.readFileSync(fullPath, "utf-8");
|
|
375
|
-
if (sqlitePattern.test(content)) {
|
|
376
|
-
return true;
|
|
377
|
-
}
|
|
378
|
-
} catch {
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
return false;
|
|
382
|
-
}
|
|
383
225
|
export {
|
|
384
226
|
handler
|
|
385
227
|
};
|
package/dist/commands/setup.js
CHANGED
|
@@ -2,6 +2,7 @@ import { terminalLink } from "termi-link";
|
|
|
2
2
|
import { detectCedarVersion } from "../middleware/detectProjectCedarVersion.js";
|
|
3
3
|
import * as setupAuth from "./setup/auth/auth.js";
|
|
4
4
|
import * as setupCache from "./setup/cache/cache.js";
|
|
5
|
+
import * as setupDatabase from "./setup/database/database.js";
|
|
5
6
|
import * as setupDeploy from "./setup/deploy/deploy.js";
|
|
6
7
|
import * as setupDocker from "./setup/docker/docker.js";
|
|
7
8
|
import * as setupGenerator from "./setup/generator/generator.js";
|
|
@@ -20,7 +21,7 @@ import * as setupUi from "./setup/ui/ui.js";
|
|
|
20
21
|
import * as setupUploads from "./setup/uploads/uploads.js";
|
|
21
22
|
const command = "setup <command>";
|
|
22
23
|
const description = "Initialize project config and install packages";
|
|
23
|
-
const builder = (yargs) => yargs.command(setupAuth).command(setupCache).command(setupDeploy).command(setupDocker).command(setupGenerator).command(setupGraphql).command(setupI18n).command(setupJobs).command(setupMailer).command(setupMiddleware).command(setupMonitoring).command(setupNeon).command(setupPackage).command(setupRealtime).command(setupServerFile).command(setupTsconfig).command(setupUi).command(setupUploads).demandCommand().middleware(detectCedarVersion).epilogue(
|
|
24
|
+
const builder = (yargs) => yargs.command(setupAuth).command(setupCache).command(setupDatabase).command(setupDeploy).command(setupDocker).command(setupGenerator).command(setupGraphql).command(setupI18n).command(setupJobs).command(setupMailer).command(setupMiddleware).command(setupMonitoring).command(setupNeon).command(setupPackage).command(setupRealtime).command(setupServerFile).command(setupTsconfig).command(setupUi).command(setupUploads).demandCommand().middleware(detectCedarVersion).epilogue(
|
|
24
25
|
`Also see the ${terminalLink(
|
|
25
26
|
"CedarJS CLI Reference",
|
|
26
27
|
"https://cedarjs.com/docs/cli-commands#setup"
|
package/dist/lib/index.js
CHANGED
|
@@ -305,20 +305,35 @@ const cleanupEmptyDirsTask = (files) => {
|
|
|
305
305
|
})
|
|
306
306
|
);
|
|
307
307
|
};
|
|
308
|
-
function wrapWithSet(routesContent, layout, routes, newLineAndIndent, props = {}) {
|
|
308
|
+
function wrapWithSet(routesContent, layout, routes, newLineAndIndent, props = {}, privateSetProps) {
|
|
309
309
|
const [_, indentOne, indentTwo] = routesContent.match(
|
|
310
310
|
/([ \t]*)<Router.*?>[^<]*[\r\n]+([ \t]+)/
|
|
311
311
|
) || ["", "", ""];
|
|
312
312
|
const oneLevelIndent = indentTwo.slice(0, indentTwo.length - indentOne.length);
|
|
313
|
-
const newRoutesWithExtraIndent = routes.map((route) => oneLevelIndent + route);
|
|
314
313
|
const propsString = Object.entries(props).map((values) => `${values[0]}="${values[1]}"`).join(" ");
|
|
314
|
+
if (!privateSetProps) {
|
|
315
|
+
const newRoutesWithExtraIndent2 = routes.map(
|
|
316
|
+
(route) => oneLevelIndent + route
|
|
317
|
+
);
|
|
318
|
+
return [
|
|
319
|
+
`<Set wrap={${layout}}${propsString && " " + propsString}>`,
|
|
320
|
+
...newRoutesWithExtraIndent2,
|
|
321
|
+
`</Set>`
|
|
322
|
+
].join(newLineAndIndent);
|
|
323
|
+
}
|
|
324
|
+
const privateSetPropsString = Object.entries(privateSetProps).map((values) => `${values[0]}="${values[1]}"`).join(" ");
|
|
325
|
+
const newRoutesWithExtraIndent = routes.map(
|
|
326
|
+
(route) => oneLevelIndent + oneLevelIndent + route
|
|
327
|
+
);
|
|
315
328
|
return [
|
|
316
|
-
`<
|
|
329
|
+
`<PrivateSet${privateSetPropsString && " " + privateSetPropsString}>`,
|
|
330
|
+
`${oneLevelIndent}<Set wrap={${layout}}${propsString && " " + propsString}>`,
|
|
317
331
|
...newRoutesWithExtraIndent,
|
|
318
|
-
|
|
332
|
+
`${oneLevelIndent}</Set>`,
|
|
333
|
+
`</PrivateSet>`
|
|
319
334
|
].join(newLineAndIndent);
|
|
320
335
|
}
|
|
321
|
-
function addRoutesToRouterTask(routes, layout, setProps = {}) {
|
|
336
|
+
function addRoutesToRouterTask(routes, layout, setProps = {}, privateSetProps) {
|
|
322
337
|
const cedarPaths = getPaths();
|
|
323
338
|
const routesContent = readFile(cedarPaths.web.routes).toString();
|
|
324
339
|
let newRoutes = routes.filter((route) => !routesContent.match(route));
|
|
@@ -342,7 +357,8 @@ ${route}`);
|
|
|
342
357
|
layout,
|
|
343
358
|
newRoutes,
|
|
344
359
|
newLineAndIndent,
|
|
345
|
-
setProps
|
|
360
|
+
setProps,
|
|
361
|
+
privateSetProps
|
|
346
362
|
) : newRoutes.join(newLineAndIndent);
|
|
347
363
|
const newRoutesContent = routesContent.replace(
|
|
348
364
|
routerStart,
|
|
@@ -7,13 +7,10 @@ import system from "systeminformation";
|
|
|
7
7
|
import { v4 as uuidv4, validate as validateUUID } from "uuid";
|
|
8
8
|
import { getPaths, getRawConfig } from "@cedarjs/project-config";
|
|
9
9
|
import { RWProject } from "@cedarjs/structure/dist/model/RWProject";
|
|
10
|
-
import {
|
|
11
|
-
name as _packageName,
|
|
12
|
-
version as _packageVersion
|
|
13
|
-
} from "../../package.js";
|
|
14
|
-
const packageName = _packageName;
|
|
15
|
-
const packageVersion = _packageVersion;
|
|
16
10
|
async function getResources() {
|
|
11
|
+
const packageJson = await import("../../package.json", { with: { type: "json" } });
|
|
12
|
+
const packageName = packageJson.default["name"];
|
|
13
|
+
const packageVersion = packageJson.default["version"];
|
|
17
14
|
let UID = uuidv4();
|
|
18
15
|
try {
|
|
19
16
|
const telemetryFile = path.join(getPaths().generated.base, "telemetry.txt");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cedarjs/cli",
|
|
3
|
-
"version": "6.0.0-rc.
|
|
3
|
+
"version": "6.0.0-rc.221",
|
|
4
4
|
"description": "The CedarJS Command Line",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -31,19 +31,22 @@
|
|
|
31
31
|
"test:watch": "vitest watch"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
+
"@babel/core": "^7.26.10",
|
|
34
35
|
"@babel/parser": "7.29.7",
|
|
35
36
|
"@babel/preset-typescript": "7.29.7",
|
|
36
|
-
"@
|
|
37
|
-
"@
|
|
38
|
-
"@cedarjs/
|
|
39
|
-
"@cedarjs/
|
|
40
|
-
"@cedarjs/
|
|
41
|
-
"@cedarjs/
|
|
42
|
-
"@cedarjs/
|
|
43
|
-
"@cedarjs/
|
|
44
|
-
"@cedarjs/
|
|
45
|
-
"@cedarjs/
|
|
46
|
-
"@cedarjs/
|
|
37
|
+
"@babel/traverse": "7.29.7",
|
|
38
|
+
"@babel/types": "7.29.7",
|
|
39
|
+
"@cedarjs/api-server": "6.0.0-rc.221",
|
|
40
|
+
"@cedarjs/babel-config": "6.0.0-rc.221",
|
|
41
|
+
"@cedarjs/cli-helpers": "6.0.0-rc.221",
|
|
42
|
+
"@cedarjs/internal": "6.0.0-rc.221",
|
|
43
|
+
"@cedarjs/prerender": "6.0.0-rc.221",
|
|
44
|
+
"@cedarjs/project-config": "6.0.0-rc.221",
|
|
45
|
+
"@cedarjs/structure": "6.0.0-rc.221",
|
|
46
|
+
"@cedarjs/telemetry": "6.0.0-rc.221",
|
|
47
|
+
"@cedarjs/utils": "6.0.0-rc.221",
|
|
48
|
+
"@cedarjs/vite": "6.0.0-rc.221",
|
|
49
|
+
"@cedarjs/web-server": "6.0.0-rc.221",
|
|
47
50
|
"@listr2/prompt-adapter-enquirer": "4.3.0",
|
|
48
51
|
"@opentelemetry/api": "1.9.1",
|
|
49
52
|
"@opentelemetry/core": "1.30.1",
|
|
@@ -61,7 +64,6 @@
|
|
|
61
64
|
"ci-info": "4.4.0",
|
|
62
65
|
"concurrently": "9.2.4",
|
|
63
66
|
"configstore": "7.1.0",
|
|
64
|
-
"cross-env": "7.0.3",
|
|
65
67
|
"decamelize": "6.0.1",
|
|
66
68
|
"dotenv-defaults": "5.0.2",
|
|
67
69
|
"enquirer": "2.4.1",
|
|
@@ -80,7 +82,6 @@
|
|
|
80
82
|
"prettier": "3.8.4",
|
|
81
83
|
"prisma": "7.8.0",
|
|
82
84
|
"prompts": "2.4.2",
|
|
83
|
-
"rimraf": "6.1.3",
|
|
84
85
|
"semver": "7.7.4",
|
|
85
86
|
"smol-toml": "1.6.1",
|
|
86
87
|
"srvx": "0.11.16",
|
|
@@ -93,8 +94,7 @@
|
|
|
93
94
|
"yargs": "17.7.3"
|
|
94
95
|
},
|
|
95
96
|
"devDependencies": {
|
|
96
|
-
"@
|
|
97
|
-
"@babel/core": "^7.26.10",
|
|
97
|
+
"@cedarjs/framework-tools": "6.0.0-rc.221",
|
|
98
98
|
"@prisma/dmmf": "7.8.0",
|
|
99
99
|
"@types/archiver": "^7.0.0",
|
|
100
100
|
"memfs": "4.64.0",
|
|
File without changes
|