@cedarjs/cli 4.1.1-next.14 → 4.1.1-next.64
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/build/buildHandler.js +76 -11
- package/dist/commands/dev/devHandler.js +55 -18
- package/dist/commands/execHandler.js +0 -1
- package/dist/commands/serve.js +216 -21
- package/dist/commands/setup/neon/neon.js +25 -0
- package/dist/commands/setup/neon/neonHandler.js +385 -0
- package/dist/commands/setup/neon/templates/db.ts.template +32 -0
- package/dist/commands/setup.js +2 -1
- package/dist/commands/upgrade/upgradeHandler.js +1 -1
- package/dist/lib/exec.js +34 -42
- package/dist/lib/generatePrismaClient.js +48 -9
- package/dist/lib/locking.js +8 -4
- package/package.json +14 -14
- package/LICENSE +0 -21
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
} from "@cedarjs/cli-helpers/packageManager/display";
|
|
12
12
|
import { runBin } from "@cedarjs/cli-helpers/packageManager/exec";
|
|
13
13
|
import {
|
|
14
|
+
buildApi,
|
|
14
15
|
buildApiWithVite,
|
|
15
16
|
cleanApiBuild
|
|
16
17
|
} from "@cedarjs/internal/dist/build/api";
|
|
@@ -20,6 +21,7 @@ import { loadAndValidateSdls } from "@cedarjs/internal/dist/validateSchema";
|
|
|
20
21
|
import { detectPrerenderRoutes } from "@cedarjs/prerender/detection";
|
|
21
22
|
import {} from "@cedarjs/project-config";
|
|
22
23
|
import { timedTelemetry } from "@cedarjs/telemetry";
|
|
24
|
+
import { buildCedarApp } from "@cedarjs/vite/build";
|
|
23
25
|
import { buildUDApiServer } from "@cedarjs/vite/buildUDApiServer";
|
|
24
26
|
import { generatePrismaCommand } from "../../lib/generatePrismaClient.js";
|
|
25
27
|
import { getPaths, getConfig } from "../../lib/index.js";
|
|
@@ -163,25 +165,59 @@ Run ` + c.info(formatCedarCommand(["build"])) + " (without specifying a workspac
|
|
|
163
165
|
title: "Verifying graphql schema...",
|
|
164
166
|
task: loadAndValidateSdls
|
|
165
167
|
},
|
|
166
|
-
//
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
|
|
170
|
-
// Step 2 depends on step 1 having completed.
|
|
171
|
-
workspace.includes("api") && {
|
|
168
|
+
// When streaming SSR is enabled, fall back to the legacy separate build
|
|
169
|
+
// paths because streaming SSR has its own complex build orchestration.
|
|
170
|
+
// Phase 7 (SSR/RSC rebuild) will address unifying this path.
|
|
171
|
+
workspace.includes("api") && getConfig().experimental?.streamingSsr?.enabled && {
|
|
172
172
|
title: "Building API...",
|
|
173
173
|
task: async () => {
|
|
174
174
|
await cleanApiBuild();
|
|
175
175
|
await buildApiWithVite();
|
|
176
176
|
}
|
|
177
177
|
},
|
|
178
|
-
|
|
179
|
-
title: "
|
|
178
|
+
workspace.includes("web") && getConfig().experimental?.streamingSsr?.enabled && {
|
|
179
|
+
title: "Building Web...",
|
|
180
180
|
task: async () => {
|
|
181
|
-
|
|
181
|
+
process.env.VITE_CJS_IGNORE_WARNING = "true";
|
|
182
|
+
const createdRequire = createRequire(import.meta.url);
|
|
183
|
+
const buildBinPath = createdRequire.resolve(
|
|
184
|
+
"@cedarjs/vite/bins/cedar-vite-build.mjs"
|
|
185
|
+
);
|
|
186
|
+
await execa(
|
|
187
|
+
`node ${buildBinPath} --webDir="${cedarPaths.web.base}" --verbose=${verbose}`,
|
|
188
|
+
{
|
|
189
|
+
stdio: verbose ? "inherit" : "pipe",
|
|
190
|
+
shell: true,
|
|
191
|
+
cwd: cedarPaths.web.base
|
|
192
|
+
}
|
|
193
|
+
);
|
|
194
|
+
if (!getConfig().experimental?.streamingSsr?.enabled) {
|
|
195
|
+
console.log("Creating 200.html...");
|
|
196
|
+
const indexHtmlPath = path.join(getPaths().web.dist, "index.html");
|
|
197
|
+
fs.copyFileSync(
|
|
198
|
+
indexHtmlPath,
|
|
199
|
+
path.join(getPaths().web.dist, "200.html")
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
// Legacy separate build path (default when not --ud, non-streaming-SSR)
|
|
205
|
+
workspace.includes("api") && !ud && !getConfig().experimental?.streamingSsr?.enabled && {
|
|
206
|
+
title: "Building API...",
|
|
207
|
+
task: async () => {
|
|
208
|
+
await cleanApiBuild();
|
|
209
|
+
const { errors, warnings } = await buildApi();
|
|
210
|
+
if (warnings.length) {
|
|
211
|
+
console.warn(warnings);
|
|
212
|
+
}
|
|
213
|
+
if (errors.length) {
|
|
214
|
+
throw new Error(
|
|
215
|
+
`API build failed with ${errors.length} error(s). See output above for details.`
|
|
216
|
+
);
|
|
217
|
+
}
|
|
182
218
|
}
|
|
183
219
|
},
|
|
184
|
-
workspace.includes("web") && {
|
|
220
|
+
workspace.includes("web") && !ud && !getConfig().experimental?.streamingSsr?.enabled && {
|
|
185
221
|
title: "Building Web...",
|
|
186
222
|
task: async () => {
|
|
187
223
|
process.env.VITE_CJS_IGNORE_WARNING = "true";
|
|
@@ -200,7 +236,30 @@ Run ` + c.info(formatCedarCommand(["build"])) + " (without specifying a workspac
|
|
|
200
236
|
cwd: cedarPaths.web.base
|
|
201
237
|
}
|
|
202
238
|
);
|
|
203
|
-
|
|
239
|
+
console.log("Creating 200.html...");
|
|
240
|
+
const indexHtmlPath = path.join(getPaths().web.dist, "index.html");
|
|
241
|
+
fs.copyFileSync(
|
|
242
|
+
indexHtmlPath,
|
|
243
|
+
path.join(getPaths().web.dist, "200.html")
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
// Unified build path (experimental, non-streaming-SSR, --ud)
|
|
248
|
+
(workspace.includes("api") || workspace.includes("web")) && ud && !getConfig().experimental?.streamingSsr?.enabled && {
|
|
249
|
+
title: workspace.includes("api") && workspace.includes("web") ? "Building App..." : workspace.includes("api") ? "Building API..." : "Building Web...",
|
|
250
|
+
task: async () => {
|
|
251
|
+
process.env.VITE_CJS_IGNORE_WARNING = "true";
|
|
252
|
+
if (workspace.includes("api")) {
|
|
253
|
+
await cleanApiBuild();
|
|
254
|
+
}
|
|
255
|
+
const originalCwd = process.cwd();
|
|
256
|
+
process.chdir(cedarPaths.web.base);
|
|
257
|
+
try {
|
|
258
|
+
await buildCedarApp({ verbose, workspace });
|
|
259
|
+
} finally {
|
|
260
|
+
process.chdir(originalCwd);
|
|
261
|
+
}
|
|
262
|
+
if (workspace.includes("web")) {
|
|
204
263
|
console.log("Creating 200.html...");
|
|
205
264
|
const indexHtmlPath = path.join(getPaths().web.dist, "index.html");
|
|
206
265
|
fs.copyFileSync(
|
|
@@ -209,6 +268,12 @@ Run ` + c.info(formatCedarCommand(["build"])) + " (without specifying a workspac
|
|
|
209
268
|
);
|
|
210
269
|
}
|
|
211
270
|
}
|
|
271
|
+
},
|
|
272
|
+
ud && workspace.includes("api") && {
|
|
273
|
+
title: "Bundling API server entry (Universal Deploy)...",
|
|
274
|
+
task: async () => {
|
|
275
|
+
await buildUDApiServer({ verbose });
|
|
276
|
+
}
|
|
212
277
|
}
|
|
213
278
|
].filter((t) => Boolean(t));
|
|
214
279
|
const triggerPrerender = async () => {
|
|
@@ -68,27 +68,64 @@ const handler = async ({
|
|
|
68
68
|
}
|
|
69
69
|
webPortChangeNeeded = webAvailablePort !== webPreferredPort;
|
|
70
70
|
}
|
|
71
|
-
if (
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
71
|
+
if (ud) {
|
|
72
|
+
if (webPortChangeNeeded) {
|
|
73
|
+
const message = [
|
|
74
|
+
"The currently configured port for the development server is",
|
|
75
|
+
"unavailable. Suggested change to your port, which can be changed in",
|
|
76
|
+
"cedar.toml (or redwood.toml):\n",
|
|
77
|
+
` - Web to use port ${webAvailablePort} instead`,
|
|
78
|
+
"of your currently configured",
|
|
79
|
+
`${webPreferredPort}
|
|
79
80
|
`,
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
81
|
+
"\nCannot run the development server until your configured port is",
|
|
82
|
+
"changed or becomes available."
|
|
83
|
+
].filter(Boolean).join(" ");
|
|
84
|
+
exitWithError(void 0, { message });
|
|
85
|
+
}
|
|
86
|
+
} else {
|
|
87
|
+
if (apiPortChangeNeeded || webPortChangeNeeded) {
|
|
88
|
+
const message = [
|
|
89
|
+
"The currently configured ports for the development server are",
|
|
90
|
+
"unavailable. Suggested changes to your ports, which can be changed in",
|
|
91
|
+
"cedar.toml (or redwood.toml), are:\n",
|
|
92
|
+
apiPortChangeNeeded && ` - API to use port ${apiAvailablePort} instead`,
|
|
93
|
+
apiPortChangeNeeded && "of your currently configured",
|
|
94
|
+
apiPortChangeNeeded && `${apiPreferredPort}
|
|
95
|
+
`,
|
|
96
|
+
webPortChangeNeeded && ` - Web to use port ${webAvailablePort} instead`,
|
|
97
|
+
webPortChangeNeeded && "of your currently configured",
|
|
98
|
+
webPortChangeNeeded && `${webPreferredPort}
|
|
99
|
+
`,
|
|
100
|
+
"\nCannot run the development server until your configured ports are",
|
|
101
|
+
"changed or become available."
|
|
102
|
+
].filter(Boolean).join(" ");
|
|
103
|
+
exitWithError(void 0, { message });
|
|
104
|
+
}
|
|
84
105
|
}
|
|
85
106
|
if (workspace.includes("api")) {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
107
|
+
if (generate) {
|
|
108
|
+
try {
|
|
109
|
+
await generatePrismaClient({ verbose: false });
|
|
110
|
+
} catch (e) {
|
|
111
|
+
const message = getErrorMessage(e);
|
|
112
|
+
errorTelemetry(
|
|
113
|
+
process.argv,
|
|
114
|
+
`Error generating prisma client: ${message}`
|
|
115
|
+
);
|
|
116
|
+
console.error(c.error(message));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (!ud && !serverFile) {
|
|
120
|
+
try {
|
|
121
|
+
await shutdownPort(apiAvailablePort);
|
|
122
|
+
} catch (e) {
|
|
123
|
+
const message = getErrorMessage(e);
|
|
124
|
+
errorTelemetry(process.argv, `Error shutting down "api": ${message}`);
|
|
125
|
+
console.error(
|
|
126
|
+
`Error whilst shutting down "api" port: ${c.error(message)}`
|
|
127
|
+
);
|
|
128
|
+
}
|
|
92
129
|
}
|
|
93
130
|
}
|
|
94
131
|
if (workspace.includes("web") && webAvailablePort !== void 0) {
|
package/dist/commands/serve.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { fork } from "node:child_process";
|
|
2
2
|
import fs from "node:fs";
|
|
3
|
+
import net from "node:net";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { terminalLink } from "termi-link";
|
|
5
6
|
import * as apiServerCLIConfig from "@cedarjs/api-server/apiCliConfig";
|
|
@@ -18,7 +19,14 @@ const builder = async (yargs) => {
|
|
|
18
19
|
yargs.command({
|
|
19
20
|
command: "$0",
|
|
20
21
|
description: bothServerCLIConfig.description,
|
|
21
|
-
builder:
|
|
22
|
+
builder: (yargs2) => {
|
|
23
|
+
bothServerCLIConfig.builder(yargs2);
|
|
24
|
+
return yargs2.option("ud", {
|
|
25
|
+
description: "Use the Universal Deploy server for the API side. The web side is served by the existing static file server. Pass --ud to opt in; the default is Fastify for both sides.",
|
|
26
|
+
type: "boolean",
|
|
27
|
+
default: false
|
|
28
|
+
});
|
|
29
|
+
},
|
|
22
30
|
handler: async (argv) => {
|
|
23
31
|
recordTelemetryAttributes({
|
|
24
32
|
command: "serve",
|
|
@@ -26,6 +34,110 @@ const builder = async (yargs) => {
|
|
|
26
34
|
host: argv.host,
|
|
27
35
|
socket: argv.socket
|
|
28
36
|
});
|
|
37
|
+
if (argv.ud) {
|
|
38
|
+
if (argv.port) {
|
|
39
|
+
console.error(
|
|
40
|
+
c.error(
|
|
41
|
+
"\n The --port flag is not supported with --ud. Use --web-port and --api-port instead.\n"
|
|
42
|
+
)
|
|
43
|
+
);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
const udEntryPath = path.join(getPaths().api.dist, "ud", "index.js");
|
|
47
|
+
if (!fs.existsSync(udEntryPath)) {
|
|
48
|
+
console.error(
|
|
49
|
+
c.error(
|
|
50
|
+
`
|
|
51
|
+
Universal Deploy server entry not found at ${udEntryPath}.
|
|
52
|
+
Please run \`yarn cedar build --ud\` before serving.
|
|
53
|
+
`
|
|
54
|
+
)
|
|
55
|
+
);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
const webDistIndexHtml = path.join(getPaths().web.dist, "index.html");
|
|
59
|
+
if (!fs.existsSync(webDistIndexHtml)) {
|
|
60
|
+
console.error(
|
|
61
|
+
c.error(
|
|
62
|
+
"\n Web build artifacts not found.\n Please run `yarn cedar build` before serving.\n"
|
|
63
|
+
)
|
|
64
|
+
);
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
if (serverFileExists()) {
|
|
68
|
+
console.warn(
|
|
69
|
+
c.warning(
|
|
70
|
+
"\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"
|
|
71
|
+
)
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
const { getAPIHost, getAPIPort, getWebHost, getWebPort } = await import("@cedarjs/api-server/cliHelpers");
|
|
75
|
+
const apiPort = argv.apiPort ?? getAPIPort();
|
|
76
|
+
const apiHost = argv.apiHost ?? getAPIHost();
|
|
77
|
+
const webPort = argv.webPort ?? getWebPort();
|
|
78
|
+
const webHost = argv.webHost ?? getWebHost();
|
|
79
|
+
const apiRootPath = argv.apiRootPath ?? "/";
|
|
80
|
+
const apiProxyTarget = [
|
|
81
|
+
"http://",
|
|
82
|
+
apiHost.includes(":") ? `[${apiHost}]` : apiHost,
|
|
83
|
+
":",
|
|
84
|
+
apiPort,
|
|
85
|
+
apiRootPath
|
|
86
|
+
].join("");
|
|
87
|
+
const { redwoodFastifyWeb } = await import("@cedarjs/fastify-web");
|
|
88
|
+
const { createFastifyInstance } = await import("@cedarjs/api-server/fastify");
|
|
89
|
+
const webFastify = await createFastifyInstance();
|
|
90
|
+
webFastify.register(redwoodFastifyWeb, {
|
|
91
|
+
redwood: {
|
|
92
|
+
apiProxyTarget
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
await webFastify.listen({
|
|
96
|
+
port: webPort,
|
|
97
|
+
host: webHost
|
|
98
|
+
});
|
|
99
|
+
const child = fork(
|
|
100
|
+
udEntryPath,
|
|
101
|
+
["--port", String(apiPort), "--host", apiHost],
|
|
102
|
+
{
|
|
103
|
+
execArgv: process.execArgv,
|
|
104
|
+
env: {
|
|
105
|
+
...process.env,
|
|
106
|
+
NODE_ENV: process.env.NODE_ENV ?? "production",
|
|
107
|
+
PORT: String(apiPort),
|
|
108
|
+
HOST: apiHost
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
);
|
|
112
|
+
child.on("error", (err) => {
|
|
113
|
+
console.error(
|
|
114
|
+
c.error(`
|
|
115
|
+
Failed to start UD API server: ${err.message}
|
|
116
|
+
`)
|
|
117
|
+
);
|
|
118
|
+
process.exit(1);
|
|
119
|
+
});
|
|
120
|
+
child.on("exit", (code) => {
|
|
121
|
+
if (code !== 0) {
|
|
122
|
+
console.error(
|
|
123
|
+
c.error(`
|
|
124
|
+
UD API server exited with code ${code}
|
|
125
|
+
`)
|
|
126
|
+
);
|
|
127
|
+
process.exit(1);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
console.log(`Web server listening at http://${webHost}:${webPort}`);
|
|
131
|
+
process.stdout.write(
|
|
132
|
+
`API server starting at http://${apiHost}:${apiPort}...`
|
|
133
|
+
);
|
|
134
|
+
await waitForPort(apiHost, apiPort);
|
|
135
|
+
process.stdout.write(
|
|
136
|
+
`\rAPI server listening at http://${apiHost}:${apiPort}
|
|
137
|
+
`
|
|
138
|
+
);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
29
141
|
if (serverFileExists()) {
|
|
30
142
|
const serveBothHandlers = await import("./serveBothHandler.js");
|
|
31
143
|
await serveBothHandlers.bothServerFileHandler(argv);
|
|
@@ -51,7 +163,7 @@ const builder = async (yargs) => {
|
|
|
51
163
|
return yargs2.option("ud", {
|
|
52
164
|
// UD serving is opt-in. Pass --ud to use the new srvx server instead
|
|
53
165
|
// of the legacy Fastify server.
|
|
54
|
-
description: "Use the Universal Deploy server
|
|
166
|
+
description: "Use the Universal Deploy server. Pass --ud to opt in; the default is Fastify.",
|
|
55
167
|
type: "boolean",
|
|
56
168
|
default: false
|
|
57
169
|
});
|
|
@@ -71,7 +183,7 @@ const builder = async (yargs) => {
|
|
|
71
183
|
c.error(
|
|
72
184
|
`
|
|
73
185
|
Universal Deploy server entry not found at ${udEntryPath}.
|
|
74
|
-
Please run \`yarn cedar build
|
|
186
|
+
Please run \`yarn cedar build --ud\` before serving.
|
|
75
187
|
`
|
|
76
188
|
)
|
|
77
189
|
);
|
|
@@ -84,17 +196,34 @@ const builder = async (yargs) => {
|
|
|
84
196
|
if (argv.host) {
|
|
85
197
|
udArgs.push("--host", argv.host);
|
|
86
198
|
}
|
|
199
|
+
const child = fork(udEntryPath, udArgs, {
|
|
200
|
+
execArgv: process.execArgv,
|
|
201
|
+
env: {
|
|
202
|
+
...process.env,
|
|
203
|
+
NODE_ENV: process.env.NODE_ENV ?? "production",
|
|
204
|
+
PORT: argv.port ? String(argv.port) : process.env.PORT,
|
|
205
|
+
HOST: argv.host ?? process.env.HOST
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
child.on("error", (err) => {
|
|
209
|
+
console.error(
|
|
210
|
+
c.error(`
|
|
211
|
+
Failed to start UD server: ${err.message}
|
|
212
|
+
`)
|
|
213
|
+
);
|
|
214
|
+
process.exit(1);
|
|
215
|
+
});
|
|
216
|
+
const apiPort = argv.port ?? parseInt(process.env.PORT ?? "8911", 10);
|
|
217
|
+
const apiHost = argv.host ?? process.env.HOST ?? "localhost";
|
|
218
|
+
process.stdout.write(
|
|
219
|
+
`API server starting at http://${apiHost}:${apiPort}...`
|
|
220
|
+
);
|
|
221
|
+
await waitForPort(apiHost, apiPort);
|
|
222
|
+
process.stdout.write(
|
|
223
|
+
`\rAPI server listening at http://${apiHost}:${apiPort}
|
|
224
|
+
`
|
|
225
|
+
);
|
|
87
226
|
await new Promise((resolve, reject) => {
|
|
88
|
-
const child = fork(udEntryPath, udArgs, {
|
|
89
|
-
execArgv: process.execArgv,
|
|
90
|
-
env: {
|
|
91
|
-
...process.env,
|
|
92
|
-
NODE_ENV: process.env.NODE_ENV ?? "production",
|
|
93
|
-
PORT: argv.port ? String(argv.port) : process.env.PORT,
|
|
94
|
-
HOST: argv.host ?? process.env.HOST
|
|
95
|
-
}
|
|
96
|
-
});
|
|
97
|
-
child.on("error", reject);
|
|
98
227
|
child.on("exit", (code) => {
|
|
99
228
|
if (code !== 0) {
|
|
100
229
|
reject(new Error(`UD server exited with code ${code}`));
|
|
@@ -166,6 +295,20 @@ const builder = async (yargs) => {
|
|
|
166
295
|
);
|
|
167
296
|
process.exit(1);
|
|
168
297
|
}
|
|
298
|
+
if (argv.ud) {
|
|
299
|
+
const udEntryPath = path.join(getPaths().api.dist, "ud", "index.js");
|
|
300
|
+
if (!fs.existsSync(udEntryPath)) {
|
|
301
|
+
console.error(
|
|
302
|
+
c.error(
|
|
303
|
+
`
|
|
304
|
+
Universal Deploy server entry not found at ${udEntryPath}.
|
|
305
|
+
Please run \`yarn cedar build --ud\` before serving.
|
|
306
|
+
`
|
|
307
|
+
)
|
|
308
|
+
);
|
|
309
|
+
process.exit(1);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
169
312
|
}
|
|
170
313
|
if (positionalArgs.length === 1) {
|
|
171
314
|
if (!apiSideExists && !rscEnabled) {
|
|
@@ -176,14 +319,38 @@ const builder = async (yargs) => {
|
|
|
176
319
|
);
|
|
177
320
|
process.exit(1);
|
|
178
321
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
322
|
+
if (argv.ud) {
|
|
323
|
+
const udEntryPath = path.join(getPaths().api.dist, "ud", "index.js");
|
|
324
|
+
if (!fs.existsSync(udEntryPath)) {
|
|
325
|
+
console.error(
|
|
326
|
+
c.error(
|
|
327
|
+
`
|
|
328
|
+
Universal Deploy server entry not found at ${udEntryPath}.
|
|
329
|
+
Please run \`yarn cedar build --ud\` before serving.
|
|
330
|
+
`
|
|
331
|
+
)
|
|
332
|
+
);
|
|
333
|
+
process.exit(1);
|
|
334
|
+
}
|
|
335
|
+
const webDistIndexHtml = path.join(getPaths().web.dist, "index.html");
|
|
336
|
+
if (!fs.existsSync(webDistIndexHtml)) {
|
|
337
|
+
console.error(
|
|
338
|
+
c.error(
|
|
339
|
+
"\n Web build artifacts not found.\n Please run `yarn cedar build` before serving.\n"
|
|
340
|
+
)
|
|
341
|
+
);
|
|
342
|
+
process.exit(1);
|
|
343
|
+
}
|
|
344
|
+
} else {
|
|
345
|
+
const apiExistsButIsNotBuilt = apiSideExists && !fs.existsSync(getPaths().api.dist);
|
|
346
|
+
if (apiExistsButIsNotBuilt || !webSideIsBuilt(streamingEnabled || rscEnabled)) {
|
|
347
|
+
console.error(
|
|
348
|
+
c.error(
|
|
349
|
+
"\nPlease run `yarn cedar build` before trying to serve your Cedar app.\n"
|
|
350
|
+
)
|
|
351
|
+
);
|
|
352
|
+
process.exit(1);
|
|
353
|
+
}
|
|
187
354
|
}
|
|
188
355
|
}
|
|
189
356
|
if (!process.env.NODE_ENV) {
|
|
@@ -205,6 +372,34 @@ function webSideIsBuilt(isStreamingOrRSC) {
|
|
|
205
372
|
return fs.existsSync(path.join(getPaths().web.dist, "index.html"));
|
|
206
373
|
}
|
|
207
374
|
}
|
|
375
|
+
function waitForPort(host, port) {
|
|
376
|
+
const maxAttempts = 50;
|
|
377
|
+
const intervalMs = 200;
|
|
378
|
+
return new Promise((resolve, reject) => {
|
|
379
|
+
let attempts = 0;
|
|
380
|
+
const tryConnect = () => {
|
|
381
|
+
attempts++;
|
|
382
|
+
const socket = net.createConnection({ host, port });
|
|
383
|
+
socket.on("connect", () => {
|
|
384
|
+
socket.destroy();
|
|
385
|
+
resolve();
|
|
386
|
+
});
|
|
387
|
+
socket.on("error", () => {
|
|
388
|
+
socket.destroy();
|
|
389
|
+
if (attempts >= maxAttempts) {
|
|
390
|
+
reject(
|
|
391
|
+
new Error(
|
|
392
|
+
`API server did not become ready on port ${port} after ${maxAttempts * intervalMs}ms`
|
|
393
|
+
)
|
|
394
|
+
);
|
|
395
|
+
} else {
|
|
396
|
+
setTimeout(tryConnect, intervalMs);
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
};
|
|
400
|
+
tryConnect();
|
|
401
|
+
});
|
|
402
|
+
}
|
|
208
403
|
export {
|
|
209
404
|
builder,
|
|
210
405
|
command,
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { recordTelemetryAttributes } from "@cedarjs/cli-helpers";
|
|
2
|
+
const command = "neon";
|
|
3
|
+
const description = "Provision a Neon Postgres database and configure your project";
|
|
4
|
+
function builder(yargs) {
|
|
5
|
+
return yargs.option("force", {
|
|
6
|
+
alias: "f",
|
|
7
|
+
default: false,
|
|
8
|
+
description: "Overwrite existing DATABASE_URL in .env",
|
|
9
|
+
type: "boolean"
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
async function handler({ force }) {
|
|
13
|
+
recordTelemetryAttributes({
|
|
14
|
+
command: "setup neon",
|
|
15
|
+
force
|
|
16
|
+
});
|
|
17
|
+
const { handler: handler2 } = await import("./neonHandler.js");
|
|
18
|
+
return handler2({ force });
|
|
19
|
+
}
|
|
20
|
+
export {
|
|
21
|
+
builder,
|
|
22
|
+
command,
|
|
23
|
+
description,
|
|
24
|
+
handler
|
|
25
|
+
};
|
|
@@ -0,0 +1,385 @@
|
|
|
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 { addWorkspacePackages } from "@cedarjs/cli-helpers/packageManager/packages";
|
|
7
|
+
import { errorTelemetry } from "@cedarjs/telemetry";
|
|
8
|
+
const cedarPaths = getPaths();
|
|
9
|
+
async function handler({ force }) {
|
|
10
|
+
const schemaPath = path.join(cedarPaths.api.base, "db", "schema.prisma");
|
|
11
|
+
const dbTsPath = path.join(cedarPaths.api.src, "lib", "db.ts");
|
|
12
|
+
const prismaConfigPathCjs = path.join(
|
|
13
|
+
cedarPaths.api.base,
|
|
14
|
+
"prisma.config.cjs"
|
|
15
|
+
);
|
|
16
|
+
const prismaConfigPathMts = path.join(
|
|
17
|
+
cedarPaths.api.base,
|
|
18
|
+
"prisma.config.mts"
|
|
19
|
+
);
|
|
20
|
+
const envPath = path.join(cedarPaths.base, ".env");
|
|
21
|
+
const rootPkgPath = path.join(cedarPaths.base, "package.json");
|
|
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;
|
|
29
|
+
if (fs.existsSync(envPath)) {
|
|
30
|
+
hasDirectDatabaseUrl = /^DATABASE_URL=/m.test(
|
|
31
|
+
fs.readFileSync(envPath, "utf-8")
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
const notes = [];
|
|
35
|
+
const tasks = new Listr(
|
|
36
|
+
[
|
|
37
|
+
{
|
|
38
|
+
title: "Checking current database configuration",
|
|
39
|
+
task: (ctx) => {
|
|
40
|
+
const schemaContent = fs.readFileSync(schemaPath, "utf-8");
|
|
41
|
+
ctx.schemaContent = schemaContent;
|
|
42
|
+
ctx.isSqlite = schemaContent.includes('provider = "sqlite"');
|
|
43
|
+
ctx.isPostgres = schemaContent.includes('provider = "postgresql"');
|
|
44
|
+
if (fs.existsSync(dbTsPath)) {
|
|
45
|
+
ctx.dbTsContent = fs.readFileSync(dbTsPath, "utf-8");
|
|
46
|
+
ctx.isNeon = ctx.dbTsContent.includes("PrismaPg");
|
|
47
|
+
} else {
|
|
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"'
|
|
141
|
+
);
|
|
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
|
+
const configPath = fs.existsSync(prismaConfigPathCjs) ? prismaConfigPathCjs : prismaConfigPathMts;
|
|
185
|
+
const configContent = fs.readFileSync(configPath, "utf-8");
|
|
186
|
+
const updated = configContent.replace(
|
|
187
|
+
/env\(["']DATABASE_URL["']\)/,
|
|
188
|
+
"env('DIRECT_DATABASE_URL')"
|
|
189
|
+
);
|
|
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
|
+
}
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
title: "Provisioning Neon database",
|
|
204
|
+
skip: (ctx) => {
|
|
205
|
+
if (ctx.unsupportedProvider) {
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
if (hasDirectDatabaseUrl && !force) {
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
return false;
|
|
212
|
+
},
|
|
213
|
+
task: async (ctx) => {
|
|
214
|
+
const res = await fetch("https://neon.new/api/v1/database", {
|
|
215
|
+
method: "POST",
|
|
216
|
+
headers: { "Content-Type": "application/json" },
|
|
217
|
+
body: JSON.stringify({ ref: "cedarjs" })
|
|
218
|
+
});
|
|
219
|
+
if (!res.ok) {
|
|
220
|
+
throw new Error(`Neon API returned ${res.status} ${res.statusText}`);
|
|
221
|
+
}
|
|
222
|
+
const data = await res.json();
|
|
223
|
+
if (!data.connection_string || !data.expires_at || !data.claim_url) {
|
|
224
|
+
throw new Error(
|
|
225
|
+
"Neon API returned an invalid response\n\n" + JSON.stringify(data, null, 2)
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
ctx.databaseUrl = data.connection_string;
|
|
229
|
+
ctx.databaseUrlDirect = data.connection_string.replace(
|
|
230
|
+
"-pooler.",
|
|
231
|
+
"."
|
|
232
|
+
);
|
|
233
|
+
if (ctx.databaseUrlDirect === ctx.databaseUrl) {
|
|
234
|
+
throw new Error(
|
|
235
|
+
'Could not derive a direct (non-pooler) connection string from the Neon response. Expected the connection string to contain "-pooler." in the hostname.'
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
ctx.neonClaimUrl = data.claim_url;
|
|
239
|
+
ctx.neonClaimExpiry = new Date(data.expires_at).toUTCString();
|
|
240
|
+
}
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
title: "Writing database connection to .env",
|
|
244
|
+
skip: (ctx) => {
|
|
245
|
+
if (ctx.unsupportedProvider) {
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
if (hasDirectDatabaseUrl && !force) {
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
251
|
+
if (!ctx.databaseUrl) {
|
|
252
|
+
return "No database URL to write (Neon provisioning skipped)";
|
|
253
|
+
}
|
|
254
|
+
return false;
|
|
255
|
+
},
|
|
256
|
+
task: (ctx) => {
|
|
257
|
+
let envContent = "";
|
|
258
|
+
if (fs.existsSync(envPath)) {
|
|
259
|
+
envContent = fs.readFileSync(envPath, "utf-8");
|
|
260
|
+
if (force) {
|
|
261
|
+
const lines = envContent.split("\n");
|
|
262
|
+
const filtered = lines.filter(
|
|
263
|
+
(line) => !line.startsWith("DATABASE_URL=") && !line.startsWith("DIRECT_DATABASE_URL=")
|
|
264
|
+
);
|
|
265
|
+
envContent = filtered.join("\n").trimEnd();
|
|
266
|
+
}
|
|
267
|
+
if (envContent && !envContent.endsWith("\n")) {
|
|
268
|
+
envContent += "\n";
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
envContent += `DATABASE_URL=${ctx.databaseUrl}
|
|
272
|
+
`;
|
|
273
|
+
envContent += `DIRECT_DATABASE_URL=${ctx.databaseUrlDirect}
|
|
274
|
+
`;
|
|
275
|
+
fs.writeFileSync(envPath, envContent);
|
|
276
|
+
}
|
|
277
|
+
},
|
|
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
|
+
{
|
|
314
|
+
title: "One more thing...",
|
|
315
|
+
task: (ctx, task) => {
|
|
316
|
+
if (ctx.unsupportedProvider) {
|
|
317
|
+
task.output = "Skipped \u2014 unsupported database provider";
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
if (ctx.skipWithNote) {
|
|
321
|
+
task.output = "Skipped \u2014 DATABASE_URL already configured";
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
const claimMsg = [
|
|
325
|
+
colors.important(
|
|
326
|
+
"Your Neon database has been created and is ready to use!"
|
|
327
|
+
),
|
|
328
|
+
"",
|
|
329
|
+
`Claim URL: ${colors.underline(ctx.neonClaimUrl || "N/A")}`,
|
|
330
|
+
`Expires: ${ctx.neonClaimExpiry || "N/A"}`,
|
|
331
|
+
"",
|
|
332
|
+
"Claim your database to keep it beyond the expiration date."
|
|
333
|
+
];
|
|
334
|
+
notes.push(...claimMsg);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
],
|
|
338
|
+
{
|
|
339
|
+
exitOnError: false
|
|
340
|
+
}
|
|
341
|
+
);
|
|
342
|
+
try {
|
|
343
|
+
await tasks.run();
|
|
344
|
+
if (notes.length > 0) {
|
|
345
|
+
console.log();
|
|
346
|
+
console.log(notes.join("\n"));
|
|
347
|
+
}
|
|
348
|
+
} catch (e) {
|
|
349
|
+
if (isErrorWithMessage(e)) {
|
|
350
|
+
errorTelemetry(process.argv, e.message);
|
|
351
|
+
console.error(colors.error(e.message));
|
|
352
|
+
}
|
|
353
|
+
if (isErrorWithExitCode(e)) {
|
|
354
|
+
process.exit(e.exitCode);
|
|
355
|
+
}
|
|
356
|
+
process.exit(1);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
function isErrorWithMessage(e) {
|
|
360
|
+
return !!e && typeof e === "object" && "message" in e;
|
|
361
|
+
}
|
|
362
|
+
function isErrorWithExitCode(e) {
|
|
363
|
+
return !!e && typeof e === "object" && "exitCode" in e && typeof e.exitCode === "number";
|
|
364
|
+
}
|
|
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
|
+
export {
|
|
384
|
+
handler
|
|
385
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { PrismaPg } from '@prisma/adapter-pg'
|
|
2
|
+
import { PrismaClient } from 'api/db/generated/prisma/client.mts'
|
|
3
|
+
|
|
4
|
+
import { emitLogLevels, handlePrismaLogging } from '@cedarjs/api/logger'
|
|
5
|
+
|
|
6
|
+
import { logger } from './logger.js'
|
|
7
|
+
|
|
8
|
+
export * from 'api/db/generated/prisma/client.mts'
|
|
9
|
+
|
|
10
|
+
if (!process.env.DATABASE_URL) {
|
|
11
|
+
throw new Error('DATABASE_URL environment variable is not set')
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL })
|
|
15
|
+
const prismaClient = new PrismaClient({
|
|
16
|
+
log: emitLogLevels(['info', 'warn', 'error']),
|
|
17
|
+
adapter,
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
handlePrismaLogging({
|
|
21
|
+
db: prismaClient,
|
|
22
|
+
logger,
|
|
23
|
+
logLevels: ['info', 'warn', 'error'],
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Global Prisma client extensions should be added here, as $extend
|
|
28
|
+
* returns a new instance.
|
|
29
|
+
* export const db = prismaClient.$extend(...)
|
|
30
|
+
* Add any .$on hooks before using $extend
|
|
31
|
+
*/
|
|
32
|
+
export const db = prismaClient
|
package/dist/commands/setup.js
CHANGED
|
@@ -11,6 +11,7 @@ import * as setupJobs from "./setup/jobs/jobs.js";
|
|
|
11
11
|
import * as setupMailer from "./setup/mailer/mailer.js";
|
|
12
12
|
import * as setupMiddleware from "./setup/middleware/middleware.js";
|
|
13
13
|
import * as setupMonitoring from "./setup/monitoring/monitoring.js";
|
|
14
|
+
import * as setupNeon from "./setup/neon/neon.js";
|
|
14
15
|
import * as setupPackage from "./setup/package/package.js";
|
|
15
16
|
import * as setupRealtime from "./setup/realtime/realtime.js";
|
|
16
17
|
import * as setupServerFile from "./setup/server-file/serverFile.js";
|
|
@@ -20,7 +21,7 @@ import * as setupUploads from "./setup/uploads/uploads.js";
|
|
|
20
21
|
import * as setupVite from "./setup/vite/vite.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(setupPackage).command(setupRealtime).command(setupServerFile).command(setupTsconfig).command(setupUi).command(setupUploads).command(setupVite).demandCommand().middleware(detectCedarVersion).epilogue(
|
|
24
|
+
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).command(setupVite).demandCommand().middleware(detectCedarVersion).epilogue(
|
|
24
25
|
`Also see the ${terminalLink(
|
|
25
26
|
"CedarJS CLI Reference",
|
|
26
27
|
"https://cedarjs.com/docs/cli-commands#setup"
|
|
@@ -440,7 +440,7 @@ async function downloadYarnPatches(ctx, { dryRun, verbose }) {
|
|
|
440
440
|
}
|
|
441
441
|
async function refreshPrismaClient(task, { verbose }) {
|
|
442
442
|
try {
|
|
443
|
-
await generatePrismaClient({ verbose, force:
|
|
443
|
+
await generatePrismaClient({ verbose, force: true });
|
|
444
444
|
} catch (e) {
|
|
445
445
|
const message = e instanceof Error ? e.message : String(e);
|
|
446
446
|
task.skip("Refreshing the Prisma client caused an Error.");
|
package/dist/lib/exec.js
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
1
2
|
import path from "node:path";
|
|
2
|
-
import { createServer,
|
|
3
|
-
import { ViteNodeRunner } from "vite-node/client";
|
|
4
|
-
import { ViteNodeServer } from "vite-node/server";
|
|
5
|
-
import { installSourcemapsSupport } from "vite-node/source-map";
|
|
3
|
+
import { createServer, isRunnableDevEnvironment } from "vite";
|
|
6
4
|
import { getPaths, importStatementPath } from "@cedarjs/project-config";
|
|
7
5
|
import {
|
|
8
6
|
cedarCellTransform,
|
|
@@ -10,8 +8,21 @@ import {
|
|
|
10
8
|
cedarjsJobPathInjectorPlugin,
|
|
11
9
|
cedarSwapApolloProvider,
|
|
12
10
|
cedarImportDirPlugin,
|
|
13
|
-
cedarAutoImportsPlugin
|
|
11
|
+
cedarAutoImportsPlugin,
|
|
12
|
+
cedarCjsCompatPlugin
|
|
14
13
|
} from "@cedarjs/vite";
|
|
14
|
+
function resolveExtension(id) {
|
|
15
|
+
if (existsSync(id)) {
|
|
16
|
+
return id;
|
|
17
|
+
}
|
|
18
|
+
const withoutExt = /\.jsx?$/.test(id) ? id.replace(/\.jsx?$/, "") : id;
|
|
19
|
+
for (const ext of [".ts", ".tsx", ".js", ".jsx"]) {
|
|
20
|
+
if (existsSync(withoutExt + ext)) {
|
|
21
|
+
return withoutExt + ext;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return id;
|
|
25
|
+
}
|
|
15
26
|
async function runScriptFunction({
|
|
16
27
|
path: scriptPath,
|
|
17
28
|
functionName,
|
|
@@ -22,10 +33,16 @@ async function runScriptFunction({
|
|
|
22
33
|
const server = await createServer({
|
|
23
34
|
mode: "production",
|
|
24
35
|
optimizeDeps: {
|
|
25
|
-
// This is recommended in the vite-node readme
|
|
26
36
|
noDiscovery: true,
|
|
27
37
|
include: void 0
|
|
28
38
|
},
|
|
39
|
+
server: {
|
|
40
|
+
hmr: false,
|
|
41
|
+
watch: null
|
|
42
|
+
},
|
|
43
|
+
environments: {
|
|
44
|
+
nodeRunnerEnv: {}
|
|
45
|
+
},
|
|
29
46
|
resolve: {
|
|
30
47
|
alias: [
|
|
31
48
|
{
|
|
@@ -52,18 +69,12 @@ async function runScriptFunction({
|
|
|
52
69
|
const webImportBase = importStatementPath(getPaths().web.base);
|
|
53
70
|
if (importer.startsWith(apiImportBase)) {
|
|
54
71
|
const apiImportSrc = importStatementPath(getPaths().api.src);
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
resolvedId = resolvedId.replace(/\.jsx?$/, "");
|
|
58
|
-
}
|
|
59
|
-
return { id: resolvedId };
|
|
72
|
+
const resolvedId = id.replace("src", apiImportSrc);
|
|
73
|
+
return { id: resolveExtension(resolvedId) };
|
|
60
74
|
} else if (importer.startsWith(webImportBase)) {
|
|
61
75
|
const webImportSrc = importStatementPath(getPaths().web.src);
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
resolvedId = resolvedId.replace(/\.jsx?$/, "");
|
|
65
|
-
}
|
|
66
|
-
return { id: resolvedId };
|
|
76
|
+
const resolvedId = id.replace("src", webImportSrc);
|
|
77
|
+
return { id: resolveExtension(resolvedId) };
|
|
67
78
|
}
|
|
68
79
|
return null;
|
|
69
80
|
}
|
|
@@ -71,6 +82,7 @@ async function runScriptFunction({
|
|
|
71
82
|
]
|
|
72
83
|
},
|
|
73
84
|
plugins: [
|
|
85
|
+
cedarCjsCompatPlugin(),
|
|
74
86
|
cedarjsResolveCedarStyleImportsPlugin(),
|
|
75
87
|
cedarCellTransform(),
|
|
76
88
|
cedarjsJobPathInjectorPlugin(),
|
|
@@ -79,41 +91,21 @@ async function runScriptFunction({
|
|
|
79
91
|
cedarAutoImportsPlugin()
|
|
80
92
|
]
|
|
81
93
|
});
|
|
82
|
-
|
|
83
|
-
|
|
94
|
+
const env = server.environments.nodeRunnerEnv;
|
|
95
|
+
if (!env || !isRunnableDevEnvironment(env)) {
|
|
96
|
+
await server.close();
|
|
97
|
+
throw new Error("Vite environment is not runnable.");
|
|
84
98
|
}
|
|
85
|
-
const node = new ViteNodeServer(server, {
|
|
86
|
-
transformMode: {
|
|
87
|
-
ssr: [/.*/],
|
|
88
|
-
web: [/\/web\//]
|
|
89
|
-
},
|
|
90
|
-
deps: {
|
|
91
|
-
fallbackCJS: true
|
|
92
|
-
}
|
|
93
|
-
});
|
|
94
|
-
installSourcemapsSupport({
|
|
95
|
-
getSourceMap: (source) => node.getSourceMap(source)
|
|
96
|
-
});
|
|
97
|
-
const runner = new ViteNodeRunner({
|
|
98
|
-
root: server.config.root,
|
|
99
|
-
base: server.config.base,
|
|
100
|
-
fetchModule(id) {
|
|
101
|
-
return node.fetchModule(id);
|
|
102
|
-
},
|
|
103
|
-
resolveId(id, importer) {
|
|
104
|
-
return node.resolveId(id, importer);
|
|
105
|
-
}
|
|
106
|
-
});
|
|
107
99
|
let returnValue;
|
|
108
100
|
let scriptError = null;
|
|
109
101
|
try {
|
|
110
|
-
const script = await runner.
|
|
102
|
+
const script = await env.runner.import(scriptPath);
|
|
111
103
|
returnValue = await script[functionName](args);
|
|
112
104
|
} catch (error) {
|
|
113
105
|
scriptError = error;
|
|
114
106
|
}
|
|
115
107
|
try {
|
|
116
|
-
const { db } = await runner.
|
|
108
|
+
const { db } = await env.runner.import(path.join(getPaths().api.lib, "db"));
|
|
117
109
|
db.$disconnect();
|
|
118
110
|
} catch (e) {
|
|
119
111
|
}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import fs from "node:fs";
|
|
2
3
|
import { createRequire } from "node:module";
|
|
3
|
-
import
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { getConfig, getPrismaSchemas } from "@cedarjs/project-config";
|
|
4
6
|
import { runCommandTask, getPaths } from "./index.js";
|
|
5
7
|
const generatePrismaCommand = async () => {
|
|
8
|
+
const config = getConfig();
|
|
6
9
|
const createdRequire = createRequire(import.meta.url);
|
|
7
10
|
const prismaIndexPath = createdRequire.resolve("prisma/build/index.js");
|
|
8
11
|
return {
|
|
@@ -10,19 +13,52 @@ const generatePrismaCommand = async () => {
|
|
|
10
13
|
args: [
|
|
11
14
|
prismaIndexPath,
|
|
12
15
|
"generate",
|
|
13
|
-
`--config=${getPaths().api.prismaConfig}
|
|
16
|
+
`--config=${getPaths().api.prismaConfig}`,
|
|
17
|
+
...config.api.prismaGenerateArgs
|
|
14
18
|
]
|
|
15
19
|
};
|
|
16
20
|
};
|
|
17
|
-
|
|
21
|
+
async function computePrismaSchemaHash() {
|
|
22
|
+
try {
|
|
23
|
+
const hash = createHash("sha256");
|
|
24
|
+
const configPath = getPaths().api.prismaConfig;
|
|
25
|
+
if (fs.existsSync(configPath)) {
|
|
26
|
+
hash.update(fs.readFileSync(configPath));
|
|
27
|
+
}
|
|
28
|
+
const { schemas } = await getPrismaSchemas();
|
|
29
|
+
for (const schema of schemas) {
|
|
30
|
+
hash.update(schema[1]);
|
|
31
|
+
}
|
|
32
|
+
return hash.digest("hex");
|
|
33
|
+
} catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function getHashFilePath() {
|
|
38
|
+
const generatedBase = getPaths().generated.base;
|
|
39
|
+
return path.join(generatedBase, "prisma-schema-hash");
|
|
40
|
+
}
|
|
41
|
+
function getStoredSchemaHash() {
|
|
42
|
+
const hashFile = getHashFilePath();
|
|
43
|
+
if (fs.existsSync(hashFile)) {
|
|
44
|
+
return fs.readFileSync(hashFile, "utf-8").trim();
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
function storeSchemaHash(hash) {
|
|
49
|
+
const hashFile = getHashFilePath();
|
|
50
|
+
fs.mkdirSync(path.dirname(hashFile), { recursive: true });
|
|
51
|
+
fs.writeFileSync(hashFile, hash);
|
|
52
|
+
}
|
|
53
|
+
async function generatePrismaClient({
|
|
18
54
|
verbose = true,
|
|
19
|
-
force =
|
|
55
|
+
force = false,
|
|
20
56
|
silent = false
|
|
21
|
-
} = {})
|
|
57
|
+
} = {}) {
|
|
58
|
+
const hash = await computePrismaSchemaHash();
|
|
22
59
|
if (!force) {
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
if (!prismaClientFile.includes("@prisma/client did not initialize yet.") && prismaClientFile.includes("exports.Prisma.")) {
|
|
60
|
+
const storedHash = hash ? getStoredSchemaHash() : null;
|
|
61
|
+
if (hash !== null && hash === storedHash) {
|
|
26
62
|
return;
|
|
27
63
|
}
|
|
28
64
|
}
|
|
@@ -38,7 +74,10 @@ const generatePrismaClient = async ({
|
|
|
38
74
|
silent
|
|
39
75
|
}
|
|
40
76
|
);
|
|
41
|
-
|
|
77
|
+
if (hash !== null) {
|
|
78
|
+
storeSchemaHash(hash);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
42
81
|
export {
|
|
43
82
|
generatePrismaClient,
|
|
44
83
|
generatePrismaCommand
|
package/dist/lib/locking.js
CHANGED
|
@@ -32,11 +32,15 @@ function unsetLock(identifier) {
|
|
|
32
32
|
}
|
|
33
33
|
function isLockSet(identifier) {
|
|
34
34
|
const lockfilePath = path.join(getPaths().generated.base, "locks", identifier);
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
let createdAt;
|
|
36
|
+
try {
|
|
37
|
+
createdAt = fs.statSync(lockfilePath).birthtimeMs;
|
|
38
|
+
} catch (error) {
|
|
39
|
+
if (isErrorWithCode(error, "ENOENT")) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
throw error;
|
|
38
43
|
}
|
|
39
|
-
const createdAt = fs.statSync(lockfilePath).birthtimeMs;
|
|
40
44
|
if (Date.now() - createdAt > 36e5) {
|
|
41
45
|
unsetLock(identifier);
|
|
42
46
|
return false;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cedarjs/cli",
|
|
3
|
-
"version": "4.1.1-next.
|
|
3
|
+
"version": "4.1.1-next.64",
|
|
4
4
|
"description": "The CedarJS Command Line",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -31,18 +31,19 @@
|
|
|
31
31
|
"test:watch": "vitest watch"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@babel/parser": "7.29.
|
|
34
|
+
"@babel/parser": "7.29.3",
|
|
35
35
|
"@babel/preset-typescript": "7.28.5",
|
|
36
|
-
"@cedarjs/api-server": "4.1.
|
|
37
|
-
"@cedarjs/cli-helpers": "4.1.
|
|
38
|
-
"@cedarjs/fastify-web": "4.1.
|
|
39
|
-
"@cedarjs/internal": "4.1.
|
|
40
|
-
"@cedarjs/prerender": "4.1.
|
|
41
|
-
"@cedarjs/project-config": "4.1.
|
|
42
|
-
"@cedarjs/structure": "4.1.
|
|
43
|
-
"@cedarjs/telemetry": "4.1.
|
|
44
|
-
"@cedarjs/utils": "4.1.
|
|
45
|
-
"@cedarjs/
|
|
36
|
+
"@cedarjs/api-server": "4.1.0",
|
|
37
|
+
"@cedarjs/cli-helpers": "4.1.0",
|
|
38
|
+
"@cedarjs/fastify-web": "4.1.0",
|
|
39
|
+
"@cedarjs/internal": "4.1.0",
|
|
40
|
+
"@cedarjs/prerender": "4.1.0",
|
|
41
|
+
"@cedarjs/project-config": "4.1.0",
|
|
42
|
+
"@cedarjs/structure": "4.1.0",
|
|
43
|
+
"@cedarjs/telemetry": "4.1.0",
|
|
44
|
+
"@cedarjs/utils": "4.1.0",
|
|
45
|
+
"@cedarjs/vite": "4.1.0",
|
|
46
|
+
"@cedarjs/web-server": "4.1.0",
|
|
46
47
|
"@listr2/prompt-adapter-enquirer": "4.2.1",
|
|
47
48
|
"@opentelemetry/api": "1.9.0",
|
|
48
49
|
"@opentelemetry/core": "1.30.1",
|
|
@@ -88,7 +89,6 @@
|
|
|
88
89
|
"title-case": "3.0.3",
|
|
89
90
|
"unionfs": "4.6.0",
|
|
90
91
|
"uuid": "11.1.0",
|
|
91
|
-
"vite-node": "3.2.4",
|
|
92
92
|
"yargs": "17.7.2"
|
|
93
93
|
},
|
|
94
94
|
"devDependencies": {
|
|
@@ -108,5 +108,5 @@
|
|
|
108
108
|
"publishConfig": {
|
|
109
109
|
"access": "public"
|
|
110
110
|
},
|
|
111
|
-
"gitHead": "
|
|
111
|
+
"gitHead": "3905ed045508b861b495f8d5630d76c7a157d8f1"
|
|
112
112
|
}
|
package/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2025 Cedar
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|