@cedarjs/cli 2.4.1 → 2.4.2-next.143

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.
Files changed (28) hide show
  1. package/README.md +6 -2
  2. package/dist/commands/deploy/baremetal/baremetalHandler.js +4 -4
  3. package/dist/commands/deploy/serverlessHandler.js +1 -1
  4. package/dist/commands/dev/devHandler.js +31 -2
  5. package/dist/commands/experimental/setupOpentelemetryHandler.js +7 -6
  6. package/dist/commands/experimental/setupReactCompilerHandler.js +10 -9
  7. package/dist/commands/experimental/setupRscHandler.js +9 -9
  8. package/dist/commands/experimental/setupStreamingSsrHandler.js +9 -9
  9. package/dist/commands/generate/package/package.js +15 -2
  10. package/dist/commands/generate/package/packageHandler.js +297 -19
  11. package/dist/commands/generate/package/templates/tsconfig.json.template +2 -2
  12. package/dist/commands/generate/service/serviceHandler.js +5 -2
  13. package/dist/commands/generate/yargsCommandHelpers.js +16 -14
  14. package/dist/commands/info.js +6 -3
  15. package/dist/commands/setup/deploy/helpers/index.js +14 -18
  16. package/dist/commands/setup/deploy/providers/coherenceHandler.js +33 -34
  17. package/dist/commands/setup/deploy/providers/serverlessHandler.js +8 -7
  18. package/dist/commands/setup/deploy/templates/netlify.js +2 -2
  19. package/dist/commands/setup/docker/dockerHandler.js +5 -4
  20. package/dist/commands/setup/docker/templates/Dockerfile +3 -3
  21. package/dist/commands/setup/monitoring/sentry/sentryHandler.js +4 -1
  22. package/dist/commands/setup/monitoring/sentry/templates/sentryWeb.ts.template +2 -2
  23. package/dist/commands/upgrade/preUpgradeScripts.js +7 -3
  24. package/dist/commands/upgrade/upgradeHandler.js +1 -1
  25. package/dist/index.js +13 -9
  26. package/dist/lib/index.js +5 -2
  27. package/dist/lib/plugin.js +1 -1
  28. package/package.json +27 -26
@@ -1,9 +1,12 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
+ import { ListrEnquirerPromptAdapter } from "@listr2/prompt-adapter-enquirer";
3
4
  import { paramCase, camelCase } from "change-case";
4
5
  import execa from "execa";
6
+ import { modify, applyEdits } from "jsonc-parser";
5
7
  import { Listr } from "listr2";
6
8
  import { terminalLink } from "termi-link";
9
+ import ts from "typescript";
7
10
  import { recordTelemetryAttributes } from "@cedarjs/cli-helpers";
8
11
  import { getConfig } from "@cedarjs/project-config";
9
12
  import { errorTelemetry } from "@cedarjs/telemetry";
@@ -20,31 +23,89 @@ function nameVariants(nameArg) {
20
23
  return { name, folderName, packageName, fileName };
21
24
  }
22
25
  async function updateTsconfig(task) {
23
- const tsconfigPath = path.join(getPaths().api.base, "tsconfig.json");
24
- const tsconfig = await fs.promises.readFile(tsconfigPath, "utf8");
25
- const tsconfigLines = tsconfig.split("\n");
26
- const moduleLineIndex = tsconfigLines.findIndex(
27
- (line) => /^\s*"module":\s*"/.test(line)
28
- );
29
- const moduleLine = tsconfigLines[moduleLineIndex];
30
- if (moduleLine.toLowerCase().includes("node20") || // While Cedar doesn't officially endorse the usage of NodeNext, it
31
- // will still work here, so I won't overwrite it
32
- moduleLine.toLowerCase().includes("nodenext")) {
26
+ const targets = [
27
+ {
28
+ name: "api",
29
+ path: path.join(getPaths().api.base, "tsconfig.json"),
30
+ expectedModule: "node20",
31
+ // While Cedar doesn't officially endorse NodeNext, it will still work
32
+ // here, so we'll keep it
33
+ acceptable: ["node20", "nodenext"]
34
+ },
35
+ {
36
+ name: "web",
37
+ path: path.join(getPaths().web.base, "tsconfig.json"),
38
+ expectedModule: "esnext",
39
+ acceptable: ["esnext", "es2022"]
40
+ },
41
+ {
42
+ name: "scripts",
43
+ path: path.join(getPaths().scripts, "tsconfig.json"),
44
+ expectedModule: "node20",
45
+ acceptable: ["node20", "nodenext"]
46
+ }
47
+ ];
48
+ let didUpdate = false;
49
+ for (const target of targets) {
50
+ if (!fs.existsSync(target.path)) {
51
+ continue;
52
+ }
53
+ const tsconfigText = await fs.promises.readFile(target.path, "utf8");
54
+ const { config: tsconfig, error } = ts.parseConfigFileTextToJson(
55
+ target.path,
56
+ tsconfigText
57
+ );
58
+ if (error) {
59
+ throw new Error(
60
+ "Failed to parse tsconfig: " + JSON.stringify(error, null, 2)
61
+ );
62
+ }
63
+ if (!tsconfig?.compilerOptions || typeof tsconfig.compilerOptions.module === "undefined") {
64
+ continue;
65
+ }
66
+ const currentModule = typeof tsconfig.compilerOptions.module === "string" ? tsconfig.compilerOptions.module.toLowerCase() : String(tsconfig.compilerOptions.module).toLowerCase();
67
+ const alreadySet = target.acceptable.some((acc) => {
68
+ return currentModule.includes(acc);
69
+ });
70
+ if (alreadySet) {
71
+ continue;
72
+ }
73
+ const edits = modify(
74
+ tsconfigText,
75
+ ["compilerOptions", "module"],
76
+ target.expectedModule,
77
+ {
78
+ formattingOptions: { insertSpaces: true, tabSize: 2 }
79
+ }
80
+ );
81
+ if (edits.length === 0) {
82
+ const newConfig = { ...tsconfig };
83
+ if (!newConfig.compilerOptions) {
84
+ newConfig.compilerOptions = {};
85
+ }
86
+ newConfig.compilerOptions.module = target.expectedModule;
87
+ await fs.promises.writeFile(
88
+ target.path,
89
+ JSON.stringify(newConfig, null, 2),
90
+ "utf8"
91
+ );
92
+ } else {
93
+ const newText = applyEdits(tsconfigText, edits);
94
+ await fs.promises.writeFile(target.path, newText, "utf8");
95
+ }
96
+ didUpdate = true;
97
+ }
98
+ if (!didUpdate) {
33
99
  task.skip("tsconfig already up to date");
34
100
  return;
35
101
  }
36
- tsconfigLines[moduleLineIndex] = moduleLine.replace(
37
- /":\s*"[\w\d]+"/,
38
- '": "Node20"'
39
- );
40
- await fs.promises.writeFile(tsconfigPath, tsconfigLines.join("\n"));
41
102
  }
42
103
  async function updateGitignore(task) {
43
104
  const gitignorePath = path.join(getPaths().base, ".gitignore");
44
105
  const gitignore = await fs.promises.readFile(gitignorePath, "utf8");
45
106
  const gitignoreLines = gitignore.split("\n");
46
107
  if (gitignoreLines.some((line) => line === "tsconfig.tsbuildinfo")) {
47
- task.skip("tsconfig already up to date");
108
+ task.skip(".gitignore already up to date");
48
109
  return;
49
110
  }
50
111
  const yarnErrorLogLineIndex = gitignoreLines.findIndex(
@@ -57,6 +118,130 @@ async function updateGitignore(task) {
57
118
  }
58
119
  await fs.promises.writeFile(gitignorePath, gitignoreLines.join("\n"));
59
120
  }
121
+ async function addDependencyToPackageJson(task, packageJsonPath, packageName) {
122
+ if (!fs.existsSync(packageJsonPath)) {
123
+ task.skip("package.json not found");
124
+ return;
125
+ }
126
+ const packageJson = JSON.parse(
127
+ await fs.promises.readFile(packageJsonPath, "utf8")
128
+ );
129
+ if (!packageJson.dependencies) {
130
+ packageJson.dependencies = {};
131
+ }
132
+ if (packageJson.dependencies[packageName]) {
133
+ task.skip("Dependency already exists");
134
+ return;
135
+ }
136
+ packageJson.dependencies[packageName] = "workspace:*";
137
+ await fs.promises.writeFile(
138
+ packageJsonPath,
139
+ JSON.stringify(packageJson, null, 2)
140
+ );
141
+ }
142
+ function parseWorkspaceFlag(workspace) {
143
+ if (workspace === void 0 || workspace === null) {
144
+ return void 0;
145
+ }
146
+ const ws = String(workspace).trim().toLowerCase();
147
+ const allowed = ["none", "api", "web", "both"];
148
+ if (!allowed.includes(ws)) {
149
+ throw new Error(
150
+ `Invalid workspace value "${workspace}". Valid options: ${allowed.join(", ")}`
151
+ );
152
+ }
153
+ return ws;
154
+ }
155
+ function updateWorkspaceTsconfigReferences(task, folderName, targetWorkspaces) {
156
+ if (!targetWorkspaces || targetWorkspaces === "none") {
157
+ task.skip("No workspace selected");
158
+ return;
159
+ }
160
+ const workspaces = [];
161
+ const packageDir = path.join(getPaths().base, "packages", folderName);
162
+ if (targetWorkspaces === "api" || targetWorkspaces === "both") {
163
+ const tsconfigPath = path.join(getPaths().api.base, "tsconfig.json");
164
+ workspaces.push({ name: "api", tsconfigPath, packageDir });
165
+ }
166
+ if (targetWorkspaces === "web" || targetWorkspaces === "both") {
167
+ const tsconfigPath = path.join(getPaths().web.base, "tsconfig.json");
168
+ workspaces.push({ name: "web", tsconfigPath, packageDir });
169
+ }
170
+ if (targetWorkspaces !== "none") {
171
+ const tsconfigPath = path.join(getPaths().scripts, "tsconfig.json");
172
+ workspaces.push({ name: "scripts", tsconfigPath, packageDir });
173
+ }
174
+ if (workspaces.length === 0) {
175
+ task.skip("No workspace selected");
176
+ return;
177
+ }
178
+ const subtasks = workspaces.map((ws) => {
179
+ return {
180
+ title: `Updating ${ws.name} tsconfig references...`,
181
+ task: async (_ctx, subtask) => {
182
+ if (!fs.existsSync(ws.tsconfigPath)) {
183
+ subtask.skip("tsconfig.json not found");
184
+ return;
185
+ }
186
+ const tsconfigText = await fs.promises.readFile(ws.tsconfigPath, "utf8");
187
+ const { config: tsconfig, error } = ts.parseConfigFileTextToJson(
188
+ ws.tsconfigPath,
189
+ tsconfigText
190
+ );
191
+ if (error) {
192
+ throw new Error(
193
+ "Failed to parse tsconfig: " + JSON.stringify(error, null, 2)
194
+ );
195
+ }
196
+ const configParseResult = ts.parseJsonConfigFileContent(
197
+ tsconfig,
198
+ {
199
+ ...ts.sys,
200
+ readFile: (p) => {
201
+ try {
202
+ return fs.readFileSync(p, "utf8");
203
+ } catch (e) {
204
+ return ts.sys.readFile(p);
205
+ }
206
+ },
207
+ fileExists: (p) => {
208
+ if (typeof fs.existsSync === "function") {
209
+ return fs.existsSync(p);
210
+ }
211
+ return ts.sys.fileExists(p);
212
+ }
213
+ },
214
+ path.dirname(ws.tsconfigPath)
215
+ );
216
+ if (configParseResult.errors && configParseResult.errors.length) {
217
+ console.error("Parse errors:", configParseResult.errors);
218
+ }
219
+ if (!Array.isArray(tsconfig.references)) {
220
+ tsconfig.references = [];
221
+ }
222
+ const packageDir2 = ws.packageDir || path.join(getPaths().base, "packages", folderName);
223
+ const referencePath = path.relative(path.dirname(ws.tsconfigPath), packageDir2).split(path.sep).join("/");
224
+ const references = tsconfig.references;
225
+ if (references.some((ref) => ref && ref.path === referencePath)) {
226
+ subtask.skip("tsconfig already up to date");
227
+ return;
228
+ }
229
+ const newReferences = references.concat([{ path: referencePath }]);
230
+ let text = await fs.promises.readFile(ws.tsconfigPath, "utf8");
231
+ const edits = modify(text, ["references"], newReferences, {
232
+ formattingOptions: { insertSpaces: true, tabSize: 2 }
233
+ });
234
+ const newText = applyEdits(text, edits);
235
+ const formattedText = newText.replace(
236
+ /"references": \[\s*\{\s*"path":\s*"([^"]+)"\s*\}\s*\]/,
237
+ '"references": [{ "path": "' + referencePath + '" }]'
238
+ );
239
+ await fs.promises.writeFile(ws.tsconfigPath, formattedText, "utf8");
240
+ }
241
+ };
242
+ });
243
+ return new Listr(subtasks);
244
+ }
60
245
  async function installAndBuild(folderName) {
61
246
  const packagePath = path.join("packages", folderName);
62
247
  await execa("yarn", ["install"], { stdio: "inherit", cwd: getPaths().base });
@@ -79,7 +264,7 @@ const handler = async ({ name, force, ...rest }) => {
79
264
  "https://github.com/cedarjs/cedar/releases"
80
265
  );
81
266
  console.error(
82
- "This is an experimental feature. Please enable it in your redwood.toml file and then run this command again."
267
+ "This is an experimental feature. Please enable it in your cedar.toml (or redwood.toml) file and then run this command again."
83
268
  );
84
269
  console.error();
85
270
  console.error(`See the ${releaseNotes} for instructions on how to enable.`);
@@ -129,7 +314,36 @@ const handler = async ({ name, force, ...rest }) => {
129
314
  }
130
315
  },
131
316
  {
132
- title: "Updating api side tsconfig file...",
317
+ title: "Choose package workspace(s)...",
318
+ task: async (ctx, task) => {
319
+ try {
320
+ const flagValue = parseWorkspaceFlag(rest.workspace);
321
+ if (flagValue !== void 0) {
322
+ ctx.targetWorkspaces = flagValue;
323
+ task.skip(
324
+ `Using workspace provided via --workspace: ${flagValue}`
325
+ );
326
+ return;
327
+ }
328
+ } catch (e) {
329
+ throw new Error(e.message);
330
+ }
331
+ const prompt = task.prompt(ListrEnquirerPromptAdapter);
332
+ const response = await prompt.run({
333
+ type: "select",
334
+ message: "Which workspace(s) should use this package?",
335
+ choices: [
336
+ { message: "none (I will add it manually later)", value: "none" },
337
+ { message: "api", value: "api" },
338
+ { message: "web", value: "web" },
339
+ { message: "both", value: "both" }
340
+ ]
341
+ });
342
+ ctx.targetWorkspaces = response;
343
+ }
344
+ },
345
+ {
346
+ title: "Updating tsconfig files...",
133
347
  task: (_ctx, task) => updateTsconfig(task)
134
348
  },
135
349
  {
@@ -143,6 +357,67 @@ const handler = async ({ name, force, ...rest }) => {
143
357
  return writeFilesTask(packageFiles, { overwriteExisting: force });
144
358
  }
145
359
  },
360
+ {
361
+ title: "Adding package to workspace dependencies...",
362
+ task: async (ctx, task) => {
363
+ if (!ctx.targetWorkspaces || ctx.targetWorkspaces === "none") {
364
+ task.skip("No workspace selected");
365
+ return;
366
+ }
367
+ const subtasks = [];
368
+ if (ctx.targetWorkspaces === "api" || ctx.targetWorkspaces === "both") {
369
+ subtasks.push({
370
+ title: "Adding to api package.json...",
371
+ task: async (_subCtx, subtask) => {
372
+ const apiPackageJsonPath = path.join(
373
+ getPaths().api.base,
374
+ "package.json"
375
+ );
376
+ return addDependencyToPackageJson(
377
+ subtask,
378
+ apiPackageJsonPath,
379
+ ctx.nameVariants.packageName
380
+ );
381
+ }
382
+ });
383
+ }
384
+ if (ctx.targetWorkspaces === "web" || ctx.targetWorkspaces === "both") {
385
+ subtasks.push({
386
+ title: "Adding to web package.json...",
387
+ task: async (_subCtx, subtask) => {
388
+ const webPackageJsonPath = path.join(
389
+ getPaths().web.base,
390
+ "package.json"
391
+ );
392
+ return addDependencyToPackageJson(
393
+ subtask,
394
+ webPackageJsonPath,
395
+ ctx.nameVariants.packageName
396
+ );
397
+ }
398
+ });
399
+ }
400
+ if (subtasks.length === 0) {
401
+ task.skip("No workspace selected");
402
+ return;
403
+ }
404
+ return new Listr(subtasks);
405
+ }
406
+ },
407
+ {
408
+ title: "Updating tsconfig references...",
409
+ task: (ctx, task) => {
410
+ if (!ctx.targetWorkspaces || ctx.targetWorkspaces === "none") {
411
+ task.skip("No workspace selected");
412
+ return;
413
+ }
414
+ return updateWorkspaceTsconfigReferences(
415
+ task,
416
+ ctx.nameVariants.folderName,
417
+ ctx.targetWorkspaces
418
+ );
419
+ }
420
+ },
146
421
  {
147
422
  title: "Installing and building...",
148
423
  task: (ctx) => installAndBuild(ctx.nameVariants.folderName)
@@ -174,8 +449,11 @@ const handler = async ({ name, force, ...rest }) => {
174
449
  }
175
450
  };
176
451
  export {
452
+ addDependencyToPackageJson,
177
453
  handler,
178
454
  nameVariants,
455
+ parseWorkspaceFlag,
179
456
  updateGitignore,
180
- updateTsconfig
457
+ updateTsconfig,
458
+ updateWorkspaceTsconfigReferences
181
459
  };
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "compilerOptions": {
3
3
  "composite": true,
4
- "target": "ES2023",
5
- "module": "Node20",
4
+ "target": "es2023",
5
+ "module": "node20",
6
6
  "esModuleInterop": true,
7
7
  "skipLibCheck": true,
8
8
  "baseUrl": ".",
@@ -25,7 +25,7 @@ const parseSchema = async (model) => {
25
25
  });
26
26
  return { scalarFields, relations, foreignKeys };
27
27
  };
28
- const scenarioFieldValue = (field) => {
28
+ function scenarioFieldValue(field) {
29
29
  const randFloat = Math.random() * 1e7;
30
30
  const randInt = parseInt(Math.random() * 1e7);
31
31
  const randIntArray = [
@@ -48,6 +48,9 @@ const scenarioFieldValue = (field) => {
48
48
  case "Json":
49
49
  return { foo: "bar" };
50
50
  case "String":
51
+ if (field.name?.toLowerCase().includes("email")) {
52
+ return field.isUnique ? `foo${randInt}@bar.com` : "foo@bar.com";
53
+ }
51
54
  return field.isUnique ? `String${randInt}` : "String";
52
55
  case "Bytes":
53
56
  return `new Uint8Array([${randIntArray}])`;
@@ -57,7 +60,7 @@ const scenarioFieldValue = (field) => {
57
60
  }
58
61
  }
59
62
  }
60
- };
63
+ }
61
64
  const fieldsToScenario = async (scalarFields, relations, foreignKeys) => {
62
65
  const data = {};
63
66
  scalarFields.forEach((field) => {
@@ -1,19 +1,21 @@
1
1
  import { terminalLink } from "termi-link";
2
2
  import { isTypeScriptProject } from "@cedarjs/cli-helpers";
3
- const getYargsDefaults = () => ({
4
- force: {
5
- alias: "f",
6
- default: false,
7
- description: "Overwrite existing files",
8
- type: "boolean"
9
- },
10
- typescript: {
11
- alias: "ts",
12
- default: isTypeScriptProject(),
13
- description: "Generate TypeScript files",
14
- type: "boolean"
15
- }
16
- });
3
+ function getYargsDefaults() {
4
+ return {
5
+ force: {
6
+ alias: "f",
7
+ default: false,
8
+ description: "Overwrite existing files",
9
+ type: "boolean"
10
+ },
11
+ typescript: {
12
+ alias: "ts",
13
+ default: isTypeScriptProject(),
14
+ description: "Generate TypeScript files",
15
+ type: "boolean"
16
+ }
17
+ };
18
+ }
17
19
  const appendPositionalsToCmd = (commandString, positionalsObj) => {
18
20
  if (Object.keys(positionalsObj).length > 0) {
19
21
  const positionalNames = Object.keys(positionalsObj).map((positionalName) => `[${positionalName}]`).join(" ");
@@ -1,8 +1,9 @@
1
1
  import fs from "node:fs";
2
+ import path from "node:path";
2
3
  import envinfo from "envinfo";
3
4
  import { terminalLink } from "termi-link";
4
5
  import { recordTelemetryAttributes } from "@cedarjs/cli-helpers";
5
- import { getPaths } from "@cedarjs/project-config";
6
+ import { getConfigPath } from "@cedarjs/project-config";
6
7
  const command = "info";
7
8
  const description = "Print your system environment information";
8
9
  const builder = (yargs) => {
@@ -23,9 +24,11 @@ const handler = async () => {
23
24
  npmPackages: "@cedarjs/*",
24
25
  Databases: ["SQLite"]
25
26
  });
26
- const redwoodToml = fs.readFileSync(getPaths().base + "/redwood.toml", "utf8");
27
+ const configTomlPath = getConfigPath();
28
+ const tomlContent = fs.readFileSync(configTomlPath, "utf8");
27
29
  console.log(
28
- output + " redwood.toml:\n" + redwoodToml.split("\n").filter((line) => line.trim().length > 0).filter((line) => !/^#/.test(line)).map((line) => ` ${line}`).join("\n")
30
+ output + ` ${path.basename(configTomlPath)}:
31
+ ` + tomlContent.split("\n").filter((line) => line.trim().length > 0).filter((line) => !/^#/.test(line)).map((line) => ` ${line}`).join("\n")
29
32
  );
30
33
  };
31
34
  export {
@@ -1,31 +1,27 @@
1
1
  import fs from "node:fs";
2
- import path from "path";
2
+ import path from "node:path";
3
3
  import execa from "execa";
4
4
  import { Listr } from "listr2";
5
+ import { getConfigPath } from "@cedarjs/project-config";
5
6
  import { getPaths, writeFilesTask } from "../../../../lib/index.js";
6
- const REDWOOD_TOML_PATH = path.join(getPaths().base, "redwood.toml");
7
7
  const updateApiURLTask = (apiUrl) => {
8
+ const configTomlPath = getConfigPath();
9
+ const configFileName = path.basename(configTomlPath);
8
10
  return {
9
- title: "Updating API URL in redwood.toml...",
11
+ title: `Updating API URL in ${configFileName}...`,
10
12
  task: () => {
11
- const redwoodToml = fs.readFileSync(REDWOOD_TOML_PATH).toString();
12
- let newRedwoodToml = redwoodToml;
13
- if (redwoodToml.match(/apiUrl/)) {
14
- newRedwoodToml = newRedwoodToml.replace(
15
- /apiUrl.*/g,
16
- `apiUrl = "${apiUrl}"`
17
- );
18
- } else if (redwoodToml.match(/\[web\]/)) {
19
- newRedwoodToml = newRedwoodToml.replace(
20
- /\[web\]/,
21
- `[web]
22
- apiUrl = "${apiUrl}"`
23
- );
13
+ const tomlContent = fs.readFileSync(configTomlPath, "utf-8");
14
+ let newToml = tomlContent;
15
+ if (tomlContent.match(/apiUrl/)) {
16
+ newToml = newToml.replace(/apiUrl.*/g, `apiUrl = "${apiUrl}"`);
17
+ } else if (tomlContent.match(/\[web\]/)) {
18
+ newToml = newToml.replace(/\[web\]/, `[web]
19
+ apiUrl = "${apiUrl}"`);
24
20
  } else {
25
- newRedwoodToml += `[web]
21
+ newToml += `[web]
26
22
  apiUrl = "${apiUrl}"`;
27
23
  }
28
- fs.writeFileSync(REDWOOD_TOML_PATH, newRedwoodToml);
24
+ fs.writeFileSync(configTomlPath, newToml);
29
25
  }
30
26
  };
31
27
  };