@cedarjs/cli 6.0.0-canary.2919 → 6.0.0-canary.2923

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.
@@ -37,6 +37,11 @@ const builder = createBuilder({
37
37
  default: false,
38
38
  description: "Include a typed `isEmpty` stub for overriding the default check for the Empty component.",
39
39
  type: "boolean"
40
+ },
41
+ fragment: {
42
+ default: "",
43
+ description: "Generate a fragment cell (exports FRAGMENT instead of QUERY) that reads its data from a parent Cell, given the GraphQL type it selects from - e.g. --fragment User.",
44
+ type: "string"
40
45
  }
41
46
  };
42
47
  }
@@ -29,7 +29,8 @@ const files = async ({
29
29
  tests,
30
30
  beforeQuery = false,
31
31
  afterQuery = false,
32
- isEmpty = false
32
+ isEmpty = false,
33
+ fragment = ""
33
34
  }) => {
34
35
  let cellName = removeGeneratorName(name, "cell");
35
36
  let idName = "id";
@@ -38,22 +39,49 @@ const files = async ({
38
39
  let model = null;
39
40
  let templateNameSuffix = "";
40
41
  let typeName = cellName;
41
- const shouldGenerateList = (isWordPluralizable(cellName) ? isPlural(cellName) : list) || list;
42
- try {
43
- model = await getSchema(pascalcase(singularize(cellName)));
44
- idName = getIdName(model);
45
- idType = getIdType(model);
46
- typeName = model.name;
47
- mockIdValues = idType === "String" ? mockIdValues.map((value) => `'${value}'`) : mockIdValues;
48
- } catch {
49
- idType = "Int";
42
+ if (fragment) {
43
+ if (list) {
44
+ throw new Error(
45
+ "The --list flag cannot be combined with --fragment; fragment cells always render a single item."
46
+ );
47
+ }
48
+ if (beforeQuery) {
49
+ throw new Error(
50
+ "The --before-query flag cannot be combined with --fragment; fragment cells never fire a query of their own."
51
+ );
52
+ }
53
+ if (query) {
54
+ throw new Error(
55
+ "The --query flag cannot be combined with --fragment; fragment cells don't have an operation name."
56
+ );
57
+ }
58
+ }
59
+ const shouldGenerateList = !fragment && ((isWordPluralizable(cellName) ? isPlural(cellName) : list) || list);
60
+ if (!fragment) {
61
+ try {
62
+ model = await getSchema(pascalcase(singularize(cellName)));
63
+ idName = getIdName(model);
64
+ idType = getIdType(model);
65
+ typeName = model.name;
66
+ mockIdValues = idType === "String" ? mockIdValues.map((value) => `'${value}'`) : mockIdValues;
67
+ } catch {
68
+ idType = "Int";
69
+ }
50
70
  }
51
71
  if (shouldGenerateList) {
52
72
  cellName = forcePluralizeWord(cellName);
53
73
  templateNameSuffix = "List";
54
74
  }
55
75
  let operationName = query;
56
- if (operationName) {
76
+ let fragmentName;
77
+ let fragmentOnType;
78
+ let fragmentPropName;
79
+ if (fragment) {
80
+ const fragmentTypeVariants = nameVariants(fragment);
81
+ fragmentOnType = fragmentTypeVariants.pascalName;
82
+ fragmentPropName = fragmentTypeVariants.camelName;
83
+ fragmentName = `${nameVariants(cellName).pascalName}Cell_${fragmentPropName}`;
84
+ } else if (operationName) {
57
85
  const userSpecifiedOperationNameIsUnique = await operationNameIsUnique(operationName);
58
86
  if (!userSpecifiedOperationNameIsUnique) {
59
87
  throw new Error(`Specified query name: "${operationName}" is not unique!`);
@@ -70,8 +98,14 @@ const files = async ({
70
98
  extension,
71
99
  webPathSection: CEDAR_WEB_PATH_NAME,
72
100
  generator: "cell",
73
- templatePath: `cell${templateNameSuffix}.tsx.template`,
74
- templateVars: {
101
+ templatePath: fragment ? "cellFragment.tsx.template" : `cell${templateNameSuffix}.tsx.template`,
102
+ templateVars: fragment ? {
103
+ fragmentName,
104
+ fragmentOnType,
105
+ camelName: fragmentPropName,
106
+ afterQuery,
107
+ isEmpty
108
+ } : {
75
109
  operationName,
76
110
  idName,
77
111
  idType,
@@ -86,8 +120,8 @@ const files = async ({
86
120
  extension: `.test${extension}`,
87
121
  webPathSection: CEDAR_WEB_PATH_NAME,
88
122
  generator: "cell",
89
- templatePath: "test.js.template",
90
- templateVars: {
123
+ templatePath: fragment ? "testFragment.js.template" : "test.js.template",
124
+ templateVars: fragment ? { camelName: fragmentPropName } : {
91
125
  idName: shouldGenerateList ? void 0 : idName,
92
126
  mockIdValues: shouldGenerateList ? void 0 : mockIdValues
93
127
  }
@@ -98,7 +132,7 @@ const files = async ({
98
132
  extension: `.stories${extension}`,
99
133
  webPathSection: CEDAR_WEB_PATH_NAME,
100
134
  generator: "cell",
101
- templatePath: "stories.tsx.template"
135
+ templatePath: fragment ? "storiesFragment.tsx.template" : "stories.tsx.template"
102
136
  });
103
137
  const mockFile = await templateForComponentFile({
104
138
  name: cellName,
@@ -107,7 +141,12 @@ const files = async ({
107
141
  webPathSection: CEDAR_WEB_PATH_NAME,
108
142
  generator: "cell",
109
143
  templatePath: `mock${templateNameSuffix}.ts.template`,
110
- templateVars: {
144
+ templateVars: fragment ? {
145
+ idName,
146
+ mockIdValues,
147
+ typeName: fragmentOnType,
148
+ camelName: fragmentPropName
149
+ } : {
111
150
  idName,
112
151
  mockIdValues,
113
152
  typeName
@@ -128,7 +167,10 @@ const files = async ({
128
167
  const handler = createHandler({
129
168
  componentName: "cell",
130
169
  filesFn: files,
131
- includeAdditionalTasks: ({ name: cellName }) => {
170
+ includeAdditionalTasks: ({
171
+ name: cellName,
172
+ fragment
173
+ }) => {
132
174
  return [
133
175
  {
134
176
  title: `Generating types ...`,
@@ -136,7 +178,7 @@ const handler = createHandler({
136
178
  const queryFieldName = nameVariants(
137
179
  removeGeneratorName(cellName, "cell")
138
180
  ).camelName;
139
- const projectHasSdl = await checkProjectForQueryField(queryFieldName);
181
+ const projectHasSdl = Boolean(fragment) || await checkProjectForQueryField(queryFieldName);
140
182
  if (projectHasSdl) {
141
183
  const { errors } = await generateTypes();
142
184
  for (const { message, error } of errors) {
@@ -0,0 +1,43 @@
1
+ import type { ${fragmentName} } from 'types/graphql'
2
+
3
+ import type {
4
+ CellSuccessProps,<% if (afterQuery || isEmpty) { %>
5
+ DataObject,<% } %>
6
+ } from '@cedarjs/web'
7
+
8
+ // A parent Cell spreads this fragment by name in its own QUERY, and passes
9
+ // the matching field down as the `${camelName}` prop - no import needed,
10
+ // just render <${pascalName}Cell />:
11
+ //
12
+ // query FindSomething($id: Int!) {
13
+ // something(id: $id) {
14
+ // id
15
+ // ...${fragmentName}
16
+ // }
17
+ // }
18
+ //
19
+ // <${pascalName}Cell ${camelName}={something} />
20
+ export const FRAGMENT = gql`
21
+ fragment ${fragmentName} on ${fragmentOnType} {
22
+ id
23
+ }
24
+ `
25
+ <% if (isEmpty) { %>
26
+ export const isEmpty = (
27
+ data: DataObject,
28
+ { isDataEmpty }: { isDataEmpty: (data: DataObject) => boolean },
29
+ ) => {
30
+ return isDataEmpty(data)
31
+ }
32
+ <% } %><% if (afterQuery) { %>
33
+ export const afterQuery = (data: DataObject): DataObject => {
34
+ return data
35
+ }
36
+ <% } %>
37
+ export const Empty = () => <div>Empty</div>
38
+
39
+ export const Success = ({
40
+ ${camelName},
41
+ }: CellSuccessProps<{ ${camelName}: ${fragmentName} }>) => {
42
+ return <div>{JSON.stringify(${camelName})}</div>
43
+ }
@@ -0,0 +1,23 @@
1
+ import type { Meta, StoryObj } from '@storybook/react'
2
+
3
+ import { Empty, Success } from './${pascalName}Cell'
4
+ import { standard } from './${pascalName}Cell.mock'
5
+
6
+ const meta: Meta = {
7
+ title: 'Cells/${pascalName}Cell',
8
+ tags: ['autodocs']
9
+ }
10
+
11
+ export default meta
12
+
13
+ export const empty: StoryObj<typeof Empty> = {
14
+ render: () => {
15
+ return Empty ? <Empty /> : <></>
16
+ }
17
+ }
18
+
19
+ export const success: StoryObj<typeof Success> = {
20
+ render: (args) => {
21
+ return Success ? <Success {...standard()} {...args} /> : <></>
22
+ }
23
+ }
@@ -0,0 +1,30 @@
1
+ import { render } from '@cedarjs/testing/web'
2
+
3
+ import { Empty, Success } from './${pascalName}Cell'
4
+ import { standard } from './${pascalName}Cell.mock'
5
+
6
+ // Generated boilerplate tests do not account for all circumstances
7
+ // and can fail without adjustments, e.g. Float and DateTime types.
8
+ // Please refer to the RedwoodJS Testing Docs:
9
+ // https://cedarjs.com/docs/testing#testing-cells
10
+ // https://cedarjs.com/docs/testing#jest-expect-type-considerations
11
+
12
+ describe('${pascalName}Cell', () => {
13
+ it('renders Empty successfully', async () => {
14
+ expect(() => {
15
+ render(<Empty />)
16
+ }).not.toThrow()
17
+ })
18
+
19
+ // When you're ready to test the actual output of your component render
20
+ // you could test that, for example, certain text is present:
21
+ //
22
+ // 1. import { screen } from '@cedarjs/testing/web'
23
+ // 2. Add test: expect(screen.getByText('Hello, world')).toBeInTheDocument()
24
+
25
+ it('renders Success successfully', async () => {
26
+ expect(() => {
27
+ render(<Success ${camelName}={standard().${camelName}} />)
28
+ }).not.toThrow()
29
+ })
30
+ })
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cedarjs/cli",
3
- "version": "6.0.0-canary.2919",
3
+ "version": "6.0.0-canary.2923",
4
4
  "description": "The CedarJS Command Line",
5
5
  "repository": {
6
6
  "type": "git",
@@ -36,17 +36,17 @@
36
36
  "@babel/preset-typescript": "7.29.7",
37
37
  "@babel/traverse": "7.29.7",
38
38
  "@babel/types": "7.29.7",
39
- "@cedarjs/api-server": "6.0.0-canary.2919",
40
- "@cedarjs/babel-config": "6.0.0-canary.2919",
41
- "@cedarjs/cli-helpers": "6.0.0-canary.2919",
42
- "@cedarjs/internal": "6.0.0-canary.2919",
43
- "@cedarjs/prerender": "6.0.0-canary.2919",
44
- "@cedarjs/project-config": "6.0.0-canary.2919",
45
- "@cedarjs/structure": "6.0.0-canary.2919",
46
- "@cedarjs/telemetry": "6.0.0-canary.2919",
47
- "@cedarjs/utils": "6.0.0-canary.2919",
48
- "@cedarjs/vite": "6.0.0-canary.2919",
49
- "@cedarjs/web-server": "6.0.0-canary.2919",
39
+ "@cedarjs/api-server": "6.0.0-canary.2923",
40
+ "@cedarjs/babel-config": "6.0.0-canary.2923",
41
+ "@cedarjs/cli-helpers": "6.0.0-canary.2923",
42
+ "@cedarjs/internal": "6.0.0-canary.2923",
43
+ "@cedarjs/prerender": "6.0.0-canary.2923",
44
+ "@cedarjs/project-config": "6.0.0-canary.2923",
45
+ "@cedarjs/structure": "6.0.0-canary.2923",
46
+ "@cedarjs/telemetry": "6.0.0-canary.2923",
47
+ "@cedarjs/utils": "6.0.0-canary.2923",
48
+ "@cedarjs/vite": "6.0.0-canary.2923",
49
+ "@cedarjs/web-server": "6.0.0-canary.2923",
50
50
  "@listr2/prompt-adapter-enquirer": "4.3.0",
51
51
  "@opentelemetry/api": "1.9.1",
52
52
  "@opentelemetry/core": "1.30.1",
@@ -82,7 +82,7 @@
82
82
  "prettier": "3.8.4",
83
83
  "prisma": "7.8.0",
84
84
  "prompts": "2.4.2",
85
- "semver": "7.7.4",
85
+ "semver": "7.8.5",
86
86
  "smol-toml": "1.6.1",
87
87
  "srvx": "0.11.16",
88
88
  "string-env-interpolation": "1.0.1",
@@ -94,7 +94,7 @@
94
94
  "yargs": "17.7.3"
95
95
  },
96
96
  "devDependencies": {
97
- "@cedarjs/framework-tools": "6.0.0-canary.2919",
97
+ "@cedarjs/framework-tools": "6.0.0-canary.2923",
98
98
  "@prisma/dmmf": "7.8.0",
99
99
  "@types/archiver": "^7.0.0",
100
100
  "memfs": "4.68.1",