@cedarjs/cli 3.0.0-canary.13258 → 3.0.0-canary.13260

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.
@@ -1,9 +1,22 @@
1
- import { createHandler, createBuilder } from "../yargsCommandHelpers.js";
1
+ import {
2
+ createHandler,
3
+ createBuilder,
4
+ getYargsDefaults
5
+ } from "../yargsCommandHelpers.js";
2
6
  const command = "package <name>";
3
7
  const description = "Generate a workspace Package";
4
8
  const builder = createBuilder({
5
9
  componentName: "package",
6
- addStories: false
10
+ addStories: false,
11
+ optionsObj: () => ({
12
+ ...getYargsDefaults(),
13
+ workspace: {
14
+ alias: "w",
15
+ description: "Which workspace(s) should use this package? One of: 'none', 'api', 'web', 'both'. If provided, the generator will skip the interactive prompt and apply the chosen workspace.",
16
+ type: "string",
17
+ choices: ["none", "api", "web", "both"]
18
+ }
19
+ })
7
20
  });
8
21
  const handler = createHandler("package");
9
22
  export {
@@ -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";
@@ -25,6 +28,8 @@ async function updateTsconfig(task) {
25
28
  name: "api",
26
29
  path: path.join(getPaths().api.base, "tsconfig.json"),
27
30
  expectedModule: "Node20",
31
+ // While Cedar doesn't officially endorse NodeNext, it will still work
32
+ // here, so we'll keep it
28
33
  acceptable: ["node20", "nodenext"]
29
34
  },
30
35
  {
@@ -40,38 +45,57 @@ async function updateTsconfig(task) {
40
45
  acceptable: ["node20", "nodenext"]
41
46
  }
42
47
  ];
43
- let updatedAny = false;
48
+ let didUpdate = false;
44
49
  for (const target of targets) {
45
50
  if (!fs.existsSync(target.path)) {
46
51
  continue;
47
52
  }
48
- const tsconfig = await fs.promises.readFile(target.path, "utf8");
49
- const tsconfigLines = tsconfig.split("\n");
50
- const moduleLineIndex = tsconfigLines.findIndex(
51
- (line) => /^\s*"module":\s*"/.test(line)
53
+ const tsconfigText = await fs.promises.readFile(target.path, "utf8");
54
+ const { config: tsconfig, error } = ts.parseConfigFileTextToJson(
55
+ target.path,
56
+ tsconfigText
52
57
  );
53
- if (moduleLineIndex === -1) {
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") {
54
64
  continue;
55
65
  }
56
- const moduleLine = tsconfigLines[moduleLineIndex];
57
- const lower = moduleLine.toLowerCase();
66
+ const currentModule = typeof tsconfig.compilerOptions.module === "string" ? tsconfig.compilerOptions.module.toLowerCase() : String(tsconfig.compilerOptions.module).toLowerCase();
58
67
  const alreadySet = target.acceptable.some((acc) => {
59
- if (lower.includes(acc)) {
60
- return true;
61
- }
62
- return false;
68
+ return currentModule.includes(acc);
63
69
  });
64
70
  if (alreadySet) {
65
71
  continue;
66
72
  }
67
- tsconfigLines[moduleLineIndex] = moduleLine.replace(
68
- /":\s*"[\w\d]+"/,
69
- `": "${target.expectedModule}"`
73
+ const edits = modify(
74
+ tsconfigText,
75
+ ["compilerOptions", "module"],
76
+ target.expectedModule,
77
+ {
78
+ formattingOptions: { insertSpaces: true, tabSize: 2 }
79
+ }
70
80
  );
71
- await fs.promises.writeFile(target.path, tsconfigLines.join("\n"));
72
- updatedAny = true;
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;
73
97
  }
74
- if (!updatedAny) {
98
+ if (!didUpdate) {
75
99
  task.skip("tsconfig already up to date");
76
100
  return;
77
101
  }
@@ -94,6 +118,130 @@ async function updateGitignore(task) {
94
118
  }
95
119
  await fs.promises.writeFile(gitignorePath, gitignoreLines.join("\n"));
96
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
+ }
97
245
  async function installAndBuild(folderName) {
98
246
  const packagePath = path.join("packages", folderName);
99
247
  await execa("yarn", ["install"], { stdio: "inherit", cwd: getPaths().base });
@@ -165,6 +313,35 @@ const handler = async ({ name, force, ...rest }) => {
165
313
  }
166
314
  }
167
315
  },
316
+ {
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
+ },
168
345
  {
169
346
  title: "Updating tsconfig files...",
170
347
  task: (_ctx, task) => updateTsconfig(task)
@@ -180,6 +357,67 @@ const handler = async ({ name, force, ...rest }) => {
180
357
  return writeFilesTask(packageFiles, { overwriteExisting: force });
181
358
  }
182
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
+ },
183
421
  {
184
422
  title: "Installing and building...",
185
423
  task: (ctx) => installAndBuild(ctx.nameVariants.folderName)
@@ -211,8 +449,11 @@ const handler = async ({ name, force, ...rest }) => {
211
449
  }
212
450
  };
213
451
  export {
452
+ addDependencyToPackageJson,
214
453
  handler,
215
454
  nameVariants,
455
+ parseWorkspaceFlag,
216
456
  updateGitignore,
217
- updateTsconfig
457
+ updateTsconfig,
458
+ updateWorkspaceTsconfigReferences
218
459
  };
@@ -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(" ");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cedarjs/cli",
3
- "version": "3.0.0-canary.13258+84abfd3de",
3
+ "version": "3.0.0-canary.13260+fc8c011ea",
4
4
  "description": "The CedarJS Command Line",
5
5
  "repository": {
6
6
  "type": "git",
@@ -33,15 +33,15 @@
33
33
  "dependencies": {
34
34
  "@babel/preset-typescript": "7.28.5",
35
35
  "@babel/runtime-corejs3": "7.28.4",
36
- "@cedarjs/api-server": "3.0.0-canary.13258",
37
- "@cedarjs/cli-helpers": "3.0.0-canary.13258",
38
- "@cedarjs/fastify-web": "3.0.0-canary.13258",
39
- "@cedarjs/internal": "3.0.0-canary.13258",
40
- "@cedarjs/prerender": "3.0.0-canary.13258",
41
- "@cedarjs/project-config": "3.0.0-canary.13258",
42
- "@cedarjs/structure": "3.0.0-canary.13258",
43
- "@cedarjs/telemetry": "3.0.0-canary.13258",
44
- "@cedarjs/web-server": "3.0.0-canary.13258",
36
+ "@cedarjs/api-server": "3.0.0-canary.13260",
37
+ "@cedarjs/cli-helpers": "3.0.0-canary.13260",
38
+ "@cedarjs/fastify-web": "3.0.0-canary.13260",
39
+ "@cedarjs/internal": "3.0.0-canary.13260",
40
+ "@cedarjs/prerender": "3.0.0-canary.13260",
41
+ "@cedarjs/project-config": "3.0.0-canary.13260",
42
+ "@cedarjs/structure": "3.0.0-canary.13260",
43
+ "@cedarjs/telemetry": "3.0.0-canary.13260",
44
+ "@cedarjs/web-server": "3.0.0-canary.13260",
45
45
  "@listr2/prompt-adapter-enquirer": "2.0.16",
46
46
  "@opentelemetry/api": "1.8.0",
47
47
  "@opentelemetry/core": "1.22.0",
@@ -68,6 +68,7 @@
68
68
  "fast-glob": "3.3.3",
69
69
  "humanize-string": "2.1.0",
70
70
  "jscodeshift": "17.0.0",
71
+ "jsonc-parser": "3.3.1",
71
72
  "latest-version": "9.0.0",
72
73
  "listr2": "7.0.2",
73
74
  "lodash": "4.17.21",
@@ -103,5 +104,5 @@
103
104
  "publishConfig": {
104
105
  "access": "public"
105
106
  },
106
- "gitHead": "84abfd3de04c3abc2a10118f2f6f5efa7444ffc8"
107
+ "gitHead": "fc8c011eae3a8664a56932f184759fa74b57e265"
107
108
  }