@astryxdesign/cli 0.7.2-canary.ebcc404 → 0.7.2-canary.f8f7cf1

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 (134) hide show
  1. package/README.md +9 -8
  2. package/api/build/help/help.mjs +1 -1
  3. package/api/component/_adapter.d.mts +14 -0
  4. package/api/component/_adapter.mjs +48 -0
  5. package/api/component/component.mjs +10 -2
  6. package/api/component/integration-export-name.test.mjs +233 -8
  7. package/api/docs/_adapter.d.mts +11 -0
  8. package/api/docs/_adapter.mjs +244 -1
  9. package/api/docs/integrationDocs.test.mjs +124 -0
  10. package/api/doctor/theme-checks.mjs +1 -1
  11. package/api/gap-report/gap-report.mjs +2 -1
  12. package/api/index.d.mts +2 -0
  13. package/api/index.mjs +2 -0
  14. package/api/init/run/run.mjs +2 -2
  15. package/api/integration/_adapter.d.mts +12 -0
  16. package/api/integration/_adapter.mjs +54 -0
  17. package/api/integration/add-contribution.component-names.test.mjs +66 -0
  18. package/api/integration/add-contribution.mjs +8 -4
  19. package/api/integration/add-contribution.test.mjs +4 -2
  20. package/api/integration/authoring-checks.mjs +11 -2
  21. package/api/integration/authoring-checks.test.mjs +39 -0
  22. package/api/integration/authoring-checks.type.d.mts +9 -0
  23. package/api/integration/authoring-checks.type.mjs +14 -1
  24. package/api/integration/check/check.d.mts +96 -0
  25. package/api/integration/check/check.mjs +357 -0
  26. package/api/integration/check/check.test.mjs +489 -0
  27. package/api/integration/integration-check.type.d.mts +118 -0
  28. package/api/integration/integration-check.type.mjs +84 -0
  29. package/api/integration/integrationCheck.doc.d.mts +10 -0
  30. package/api/integration/integrationCheck.doc.mjs +73 -0
  31. package/api/integration/integrationComponentConflicts.doc.mjs +36 -5
  32. package/api/integration/pack-check.mjs +48 -3
  33. package/api/integration/pack-check.test.mjs +247 -0
  34. package/api/integration/validate-integration.mjs +8 -0
  35. package/api/integration/validate-integration.test.mjs +11 -0
  36. package/api/integration/validate-integration.type.d.mts +8 -0
  37. package/api/integration/validate-integration.type.mjs +12 -1
  38. package/api/integration/validateIntegration.doc.mjs +17 -3
  39. package/api/search/search.integration-check.test.mjs +24 -0
  40. package/api/search/search.mjs +20 -8
  41. package/api/theme/build/build.family.test.mjs +54 -6
  42. package/api/theme/build/build.mjs +114 -17
  43. package/api/theme/themeBuild.doc.mjs +4 -4
  44. package/assets/docs/README.md +10 -0
  45. package/assets/docs/author-a-theme.doc.mjs +1 -1
  46. package/assets/docs/getting-started.doc.mjs +3 -3
  47. package/assets/docs/theme.doc.dense.mjs +1 -1
  48. package/assets/docs/theme.doc.mjs +1 -1
  49. package/assets/docs/theme.doc.zh.mjs +1 -1
  50. package/assets/docs/tree/add-a-component.doc.mjs +10 -0
  51. package/assets/docs/tree/block-template.doc.mjs +2 -2
  52. package/assets/docs/tree/check-the-props.doc.mjs +196 -0
  53. package/assets/docs/tree/check-your-docs.doc.mjs +44 -3
  54. package/assets/docs/tree/checks.doc.mjs +21 -13
  55. package/assets/docs/tree/component-family.doc.mjs +5 -1
  56. package/assets/docs/tree/component-imports.doc.mjs +1 -1
  57. package/assets/docs/tree/describe-the-component.doc.mjs +1 -1
  58. package/assets/docs/tree/links.doc.mjs +2 -2
  59. package/assets/docs/tree/page-template.doc.mjs +1 -1
  60. package/assets/docs/tree/quick-start.doc.mjs +1 -1
  61. package/assets/docs/tree/replace-a-core-template.doc.mjs +1 -1
  62. package/assets/docs/tree/single-component.doc.mjs +3 -2
  63. package/assets/docs/tree/troubleshooting.doc.mjs +9 -1
  64. package/assets/docs/tree/type-scale.doc.mjs +3 -3
  65. package/assets/docs/tree/versioning.doc.mjs +1 -0
  66. package/assets/docs/tree/write-the-template-file.doc.mjs +2 -1
  67. package/assets/docs/use-a-theme.doc.mjs +2 -2
  68. package/assets/theme.template.ts +1 -1
  69. package/authoring/doctypes/component/component.doc.mjs +1 -1
  70. package/authoring/doctypes/template/integration-template.doc.d.mts +11 -0
  71. package/authoring/doctypes/template/integration-template.doc.mjs +49 -0
  72. package/authoring/doctypes/template/parse.d.mts +6 -2
  73. package/authoring/doctypes/template/parse.mjs +3 -1
  74. package/authoring/doctypes/template/template.doc.mjs +1 -1
  75. package/authoring/doctypes/template/type.ts +22 -0
  76. package/authoring/index.d.ts +1 -0
  77. package/authoring/integration/integration.doc.mjs +8 -1
  78. package/authoring/integration/parse.test.mjs +10 -0
  79. package/authoring/integration/schema.d.mts +2 -0
  80. package/authoring/integration/schema.mjs +1 -0
  81. package/authoring/integration/type.ts +4 -0
  82. package/clients/cli/commands/build-theme.batch-report.test.mjs +3 -1
  83. package/clients/cli/commands/build-theme.mjs +1 -1
  84. package/clients/cli/commands/build-theme.path-safety.test.mjs +155 -21
  85. package/clients/cli/commands/doctor-integration-components.doc.mjs +15 -4
  86. package/clients/cli/commands/doctor-integration-validate.doc.mjs +11 -2
  87. package/clients/cli/commands/doctor-integration.test.mjs +837 -0
  88. package/clients/cli/commands/doctor.mjs +190 -0
  89. package/clients/cli/commands/integration-check.doc.mjs +85 -0
  90. package/clients/cli/commands/integration-check.test.mjs +267 -0
  91. package/clients/cli/commands/integration.doc.mjs +6 -2
  92. package/clients/cli/commands/integration.mjs +107 -1
  93. package/clients/cli/commands/text-json-parity.test.mjs +5 -0
  94. package/clients/cli/commands/theme-build.doc.mjs +2 -2
  95. package/clients/cli/index.mjs +1 -0
  96. package/foundation/config/project.mjs +82 -0
  97. package/foundation/discovery/authoring-self-docs.mjs +1 -0
  98. package/foundation/discovery/component-discovery.d.mts +108 -17
  99. package/foundation/discovery/component-discovery.export-names.test.mjs +469 -2
  100. package/foundation/discovery/component-discovery.mjs +380 -47
  101. package/foundation/discovery/component-replacement.test.mjs +20 -0
  102. package/foundation/discovery/doc-types.d.mts +157 -0
  103. package/foundation/discovery/doc-types.mjs +1224 -0
  104. package/foundation/discovery/doc-types.test.mjs +652 -0
  105. package/foundation/discovery/docs-discovery.d.mts +81 -0
  106. package/foundation/discovery/docs-discovery.mjs +57 -0
  107. package/foundation/discovery/integration-doc-files.d.mts +22 -0
  108. package/foundation/discovery/integration-doc-files.mjs +231 -0
  109. package/foundation/discovery/integration-doc-files.test.mjs +246 -0
  110. package/foundation/discovery/module-exports.d.mts +6 -0
  111. package/foundation/discovery/module-exports.mjs +15 -2
  112. package/foundation/discovery/package-tsconfig.d.mts +14 -0
  113. package/foundation/discovery/package-tsconfig.mjs +228 -0
  114. package/foundation/discovery/package-tsconfig.test.mjs +210 -0
  115. package/foundation/discovery/project-typescript.d.mts +23 -0
  116. package/foundation/discovery/project-typescript.mjs +61 -0
  117. package/foundation/discovery/prop-drift.d.mts +278 -0
  118. package/foundation/discovery/prop-drift.mjs +2263 -0
  119. package/foundation/discovery/prop-drift.test.mjs +2219 -0
  120. package/foundation/discovery/type-match.d.mts +53 -0
  121. package/foundation/discovery/type-match.mjs +660 -0
  122. package/foundation/discovery/type-match.test.mjs +169 -0
  123. package/foundation/discovery/type-text.d.mts +68 -0
  124. package/foundation/discovery/type-text.mjs +658 -0
  125. package/foundation/discovery/type-text.test.mjs +196 -0
  126. package/foundation/discovery/unresolved-references.d.mts +62 -0
  127. package/foundation/discovery/unresolved-references.mjs +790 -0
  128. package/foundation/discovery/unresolved-references.test.mjs +567 -0
  129. package/foundation/doc-compiler/doc-loads.test.mjs +6 -0
  130. package/foundation/doc-compiler/links.d.mts +9 -5
  131. package/foundation/doc-compiler/links.mjs +4 -2
  132. package/foundation/integrations/contribution-inventory.mjs +6 -1
  133. package/foundation/response/response-types.doc.mjs +7 -2
  134. package/package.json +9 -9
@@ -0,0 +1,49 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file SchemaDoc for the `IntegrationTemplateDoc` doc-type — the template
5
+ * metadata an integration package ships: TemplateDoc, with the package's own
6
+ * `category`. Colocated with the type it describes (`type.ts`).
7
+ * @position packages/cli/authoring/doctypes/template — doc-type documentation
8
+ */
9
+
10
+ /** @type {import('@astryxdesign/cli/authoring').SchemaDoc} */
11
+ export const doc = {
12
+ type: 'schema',
13
+ name: 'integration-template-doc',
14
+ displayName: 'IntegrationTemplateDoc',
15
+ namespace: 'authoring',
16
+ description:
17
+ "The doc-type for an integration package's template metadata. It has every " +
18
+ "TemplateDoc field and rule, except that `category` is the package's own " +
19
+ 'string instead of a TemplateCategory value.',
20
+ appliesTo: '<Name>.doc.mjs',
21
+ fields: [
22
+ {
23
+ name: 'category',
24
+ type: 'string',
25
+ description:
26
+ "The package's own category. Bare `astryx template` groups the package's templates by it.",
27
+ },
28
+ ],
29
+ examples: [
30
+ {
31
+ label: 'Block template in an integration package',
32
+ code: `/** @type {import('@astryxdesign/cli/authoring').IntegrationTemplateDoc} */
33
+ export default {
34
+ type: 'block',
35
+ name: 'acme-pricing-table',
36
+ displayName: 'Pricing Table',
37
+ description: 'Plans side by side, with the chosen plan highlighted.',
38
+ aspectRatio: 16 / 9,
39
+ category: 'Acme - Commerce',
40
+ };`,
41
+ },
42
+ ],
43
+ notes: [
44
+ {
45
+ type: 'prose',
46
+ text: "Every other field, and the page and block variants, are TemplateDoc's. Core's own templates use TemplateDoc, whose `category` is a TemplateCategory value. `astryx doctor integration validate` checks an integration's template docs against IntegrationTemplateDoc.",
47
+ },
48
+ ],
49
+ };
@@ -18,7 +18,9 @@ export type BlockTemplateDoc = import("./type.js").BlockTemplateDoc;
18
18
  * Templates as they load. Integration templates already published omit
19
19
  * `displayName` and `aspectRatio` and group themselves under their own
20
20
  * `category`, so the loader accepts those (template discovery falls back to an
21
- * aspect ratio of 1). The published types keep asking authors for all three.
21
+ * aspect ratio of 1). The published `TemplateDoc` keeps asking authors for all
22
+ * three; `IntegrationTemplateDoc`, for an integration package's templates,
23
+ * takes the package's own `category`.
22
24
  */
23
25
  export type LoadedPageTemplateDoc = Omit<PageTemplateDoc, "displayName" | "category"> & {
24
26
  displayName?: string;
@@ -28,7 +30,9 @@ export type LoadedPageTemplateDoc = Omit<PageTemplateDoc, "displayName" | "categ
28
30
  * Templates as they load. Integration templates already published omit
29
31
  * `displayName` and `aspectRatio` and group themselves under their own
30
32
  * `category`, so the loader accepts those (template discovery falls back to an
31
- * aspect ratio of 1). The published types keep asking authors for all three.
33
+ * aspect ratio of 1). The published `TemplateDoc` keeps asking authors for all
34
+ * three; `IntegrationTemplateDoc`, for an integration package's templates,
35
+ * takes the package's own `category`.
32
36
  */
33
37
  export type LoadedBlockTemplateDoc = Omit<BlockTemplateDoc, "displayName" | "aspectRatio" | "category"> & {
34
38
  displayName?: string;
@@ -78,7 +78,9 @@ const blockTemplateSchema = z
78
78
  * Templates as they load. Integration templates already published omit
79
79
  * `displayName` and `aspectRatio` and group themselves under their own
80
80
  * `category`, so the loader accepts those (template discovery falls back to an
81
- * aspect ratio of 1). The published types keep asking authors for all three.
81
+ * aspect ratio of 1). The published `TemplateDoc` keeps asking authors for all
82
+ * three; `IntegrationTemplateDoc`, for an integration package's templates,
83
+ * takes the package's own `category`.
82
84
  *
83
85
  * @typedef {Omit<PageTemplateDoc, 'displayName' | 'category'>
84
86
  * & {displayName?: string, category?: string}} LoadedPageTemplateDoc
@@ -162,7 +162,7 @@ export default {
162
162
  notes: [
163
163
  {
164
164
  type: 'prose',
165
- text: 'When it loads, a template may leave out `displayName` and `aspectRatio` and use its own `category`, as integration templates already published do; a block with no `aspectRatio` previews at 1. Write to the type anyway; it is the contract.',
165
+ text: 'When it loads, a template may leave out `displayName` and `aspectRatio` and use its own `category`, as integration templates already published do; a block with no `aspectRatio` previews at 1. Write to the type anyway; it is the contract, and `astryx doctor integration validate` warns about an integration template that leaves out `displayName` or `aspectRatio`.',
166
166
  },
167
167
  {
168
168
  type: 'prose',
@@ -201,3 +201,25 @@ export type TemplateCategory =
201
201
  | 'Shell - Blank';
202
202
 
203
203
  export type TemplateDoc = PageTemplateDoc | BlockTemplateDoc;
204
+
205
+ /** A page template an integration package ships, with its own `category`. */
206
+ export interface IntegrationPageTemplateDoc extends Omit<
207
+ PageTemplateDoc,
208
+ 'category'
209
+ > {
210
+ category?: string;
211
+ }
212
+
213
+ /** A block template an integration package ships, with its own `category`. */
214
+ export interface IntegrationBlockTemplateDoc extends Omit<
215
+ BlockTemplateDoc,
216
+ 'category'
217
+ > {
218
+ category?: string;
219
+ }
220
+
221
+ /** The doc-type for an integration package's template metadata: TemplateDoc,
222
+ * with `category` the package's own string (bare `astryx template` groups
223
+ * the package's templates by it) instead of a TemplateCategory value. */
224
+ export type IntegrationTemplateDoc =
225
+ IntegrationPageTemplateDoc | IntegrationBlockTemplateDoc;
@@ -28,6 +28,7 @@ export type {HookDoc} from './doctypes/types.js'; // useToast.doc.mjs
28
28
  export type {FunctionDoc} from './doctypes/types.js'; // search.doc.mjs (hook | api)
29
29
  export type {ReferenceDoc} from './doctypes/types.js'; // theming.doc.mjs
30
30
  export type {TemplateDoc} from './doctypes/types.js'; // Foo.doc.mjs
31
+ export type {IntegrationTemplateDoc} from './doctypes/types.js'; // an integration's Foo.doc.mjs
31
32
  export type {SchemaDoc} from './doctypes/types.js'; // config.doc.mjs (object shape)
32
33
  export type {CommandDoc} from './doctypes/types.js'; // search.doc.mjs (CLI command)
33
34
  export type {EnumDoc} from './doctypes/types.js'; // error-codes.doc.mjs (vocabulary)
@@ -74,6 +74,13 @@ export const doc = {
74
74
  description: 'Where to file issues/feedback for this integration.',
75
75
  example: "'https://github.com/acme/widgets/issues'",
76
76
  },
77
+ {
78
+ name: 'docsiteUrl',
79
+ type: 'string',
80
+ description:
81
+ "Link to this integration's docs site, such as its home page. Catalogs that list the package may show it.",
82
+ example: "'https://widgets.acme.dev'",
83
+ },
77
84
  ],
78
85
  examples: [
79
86
  {
@@ -118,7 +125,7 @@ export const doc = {
118
125
  text:
119
126
  'Validate the manifest with `astryx doctor integration validate`. At the ' +
120
127
  'load boundary, a known field of the wrong type is an error, issuesUrl ' +
121
- 'must be a valid URL, and unknown fields become warnings so an older CLI ' +
128
+ 'and docsiteUrl must be valid URLs, and unknown fields become warnings so an older CLI ' +
122
129
  'can still load the fields it understands. Before publishing, also run ' +
123
130
  '`templates`, `components`, and `docs` under the same `doctor integration` ' +
124
131
  'group. Those leaves compare authored identities with Core and explain ' +
@@ -87,6 +87,16 @@ describe('parseIntegration (load boundary)', () => {
87
87
  expect(reason({issuesUrl: 'nope'})).toContain('issuesUrl');
88
88
  });
89
89
 
90
+ it('accepts a docsiteUrl as a known key', () => {
91
+ const manifest = {docsiteUrl: 'https://widgets.example.com'};
92
+ expect(parseIntegration(manifest)).toEqual(manifest);
93
+ expect(unknownIntegrationKeys(manifest)).toEqual([]);
94
+ });
95
+
96
+ it('rejects a non-URL docsiteUrl', () => {
97
+ expect(reason({docsiteUrl: 'nope'})).toContain('docsiteUrl');
98
+ });
99
+
90
100
  it('rejects a noncanonical providerId', () => {
91
101
  expect(reason({providerId: 'Acme Widgets'})).toContain('providerId');
92
102
  });
@@ -39,6 +39,7 @@ export const integrationBaseSchema: z.ZodObject<{
39
39
  docs: z.ZodOptional<z.ZodString>;
40
40
  themes: z.ZodOptional<z.ZodString>;
41
41
  issuesUrl: z.ZodOptional<z.ZodString>;
42
+ docsiteUrl: z.ZodOptional<z.ZodString>;
42
43
  }, z.core.$strip>;
43
44
  export const integrationSchema: z.ZodObject<{
44
45
  providerId: z.ZodOptional<z.ZodString>;
@@ -48,6 +49,7 @@ export const integrationSchema: z.ZodObject<{
48
49
  docs: z.ZodOptional<z.ZodString>;
49
50
  themes: z.ZodOptional<z.ZodString>;
50
51
  issuesUrl: z.ZodOptional<z.ZodString>;
52
+ docsiteUrl: z.ZodOptional<z.ZodString>;
51
53
  agentDocs: z.ZodOptional<z.ZodObject<{
52
54
  append: z.ZodOptional<z.ZodReadonly<z.ZodArray<z.ZodString>>>;
53
55
  }, z.core.$strip>>;
@@ -78,6 +78,7 @@ export const integrationBaseSchema = z.object({
78
78
  docs: z.string().optional(),
79
79
  themes: z.string().optional(),
80
80
  issuesUrl: z.string().url().optional(),
81
+ docsiteUrl: z.string().url().optional(),
81
82
  });
82
83
 
83
84
  // Unknown top-level keys are stripped rather than rejected. `.strict()` made a key from a
@@ -44,6 +44,10 @@ export interface AstryxIntegration {
44
44
  };
45
45
  /** Where to file issues/feedback for this integration. */
46
46
  issuesUrl?: string;
47
+ /** Link to this integration's docs site, such as its home page. The CLI
48
+ * accepts it as a known field; catalogs that list the package may show
49
+ * it. */
50
+ docsiteUrl?: string;
47
51
  }
48
52
 
49
53
  /**
@@ -168,7 +168,9 @@ describe('theme build report', () => {
168
168
  }
169
169
  expect(count(out, 'Install in your app')).toBe(1);
170
170
  expect(count(out, 'Or with a <link> tag:')).toBe(1);
171
- expect(count(out, "import { Theme } from '@astryxdesign/core';")).toBe(2);
171
+ expect(
172
+ count(out, "import { Theme } from '@astryxdesign/core/theme';"),
173
+ ).toBe(2);
172
174
  expect(count(out, '[note]')).toBe(1);
173
175
  expect(count(out, 'note: Font')).toBe(0);
174
176
  expect(count(out, 'fonts.googleapis.com/css2')).toBe(1);
@@ -397,7 +397,7 @@ function printThemeApp(result) {
397
397
  emit(
398
398
  text('Wire the generated module once in the app root:'),
399
399
  code(
400
- "import {Theme} from '@astryxdesign/core';\n" +
400
+ "import {Theme} from '@astryxdesign/core/theme';\n" +
401
401
  `import {themes, defaultThemeSlug} from './${stem}';\n\n` +
402
402
  '<Theme theme={themes[defaultThemeSlug]}>\n <App />\n</Theme>',
403
403
  ),
@@ -9,9 +9,12 @@
9
9
  * - Multi-file write is atomic: if one file fails, none are left behind.
10
10
  * - An `--out` escape, or an output directory that cannot be created, fails
11
11
  * with a registered error code.
12
- * - Every output stays inside the working directory: an absolute `--out`
13
- * path or a theme file outside it fails before anything is written, while
14
- * absolute paths inside it build, also through a symlinked spelling.
12
+ * - Every output a build writes stays inside the working directory: an
13
+ * absolute `--out` path or a theme file outside it fails before anything
14
+ * is written, while absolute paths inside it build, also through a
15
+ * symlinked spelling. `--check` writes nothing, so it compares those
16
+ * outside outputs read-only, and a stale report for them ends with a
17
+ * remedy that works: the directory to build and check from.
15
18
  *
16
19
  * Runs the CLI in-process (shared runCli harness) against a tiny synthetic theme file.
17
20
  */
@@ -25,6 +28,7 @@ import {runCli} from '../../../test-utils/run-cli.mjs';
25
28
  import {themeBuild} from '../../../api/theme/build/build.mjs';
26
29
  import {AstryxError} from '../../../api/error.mjs';
27
30
  import {ERROR_CODES} from '../../../foundation/response/error-codes.mjs';
31
+ import {getCliInvocation} from '../../../foundation/env/package-manager.mjs';
28
32
 
29
33
  function writeTheme(dir, name) {
30
34
  fs.mkdirSync(dir, {recursive: true});
@@ -62,7 +66,9 @@ describe('theme build path safety', () => {
62
66
  );
63
67
 
64
68
  expect(result.code).not.toBe(0);
65
- expect(result.stderr + result.stdout).toMatch(/path separator|invalid theme name/i);
69
+ expect(result.stderr + result.stdout).toMatch(
70
+ /path separator|invalid theme name/i,
71
+ );
66
72
 
67
73
  // The traversal target must not exist.
68
74
  expect(fs.existsSync(path.join(tmpDir, 'escaped.js'))).toBe(false);
@@ -95,7 +101,10 @@ describe('theme build path safety', () => {
95
101
 
96
102
  const themeFile = writeTheme(themesDir, '../leak');
97
103
 
98
- await runCli(['theme', 'build', path.relative(project, themeFile)], project);
104
+ await runCli(
105
+ ['theme', 'build', path.relative(project, themeFile)],
106
+ project,
107
+ );
99
108
 
100
109
  // Neither the CSS nor any sibling JS should have been written.
101
110
  const entries = fs.readdirSync(themesDir).sort();
@@ -117,41 +126,88 @@ describe('theme build output-path failures carry registered codes', () => {
117
126
  return {result, envelope: JSON.parse(result.stdout)};
118
127
  }
119
128
 
120
- it('reports an --out path outside the project as ERR_PATH_TRAVERSAL', async () => {
129
+ it('reports a relative --out path outside the project as ERR_PATH_TRAVERSAL, with --check too', async () => {
121
130
  const project = path.join(tmpDir, 'project');
122
131
  const themeFile = writeTheme(path.join(project, 'themes'), 'ocean');
123
132
 
133
+ for (const extra of [[], ['--check']]) {
134
+ const {result, envelope} = await buildJson(
135
+ [
136
+ path.relative(project, themeFile),
137
+ '--out',
138
+ '../escaped.css',
139
+ ...extra,
140
+ ],
141
+ project,
142
+ );
143
+
144
+ expect(result.code).toBe(1);
145
+ expect(envelope.code).toBe(ERROR_CODES.ERR_PATH_TRAVERSAL);
146
+ expect(registered.has(envelope.code)).toBe(true);
147
+ }
148
+ expect(fs.existsSync(path.join(tmpDir, 'escaped.css'))).toBe(false);
149
+ });
150
+
151
+ it('reports an absolute --out path outside the project as ERR_PATH_TRAVERSAL', async () => {
152
+ const project = path.join(tmpDir, 'project');
153
+ const outside = path.join(tmpDir, 'outside');
154
+ fs.mkdirSync(outside, {recursive: true});
155
+ const themeFile = writeTheme(path.join(project, 'themes'), 'ocean');
156
+ const out = path.join(outside, 'ocean.css');
157
+
124
158
  const {result, envelope} = await buildJson(
125
- [path.relative(project, themeFile), '--out', '../escaped.css'],
159
+ [path.relative(project, themeFile), '--out', out],
126
160
  project,
127
161
  );
128
162
 
129
163
  expect(result.code).toBe(1);
130
164
  expect(envelope.code).toBe(ERROR_CODES.ERR_PATH_TRAVERSAL);
131
- expect(registered.has(envelope.code)).toBe(true);
132
- expect(fs.existsSync(path.join(tmpDir, 'escaped.css'))).toBe(false);
165
+ expect(fs.readdirSync(outside)).toEqual([]);
133
166
  });
134
167
 
135
- it('reports an absolute --out path outside the project as ERR_PATH_TRAVERSAL, with --check too', async () => {
168
+ it('checks an absolute --out path outside the project read-only', async () => {
136
169
  const project = path.join(tmpDir, 'project');
137
170
  const outside = path.join(tmpDir, 'outside');
138
171
  fs.mkdirSync(outside, {recursive: true});
139
- const themeFile = writeTheme(path.join(project, 'themes'), 'ocean');
172
+ const themeFile = path.relative(
173
+ project,
174
+ writeTheme(path.join(project, 'themes'), 'ocean'),
175
+ );
140
176
  const out = path.join(outside, 'ocean.css');
141
177
 
142
- for (const extra of [[], ['--check']]) {
143
- const {result, envelope} = await buildJson(
144
- [path.relative(project, themeFile), '--out', out, ...extra],
145
- project,
146
- );
178
+ const missing = await buildJson(
179
+ [themeFile, '--out', out, '--check'],
180
+ project,
181
+ );
182
+ expect(missing.result.code).toBe(1);
183
+ expect(missing.envelope.type).toBe('theme.build.check');
184
+ expect(missing.envelope.data.upToDate).toBe(false);
185
+ expect(fs.readdirSync(outside)).toEqual([]);
147
186
 
148
- expect(result.code).toBe(1);
149
- expect(envelope.code).toBe(ERROR_CODES.ERR_PATH_TRAVERSAL);
187
+ // Outputs built inside the project compare equal once copied outside:
188
+ // only the volatile Command line, which --check ignores, names --out.
189
+ const dist = path.join(project, 'dist');
190
+ const built = await buildJson(
191
+ [themeFile, '--out', path.join(dist, 'ocean.css')],
192
+ project,
193
+ );
194
+ expect(built.result.code).toBe(0);
195
+ for (const file of fs.readdirSync(dist)) {
196
+ fs.copyFileSync(path.join(dist, file), path.join(outside, file));
150
197
  }
151
- expect(fs.readdirSync(outside)).toEqual([]);
198
+ const copied = fs.readdirSync(outside).sort();
199
+
200
+ const current = await buildJson(
201
+ [themeFile, '--out', out, '--check'],
202
+ project,
203
+ );
204
+ expect(current.result.code).toBe(0);
205
+ expect(current.envelope.type).toBe('theme.build.check');
206
+ expect(current.envelope.data.upToDate).toBe(true);
207
+ expect(fs.readdirSync(outside).sort()).toEqual(copied);
152
208
  });
153
209
 
154
- it('reports a theme file outside the project as ERR_PATH_TRAVERSAL, because its outputs go beside it', async () => {
210
+ it('reports a theme file outside the project as ERR_PATH_TRAVERSAL, because its outputs go beside it; --check only compares them', async () => {
155
211
  const project = path.join(tmpDir, 'project');
156
212
  fs.mkdirSync(project, {recursive: true});
157
213
  const shared = path.join(tmpDir, 'shared');
@@ -162,10 +218,82 @@ describe('theme build output-path failures carry registered codes', () => {
162
218
 
163
219
  expect(result.code).toBe(1);
164
220
  expect(envelope.code).toBe(ERROR_CODES.ERR_PATH_TRAVERSAL);
221
+
222
+ const checked = await buildJson([file, '--check'], project);
223
+ expect(checked.result.code).toBe(1);
224
+ expect(checked.envelope.type).toBe('theme.build.check');
225
+ expect(checked.envelope.data.upToDate).toBe(false);
165
226
  }
166
227
  expect(fs.readdirSync(shared)).toEqual(['theme.mjs']);
167
228
  });
168
229
 
230
+ /**
231
+ * Read the remedy that ends a stale report for outputs outside cwd: the
232
+ * directory to run from, then the build and check arguments after `theme
233
+ * build`. Each command starts with the install-aware invocation for it.
234
+ * @param {string} stderr
235
+ */
236
+ function readRemedy(stderr) {
237
+ const lines = stderr.split('\n').map(line => line.trim());
238
+ const cd = lines.find(line => line.startsWith('cd '));
239
+ expect(cd).toBeDefined();
240
+ const dir = /** @type {string} */ (cd).slice('cd '.length);
241
+ const stem = `${getCliInvocation(dir)} theme build `;
242
+ const commands = lines.filter(line => line.startsWith(stem));
243
+ expect(commands).toHaveLength(2);
244
+ const [build, check] = commands.map(line =>
245
+ line.slice(stem.length).split(' '),
246
+ );
247
+ expect(check).toEqual([...build, '--check']);
248
+ return {dir, build, check};
249
+ }
250
+
251
+ it('ends a stale report for outputs outside cwd with a remedy that works, and keeps the report for outputs inside it', async () => {
252
+ const project = path.join(tmpDir, 'project');
253
+ const inside = path.relative(
254
+ project,
255
+ writeTheme(path.join(project, 'themes'), 'ocean'),
256
+ );
257
+ const shared = writeTheme(path.join(tmpDir, 'shared'), 'ocean');
258
+ const outside = path.join(tmpDir, 'outside');
259
+
260
+ for (const args of [
261
+ [path.relative(project, shared)],
262
+ [inside, '--out', path.join(outside, 'ocean.css')],
263
+ ]) {
264
+ const stale = await runCli(
265
+ ['theme', 'build', ...args, '--check'],
266
+ project,
267
+ );
268
+ expect(stale.code).toBe(1);
269
+ // The old remedy ran the build from cwd, which refuses these outputs.
270
+ expect(stale.stderr).not.toContain('Rebuild with:');
271
+ expect(stale.stderr).not.toContain(`theme build ${args.join(' ')}`);
272
+
273
+ // Following the remedy builds, and the check from there passes.
274
+ const remedy = readRemedy(stale.stderr);
275
+ const built = await runCli(
276
+ ['theme', 'build', ...remedy.build],
277
+ remedy.dir,
278
+ );
279
+ expect(built.code).toBe(0);
280
+ const checked = await runCli(
281
+ ['--json', 'theme', 'build', ...remedy.check],
282
+ remedy.dir,
283
+ );
284
+ expect(checked.code).toBe(0);
285
+ expect(JSON.parse(checked.stdout).data.upToDate).toBe(true);
286
+ }
287
+
288
+ // Outputs inside cwd keep the plain rebuild line.
289
+ const local = await runCli(['theme', 'build', inside, '--check'], project);
290
+ expect(local.code).toBe(1);
291
+ expect(local.stderr).toContain(
292
+ `Rebuild with: astryx theme build ${inside}`,
293
+ );
294
+ expect(local.stderr).not.toMatch(/^\s*cd /m);
295
+ });
296
+
169
297
  it('builds absolute paths inside the project, also when they reach it through a symlink', async () => {
170
298
  const project = path.join(tmpDir, 'project');
171
299
  const themeFile = writeTheme(path.join(project, 'themes'), 'ocean');
@@ -243,7 +371,7 @@ describe('theme build output-path failures carry registered codes', () => {
243
371
  });
244
372
  });
245
373
 
246
- it('themeBuild() rejects an output outside cwd: an absolute out path, or the default beside a theme file outside cwd', async () => {
374
+ it('themeBuild() rejects an output outside cwd, an absolute out path or the default beside a theme file outside cwd, and checks it read-only', async () => {
247
375
  const project = path.join(tmpDir, 'project');
248
376
  const inside = writeTheme(path.join(project, 'themes'), 'ocean');
249
377
  const shared = path.join(tmpDir, 'shared');
@@ -257,6 +385,12 @@ describe('theme build output-path failures carry registered codes', () => {
257
385
  await expect(
258
386
  themeBuild(file, options, {cwd: project}),
259
387
  ).rejects.toMatchObject({code: ERROR_CODES.ERR_PATH_TRAVERSAL});
388
+ await expect(
389
+ themeBuild(file, {...options, check: true}, {cwd: project}),
390
+ ).resolves.toMatchObject({
391
+ type: 'theme.build.check',
392
+ data: {upToDate: false},
393
+ });
260
394
  }
261
395
  expect(fs.existsSync(outDir)).toBe(false);
262
396
  expect(fs.readdirSync(shared)).toEqual(['theme.mjs']);
@@ -8,7 +8,8 @@ export const doc = {
8
8
  name: 'doctor integration components',
9
9
  displayName: 'astryx doctor integration components',
10
10
  namespace: 'cli/commands',
11
- summary: 'Check integration component names and replacements against Core',
11
+ summary:
12
+ 'Check integration component names, replacements, and documented props',
12
13
  description:
13
14
  'Compares one local or installed integration with Core component names. A ' +
14
15
  'conflict is allowed and exits successfully, but the report recommends ' +
@@ -17,7 +18,14 @@ export const doc = {
17
18
  'range that turns replacement on, a missing Core target, an invalid value, a ' +
18
19
  'component named after a different Core component, or two replacements for one ' +
19
20
  'Core component is an error. In a package without that range they are warnings, ' +
20
- 'with a warning for each such component, naming the range to declare.',
21
+ 'with a warning for each such component, naming the range to declare. ' +
22
+ "Then it compares each component doc's props with the component's TypeScript " +
23
+ "props, using the project's own `typescript`, and warns about a documented prop " +
24
+ 'the code lacks, a prop the package declares that the doc omits, a wrong ' +
25
+ '`required`, and a type that does not match where both sides read exactly ' +
26
+ '(it lists every other documented type as not compared, with the reason). ' +
27
+ 'Without TypeScript it prints one ' +
28
+ 'line saying the comparison was skipped.',
21
29
  fn: 'integrationComponentConflicts',
22
30
  args: [
23
31
  {
@@ -29,7 +37,10 @@ export const doc = {
29
37
  },
30
38
  ],
31
39
  examples: [
32
- {label: 'Check the local integration', cli: 'astryx doctor integration components'},
40
+ {
41
+ label: 'Check the local integration',
42
+ cli: 'astryx doctor integration components',
43
+ },
33
44
  {
34
45
  label: 'Check an installed integration',
35
46
  cli: 'astryx doctor integration components @acme/widgets --json',
@@ -38,7 +49,7 @@ export const doc = {
38
49
  exitCodes: [
39
50
  {
40
51
  code: 0,
41
- when: 'the check completed; component name conflicts, and replaces findings for a package without the CLI range, are warnings',
52
+ when: 'the check completed; component name conflicts, replaces findings for a package without the CLI range, and props that differ from the code are warnings',
42
53
  },
43
54
  {
44
55
  code: 1,
@@ -14,7 +14,13 @@ export const doc = {
14
14
  description:
15
15
  'Validates one integration at a time: the local package rooted at cwd, or an ' +
16
16
  'installed package resolved by name. It schema-checks the manifest, verifies ' +
17
- 'every declared contribution root, and reports every finding. Safe as a CI gate.',
17
+ 'every declared contribution root, and reports every finding. Safe as a CI gate. ' +
18
+ 'It then checks every doc the package ships under its roots against the ' +
19
+ "published authoring type for its kind, with the project's own `typescript`, and " +
20
+ 'warns about each field that does not match, naming the field and a fix. A doc ' +
21
+ 'it cannot fully check is listed with the reason, and test fixtures under ' +
22
+ '`__tests__` in the components or docs root are counted, not checked. Without ' +
23
+ 'TypeScript it prints one line saying the docs were not type-checked.',
18
24
  fn: 'validateIntegration',
19
25
  args: [
20
26
  {
@@ -36,7 +42,10 @@ export const doc = {
36
42
  },
37
43
  ],
38
44
  exitCodes: [
39
- {code: 0, when: 'no error-severity issues (warnings are allowed)'},
45
+ {
46
+ code: 0,
47
+ when: 'no error-severity issues (warnings, including doc type findings, are allowed)',
48
+ },
40
49
  {code: 1, when: 'one or more error-severity issues'},
41
50
  ],
42
51
  related: ['doctor integration templates', 'template'],