@astryxdesign/cli 0.7.2-canary.db3a2e7 → 0.7.2-canary.e26723d

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 (142) hide show
  1. package/README.md +7 -7
  2. package/api/build/help/help.mjs +1 -1
  3. package/api/component/_adapter.d.mts +41 -1
  4. package/api/component/_adapter.mjs +93 -7
  5. package/api/component/component.mjs +22 -5
  6. package/api/component/component.test.mjs +420 -167
  7. package/api/component/integration-export-name.test.mjs +644 -0
  8. package/api/component/list/list.mjs +42 -4
  9. package/api/docs/_adapter.mjs +207 -1
  10. package/api/docs/integrationDocs.test.mjs +124 -0
  11. package/api/doctor/theme-checks.mjs +1 -1
  12. package/api/gap-report/gap-report.mjs +67 -4
  13. package/api/init/run/run.mjs +2 -2
  14. package/api/integration/_adapter.d.mts +12 -0
  15. package/api/integration/_adapter.mjs +54 -0
  16. package/api/integration/add-contribution.component-names.test.mjs +66 -0
  17. package/api/integration/add-contribution.mjs +7 -3
  18. package/api/integration/authoring-checks.mjs +11 -2
  19. package/api/integration/authoring-checks.test.mjs +39 -0
  20. package/api/integration/authoring-checks.type.d.mts +9 -0
  21. package/api/integration/authoring-checks.type.mjs +14 -1
  22. package/api/integration/integrationComponentConflicts.doc.mjs +21 -5
  23. package/api/integration/pack-check.mjs +61 -107
  24. package/api/integration/pack-check.test.mjs +359 -0
  25. package/api/integration/validate-integration.test.mjs +11 -0
  26. package/api/search/search.d.mts +9 -0
  27. package/api/search/search.mjs +108 -16
  28. package/api/search/search.test.mjs +36 -0
  29. package/api/swizzle/copy/copy.mjs +28 -14
  30. package/api/template/template.test.mjs +120 -3
  31. package/api/theme/build/build.family.test.mjs +16 -0
  32. package/api/theme/build/build.mjs +81 -22
  33. package/api/theme/themeBuild.doc.mjs +3 -3
  34. package/assets/codemods/transforms/next/__tests__/migrate-list-edge-compensation-to-edge.test.mjs +120 -0
  35. package/assets/codemods/transforms/next/index.mjs +11 -1
  36. package/assets/codemods/transforms/next/migrate-list-edge-compensation-to-edge.mjs +198 -0
  37. package/assets/docs/README.md +10 -0
  38. package/assets/docs/author-a-theme.doc.mjs +1 -1
  39. package/assets/docs/getting-started.doc.mjs +3 -3
  40. package/assets/docs/theme.doc.dense.mjs +1 -1
  41. package/assets/docs/theme.doc.mjs +1 -1
  42. package/assets/docs/theme.doc.zh.mjs +1 -1
  43. package/assets/docs/tree/add-a-component.doc.mjs +10 -0
  44. package/assets/docs/tree/check-the-props.doc.mjs +145 -0
  45. package/assets/docs/tree/checks.doc.mjs +4 -4
  46. package/assets/docs/tree/component-family.doc.mjs +5 -1
  47. package/assets/docs/tree/component-imports.doc.mjs +8 -0
  48. package/assets/docs/tree/describe-the-component.doc.mjs +1 -1
  49. package/assets/docs/tree/links.doc.mjs +2 -2
  50. package/assets/docs/tree/single-component.doc.mjs +3 -2
  51. package/assets/docs/tree/troubleshooting.doc.mjs +9 -1
  52. package/assets/docs/tree/type-scale.doc.mjs +3 -3
  53. package/assets/docs/tree/versioning.doc.mjs +1 -0
  54. package/assets/docs/tree/write-the-template-file.doc.mjs +2 -1
  55. package/assets/docs/use-a-theme.doc.mjs +2 -2
  56. package/assets/docs/working-with-ai.doc.mjs +43 -4
  57. package/assets/templates/blocks/components/Drawer/DrawerForm.doc.mjs +24 -0
  58. package/assets/templates/blocks/components/Drawer/DrawerForm.tsx +62 -0
  59. package/assets/templates/blocks/components/Drawer/DrawerScrim.doc.mjs +22 -0
  60. package/assets/templates/blocks/components/Drawer/DrawerScrim.tsx +68 -0
  61. package/assets/templates/blocks/components/Drawer/DrawerShowcase.doc.mjs +22 -0
  62. package/assets/templates/blocks/components/Drawer/DrawerShowcase.tsx +41 -0
  63. package/assets/templates/blocks/components/Drawer/DrawerStart.doc.mjs +21 -0
  64. package/assets/templates/blocks/components/Drawer/DrawerStart.tsx +41 -0
  65. package/assets/templates/blocks/components/Drawer/DrawerWidths.doc.mjs +22 -0
  66. package/assets/templates/blocks/components/Drawer/DrawerWidths.tsx +54 -0
  67. package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderShowcase.doc.mjs +15 -0
  68. package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderShowcase.tsx +73 -0
  69. package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderSubtitleEndContent.doc.mjs +22 -0
  70. package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderSubtitleEndContent.tsx +49 -0
  71. package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderWithCloseButton.doc.mjs +21 -0
  72. package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderWithCloseButton.tsx +42 -0
  73. package/assets/templates/pages/detail-page/page.tsx +3 -3
  74. package/assets/templates/pages/table-inbox/page.tsx +10 -8
  75. package/assets/theme.template.ts +1 -1
  76. package/authoring/doctypes/base/type.ts +6 -0
  77. package/authoring/doctypes/component/component.doc.mjs +4 -4
  78. package/authoring/doctypes/component/type.ts +6 -3
  79. package/authoring/doctypes/template/integration-template.doc.d.mts +11 -0
  80. package/authoring/doctypes/template/integration-template.doc.mjs +49 -0
  81. package/authoring/doctypes/template/parse.d.mts +6 -2
  82. package/authoring/doctypes/template/parse.mjs +3 -1
  83. package/authoring/doctypes/template/type.ts +22 -0
  84. package/authoring/index.d.ts +1 -0
  85. package/authoring/integration/integration.doc.mjs +8 -1
  86. package/authoring/integration/parse.test.mjs +10 -0
  87. package/authoring/integration/schema.d.mts +2 -0
  88. package/authoring/integration/schema.mjs +1 -0
  89. package/authoring/integration/type.ts +4 -0
  90. package/clients/cli/commands/build-theme.batch-report.test.mjs +3 -1
  91. package/clients/cli/commands/build-theme.mjs +1 -1
  92. package/clients/cli/commands/build-theme.path-safety.test.mjs +84 -0
  93. package/clients/cli/commands/build-theme.registry.test.mjs +5 -2
  94. package/clients/cli/commands/doctor-integration-components.doc.mjs +9 -3
  95. package/clients/cli/commands/doctor-integration.test.mjs +392 -0
  96. package/clients/cli/commands/doctor.mjs +74 -0
  97. package/clients/cli/commands/theme-build.doc.mjs +2 -2
  98. package/clients/cli/lib/component-format.mjs +5 -1
  99. package/foundation/config/project.mjs +82 -0
  100. package/foundation/discovery/authoring-self-docs.mjs +1 -0
  101. package/foundation/discovery/component-discovery.d.mts +190 -10
  102. package/foundation/discovery/component-discovery.export-names.test.mjs +760 -0
  103. package/foundation/discovery/component-discovery.mjs +864 -56
  104. package/foundation/discovery/component-discovery.test.mjs +96 -0
  105. package/foundation/discovery/component-replacement.mjs +18 -0
  106. package/foundation/discovery/component-replacement.test.mjs +40 -0
  107. package/foundation/discovery/doc-types.d.mts +157 -0
  108. package/foundation/discovery/doc-types.mjs +1224 -0
  109. package/foundation/discovery/doc-types.test.mjs +652 -0
  110. package/foundation/discovery/docs-discovery.d.mts +81 -0
  111. package/foundation/discovery/docs-discovery.mjs +57 -0
  112. package/foundation/discovery/module-exports.d.mts +27 -0
  113. package/foundation/discovery/module-exports.mjs +356 -31
  114. package/foundation/discovery/module-exports.test.mjs +172 -0
  115. package/foundation/discovery/package-tsconfig.d.mts +14 -0
  116. package/foundation/discovery/package-tsconfig.mjs +228 -0
  117. package/foundation/discovery/package-tsconfig.test.mjs +210 -0
  118. package/foundation/discovery/project-typescript.d.mts +23 -0
  119. package/foundation/discovery/project-typescript.mjs +61 -0
  120. package/foundation/discovery/prop-drift.d.mts +220 -0
  121. package/foundation/discovery/prop-drift.mjs +1765 -0
  122. package/foundation/discovery/prop-drift.test.mjs +1197 -0
  123. package/foundation/discovery/template-adapter.mjs +74 -5
  124. package/foundation/discovery/type-match.d.mts +53 -0
  125. package/foundation/discovery/type-match.mjs +660 -0
  126. package/foundation/discovery/type-match.test.mjs +169 -0
  127. package/foundation/discovery/type-text.d.mts +68 -0
  128. package/foundation/discovery/type-text.mjs +658 -0
  129. package/foundation/discovery/type-text.test.mjs +196 -0
  130. package/foundation/discovery/unresolved-references.d.mts +62 -0
  131. package/foundation/discovery/unresolved-references.mjs +790 -0
  132. package/foundation/discovery/unresolved-references.test.mjs +567 -0
  133. package/foundation/doc-compiler/doc-loads.test.mjs +9 -1
  134. package/foundation/doc-compiler/inputs.mjs +23 -0
  135. package/foundation/doc-compiler/inputs.test.mjs +0 -1
  136. package/foundation/doc-compiler/links.d.mts +9 -5
  137. package/foundation/doc-compiler/links.mjs +4 -2
  138. package/foundation/integrations/contribution-inventory.mjs +6 -1
  139. package/foundation/response/response-types.doc.mjs +1 -1
  140. package/foundation/xle/registry.mjs +4 -0
  141. package/foundation/xle/registry.test.mjs +28 -0
  142. package/package.json +10 -10
@@ -0,0 +1,172 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file Static reads of a module's exports, without running it: the file that
5
+ * declares an export, and an exported object literal as plain data.
6
+ */
7
+
8
+ import {afterEach, beforeEach, describe, expect, it} from 'vitest';
9
+ import * as fs from 'node:fs';
10
+ import * as os from 'node:os';
11
+ import * as path from 'node:path';
12
+ import {moduleExportSource, staticExportObject} from './module-exports.mjs';
13
+
14
+ /** @type {string} */
15
+ let dir;
16
+
17
+ beforeEach(() => {
18
+ dir = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-export-field-'));
19
+ });
20
+
21
+ afterEach(() => {
22
+ fs.rmSync(dir, {recursive: true, force: true});
23
+ });
24
+
25
+ /**
26
+ * @param {string} file
27
+ * @param {string} source
28
+ * @returns {string}
29
+ */
30
+ function write(file, source) {
31
+ const filePath = path.join(dir, file);
32
+ fs.writeFileSync(filePath, source);
33
+ return filePath;
34
+ }
35
+
36
+ const DOC_EXPORTS = ['default', 'docs'];
37
+
38
+ describe('staticExportObject', () => {
39
+ it('reads a default-exported object literal as plain data', () => {
40
+ const file = write(
41
+ 'A.doc.mjs',
42
+ "export default {type: 'component', name: 'AcmeCarousel', order: 2, hidden: false, extra: null};\n",
43
+ );
44
+ expect(staticExportObject(file, DOC_EXPORTS)).toEqual({
45
+ type: 'component',
46
+ name: 'AcmeCarousel',
47
+ order: 2,
48
+ hidden: false,
49
+ extra: null,
50
+ });
51
+ });
52
+
53
+ it('reads a named export, an export list, and the first export it looks for', () => {
54
+ const named = write(
55
+ 'B.doc.mjs',
56
+ "/** @type {object} */\nexport const docs = {\n name: 'AcmeCarousel',\n};\n",
57
+ );
58
+ const both = write(
59
+ 'C.doc.mjs',
60
+ "export const docs = {name: 'Legacy'};\nexport default {name: 'Stamped'};\n",
61
+ );
62
+ const list = write(
63
+ 'E.doc.mjs',
64
+ "const local = {name: 'AcmeCarousel'};\nexport {local as docs};\n",
65
+ );
66
+ expect(staticExportObject(named, DOC_EXPORTS)?.name).toBe('AcmeCarousel');
67
+ expect(staticExportObject(both, DOC_EXPORTS)?.name).toBe('Stamped');
68
+ expect(staticExportObject(list, DOC_EXPORTS)?.name).toBe('AcmeCarousel');
69
+ });
70
+
71
+ it('follows top-level bindings through TypeScript wrappers, at any depth', () => {
72
+ const file = write(
73
+ 'D.doc.ts',
74
+ [
75
+ "const slide = {name: 'AcmeSlide', description: 'One slide.'} as const;",
76
+ "const doc = {name: 'AcmeCarousel', components: [slide, {name: 'AcmeDots', description: 'Dots.'}]} satisfies {name: string};",
77
+ 'export default doc;',
78
+ '',
79
+ ].join('\n'),
80
+ );
81
+ expect(staticExportObject(file, DOC_EXPORTS)).toEqual({
82
+ name: 'AcmeCarousel',
83
+ components: [
84
+ {name: 'AcmeSlide', description: 'One slide.'},
85
+ {name: 'AcmeDots', description: 'Dots.'},
86
+ ],
87
+ });
88
+ });
89
+
90
+ it('leaves out what it cannot read without running the module', () => {
91
+ const file = write(
92
+ 'F.doc.mjs',
93
+ [
94
+ "const prefix = 'Acme';",
95
+ 'const shared = {group: "Media"};',
96
+ 'export default {',
97
+ ' // A computed string, a call and a spread: none is read.',
98
+ ' name: `${prefix}Carousel`,',
99
+ ' usage: makeUsage(),',
100
+ " group: 'Overridden',",
101
+ ' ...shared,',
102
+ " category: 'Media',",
103
+ " components: [...more, {name: 'AcmeSlide'}],",
104
+ '};',
105
+ '',
106
+ ].join('\n'),
107
+ );
108
+ expect(staticExportObject(file, DOC_EXPORTS)).toEqual({
109
+ category: 'Media',
110
+ components: [{name: 'AcmeSlide'}],
111
+ });
112
+ });
113
+
114
+ it('answers null when there is no exported object', () => {
115
+ const files = [
116
+ write(
117
+ 'G.doc.mjs',
118
+ "export const docs = {name: 'Legacy'};\nexport default makeDoc();\n",
119
+ ),
120
+ write('H.doc.mjs', 'export default {name: '),
121
+ write('I.doc.mjs', "export const other = {name: 'AcmeCarousel'};\n"),
122
+ write('J.doc.mjs', "export default ['AcmeCarousel'];\n"),
123
+ ];
124
+ for (const file of files) {
125
+ expect(staticExportObject(file, DOC_EXPORTS)).toBeNull();
126
+ }
127
+ });
128
+ });
129
+
130
+ describe('moduleExportSource', () => {
131
+ it('names the file that declares an export, following local re-exports', () => {
132
+ const impl = write(
133
+ 'impl.tsx',
134
+ 'export function AcmeSlide() { return null; }\n',
135
+ );
136
+ const barrel = write(
137
+ 'index.ts',
138
+ "export {AcmeSlide} from './impl';\nexport * from './dots';\nexport function AcmeCarousel() { return null; }\n",
139
+ );
140
+ const dots = write('dots.tsx', 'export const AcmeDots = () => null;\n');
141
+ expect(moduleExportSource(barrel, 'AcmeSlide')).toBe(impl);
142
+ expect(moduleExportSource(barrel, 'AcmeDots')).toBe(dots);
143
+ expect(moduleExportSource(barrel, 'AcmeCarousel')).toBe(barrel);
144
+ expect(moduleExportSource(barrel, 'AcmeMissing')).toBeNull();
145
+ });
146
+
147
+ it('does not follow a re-export written in a comment', () => {
148
+ write('legacy.tsx', 'export function AcmeTab() { return null; }\n');
149
+ const barrel = write(
150
+ 'tabs.ts',
151
+ "// export * from './legacy';\nexport function AcmeTabs() { return null; }\n",
152
+ );
153
+ expect(moduleExportSource(barrel, 'AcmeTab')).toBeNull();
154
+ });
155
+
156
+ it('does not count a type-only export', () => {
157
+ const file = write('types.ts', 'export type AcmeShape = {id: string};\n');
158
+ expect(moduleExportSource(file, 'AcmeShape')).toBeNull();
159
+ });
160
+
161
+ it('sees a file npm replaced with one of the same size and mtime', () => {
162
+ // npm stamps every installed file with one mtime and writes new files.
163
+ const npmTime = new Date('1985-10-26T08:15:00Z');
164
+ const file = write('Panel.tsx', 'export function AcmePanel() {}\n');
165
+ fs.utimesSync(file, npmTime, npmTime);
166
+ expect(moduleExportSource(file, 'AcmePanel')).toBe(file);
167
+ const next = write('Panel.next.tsx', 'export function AcmePanol() {}\n');
168
+ fs.utimesSync(next, npmTime, npmTime);
169
+ fs.renameSync(next, file);
170
+ expect(moduleExportSource(file, 'AcmePanel')).toBeNull();
171
+ });
172
+ });
@@ -0,0 +1,14 @@
1
+ // @generated by scripts/sync-api-types.mjs from the JSDoc in foundation/**/*.mjs.
2
+ // DO NOT EDIT — run `pnpm sync:api-types` to regenerate.
3
+
4
+ /**
5
+ * @param {any} ts the project's own `typescript`
6
+ * @param {string} packageDir
7
+ * @param {string[]} sources the files to read
8
+ * @returns {{options: any, declarations: string[], error?: string}}
9
+ */
10
+ export function readPackageTsconfig(ts: any, packageDir: string, sources: string[]): {
11
+ options: any;
12
+ declarations: string[];
13
+ error?: string;
14
+ };
@@ -0,0 +1,228 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file Read a package's TypeScript project the way `tsc` reads it, so a
5
+ * check that reads the package's types with its own compiler sees what the
6
+ * package compiles against.
7
+ *
8
+ * - The config is the nearest `tsconfig.json`: the package's own, or a
9
+ * monorepo's root config for a local package. An app's config above an
10
+ * installed package (one below `node_modules`) is not the package's, and is
11
+ * not read.
12
+ * - A solution-style config (`"files": []` with `references`) lends the
13
+ * referenced config, at any depth, that includes the files to read.
14
+ * - The declaration files the config includes, included files that declare
15
+ * globals or augment a module, and included scripts (no import or export)
16
+ * join the files to read, so ambient types resolve as `tsc` resolves them.
17
+ * - Without a config, defaults read TSX the way bundlers do. A config with
18
+ * errors is reported, never read as defaults.
19
+ *
20
+ * @input The project's own `typescript`, the package directory, and the
21
+ * source files to read.
22
+ * @output Compiler options, the declaration files to add, and the config's
23
+ * error, if any.
24
+ * @position Foundation discovery, used by the documented-props comparison.
25
+ */
26
+
27
+ import * as fs from 'node:fs';
28
+ import * as path from 'node:path';
29
+
30
+ /**
31
+ * Whether a file the config includes declares types every file sees: a
32
+ * declaration file, a file that declares globals or augments a module, or a
33
+ * script (a `.ts` file that TypeScript reads as no module).
34
+ * @param {any} ts
35
+ * @param {string} file
36
+ */
37
+ function isAmbient(ts, file) {
38
+ if (/\.d\.[cm]?ts$/.test(file)) return true;
39
+ let text;
40
+ try {
41
+ text = fs.readFileSync(file, 'utf8');
42
+ } catch {
43
+ return false;
44
+ }
45
+ if (/\bdeclare\s+(?:global|module)\b/.test(text)) return true;
46
+ if (!/\.tsx?$/.test(file)) return false;
47
+ // Without the words, no module; with them, TypeScript decides.
48
+ return (
49
+ !/\b(?:import|export)\b/.test(text) ||
50
+ !ts.isExternalModule(
51
+ ts.createSourceFile(file, text, ts.ScriptTarget.Latest),
52
+ )
53
+ );
54
+ }
55
+
56
+ /**
57
+ * Read a config's `files` entries as `tsc` does, and report the first it
58
+ * cannot find. TypeScript itself substitutes `${configDir}` (on a version
59
+ * that has it), resolves each entry against the config that writes it, and
60
+ * tries the TypeScript extensions for one written without its own, so this
61
+ * cannot drift from `tsc`. The parsed file names then hold the files found.
62
+ * A file the `include` matched that is gone by the time it is read was
63
+ * another process's, and is not checked here.
64
+ * @param {any} ts
65
+ * @param {any} raw the config's JSON
66
+ * @param {string} configPath
67
+ * @param {any} config the parsed config, whose file names are updated
68
+ * @returns {string | null} the message `tsc` gives for the first entry it
69
+ * cannot find (TS6053, or TS6231 for a folder)
70
+ */
71
+ function readFilesEntries(ts, raw, configPath, config) {
72
+ const literal = ts.parseJsonConfigFileContent(
73
+ raw,
74
+ {...ts.sys, readDirectory: () => []},
75
+ path.dirname(configPath),
76
+ undefined,
77
+ configPath,
78
+ ).fileNames;
79
+ if (literal.length === 0 || typeof ts.createProgram !== 'function') {
80
+ return null;
81
+ }
82
+ // Only the entries themselves: no library, no types, no imports.
83
+ const program = ts.createProgram({
84
+ rootNames: literal,
85
+ options: {
86
+ ...config.options,
87
+ noLib: true,
88
+ noResolve: true,
89
+ types: [],
90
+ noEmit: true,
91
+ },
92
+ });
93
+ const missing = program
94
+ .getOptionsDiagnostics()
95
+ .find(/** @param {any} d */ d => d.code === 6053 || d.code === 6231);
96
+ if (missing) {
97
+ const text = missing.messageText;
98
+ return typeof text === 'string' ? text : text.messageText;
99
+ }
100
+ const entries = new Set(
101
+ literal.map(/** @param {string} f */ f => path.resolve(f)),
102
+ );
103
+ config.fileNames = [
104
+ ...config.fileNames.filter(
105
+ /** @param {string} f */ f => !entries.has(path.resolve(f)),
106
+ ),
107
+ ...program.getSourceFiles().map(/** @param {any} sf */ sf => sf.fileName),
108
+ ];
109
+ return null;
110
+ }
111
+
112
+ /**
113
+ * @param {any} ts the project's own `typescript`
114
+ * @param {string} packageDir
115
+ * @param {string[]} sources the files to read
116
+ * @returns {{options: any, declarations: string[], error?: string}}
117
+ */
118
+ export function readPackageTsconfig(ts, packageDir, sources) {
119
+ const defaults = {
120
+ jsx: ts.JsxEmit.ReactJSX,
121
+ target: ts.ScriptTarget.ES2022,
122
+ module: ts.ModuleKind.ESNext,
123
+ moduleResolution: ts.ModuleResolutionKind.Bundler,
124
+ strict: true,
125
+ allowJs: true,
126
+ esModuleInterop: true,
127
+ resolveJsonModule: true,
128
+ allowImportingTsExtensions: true,
129
+ };
130
+ /** @type {string | undefined} */
131
+ let error;
132
+ /** @type {(configPath: string) => any} */
133
+ const parse = configPath => {
134
+ const read = ts.readConfigFile(configPath, ts.sys.readFile);
135
+ const config = read.error
136
+ ? {errors: [read.error], fileNames: [], options: {}}
137
+ : ts.parseJsonConfigFileContent(
138
+ read.config,
139
+ ts.sys,
140
+ path.dirname(configPath),
141
+ undefined,
142
+ configPath,
143
+ );
144
+ // "No inputs were found" is how a solution-style config reads.
145
+ const problem = config.errors.find(
146
+ /** @param {any} d */ d => d.code !== 18003,
147
+ );
148
+ const where = path.relative(packageDir, configPath);
149
+ if (problem) {
150
+ config.error = `${where}: ${ts.flattenDiagnosticMessageText(problem.messageText, ' ')}`;
151
+ } else if (!read.error) {
152
+ const missing = readFilesEntries(ts, read.config, configPath, config);
153
+ if (missing) {
154
+ // Paths read from the package, as the config's other errors are.
155
+ const dir = path.dirname(configPath).split(path.sep).join('/');
156
+ const rel = path.relative(packageDir, path.dirname(configPath));
157
+ config.error = `${where}: ${missing
158
+ .split(`${dir}/`)
159
+ .join(rel ? `${rel.split(path.sep).join('/')}/` : '')}`;
160
+ }
161
+ }
162
+ return config;
163
+ };
164
+ /** @type {any} */
165
+ let parsed;
166
+ try {
167
+ const found = ts.findConfigFile(
168
+ packageDir,
169
+ ts.sys.fileExists,
170
+ 'tsconfig.json',
171
+ );
172
+ // A config above the package is an app's when the package is installed
173
+ // below it (`node_modules`), and the package's own otherwise (a
174
+ // monorepo's root config, as `tsc` reads it).
175
+ const installed =
176
+ found &&
177
+ path
178
+ .relative(path.dirname(found), packageDir)
179
+ .split(path.sep)
180
+ .includes('node_modules');
181
+ parsed = found && !installed ? parse(found) : null;
182
+ const own = new Set(sources.map(file => path.resolve(file)));
183
+ /** @type {(config: any) => boolean} */
184
+ const includes = config =>
185
+ config.fileNames.some(
186
+ /** @param {string} file */ file => own.has(path.resolve(file)),
187
+ );
188
+ // Every reference, at any depth, each config once (a cycle ends). One
189
+ // that is missing or doesn't parse is the project's error when no config
190
+ // includes the sources, as `tsc` reports it.
191
+ const visited = new Set(found ? [path.resolve(found)] : []);
192
+ /** @type {any} */
193
+ let broken = null;
194
+ /** @type {(config: any) => any} */
195
+ const including = config => {
196
+ if (includes(config)) return config;
197
+ for (const ref of config.projectReferences ?? []) {
198
+ const next = path.resolve(ts.resolveProjectReferencePath(ref));
199
+ if (visited.has(next)) continue;
200
+ visited.add(next);
201
+ const referenced = parse(next);
202
+ if (referenced.error) broken ??= referenced;
203
+ const hit = including(referenced);
204
+ if (hit) return hit;
205
+ }
206
+ return null;
207
+ };
208
+ if (parsed) parsed = including(parsed) ?? broken ?? parsed;
209
+ } catch {
210
+ parsed = null;
211
+ }
212
+ error = parsed?.error;
213
+ const options = parsed ? {...parsed.options} : defaults;
214
+ if (options.jsx == null) options.jsx = defaults.jsx;
215
+ return {
216
+ options: {
217
+ ...options,
218
+ noEmit: true,
219
+ skipLibCheck: true,
220
+ incremental: false,
221
+ composite: false,
222
+ },
223
+ ...(error ? {error} : {}),
224
+ declarations: (parsed?.fileNames ?? []).filter(
225
+ /** @param {string} file */ file => isAmbient(ts, file),
226
+ ),
227
+ };
228
+ }
@@ -0,0 +1,210 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file readPackageTsconfig(): the package's own config, a solution-style
5
+ * config's references, a monorepo root's config for a local package but not
6
+ * an app's above an installed one, ambient declaration files, and a config
7
+ * with errors.
8
+ */
9
+
10
+ import * as fs from 'node:fs';
11
+ import * as os from 'node:os';
12
+ import * as path from 'node:path';
13
+ import {createRequire} from 'node:module';
14
+ import {afterEach, beforeEach, describe, expect, it} from 'vitest';
15
+ import {readPackageTsconfig} from './package-tsconfig.mjs';
16
+
17
+ const require = createRequire(import.meta.url);
18
+ const ts = require('typescript');
19
+
20
+ /** @type {string} */
21
+ let root;
22
+
23
+ beforeEach(() => {
24
+ root = fs.realpathSync(
25
+ fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-tsconfig-')),
26
+ );
27
+ });
28
+
29
+ afterEach(() => {
30
+ fs.rmSync(root, {recursive: true, force: true});
31
+ });
32
+
33
+ /**
34
+ * @param {string} rel
35
+ * @param {string | object} content
36
+ */
37
+ function write(rel, content) {
38
+ const file = path.join(root, rel);
39
+ fs.mkdirSync(path.dirname(file), {recursive: true});
40
+ fs.writeFileSync(
41
+ file,
42
+ typeof content === 'string' ? content : JSON.stringify(content),
43
+ );
44
+ return file;
45
+ }
46
+
47
+ const OPTIONS = {strict: true, jsx: 'react-jsx', paths: {'@/*': ['./src/*']}};
48
+
49
+ describe('readPackageTsconfig', () => {
50
+ it('reads defaults without a config, and the config the package has, with the declaration files it includes', () => {
51
+ const source = write('src/Button.tsx', 'export {};\n');
52
+ const defaults = readPackageTsconfig(ts, root, [source]);
53
+ expect(defaults.options).toMatchObject({
54
+ jsx: ts.JsxEmit.ReactJSX,
55
+ strict: true,
56
+ noEmit: true,
57
+ });
58
+ expect(defaults.declarations).toEqual([]);
59
+ expect(defaults.error).toBeUndefined();
60
+
61
+ write('tsconfig.json', {
62
+ compilerOptions: OPTIONS,
63
+ include: ['src', 'types'],
64
+ });
65
+ const ambient = write('types/ambient.d.ts', 'interface A {}\n');
66
+ const global = write('src/global.ts', 'declare global {}\nexport {};\n');
67
+ // A script (no import or export) declares globals too, an `export`
68
+ // inside its namespace included.
69
+ const script = write('types/props.ts', 'interface ScriptProps {}\n');
70
+ const namespace = write(
71
+ 'types/acme.ts',
72
+ 'declare namespace Acme {\n export interface Props {}\n}\n',
73
+ );
74
+ write('src/module.ts', 'export interface ModuleProps {}\n');
75
+
76
+ const own = readPackageTsconfig(ts, root, [source]);
77
+
78
+ expect(own.options.paths).toEqual(OPTIONS.paths);
79
+ expect(own.declarations.sort()).toEqual(
80
+ [global, ambient, script, namespace].sort(),
81
+ );
82
+ });
83
+
84
+ it("follows a solution-style config's references, at any depth and through a cycle, to the config that includes the sources", () => {
85
+ const source = write('src/Button.tsx', 'export {};\n');
86
+ const chain = [
87
+ 'tsconfig.json',
88
+ 'l1.json',
89
+ 'l2.json',
90
+ 'l3.json',
91
+ 'l4.json',
92
+ 'l5.json',
93
+ ];
94
+ chain.forEach((file, i) =>
95
+ write(file, {
96
+ files: [],
97
+ // Each level also refers back to the root, a cycle tsc rejects but
98
+ // the walk must not loop on.
99
+ references: [
100
+ {path: `./${chain[i + 1] ?? 'app.json'}`},
101
+ {path: './tsconfig.json'},
102
+ ],
103
+ }),
104
+ );
105
+ write('app.json', {compilerOptions: OPTIONS, include: ['src']});
106
+
107
+ const project = readPackageTsconfig(ts, root, [source]);
108
+
109
+ expect(project.error).toBeUndefined();
110
+ expect(project.options.paths).toEqual(OPTIONS.paths);
111
+
112
+ // A reference that is missing or doesn't parse, when no config includes
113
+ // the sources, is the project's error, as `tsc` reports it.
114
+ write('tsconfig.json', {files: [], references: [{path: './gone.json'}]});
115
+ expect(readPackageTsconfig(ts, root, [source]).error).toMatch(
116
+ /^gone\.json: .*gone\.json/,
117
+ );
118
+ write('broken.json', '{"compilerOptions": ');
119
+ write('tsconfig.json', {files: [], references: [{path: './broken.json'}]});
120
+ expect(readPackageTsconfig(ts, root, [source]).error).toMatch(
121
+ /^broken\.json: /,
122
+ );
123
+ });
124
+
125
+ it("reads a monorepo root's config for a local package, and not an app's config for a package installed below it", () => {
126
+ write('tsconfig.json', {compilerOptions: OPTIONS});
127
+ const local = path.join(root, 'packages', 'local');
128
+ const installed = path.join(root, 'node_modules', '@acme', 'widgets');
129
+ const localSource = write('packages/local/src/Button.tsx', 'export {};\n');
130
+ const installedSource = write(
131
+ 'node_modules/@acme/widgets/src/Button.tsx',
132
+ 'export {};\n',
133
+ );
134
+
135
+ expect(readPackageTsconfig(ts, local, [localSource]).options.paths).toEqual(
136
+ OPTIONS.paths,
137
+ );
138
+ expect(
139
+ readPackageTsconfig(ts, installed, [installedSource]).options.paths,
140
+ ).toBeUndefined();
141
+ });
142
+
143
+ it('reports a config with errors instead of reading defaults', () => {
144
+ const source = write('src/Button.tsx', 'export {};\n');
145
+ write('tsconfig.json', {files: ['src/Button.tsx', 'src/missing.ts']});
146
+ expect(readPackageTsconfig(ts, root, [source]).error).toMatch(
147
+ /^tsconfig\.json: .*src\/missing\.ts.* not found/,
148
+ );
149
+
150
+ write('tsconfig.json', {extends: './missing-base.json'});
151
+ expect(readPackageTsconfig(ts, root, [source]).error).toMatch(
152
+ /^tsconfig\.json: .*missing-base\.json/,
153
+ );
154
+
155
+ write('tsconfig.json', '{"compilerOptions": ');
156
+ expect(readPackageTsconfig(ts, root, [source]).error).toMatch(
157
+ /^tsconfig\.json: /,
158
+ );
159
+ });
160
+
161
+ it('reads each `files` entry as `tsc` finds it', () => {
162
+ const source = write('src/Button.tsx', 'export {};\n');
163
+ write('src/Card.ts', 'export {};\n');
164
+ write('types/globals.ts', 'interface AcmeGlobal { tone?: string }\n');
165
+ write('configs/base.json', {files: ['${configDir}/src/Button.tsx']});
166
+ /** @type {(config: object) => any} */
167
+ const read = config => {
168
+ write('tsconfig.json', {compilerOptions: OPTIONS, ...config});
169
+ return readPackageTsconfig(ts, root, [source]);
170
+ };
171
+
172
+ // `${configDir}`, written in the package's config or in a shared base it
173
+ // extends, and an entry without its extension compile with `tsc -p`.
174
+ expect(
175
+ read({files: ['${configDir}/src/Button.tsx']}).error,
176
+ ).toBeUndefined();
177
+ expect(read({extends: './configs/base.json'}).error).toBeUndefined();
178
+ const bare = read({files: ['src/Button', 'src/Card', 'types/globals']});
179
+ expect(bare.error).toBeUndefined();
180
+ expect(bare.declarations).toEqual([path.join(root, 'types', 'globals.ts')]);
181
+
182
+ // An entry `tsc` can't find is an error: a missing file (TS6053), and a
183
+ // folder (TS6231).
184
+ expect(read({files: ['src/Button.tsx', 'src/gone.ts']}).error).toMatch(
185
+ /^tsconfig\.json: File 'src\/gone\.ts' not found\.$/,
186
+ );
187
+ expect(read({files: ['src']}).error).toMatch(
188
+ /^tsconfig\.json: Could not resolve the path 'src' with the extensions/,
189
+ );
190
+ });
191
+
192
+ it('reports only a missing `files` entry, not a file the `include` matched that is gone by the time it is read', () => {
193
+ const source = write('src/Button.tsx', 'export {};\n');
194
+ write('tsconfig.json', {compilerOptions: OPTIONS, include: ['src']});
195
+ // A file another process writes and removes while the config is read.
196
+ const gone = path.join(root, 'src', 'gone.ts');
197
+ const racing = {
198
+ ...ts,
199
+ sys: {
200
+ ...ts.sys,
201
+ readDirectory: (/** @type {any[]} */ ...args) => [
202
+ ...ts.sys.readDirectory(...args),
203
+ gone,
204
+ ],
205
+ },
206
+ };
207
+
208
+ expect(readPackageTsconfig(racing, root, [source]).error).toBeUndefined();
209
+ });
210
+ });
@@ -0,0 +1,23 @@
1
+ // @generated by scripts/sync-api-types.mjs from the JSDoc in foundation/**/*.mjs.
2
+ // DO NOT EDIT — run `pnpm sync:api-types` to regenerate.
3
+
4
+ /**
5
+ * Resolve the project's own `typescript`, trying each directory in order.
6
+ * @param {Array<string | null | undefined>} dirs
7
+ * @returns {{ts: any, version: string} | {ts: null, version: string} | null}
8
+ * `ts: null` when a TypeScript is installed but has no `createProgram`
9
+ */
10
+ export function resolveProjectTypeScript(dirs: Array<string | null | undefined>): {
11
+ ts: any;
12
+ version: string;
13
+ } | {
14
+ ts: null;
15
+ version: string;
16
+ } | null;
17
+ /**
18
+ * Why a check that needs the compiler did not run, when the TypeScript
19
+ * installed has no JavaScript compiler API.
20
+ * @param {string} version
21
+ * @param {string} outcome what was not done, such as `docs were not checked`
22
+ */
23
+ export function unusableTypeScriptReason(version: string, outcome: string): string;