@astryxdesign/cli 0.6.4-canary.ed2e54e → 0.6.4

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 (145) hide show
  1. package/README.md +66 -64
  2. package/api/component/_adapter.d.mts +0 -25
  3. package/api/component/_adapter.mjs +5 -59
  4. package/api/component/component.d.mts +3 -6
  5. package/api/component/component.doc.mjs +10 -23
  6. package/api/component/component.mjs +9 -249
  7. package/api/component/component.type.d.mts +0 -25
  8. package/api/component/component.type.mjs +0 -44
  9. package/api/discover/_adapter.d.mts +6 -114
  10. package/api/discover/_adapter.mjs +17 -372
  11. package/api/discover/detail/detail.d.mts +6 -18
  12. package/api/discover/detail/detail.mjs +13 -67
  13. package/api/discover/detail/detail.test.mjs +0 -85
  14. package/api/discover/discover.d.mts +9 -3
  15. package/api/discover/discover.doc.mjs +18 -61
  16. package/api/discover/discover.mjs +36 -220
  17. package/api/discover/discover.test.mjs +2 -11
  18. package/api/discover/discover.type.d.mts +8 -147
  19. package/api/discover/discover.type.mjs +12 -102
  20. package/api/discover/list/list.d.mts +6 -20
  21. package/api/discover/list/list.mjs +12 -45
  22. package/api/discover/list/list.test.mjs +0 -46
  23. package/api/discover/search/search.d.mts +16 -18
  24. package/api/discover/search/search.mjs +56 -102
  25. package/api/discover/search/search.test.mjs +10 -144
  26. package/api/docs/docs.test.mjs +0 -2
  27. package/api/doctor/doctor.d.mts +3 -8
  28. package/api/doctor/doctor.mjs +9 -90
  29. package/api/doctor/doctor.test.mjs +10 -122
  30. package/api/index.d.mts +2 -1
  31. package/api/index.mjs +4 -4
  32. package/api/integration/add-helpers.d.mts +2 -5
  33. package/api/integration/add-helpers.mjs +9 -36
  34. package/api/integration/pack-check.mjs +3 -28
  35. package/api/json/index.ts +1 -0
  36. package/api/layout/_adapter.d.mts +34 -0
  37. package/api/layout/_adapter.mjs +148 -0
  38. package/api/layout/check/check.d.mts +16 -0
  39. package/api/layout/check/check.mjs +40 -0
  40. package/api/layout/expand/expand.d.mts +22 -0
  41. package/api/layout/expand/expand.mjs +155 -0
  42. package/api/layout/expand/expand.path-safety.test.mjs +53 -0
  43. package/api/layout/grammar/grammar.d.mts +13 -0
  44. package/api/layout/grammar/grammar.mjs +87 -0
  45. package/api/layout/layout.d.mts +6 -0
  46. package/api/layout/layout.mjs +17 -0
  47. package/api/layout/layout.test.mjs +297 -0
  48. package/api/layout/layout.type.d.mts +89 -0
  49. package/api/layout/layout.type.mjs +103 -0
  50. package/api/layout/layoutCheck.doc.d.mts +11 -0
  51. package/api/layout/layoutCheck.doc.mjs +85 -0
  52. package/api/layout/layoutExpand.doc.d.mts +11 -0
  53. package/api/layout/layoutExpand.doc.mjs +107 -0
  54. package/api/layout/layoutGrammar.doc.d.mts +11 -0
  55. package/api/layout/layoutGrammar.doc.mjs +57 -0
  56. package/api/search/search.test.mjs +0 -18
  57. package/api/template/template-integration.test.mjs +65 -1
  58. package/api/template/template.mjs +1 -1
  59. package/api/theme/add/add.mjs +25 -17
  60. package/api/theme/add/add.staging.test.mjs +23 -40
  61. package/api/theme/build/build.family.test.mjs +12 -7
  62. package/api/theme/build/build.mjs +18 -8
  63. package/api/upgrade/run/run.mjs +4 -6
  64. package/api/upgrade/upgrade.type.mjs +2 -2
  65. package/assets/codemods/__tests__/runner.test.mjs +1 -3
  66. package/assets/codemods/integration-runner.mjs +3 -3
  67. package/assets/codemods/runner.mjs +4 -5
  68. package/assets/docs/internationalization.doc.mjs +5 -7
  69. package/assets/docs/tree/integrations.doc.mjs +1 -20
  70. package/assets/templates/blocks/components/InternationalizationProvider/InternationalizationProvider01ShippedLocale.tsx +1 -1
  71. package/authoring/config/config.doc.mjs +1 -9
  72. package/authoring/config/parse.d.mts +0 -2
  73. package/authoring/config/parse.mjs +0 -19
  74. package/authoring/config/parse.test.mjs +0 -8
  75. package/authoring/config/type.ts +2 -13
  76. package/authoring/doctypes/command/command.doc.mjs +1 -1
  77. package/authoring/doctypes/command/type.ts +1 -1
  78. package/authoring/index.d.mts +0 -1
  79. package/authoring/index.d.ts +0 -10
  80. package/authoring/index.mjs +0 -1
  81. package/clients/cli/command-result-coverage.test.mjs +7 -7
  82. package/clients/cli/commands/component/index.mjs +55 -152
  83. package/clients/cli/commands/component-ownership.test.mjs +0 -89
  84. package/clients/cli/commands/component.doc.mjs +6 -23
  85. package/clients/cli/commands/debug-result-summary.test.mjs +2 -2
  86. package/clients/cli/commands/discover.doc.mjs +9 -53
  87. package/clients/cli/commands/discover.mjs +118 -393
  88. package/clients/cli/commands/docs.test.mjs +0 -29
  89. package/clients/cli/commands/layout-check.doc.mjs +65 -0
  90. package/clients/cli/commands/layout-expand.doc.mjs +83 -0
  91. package/clients/cli/commands/layout-grammar.doc.mjs +30 -0
  92. package/clients/cli/commands/layout.doc.mjs +34 -0
  93. package/clients/cli/commands/layout.error-codes.test.mjs +66 -0
  94. package/clients/cli/commands/layout.exit-parity.test.mjs +41 -0
  95. package/clients/cli/commands/layout.mjs +275 -0
  96. package/clients/cli/commands/layout.path-help.test.mjs +33 -0
  97. package/clients/cli/commands/layout.stdin-cap.test.mjs +47 -0
  98. package/clients/cli/commands/layout.text-fields.test.mjs +39 -0
  99. package/clients/cli/commands/text-json-parity.test.mjs +17 -0
  100. package/clients/cli/index.mjs +4 -0
  101. package/clients/cli/lib/exit-codes.test.mjs +8 -1
  102. package/clients/cli/lib/json-shim.mjs +14 -24
  103. package/clients/cli/lib/json-shim.test.mjs +20 -6
  104. package/clients/cli/lib/manifest.mjs +8 -3
  105. package/clients/cli/lib/manifest.test.mjs +2 -5
  106. package/foundation/discovery/authoring-self-docs.mjs +0 -1
  107. package/foundation/discovery/template-adapter.mjs +1 -1
  108. package/foundation/doc-compiler/doc-loads.test.mjs +12 -0
  109. package/foundation/doc-compiler/tree.test.mjs +1 -9
  110. package/foundation/integrations/integrations.d.mts +1 -14
  111. package/foundation/integrations/integrations.mjs +1 -41
  112. package/foundation/integrations/integrations.test.mjs +0 -31
  113. package/foundation/response/response-types.doc.mjs +21 -15
  114. package/foundation/response/response-types.doc.test.mjs +0 -23
  115. package/foundation/xle/browser.d.mts +3 -3
  116. package/foundation/xle/browser.mjs +3 -3
  117. package/foundation/xle/expand.mjs +2 -2
  118. package/foundation/xle/parse.mjs +1 -1
  119. package/foundation/xle/print.mjs +2 -2
  120. package/foundation/xle/splice.mjs +1 -1
  121. package/package.json +9 -9
  122. package/api/discover/_adapter.test.mjs +0 -215
  123. package/api/discover/_catalog-view.d.mts +0 -115
  124. package/api/discover/_catalog-view.mjs +0 -203
  125. package/api/discover/_catalog-view.test.mjs +0 -128
  126. package/api/discover/detail/item/item.d.mts +0 -26
  127. package/api/discover/detail/item/item.mjs +0 -78
  128. package/api/discover/detail/item/item.test.mjs +0 -73
  129. package/api/integration/pack-check.lifecycle-output.test.mjs +0 -105
  130. package/api/theme/add/add.rollback.test.mjs +0 -158
  131. package/api/theme/build/build.rollback.test.mjs +0 -148
  132. package/api/upgrade/run/files-changed.test.mjs +0 -111
  133. package/assets/codemods/file-count.test.mjs +0 -163
  134. package/assets/docs/tree/component-lookups.doc.mjs +0 -149
  135. package/authoring/discover/discover.doc.d.mts +0 -13
  136. package/authoring/discover/discover.doc.mjs +0 -138
  137. package/authoring/discover/parse.d.mts +0 -24
  138. package/authoring/discover/parse.mjs +0 -128
  139. package/authoring/discover/parse.test.mjs +0 -124
  140. package/authoring/discover/type.ts +0 -87
  141. package/clients/cli/commands/component-batch.test.mjs +0 -341
  142. package/clients/cli/commands/discover.sources.test.mjs +0 -267
  143. package/clients/cli/lib/parse-error-format.test.mjs +0 -81
  144. package/foundation/response/batch.type.d.mts +0 -33
  145. package/foundation/response/batch.type.mjs +0 -34
@@ -1,128 +0,0 @@
1
- // Copyright (c) Meta Platforms, Inc. and affiliates.
2
-
3
- /**
4
- * @file Tests for the pure views over discover sources' catalogs.
5
- */
6
-
7
- import {describe, expect, it} from 'vitest';
8
- import {
9
- availableEntries,
10
- catalogState,
11
- kindLists,
12
- searchItems,
13
- withLatest,
14
- } from './_catalog-view.mjs';
15
-
16
- /**
17
- * @param {string} name
18
- * @param {string} integration
19
- * @param {object} [extra]
20
- * @returns {any}
21
- */
22
- function entry(name, integration, extra = {}) {
23
- return {
24
- package: name,
25
- integration,
26
- aliases: [],
27
- latest: '1.0.0',
28
- versions: [],
29
- contributions: [{kind: 'component', name: 'A'}],
30
- source: 'S',
31
- ...extra,
32
- };
33
- }
34
-
35
- const CATALOG = {
36
- sources: [],
37
- packages: [
38
- entry('@acme/ui', 'acme-ui', {aliases: ['@acme/ui-classic']}),
39
- entry('@acme/ui-classic', 'acme-ui', {aliases: ['@acme/ui']}),
40
- entry('@acme/charts', 'charts', {
41
- latest: '2.0.0',
42
- contributions: [{kind: 'theme', name: 'ocean'}],
43
- }),
44
- ],
45
- };
46
-
47
- /** @param {string} name @returns {any} */
48
- const installed = name => ({name, category: name, components: []});
49
-
50
- describe('availableEntries', () => {
51
- it('offers each integration once, under the first name a source lists', () => {
52
- expect(
53
- availableEntries(CATALOG, [], new Set()).map(e => [e.name, e.aliases]),
54
- ).toEqual([
55
- ['@acme/ui', ['@acme/ui-classic']],
56
- ['@acme/charts', undefined],
57
- ]);
58
- });
59
-
60
- it('never offers an integration the project has under another name', () => {
61
- expect(
62
- availableEntries(CATALOG, [installed('@acme/ui-classic')], new Set()).map(
63
- e => e.name,
64
- ),
65
- ).toEqual(['@acme/charts']);
66
- });
67
-
68
- it('does not offer a package the project declares, even when it did not load', () => {
69
- expect(
70
- availableEntries(CATALOG, [], new Set(['@acme/charts'])).map(e => e.name),
71
- ).toEqual(['@acme/ui']);
72
- });
73
-
74
- it('describes an entry the way an installed one is described', () => {
75
- expect(availableEntries(CATALOG, [], new Set())[1]).toEqual({
76
- name: '@acme/charts',
77
- components: [],
78
- version: '2.0.0',
79
- themes: ['ocean'],
80
- source: 'S',
81
- });
82
- });
83
- });
84
-
85
- describe('catalogState', () => {
86
- it('names the package the project has instead', () => {
87
- expect(
88
- catalogState(CATALOG.packages[0], new Set(['@acme/ui-classic']), new Set()),
89
- ).toEqual({state: 'alias', installedAs: '@acme/ui-classic'});
90
- });
91
- });
92
-
93
- describe('withLatest', () => {
94
- it('adds the latest release a source knows to an installed package', () => {
95
- expect(withLatest([installed('@acme/charts')], CATALOG)[0]).toMatchObject({
96
- latest: '2.0.0',
97
- });
98
- });
99
- });
100
-
101
- describe('kindLists', () => {
102
- it('groups items by kind, always with components', () => {
103
- expect(
104
- kindLists([
105
- {kind: 'theme', name: 'ocean'},
106
- {kind: 'component', name: 'A'},
107
- ]),
108
- ).toEqual({components: ['A'], themes: ['ocean']});
109
- });
110
- });
111
-
112
- describe('searchItems', () => {
113
- it('covers installed packages and their other items, then what could be added', () => {
114
- const items = searchItems(
115
- [{...installed('@acme/widgets'), templates: ['pages/Home']}],
116
- CATALOG,
117
- new Set(),
118
- );
119
- expect(items.map(i => [i.package, i.kind, i.name, i.installed])).toEqual([
120
- ['@acme/widgets', 'package', '@acme/widgets', true],
121
- ['@acme/widgets', 'template', 'pages/Home', true],
122
- ['@acme/ui', 'package', '@acme/ui', false],
123
- ['@acme/ui', 'component', 'A', false],
124
- ['@acme/charts', 'package', '@acme/charts', false],
125
- ['@acme/charts', 'theme', 'ocean', false],
126
- ]);
127
- });
128
- });
@@ -1,26 +0,0 @@
1
- // @generated by scripts/sync-api-types.mjs from the JSDoc in api/**/*.mjs.
2
- // DO NOT EDIT — run `pnpm sync:api-types` to regenerate.
3
-
4
- /**
5
- * Find one item of a package: an installed package's own templates, docs,
6
- * themes, and codemods first, then what a discover source lists for the
7
- * package. Returns null when neither has it, so the caller can report the
8
- * component lookup's own error.
9
- *
10
- * @param {import('../../_adapter.mjs').InstalledPackage[]} packages
11
- * @param {string} packageName
12
- * @param {string} itemName
13
- * @param {{
14
- * catalog?: import('../../_catalog-view.mjs').CatalogPackage,
15
- * version?: string,
16
- * add?: (name: string, version?: string) => string,
17
- * installedAs?: string,
18
- * }} [options]
19
- * @returns {import('../../discover.type.mjs').DiscoverItemResponse | null}
20
- */
21
- export function item(packages: import("../../_adapter.mjs").InstalledPackage[], packageName: string, itemName: string, options?: {
22
- catalog?: import("../../_catalog-view.mjs").CatalogPackage;
23
- version?: string;
24
- add?: (name: string, version?: string) => string;
25
- installedAs?: string;
26
- }): import("../../discover.type.mjs").DiscoverItemResponse | null;
@@ -1,78 +0,0 @@
1
- // Copyright (c) Meta Platforms, Inc. and affiliates.
2
-
3
- /**
4
- * @file discover.item leaf — one item a package adds, when it is not an
5
- * installed component (those keep resolving to their full doc through
6
- * ../doc).
7
- *
8
- * @position api/discover/detail/item — pure projection over the packages and
9
- * catalog entry resolved by ../../_adapter.
10
- */
11
-
12
- import {KIND_FIELDS} from '../../_adapter.mjs';
13
- import {defaultVersion} from '../../_catalog-view.mjs';
14
-
15
- /**
16
- * Find one item of a package: an installed package's own templates, docs,
17
- * themes, and codemods first, then what a discover source lists for the
18
- * package. Returns null when neither has it, so the caller can report the
19
- * component lookup's own error.
20
- *
21
- * @param {import('../../_adapter.mjs').InstalledPackage[]} packages
22
- * @param {string} packageName
23
- * @param {string} itemName
24
- * @param {{
25
- * catalog?: import('../../_catalog-view.mjs').CatalogPackage,
26
- * version?: string,
27
- * add?: (name: string, version?: string) => string,
28
- * installedAs?: string,
29
- * }} [options]
30
- * @returns {import('../../discover.type.mjs').DiscoverItemResponse | null}
31
- */
32
- export function item(packages, packageName, itemName, options = {}) {
33
- const {catalog, version, add, installedAs} = options;
34
- const pkg = packages.find(p => p.name === packageName);
35
- if (pkg && (!version || version === pkg.version)) {
36
- for (const [kind, field] of KIND_FIELDS) {
37
- if (kind === 'component') continue;
38
- const names = /** @type {Record<string, unknown>} */ (pkg)[field];
39
- if (Array.isArray(names) && names.includes(itemName)) {
40
- return {
41
- type: 'discover.item',
42
- data: {
43
- package: packageName,
44
- ...(pkg.version ? {version: pkg.version} : {}),
45
- kind,
46
- name: itemName,
47
- installed: true,
48
- },
49
- };
50
- }
51
- }
52
- }
53
-
54
- const lower = itemName.toLowerCase();
55
- const found =
56
- catalog?.contributions.find(c => c.name === itemName) ??
57
- catalog?.contributions.find(c => c.name.toLowerCase() === lower);
58
- if (!catalog || !found) return null;
59
- const shown = version ?? defaultVersion(catalog);
60
- return {
61
- type: 'discover.item',
62
- data: {
63
- package: packageName,
64
- ...(shown ? {version: shown} : {}),
65
- kind: found.kind,
66
- name: found.name,
67
- ...(found.title ? {title: found.title} : {}),
68
- ...(found.summary ? {summary: found.summary} : {}),
69
- ...(found.keywords ? {keywords: found.keywords} : {}),
70
- installed: pkg != null,
71
- ...(pkg == null && installedAs ? {installedAs} : {}),
72
- ...(pkg == null && !installedAs && add
73
- ? {install: add(packageName, version)}
74
- : {}),
75
- source: catalog.source,
76
- },
77
- };
78
- }
@@ -1,73 +0,0 @@
1
- // Copyright (c) Meta Platforms, Inc. and affiliates.
2
-
3
- /**
4
- * @file Tests for the discover.item leaf, a pure projection over installed
5
- * packages and a source's catalog entry.
6
- */
7
-
8
- import {describe, expect, it} from 'vitest';
9
- import {item} from './item.mjs';
10
-
11
- /** @type {any[]} */
12
- const PACKAGES = [
13
- {
14
- name: '@acme/widgets',
15
- category: '@acme/widgets',
16
- version: '1.2.3',
17
- components: ['Alpha'],
18
- templates: ['pages/Home'],
19
- docs: ['guide'],
20
- },
21
- ];
22
-
23
- /** @type {any} */
24
- const CATALOG = {
25
- package: '@acme/charts',
26
- integration: 'acme-charts',
27
- aliases: [],
28
- latest: '2.0.0',
29
- source: 'Acme',
30
- versions: [],
31
- contributions: [{kind: 'template', name: 'pages/Report', title: 'Report page'}],
32
- };
33
-
34
- /** @param {string} name @param {string} [version] */
35
- const add = (name, version) => `pnpm add ${version ? `${name}@${version}` : name}`;
36
-
37
- describe('discover.item leaf', () => {
38
- it("finds an installed package's own template, doc, theme, or codemod", () => {
39
- expect(item(PACKAGES, '@acme/widgets', 'pages/Home')).toEqual({
40
- type: 'discover.item',
41
- data: {
42
- package: '@acme/widgets',
43
- version: '1.2.3',
44
- kind: 'template',
45
- name: 'pages/Home',
46
- installed: true,
47
- },
48
- });
49
- });
50
-
51
- it('finds an item a source lists, with the command that adds its package', () => {
52
- expect(
53
- item(PACKAGES, '@acme/charts', 'pages/report', {catalog: CATALOG, add}),
54
- ).toEqual({
55
- type: 'discover.item',
56
- data: {
57
- package: '@acme/charts',
58
- version: '2.0.0',
59
- kind: 'template',
60
- name: 'pages/Report',
61
- title: 'Report page',
62
- installed: false,
63
- install: 'pnpm add @acme/charts',
64
- source: 'Acme',
65
- },
66
- });
67
- });
68
-
69
- it('returns null when neither the project nor a source has it', () => {
70
- expect(item(PACKAGES, '@acme/widgets', 'Nope')).toBeNull();
71
- expect(item(PACKAGES, '@acme/charts', 'Nope', {catalog: CATALOG})).toBeNull();
72
- });
73
- });
@@ -1,105 +0,0 @@
1
- // Copyright (c) Meta Platforms, Inc. and affiliates.
2
-
3
- import {afterEach, beforeEach, describe, expect, it} from 'vitest';
4
- import * as fs from 'node:fs';
5
- import * as path from 'node:path';
6
- import {integrationPackCheck} from './pack-check.mjs';
7
-
8
- let tmpDir;
9
-
10
- beforeEach(() => {
11
- tmpDir = fs.mkdtempSync(path.join(process.cwd(), '.astryx-pack-check-'));
12
- });
13
-
14
- afterEach(() => {
15
- fs.rmSync(tmpDir, {recursive: true, force: true});
16
- });
17
-
18
- /** @param {Record<string, string>} scripts */
19
- function writeThemePackage(scripts) {
20
- fs.writeFileSync(
21
- path.join(tmpDir, 'package.json'),
22
- `${JSON.stringify(
23
- {
24
- name: '@acme/widgets',
25
- version: '1.0.0',
26
- files: ['astryx.integration.mjs', 'themes'],
27
- scripts,
28
- },
29
- null,
30
- 2,
31
- )}\n`,
32
- );
33
- fs.writeFileSync(
34
- path.join(tmpDir, 'astryx.integration.mjs'),
35
- "export default {themes: './themes'};\n",
36
- );
37
- const root = path.join(tmpDir, 'themes');
38
- fs.mkdirSync(path.join(root, 'ocean'), {recursive: true});
39
- fs.writeFileSync(
40
- path.join(root, 'ocean', 'oceanTheme.ts'),
41
- "import {defineTheme} from '@astryxdesign/core/theme';\n\nexport const oceanTheme = defineTheme({name: 'ocean'});\n",
42
- );
43
- fs.writeFileSync(
44
- path.join(root, 'ocean', 'oceanTheme.doc.mjs'),
45
- `/** @type {import('@astryxdesign/cli/authoring').ThemeDoc} */
46
- export default {type: 'theme', name: 'ocean', displayName: 'Ocean', description: 'Ocean theme.', maintained: true};
47
- `,
48
- );
49
- }
50
-
51
- describe('integrationPackCheck with lifecycle script output', () => {
52
- it('checks the tarball when lifecycle scripts print to stdout', async () => {
53
- writeThemePackage({
54
- prepack: 'node -e "console.log(\'building the package\')"',
55
- prepare: 'node -e "console.log(\'preparing\')"',
56
- postpack: 'node -e "console.log(\'packed\')"',
57
- });
58
-
59
- const result = await integrationPackCheck({cwd: tmpDir});
60
-
61
- expect(result.data.issues).toEqual([]);
62
- expect(result.data.packable).toBe(true);
63
- expect(result.data.tarball?.filename).toBe('acme-widgets-1.0.0.tgz');
64
- expect(result.data.contributions.packed).toEqual(
65
- result.data.contributions.local,
66
- );
67
- });
68
-
69
- it('still runs a chatty lifecycle script and compares what it packed', async () => {
70
- const renameTheme = [
71
- "console.log('renaming ocean to storm')",
72
- "const fs=require('fs')",
73
- "fs.renameSync('themes/ocean','themes/storm')",
74
- "const p='themes/storm/oceanTheme.doc.mjs'",
75
- "let x=fs.readFileSync(p,'utf8')",
76
- "x=x.replace(/name: 'ocean'/, 'name: '+String.fromCharCode(39)+'storm'+String.fromCharCode(39))",
77
- 'fs.writeFileSync(p,x)',
78
- ].join(';');
79
- writeThemePackage({prepack: `node -e "${renameTheme}"`});
80
-
81
- const result = await integrationPackCheck({cwd: tmpDir});
82
- const codes = result.data.issues.map(issue => issue.code);
83
-
84
- expect(codes).not.toContain('pack_failed');
85
- expect(codes).toContain('identity_not_packed');
86
- expect(codes).toContain('packed_identity_unexpected');
87
- expect(result.data.packable).toBe(false);
88
- });
89
-
90
- it('keeps a failing lifecycle script output in the pack_failed issue', async () => {
91
- writeThemePackage({
92
- prepack:
93
- 'node -e "console.error(\'prepack exploded\'); process.exit(3)"',
94
- });
95
-
96
- const result = await integrationPackCheck({cwd: tmpDir});
97
- const failure = result.data.issues.find(
98
- issue => issue.code === 'pack_failed',
99
- );
100
-
101
- expect(result.data.packable).toBe(false);
102
- expect(failure?.message).toContain('npm pack failed (exit 3)');
103
- expect(failure?.message).toContain('prepack exploded');
104
- });
105
- });
@@ -1,158 +0,0 @@
1
- // Copyright (c) Meta Platforms, Inc. and affiliates.
2
-
3
- /**
4
- * `theme add` writes a theme as one transaction: when a later file fails to
5
- * publish, every file it already replaced gets its previous bytes back and
6
- * every file it created is removed. Publishing is forced to fail by wrapping
7
- * the two calls that publish a staged file: `linkSync` for a new file and
8
- * `renameSync` for a replacement.
9
- *
10
- * Separate file because vi.mock is hoisted and affects the whole module.
11
- */
12
-
13
- import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
14
- import * as os from 'node:os';
15
- import * as path from 'node:path';
16
-
17
- const failures = vi.hoisted(() => ({
18
- /** Fail the Nth staged-file publish (1-based); 0 disables. */
19
- publish: 0,
20
- /** Also fail every rollback restore. */
21
- restore: false,
22
- count: 0,
23
- }));
24
-
25
- vi.mock('node:fs', async importOriginal => {
26
- const actual = /** @type {typeof import('node:fs')} */ (
27
- await importOriginal()
28
- );
29
- /** @param {string} source */
30
- const isStaged = source => path.basename(String(source)).includes('.tmp');
31
- /** @param {string} source */
32
- const isRestore = source =>
33
- path.basename(String(source)).includes('.restore-');
34
- /** @param {string} source @param {string} op */
35
- const maybeFail = (source, op) => {
36
- if (isStaged(source) && failures.publish > 0) {
37
- failures.count++;
38
- if (failures.count === failures.publish) {
39
- throw Object.assign(new Error(`EIO: forced ${op} failure`), {
40
- code: 'EIO',
41
- });
42
- }
43
- }
44
- if (isRestore(source) && failures.restore) {
45
- throw Object.assign(new Error(`EIO: forced restore failure`), {
46
- code: 'EIO',
47
- });
48
- }
49
- };
50
- return {
51
- ...actual,
52
- linkSync: vi.fn((source, destination) => {
53
- maybeFail(source, 'link');
54
- return actual.linkSync(source, destination);
55
- }),
56
- renameSync: vi.fn((source, destination) => {
57
- maybeFail(source, 'rename');
58
- return actual.renameSync(source, destination);
59
- }),
60
- };
61
- });
62
-
63
- const fs = await import('node:fs');
64
- const {themeAdd} = await import('./add.mjs');
65
- const {listThemes} = await import('../_adapter.mjs');
66
-
67
- let tmpDir;
68
-
69
- beforeEach(() => {
70
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-themeadd-rollback-'));
71
- failures.publish = 0;
72
- failures.restore = false;
73
- failures.count = 0;
74
- });
75
-
76
- afterEach(() => {
77
- failures.publish = 0;
78
- failures.restore = false;
79
- fs.rmSync(tmpDir, {recursive: true, force: true});
80
- });
81
-
82
- function stoneFiles() {
83
- const theme = listThemes().find(entry => entry.slug === 'stone');
84
- if (!theme) throw new Error('missing bundled theme stone');
85
- expect(theme.files.length).toBeGreaterThanOrEqual(2);
86
- return theme.files.map(name =>
87
- path.join(tmpDir, 'src', 'themes', 'stone', name),
88
- );
89
- }
90
-
91
- /** Entries staging or rollback left behind in the theme directory. */
92
- function strays() {
93
- const dir = path.join(tmpDir, 'src', 'themes', 'stone');
94
- if (!fs.existsSync(dir)) return [];
95
- return fs
96
- .readdirSync(dir, {recursive: true})
97
- .map(String)
98
- .filter(name => name.includes('.tmp') || name.includes('.restore-'));
99
- }
100
-
101
- describe('themeAdd rolls back a partial write', () => {
102
- it('removes every created file when the second publish fails', async () => {
103
- const files = stoneFiles();
104
- failures.publish = 2;
105
-
106
- await expect(themeAdd('stone', {cwd: tmpDir})).rejects.toMatchObject({
107
- code: 'ERR_WRITE_FAILED',
108
- });
109
-
110
- for (const file of files) expect(fs.existsSync(file)).toBe(false);
111
- expect(strays()).toEqual([]);
112
- });
113
-
114
- it('restores every replaced byte when the second publish fails', async () => {
115
- const files = stoneFiles();
116
- fs.mkdirSync(path.dirname(files[0]), {recursive: true});
117
- const before = Buffer.from([0x00, 0xff, 0x0a, 0x62, 0x65, 0x66]);
118
- fs.writeFileSync(files[0], before);
119
- failures.publish = 2;
120
-
121
- await expect(
122
- themeAdd('stone', {cwd: tmpDir, overwrite: true}),
123
- ).rejects.toMatchObject({code: 'ERR_WRITE_FAILED'});
124
-
125
- expect(fs.readFileSync(files[0]).equals(before)).toBe(true);
126
- for (const file of files.slice(1)) expect(fs.existsSync(file)).toBe(false);
127
- expect(strays()).toEqual([]);
128
- });
129
-
130
- it('names a file it could not restore', async () => {
131
- const files = stoneFiles();
132
- fs.mkdirSync(path.dirname(files[0]), {recursive: true});
133
- fs.writeFileSync(files[0], 'before\n');
134
- failures.publish = 2;
135
- failures.restore = true;
136
-
137
- let error;
138
- try {
139
- await themeAdd('stone', {cwd: tmpDir, overwrite: true});
140
- } catch (caught) {
141
- error = caught;
142
- }
143
-
144
- expect(error?.code).toBe('ERR_WRITE_FAILED');
145
- expect(error?.message).toContain('Could not restore');
146
- expect(error?.message).toContain(files[0]);
147
- });
148
-
149
- it('writes every file when nothing fails', async () => {
150
- const files = stoneFiles();
151
-
152
- const result = await themeAdd('stone', {cwd: tmpDir});
153
-
154
- expect(result.type).toBe('theme.add');
155
- for (const file of files) expect(fs.existsSync(file)).toBe(true);
156
- expect(strays()).toEqual([]);
157
- });
158
- });