@cedarjs/cli 6.0.0-rc.260 → 6.0.0-rc.340

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.
@@ -48,7 +48,8 @@ const handler = async ({
48
48
  apiDebugPort,
49
49
  debugBrk,
50
50
  ud = false,
51
- nodeArgs = ""
51
+ nodeArgs = "",
52
+ jobs: jobsOption
52
53
  }) => {
53
54
  recordTelemetryAttributes({
54
55
  command: "dev",
@@ -206,6 +207,11 @@ const handler = async ({
206
207
  ` --apiPort ${apiAvailablePort}`,
207
208
  getApiDebugFlag(apiDebugPort, apiAvailablePort),
208
209
  debugBrk ? "--debug-brk" : "",
210
+ // `cedar-unified-dev` starts its own in-process jobs worker pool (see
211
+ // `jobsDevMiddleware.ts`) rather than relying on the nodemon-wrapped
212
+ // worker pushed below, so `--no-jobs` has to be forwarded explicitly -
213
+ // it isn't part of `forward` unless the user passed it after `--`.
214
+ jobsOption === false ? "--no-jobs" : "",
209
215
  forward
210
216
  ].join(" ").replace(/\s+/g, " ").trim();
211
217
  };
@@ -280,6 +286,37 @@ const handler = async ({
280
286
  prefixColor: "green"
281
287
  });
282
288
  }
289
+ const jobsConfigured = jobsOption !== false && workspace.includes("api") && !!cedarPaths.api.jobsConfig && // `getPaths()` resolves `jobsConfig` once and caches it in-process, so
290
+ // if `api/src/lib/jobs.ts` is deleted mid-session the cached path would
291
+ // otherwise still look "configured". Guard against starting a worker
292
+ // that can't load a jobs config that no longer exists.
293
+ fs.existsSync(cedarPaths.api.jobsConfig) && fs.existsSync(cedarPaths.api.jobs) && // Job files always live in `api/src/jobs/<ComponentName>Job/`
294
+ // subdirectories (see `generate/job/jobHandler.ts`), so entries here are
295
+ // directories, not files — only the `.keep` placeholder (and any stray
296
+ // dotfiles, e.g. `.DS_Store`) should be excluded.
297
+ fs.readdirSync(cedarPaths.api.jobs).some((entry) => !entry.startsWith("."));
298
+ if (jobsConfigured && unifiedDevCommand) {
299
+ } else if (jobsConfigured) {
300
+ jobs.push({
301
+ name: "jobs",
302
+ // `cedar-jobs work` loads its config and job files from `api/dist`
303
+ // (compiled output), not `api/src`. That dist output is only written
304
+ // once the `api` watcher's initial build finishes, which happens
305
+ // asynchronously — so on a clean `cedar dev` start there's a window
306
+ // where `api/dist` doesn't exist yet and the worker would exit
307
+ // immediately. Wrapping it in nodemon (same tool the `api` job above
308
+ // uses) means it retries as soon as `api/dist` changes, instead of
309
+ // staying dead for the rest of the session. This also means the
310
+ // worker restarts automatically whenever job code is rebuilt, since
311
+ // Node's ESM cache would otherwise keep serving stale job code.
312
+ command: formatRunBinCommand("nodemon", [
313
+ "--quiet",
314
+ `--watch "${cedarPaths.api.dist}"`,
315
+ `--exec "${formatRunBinCommand("cedar-jobs", ["work"])}"`
316
+ ]),
317
+ prefixColor: "magenta"
318
+ });
319
+ }
283
320
  const packageWorkspaces = workspace.filter(
284
321
  (w) => w !== "api" && w !== "web" && w !== "gen"
285
322
  );
@@ -31,6 +31,9 @@ const builder = (yargs) => {
31
31
  }).option("nodeArgs", {
32
32
  type: "string",
33
33
  description: 'CLI args to pass to the node process running the web dev server, for example: `--node-args="--inspect --max-old-space-size=8192"`.'
34
+ }).option("jobs", {
35
+ type: "boolean",
36
+ description: "Start the background jobs worker alongside the other dev servers when jobs are configured (`cedar setup jobs` + at least one `cedar g job`). Pass `--no-jobs` to opt out and run `cedar jobs work` yourself instead."
34
37
  }).middleware(() => {
35
38
  const check = checkNodeVersion();
36
39
  if (check.ok) {
@@ -3,6 +3,7 @@ import path from "node:path";
3
3
  import { context } from "@opentelemetry/api";
4
4
  import { suppressTracing } from "@opentelemetry/core";
5
5
  import { Listr } from "listr2";
6
+ import { Parser } from "yargs/helpers";
6
7
  import { recordTelemetryAttributes, colors as c } from "@cedarjs/cli-helpers";
7
8
  import { findScripts } from "@cedarjs/internal/dist/files";
8
9
  import { runScriptFunction } from "../lib/exec.js";
@@ -50,6 +51,17 @@ const handler = async (args) => {
50
51
  delete scriptArgs.l;
51
52
  delete scriptArgs.s;
52
53
  delete scriptArgs.silent;
54
+ if (Array.isArray(scriptArgs._)) {
55
+ const dashBlockIndex = scriptArgs._.findIndex(
56
+ (arg) => typeof arg === "string" && arg.startsWith("-")
57
+ );
58
+ if (dashBlockIndex !== -1) {
59
+ const unparsedTail = scriptArgs._.splice(dashBlockIndex);
60
+ const { _: reparsedPositionals, ...reparsedFlags } = Parser(unparsedTail);
61
+ scriptArgs._.push(...reparsedPositionals);
62
+ Object.assign(scriptArgs, reparsedFlags);
63
+ }
64
+ }
53
65
  const scriptPath = resolveScriptPath(name);
54
66
  if (!scriptPath) {
55
67
  console.error(
@@ -8,6 +8,7 @@ const files = async ({
8
8
  fileName,
9
9
  typescript,
10
10
  tests: generateTests = true,
11
+ esm = false,
11
12
  ...rest
12
13
  }) => {
13
14
  const extension = typescript ? ".ts" : ".js";
@@ -58,6 +59,17 @@ const files = async ({
58
59
  outputPath: path.join(folderName, "src", `${fileName}.test${extension}`)
59
60
  });
60
61
  outputFiles.push(testFile);
62
+ if (esm) {
63
+ const vitestConfigFile = await templateForFile({
64
+ name,
65
+ side: "packages",
66
+ generator: "package",
67
+ templatePath: "vitest.config.ts.template",
68
+ templateVars: { folderName, ...rest },
69
+ outputPath: path.join(folderName, `vitest.config${extension}`)
70
+ });
71
+ outputFiles.push(vitestConfigFile);
72
+ }
61
73
  }
62
74
  return outputFiles.reduce(async (accP, [outputPath, content]) => {
63
75
  const acc = await accP;
@@ -11,7 +11,7 @@ import { workspacePackageSpecifier } from "@cedarjs/cli-helpers/packageManager";
11
11
  import { runScript, runBinSync } from "@cedarjs/cli-helpers/packageManager/exec";
12
12
  import { installPackages } from "@cedarjs/cli-helpers/packageManager/packages";
13
13
  import { addWorkspaceDir } from "@cedarjs/cli-helpers/packageManager/workspaces";
14
- import { getConfig } from "@cedarjs/project-config";
14
+ import { getConfig, projectIsEsm } from "@cedarjs/project-config";
15
15
  import { getPackageManager } from "@cedarjs/project-config/packageManager";
16
16
  import { errorTelemetry } from "@cedarjs/telemetry";
17
17
  import { getPaths, writeFilesTask } from "../../../lib/index.js";
@@ -32,8 +32,10 @@ async function updateTsconfig(task) {
32
32
  path: path.join(getPaths().api.base, "tsconfig.json"),
33
33
  expectedModule: "node20",
34
34
  // While Cedar doesn't officially endorse NodeNext, it will still work
35
- // here, so we'll keep it
36
- acceptable: ["node20", "nodenext"]
35
+ // here, so we'll keep it. ESNext is also acceptable: it's the current
36
+ // create-cedar-app default, paired with `moduleResolution: bundler` so
37
+ // extensionless scaffold/generator imports resolve correctly.
38
+ acceptable: ["node20", "nodenext", "esnext"]
37
39
  },
38
40
  {
39
41
  name: "web",
@@ -338,7 +340,12 @@ const handler = async ({
338
340
  {
339
341
  title: "Generating package files...",
340
342
  task: async (ctx) => {
341
- packageFiles = await files({ ...ctx.nameVariants, ...rest });
343
+ packageFiles = await files({
344
+ ...ctx.nameVariants,
345
+ // Only Vitest projects get a config generated for them
346
+ esm: projectIsEsm(),
347
+ ...rest
348
+ });
342
349
  return writeFilesTask(packageFiles, { overwriteExisting: force });
343
350
  }
344
351
  },
@@ -1,7 +1,7 @@
1
1
  import { ${camelName} } from './index.js'
2
2
 
3
3
  describe('${camelName}', () => {
4
- it('should not throw any errors', async () => {
5
- expect(${camelName}()).not.toThrow()
4
+ it('should not throw any errors', () => {
5
+ expect(() => ${camelName}()).not.toThrow()
6
6
  })
7
7
  })
@@ -0,0 +1,9 @@
1
+ import { defineConfig } from 'vitest/config'
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ name: '${folderName}',
6
+ // Enables global test APIs like describe, it, expect
7
+ globals: true,
8
+ },
9
+ })
@@ -6,6 +6,7 @@ import humanize from "humanize-string";
6
6
  import { Listr } from "listr2";
7
7
  import pascalcase from "pascalcase";
8
8
  import { recordTelemetryAttributes, colors as c } from "@cedarjs/cli-helpers";
9
+ import { formatCedarCommand } from "@cedarjs/cli-helpers/packageManager/display";
9
10
  import {
10
11
  addWorkspacePackages,
11
12
  removeWorkspacePackages
@@ -38,10 +39,14 @@ import {
38
39
  import { builder as sdlBuilder } from "../sdl/sdl.js";
39
40
  import {
40
41
  files as sdlFiles,
42
+ stubFiles as sdlStubFiles,
41
43
  printRedactedFieldsNote,
42
44
  redactedSensitiveFields
43
45
  } from "../sdl/sdlHandler.js";
44
- import { writeFilesWithStubsTask } from "../sdl/stubFiles.js";
46
+ import {
47
+ missingRelatedModels,
48
+ writeFilesWithStubsTask
49
+ } from "../sdl/stubFiles.js";
45
50
  import { builder as serviceBuilder } from "../service/service.js";
46
51
  import { files as serviceFiles } from "../service/serviceHandler.js";
47
52
  import { customOrDefaultTemplatePath } from "../yargsHandlerHelpers.js";
@@ -630,22 +635,26 @@ const tasks = ({
630
635
  tests,
631
636
  typescript,
632
637
  javascript: _javascript,
633
- tailwind
638
+ tailwind,
639
+ missingModels
634
640
  }) => {
635
641
  return new Listr(
636
642
  [
637
643
  {
638
644
  title: "Generating scaffold files...",
639
645
  task: async () => {
640
- const f = await files({
641
- docs,
642
- model,
643
- path: path2,
644
- tests,
645
- typescript,
646
- tailwind,
647
- force
648
- });
646
+ const f = {
647
+ ...await files({
648
+ docs,
649
+ model,
650
+ path: path2,
651
+ tests,
652
+ typescript,
653
+ tailwind,
654
+ force
655
+ }),
656
+ ...await sdlStubFiles(missingModels, model, { docs, typescript })
657
+ };
649
658
  return writeFilesWithStubsTask(f, { overwriteExisting: force });
650
659
  }
651
660
  },
@@ -711,6 +720,7 @@ const handler = async ({
711
720
  tailwind = shouldUseTailwindCSS(tailwind);
712
721
  try {
713
722
  const { name } = await verifyModelName({ name: model });
723
+ const missingModels = await missingRelatedModels(name);
714
724
  const t = tasks({
715
725
  docs,
716
726
  model: name,
@@ -718,13 +728,44 @@ const handler = async ({
718
728
  force,
719
729
  tests,
720
730
  typescript,
721
- tailwind
731
+ tailwind,
732
+ missingModels
722
733
  });
723
734
  if (rollback && !force) {
724
735
  prepareForRollback(t);
725
736
  }
726
737
  await t.run();
727
- printRedactedFieldsNote(await redactedSensitiveFields([name]));
738
+ if (missingModels.length > 0) {
739
+ console.log();
740
+ console.log(
741
+ c.info(
742
+ `${name} has relations to models that don't have SDL files of their own yet: ${missingModels.join(", ")}`
743
+ )
744
+ );
745
+ console.log(
746
+ c.info(
747
+ "Read-only SDL stubs were generated for them, since GraphQL type generation fails otherwise."
748
+ )
749
+ );
750
+ console.log(
751
+ c.info("To replace a stub, run one of the following for each model:")
752
+ );
753
+ for (const stubModel of missingModels) {
754
+ console.log(
755
+ c.info(
756
+ ` ${formatCedarCommand(["generate", "sdl", stubModel])} (SDL + service only)`
757
+ )
758
+ );
759
+ console.log(
760
+ c.info(
761
+ ` ${formatCedarCommand(["generate", "scaffold", stubModel])} (adds pages, cells, and forms too)`
762
+ )
763
+ );
764
+ }
765
+ }
766
+ printRedactedFieldsNote(
767
+ await redactedSensitiveFields([name, ...missingModels])
768
+ );
728
769
  } catch (e) {
729
770
  const message = e instanceof Error ? e.message : String(e);
730
771
  const exitCode = e instanceof Error && "exitCode" in e ? e.exitCode ?? 1 : 1;
@@ -3,12 +3,17 @@
3
3
  // To access your database uncomment the line below
4
4
  // import { db } from 'api/src/lib/db'
5
5
 
6
- interface Args {
7
- _: string[]
8
- [key: string]: unknown
6
+ interface ScriptArgs {
7
+ args: {
8
+ // positional args, e.g. `yarn cedar exec myScript foo 123` -> ['foo', 123]
9
+ // numeric-looking args are parsed as numbers, others as strings
10
+ _: Array<string | number>
11
+ // named flags, e.g. `--force` -> { force: true }
12
+ [flag: string]: unknown
13
+ }
9
14
  }
10
15
 
11
- export default async ({ args }: Args) => {
16
+ export default async ({ args }: ScriptArgs) => {
12
17
  // Your script here...
13
18
  console.log(':: Executing script with args ::')
14
19
  console.log(args)
@@ -107,7 +107,7 @@ const builder = async (yargs) => {
107
107
  const webHost = argv.webHost ?? getWebHost({ isPublicSide: true });
108
108
  const apiRootPath = argv.apiRootPath ?? "/";
109
109
  const apiTarget = `http://${apiHost.includes(":") ? `[${apiHost}]` : apiHost}:${apiPort}`;
110
- const { serveStatic } = await import("srvx/static");
110
+ const { staticMiddleware } = await import("srvx/static");
111
111
  const apiUrl = getConfig().web.apiUrl;
112
112
  const webDist = getPaths().web.dist;
113
113
  const prerenderIndexPath = path.join(webDist, "200.html");
@@ -117,7 +117,7 @@ const builder = async (yargs) => {
117
117
  // Dummy fetch handler. All requests are handled by middleware
118
118
  fetch: async () => new Response("Not Found", { status: 404 }),
119
119
  middleware: [
120
- serveStatic({ dir: webDist }),
120
+ staticMiddleware({ dir: webDist }),
121
121
  async (req, next) => {
122
122
  const url = new URL(req.url, "http://localhost");
123
123
  if (!url.pathname.startsWith(apiUrl)) {
@@ -1,12 +1,14 @@
1
1
  import path from "path";
2
2
  import prismaInternals from "@prisma/internals";
3
3
  import { Listr } from "listr2";
4
+ import prompts from "prompts";
4
5
  import { recordTelemetryAttributes, colors as c } from "@cedarjs/cli-helpers";
5
6
  import { getPaths, getPrismaSchemas } from "@cedarjs/project-config";
6
7
  import { errorTelemetry } from "@cedarjs/telemetry";
7
8
  import { writeFilesTask, printSetupNotes } from "../../../../lib/index.js";
8
9
  import { POSTGRES_YAML, RENDER_YAML, SQLITE_YAML } from "../templates/render.js";
9
10
  const { getConfig } = prismaInternals;
11
+ const SQLITE_API_PLAN = "starter";
10
12
  const getRenderYamlContent = async (database) => {
11
13
  if (database === "none") {
12
14
  return {
@@ -27,7 +29,7 @@ const getRenderYamlContent = async (database) => {
27
29
  case "sqlite":
28
30
  return {
29
31
  path: path.join(getPaths().base, "render.yaml"),
30
- content: RENDER_YAML(SQLITE_YAML)
32
+ content: RENDER_YAML(SQLITE_YAML, SQLITE_API_PLAN)
31
33
  };
32
34
  default:
33
35
  throw new Error(`
@@ -61,6 +63,26 @@ const handler = async ({
61
63
  force,
62
64
  database
63
65
  });
66
+ if (database === "sqlite") {
67
+ console.warn(
68
+ c.warning(
69
+ `Render's free plan doesn't support persistent disks, which the \`sqlite\` deploy option requires for its database file. The generated render.yaml will set the api service's plan to "${SQLITE_API_PLAN}" (a paid plan) instead of "free" so the disk can actually attach.
70
+
71
+ If you want to stay on the free plan, rerun this command with \`--database postgresql\` (a managed database, not a disk) or \`--database none\`.`
72
+ )
73
+ );
74
+ console.log();
75
+ const { confirmed } = await prompts({
76
+ type: "confirm",
77
+ name: "confirmed",
78
+ message: `Generate render.yaml with the api service on the "${SQLITE_API_PLAN}" plan?`
79
+ });
80
+ if (!confirmed) {
81
+ console.log("Aborting render setup.");
82
+ return;
83
+ }
84
+ console.log();
85
+ }
64
86
  const tasks = new Listr(
65
87
  [
66
88
  {
@@ -2,7 +2,7 @@ import path from "path";
2
2
  import { getPaths } from "../../../../lib/index.js";
3
3
  import { getUserApiUrl } from "../helpers/index.js";
4
4
  const PROJECT_NAME = path.basename(getPaths().base);
5
- const RENDER_YAML = (database) => {
5
+ const RENDER_YAML = (database, plan = "free") => {
6
6
  const apiUrl = getUserApiUrl().replace(/\/$/, "");
7
7
  return `# Quick links to the docs:
8
8
  # - Deploying Cedar: https://cedarjs.com/docs/deploy/render
@@ -41,7 +41,7 @@ services:
41
41
 
42
42
  - name: ${PROJECT_NAME}-api
43
43
  type: web
44
- plan: free
44
+ plan: ${plan}
45
45
  runtime: node
46
46
  region: oregon
47
47
  buildCommand: npm install --global corepack && yarn install && yarn cedar build api
@@ -69,9 +69,12 @@ const POSTGRES_YAML = ` - key: DATABASE_URL
69
69
 
70
70
  databases:
71
71
  - name: ${PROJECT_NAME}-db
72
+ plan: free
72
73
  region: oregon`;
73
74
  const SQLITE_YAML = ` - key: DATABASE_URL
74
75
  value: file:./data/sqlite.db
76
+ # Persistent disks aren't available on Render's free plan, which is why
77
+ # the api service above is on a paid plan when SQLite is selected.
75
78
  disk:
76
79
  name: sqlite-data
77
80
  mountPath: /opt/render/project/src/api/db/data
@@ -7,15 +7,25 @@ function builder(yargs) {
7
7
  default: false,
8
8
  description: "Overwrite existing DATABASE_URL in .env",
9
9
  type: "boolean"
10
+ }).option("migrations", {
11
+ description: "Run Prisma migrations after setup. Omit to be prompted. Use --no-migrations to skip.",
12
+ type: "boolean"
13
+ }).option("verbose", {
14
+ alias: "v",
15
+ default: false,
16
+ description: "Show full output from migration commands (stderr visible in terminal)",
17
+ type: "boolean"
10
18
  });
11
19
  }
12
- async function handler({ force }) {
20
+ async function handler({ force, migrations, verbose }) {
13
21
  recordTelemetryAttributes({
14
22
  command: "setup neon",
15
- force
23
+ force,
24
+ migrations,
25
+ verbose
16
26
  });
17
27
  const { handler: handler2 } = await import("./neonHandler.js");
18
- return handler2({ force });
28
+ return handler2({ force, migrations, verbose });
19
29
  }
20
30
  export {
21
31
  builder,
@@ -2,6 +2,7 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import execa from "execa";
4
4
  import { Listr } from "listr2";
5
+ import prompts from "prompts";
5
6
  import { colors, getPaths, installPackages } from "@cedarjs/cli-helpers";
6
7
  import { prettyPrintCedarCommand } from "@cedarjs/cli-helpers/packageManager";
7
8
  import { errorTelemetry } from "@cedarjs/telemetry";
@@ -25,7 +26,7 @@ function isPostgresConnectionString(rawValue) {
25
26
  return false;
26
27
  }
27
28
  }
28
- async function handler({ force }) {
29
+ async function handler({ force, migrations, verbose }) {
29
30
  const cedarPaths = getPaths();
30
31
  const shape = checkProjectShape(cedarPaths);
31
32
  if (!shape.ok) {
@@ -51,6 +52,29 @@ async function handler({ force }) {
51
52
  )
52
53
  );
53
54
  }
55
+ let runMigrations = migrations;
56
+ if (!skipProvisioning && runMigrations === void 0) {
57
+ if (!process.stdin.isTTY) {
58
+ const error = new Error(
59
+ "Cannot prompt for confirmation in a non-interactive terminal. Please pass --migrations or --no-migrations explicitly."
60
+ );
61
+ errorTelemetry(process.argv, error.message);
62
+ console.error(colors.error(error.message));
63
+ process.exit(1);
64
+ }
65
+ const response = await prompts({
66
+ type: "toggle",
67
+ name: "runMigrations",
68
+ message: "Run Prisma migrations now?",
69
+ initial: true,
70
+ active: "Yes",
71
+ inactive: "No"
72
+ });
73
+ if (response.runMigrations === void 0) {
74
+ process.exit(0);
75
+ }
76
+ runMigrations = response.runMigrations;
77
+ }
54
78
  const tasks = new Listr(
55
79
  [
56
80
  ...getSqliteToPostgresTasks({ dbPath: shape.dbPath }),
@@ -124,6 +148,9 @@ async function handler({ force }) {
124
148
  if (skipProvisioning) {
125
149
  return true;
126
150
  }
151
+ if (!runMigrations) {
152
+ return migrations === false ? "Skipped (--no-migrations)" : "Skipped";
153
+ }
127
154
  if (ctx.directDatabaseUrlNotSet) {
128
155
  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.`;
129
156
  }
@@ -134,7 +161,7 @@ async function handler({ force }) {
134
161
  "yarn cedar prisma migrate dev --name init-neon",
135
162
  {
136
163
  cwd: cedarPaths.base,
137
- stdio: ["inherit", "inherit", "pipe"],
164
+ stdio: verbose ? "inherit" : ["inherit", "inherit", "pipe"],
138
165
  reject: false,
139
166
  env: {
140
167
  ...process.env,
@@ -144,7 +171,8 @@ async function handler({ force }) {
144
171
  );
145
172
  if (result.exitCode !== 0) {
146
173
  throw new Error(
147
- "Prisma migration failed:\n\n" + result.stderr + `
174
+ verbose ? `Prisma migration failed. You can try running it manually:
175
+ ${prettyPrintCedarCommand(["prisma", "migrate", "dev", "--name", "init-neon"])}` : "Prisma migration failed:\n\n" + result.stderr + `
148
176
 
149
177
  You can try running it manually:
150
178
  ${prettyPrintCedarCommand(["prisma", "migrate", "dev", "--name", "init-neon"])}`
@@ -83,7 +83,7 @@ const handler = async ({ force }) => {
83
83
  if (transformResult.error) {
84
84
  if (transformResult.error === "RW_CODEMOD_ERR_OLD_FORMAT") {
85
85
  throw new Error(
86
- "It looks like your src/lib/db file is using the old format. Please update it as per the v8 upgrade guide: https://cedarjs.com/docs/upgrade-guides/v8#database-file-structure-change. And run again. \n\nYou can also manually modify your api/src/lib/db to include the prisma extension: https://cedarjs.com/docs/uploads/#attaching-the-prisma-extension"
86
+ "It looks like your src/lib/db file is using the old format. Please update it as per the v8 upgrade guide: https://cedarjs.com/docs/8.x/upgrade-guides/v8#database-file-structure-change. And run again. \n\nYou can also manually modify your api/src/lib/db to include the prisma extension: https://cedarjs.com/docs/uploads/#attaching-the-prisma-extension"
87
87
  );
88
88
  }
89
89
  throw new Error(
@@ -59,10 +59,10 @@ const handler = async ({
59
59
  vitestArgs.push("--run");
60
60
  }
61
61
  if (!others["config"]) {
62
+ sides.forEach((side) => vitestArgs.push("--project", side));
62
63
  if (!sides.length) {
63
64
  project.workspaces().forEach((side) => sides.push(side));
64
65
  }
65
- sides.forEach((side) => vitestArgs.push("--project", side));
66
66
  }
67
67
  try {
68
68
  const cacheDirDb = `file:${ensurePosixPath(
@@ -4,6 +4,7 @@ import os from "node:os";
4
4
  import path from "node:path";
5
5
  import execa from "execa";
6
6
  import semver from "semver";
7
+ const MAIN_BRANCH_PRERELEASE_TAGS = ["canary"];
7
8
  function isExecaError(e) {
8
9
  return e instanceof Error && ("stdout" in e || "stderr" in e || "exitCode" in e);
9
10
  }
@@ -33,8 +34,15 @@ async function runPreUpgradeScripts(ctx, task, { verbose, force }) {
33
34
  if (!Array.isArray(manifest) || manifest.length === 0) {
34
35
  return;
35
36
  }
37
+ const prereleaseTag = parsed?.prerelease[0];
38
+ const isMainBranchPrerelease = typeof prereleaseTag === "string" && MAIN_BRANCH_PRERELEASE_TAGS.includes(prereleaseTag);
36
39
  const checkLevels = [];
37
- if (parsed && !parsed.prerelease.length) {
40
+ if (parsed && isMainBranchPrerelease) {
41
+ checkLevels.push({
42
+ id: "tag",
43
+ candidates: [`${prereleaseTag}.ts`, `${prereleaseTag}/index.ts`]
44
+ });
45
+ } else if (parsed) {
38
46
  checkLevels.push({
39
47
  id: "exact",
40
48
  candidates: [`${version}.ts`, `${version}/index.ts`]
@@ -50,14 +58,6 @@ async function runPreUpgradeScripts(ctx, task, { verbose, force }) {
50
58
  id: "minor",
51
59
  candidates: [`${parsed.major}.x.ts`, `${parsed.major}.x/index.ts`]
52
60
  });
53
- } else if (parsed && parsed.prerelease.length > 0) {
54
- checkLevels.push({
55
- id: "tag",
56
- candidates: [
57
- `${parsed.prerelease[0]}.ts`,
58
- `${parsed.prerelease[0]}/index.ts`
59
- ]
60
- });
61
61
  }
62
62
  const scriptsToRun = [];
63
63
  for (const level of checkLevels) {
@@ -33,6 +33,7 @@ const handler = async (upgradeOptions) => {
33
33
  });
34
34
  let preUpgradeMessage = "";
35
35
  let preUpgradeError = "";
36
+ const notBlockedByPreUpgradeChecks = (ctx) => force || !ctx.preUpgradeError;
36
37
  const tasks = new Listr(
37
38
  [
38
39
  {
@@ -87,39 +88,46 @@ const handler = async (upgradeOptions) => {
87
88
  {
88
89
  title: "Updating your CedarJS version",
89
90
  task: (ctx) => updateCedarJSDepsForAllSides(ctx, { dryRun, verbose }),
90
- enabled: (ctx) => !!ctx.versionToUpgradeTo && !ctx.preUpgradeError
91
+ enabled: (ctx) => !!ctx.versionToUpgradeTo && notBlockedByPreUpgradeChecks(ctx)
91
92
  },
92
93
  {
93
94
  title: "Updating other packages in your package.json(s)",
94
95
  task: (ctx) => updatePackageVersionsFromTemplate(ctx, { dryRun, verbose }),
95
- enabled: (ctx) => String(ctx.versionToUpgradeTo).includes("canary") && !ctx.preUpgradeError
96
+ // Canary only. This forces the template's dependency versions onto the
97
+ // project and adds back any the project is missing, which resurrects
98
+ // packages people have deliberately removed — a project that moved off
99
+ // SQLite gets better-sqlite3 back, for example. That's too blunt for
100
+ // regular upgrades, but people running canary are already signed up
101
+ // for rougher edges.
102
+ // https://github.com/redwoodjs/redwood/pull/8855
103
+ enabled: (ctx) => String(ctx.versionToUpgradeTo).includes("canary") && notBlockedByPreUpgradeChecks(ctx)
96
104
  },
97
105
  {
98
106
  title: "Downloading yarn patches",
99
107
  task: (ctx) => downloadYarnPatches(ctx, { dryRun, verbose }),
100
- enabled: (ctx) => String(ctx.versionToUpgradeTo).includes("canary") && !ctx.preUpgradeError
108
+ enabled: (ctx) => String(ctx.versionToUpgradeTo).includes("canary") && notBlockedByPreUpgradeChecks(ctx)
101
109
  },
102
110
  {
103
111
  title: "Removing CLI cache",
104
112
  task: () => removeCliCache({ dryRun, verbose }),
105
- enabled: (ctx) => !ctx.preUpgradeError
113
+ enabled: (ctx) => notBlockedByPreUpgradeChecks(ctx)
106
114
  },
107
115
  {
108
116
  title: `Running ${getPackageManager()} ${install()}`,
109
117
  task: () => packageManagerInstall({ verbose }),
110
- enabled: (ctx) => !ctx.preUpgradeError,
118
+ enabled: (ctx) => notBlockedByPreUpgradeChecks(ctx),
111
119
  skip: () => !!dryRun
112
120
  },
113
121
  {
114
122
  title: "Refreshing the Prisma client",
115
123
  task: (_ctx, task) => refreshPrismaClient(task, { verbose }),
116
- enabled: (ctx) => !ctx.preUpgradeError,
124
+ enabled: (ctx) => notBlockedByPreUpgradeChecks(ctx),
117
125
  skip: () => !!dryRun
118
126
  },
119
127
  {
120
128
  title: "De-duplicating dependencies",
121
129
  skip: () => !!dryRun || !dedupe2,
122
- enabled: (ctx) => dedupeIsSupported() && !ctx.preUpgradeError,
130
+ enabled: (ctx) => dedupeIsSupported() && notBlockedByPreUpgradeChecks(ctx),
123
131
  task: (_ctx, task) => dedupeDeps(task, { verbose })
124
132
  },
125
133
  {
@@ -303,6 +311,22 @@ function updateCedarJSDepsForAllSides(ctx, options) {
303
311
  })
304
312
  );
305
313
  }
314
+ function mergeTemplateDependencies(field, templatePackageJson, localPackageJson, messages, { dryRun, verbose } = {}) {
315
+ const templateDeps = templatePackageJson[field];
316
+ if (!templateDeps) {
317
+ return;
318
+ }
319
+ for (const [depName, depVersion] of Object.entries(templateDeps)) {
320
+ if (depName.startsWith("@cedarjs/")) {
321
+ continue;
322
+ }
323
+ const localDeps = localPackageJson[field] ??= {};
324
+ if (verbose || dryRun) {
325
+ messages.push(` - ${depName}: ${localDeps[depName]} => ${depVersion}`);
326
+ }
327
+ localDeps[depName] = depVersion;
328
+ }
329
+ }
306
330
  async function updatePackageVersionsFromTemplate(ctx, { dryRun, verbose }) {
307
331
  if (!ctx.versionToUpgradeTo) {
308
332
  throw new Error("Failed to upgrade");
@@ -338,30 +362,15 @@ async function updatePackageVersionsFromTemplate(ctx, { dryRun, verbose }) {
338
362
  const localPackageJsonText = fs.readFileSync(pkgJsonPath, "utf-8");
339
363
  const localPackageJson = JSON.parse(localPackageJsonText);
340
364
  const messages = [];
341
- Object.entries(templatePackageJson.dependencies || {}).forEach(
342
- ([depName, depVersion]) => {
343
- if (!depName.startsWith("@cedarjs/")) {
344
- if (verbose || dryRun) {
345
- messages.push(
346
- ` - ${depName}: ${localPackageJson.dependencies[depName]} => ${depVersion}`
347
- );
348
- }
349
- localPackageJson.dependencies[depName] = depVersion;
350
- }
351
- }
352
- );
353
- Object.entries(templatePackageJson.devDependencies || {}).forEach(
354
- ([depName, depVersion]) => {
355
- if (!depName.startsWith("@cedarjs/")) {
356
- if (verbose || dryRun) {
357
- messages.push(
358
- ` - ${depName}: ${localPackageJson.devDependencies[depName]} => ${depVersion}`
359
- );
360
- }
361
- localPackageJson.devDependencies[depName] = depVersion;
362
- }
363
- }
364
- );
365
+ for (const field of ["dependencies", "devDependencies"]) {
366
+ mergeTemplateDependencies(
367
+ field,
368
+ templatePackageJson,
369
+ localPackageJson,
370
+ messages,
371
+ { dryRun, verbose }
372
+ );
373
+ }
365
374
  if (messages.length > 0) {
366
375
  task.title = task.title + "\n" + messages.join("\n");
367
376
  }
@@ -469,5 +478,6 @@ async function dedupeDeps(_task, { verbose }) {
469
478
  await packageManagerInstall({ verbose });
470
479
  }
471
480
  export {
472
- handler
481
+ handler,
482
+ mergeTemplateDependencies
473
483
  };
@@ -3,6 +3,23 @@ import fs from "node:fs";
3
3
  import os from "os";
4
4
  import path from "path";
5
5
  import { getPaths } from "@cedarjs/project-config";
6
+ function quoteForWindowsShell(arg) {
7
+ let quoted = '"';
8
+ let backslashes = 0;
9
+ for (const char of arg) {
10
+ if (char === "\\") {
11
+ backslashes += 1;
12
+ continue;
13
+ }
14
+ if (char === '"') {
15
+ quoted += "\\".repeat(backslashes * 2 + 1) + '"';
16
+ } else {
17
+ quoted += "\\".repeat(backslashes) + char;
18
+ }
19
+ backslashes = 0;
20
+ }
21
+ return quoted + "\\".repeat(backslashes * 2) + '"';
22
+ }
6
23
  function spawnBackgroundProcess(name, cmd, args) {
7
24
  const logDirectory = path.join(getPaths().generated.base, "logs");
8
25
  fs.mkdirSync(logDirectory, { recursive: true });
@@ -37,7 +54,8 @@ function spawnBackgroundProcess(name, cmd, args) {
37
54
  shell: true,
38
55
  stdio: ["ignore", stdout, stderr]
39
56
  };
40
- const child = spawn(cmd + " " + args.join(" "), spawnOptions);
57
+ const command = [cmd, ...args].map(quoteForWindowsShell).join(" ");
58
+ const child = spawn(command, spawnOptions);
41
59
  child.unref();
42
60
  } else {
43
61
  const spawnOptions = {
@@ -51,5 +69,6 @@ function spawnBackgroundProcess(name, cmd, args) {
51
69
  fs.closeSync(stderr);
52
70
  }
53
71
  export {
72
+ quoteForWindowsShell,
54
73
  spawnBackgroundProcess
55
74
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cedarjs/cli",
3
- "version": "6.0.0-rc.260",
3
+ "version": "6.0.0-rc.340",
4
4
  "description": "The CedarJS Command Line",
5
5
  "repository": {
6
6
  "type": "git",
@@ -32,21 +32,21 @@
32
32
  },
33
33
  "dependencies": {
34
34
  "@babel/core": "^7.26.10",
35
- "@babel/parser": "7.29.7",
35
+ "@babel/parser": "7.29.8",
36
36
  "@babel/preset-typescript": "7.29.7",
37
- "@babel/traverse": "7.29.7",
38
- "@babel/types": "7.29.7",
39
- "@cedarjs/api-server": "6.0.0-rc.260",
40
- "@cedarjs/babel-config": "6.0.0-rc.260",
41
- "@cedarjs/cli-helpers": "6.0.0-rc.260",
42
- "@cedarjs/internal": "6.0.0-rc.260",
43
- "@cedarjs/prerender": "6.0.0-rc.260",
44
- "@cedarjs/project-config": "6.0.0-rc.260",
45
- "@cedarjs/structure": "6.0.0-rc.260",
46
- "@cedarjs/telemetry": "6.0.0-rc.260",
47
- "@cedarjs/utils": "6.0.0-rc.260",
48
- "@cedarjs/vite": "6.0.0-rc.260",
49
- "@cedarjs/web-server": "6.0.0-rc.260",
37
+ "@babel/traverse": "7.29.8",
38
+ "@babel/types": "7.29.8",
39
+ "@cedarjs/api-server": "6.0.0-rc.340",
40
+ "@cedarjs/babel-config": "6.0.0-rc.340",
41
+ "@cedarjs/cli-helpers": "6.0.0-rc.340",
42
+ "@cedarjs/internal": "6.0.0-rc.340",
43
+ "@cedarjs/prerender": "6.0.0-rc.340",
44
+ "@cedarjs/project-config": "6.0.0-rc.340",
45
+ "@cedarjs/structure": "6.0.0-rc.340",
46
+ "@cedarjs/telemetry": "6.0.0-rc.340",
47
+ "@cedarjs/utils": "6.0.0-rc.340",
48
+ "@cedarjs/vite": "6.0.0-rc.340",
49
+ "@cedarjs/web-server": "6.0.0-rc.340",
50
50
  "@listr2/prompt-adapter-enquirer": "4.3.0",
51
51
  "@opentelemetry/api": "1.9.1",
52
52
  "@opentelemetry/core": "1.30.1",
@@ -82,11 +82,11 @@
82
82
  "prettier": "3.8.4",
83
83
  "prisma": "7.8.0",
84
84
  "prompts": "2.4.2",
85
- "semver": "7.7.4",
86
- "smol-toml": "1.6.1",
87
- "srvx": "0.11.16",
85
+ "semver": "7.8.5",
86
+ "smol-toml": "1.8.0",
87
+ "srvx": "0.12.5",
88
88
  "string-env-interpolation": "1.0.1",
89
- "systeminformation": "5.31.7",
89
+ "systeminformation": "5.33.1",
90
90
  "termi-link": "1.1.0",
91
91
  "title-case": "3.0.3",
92
92
  "unionfs": "4.6.0",
@@ -94,10 +94,10 @@
94
94
  "yargs": "17.7.3"
95
95
  },
96
96
  "devDependencies": {
97
- "@cedarjs/framework-tools": "6.0.0-rc.260",
97
+ "@cedarjs/framework-tools": "6.0.0-rc.340",
98
98
  "@prisma/dmmf": "7.8.0",
99
99
  "@types/archiver": "^7.0.0",
100
- "memfs": "4.64.0",
100
+ "memfs": "4.68.1",
101
101
  "node-ssh": "13.2.1",
102
102
  "ts-dedent": "2.3.0",
103
103
  "typescript": "5.9.3",