@servicetitan/startup 36.1.1 → 36.1.2-canary.2

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/bin/_run.js +77 -0
  2. package/bin/cpx.js +3 -0
  3. package/bin/eslint.js +3 -0
  4. package/bin/jest.js +3 -0
  5. package/bin/js-yaml.js +3 -0
  6. package/bin/json5.js +3 -0
  7. package/bin/kendo-ui-license.js +3 -0
  8. package/bin/lerna.js +3 -0
  9. package/bin/lessc.js +3 -0
  10. package/bin/patch-package.js +3 -0
  11. package/bin/prettier.js +3 -0
  12. package/bin/sass.js +3 -0
  13. package/bin/semver.js +3 -0
  14. package/bin/spack.js +3 -0
  15. package/bin/stylelint.js +3 -0
  16. package/bin/swc.js +3 -0
  17. package/bin/swcx.js +3 -0
  18. package/bin/tcm.js +3 -0
  19. package/bin/ts-jest.js +3 -0
  20. package/bin/ts-node-cwd.js +3 -0
  21. package/bin/ts-node-esm.js +3 -0
  22. package/bin/ts-node-script.js +3 -0
  23. package/bin/ts-node-transpile-only.js +3 -0
  24. package/bin/ts-node.js +3 -0
  25. package/bin/ts-script.js +3 -0
  26. package/bin/tsc.js +3 -0
  27. package/bin/tsserver.js +3 -0
  28. package/bin/vitest.js +3 -0
  29. package/bin/webpack-bundle-analyzer.js +3 -0
  30. package/bin/webpack-dev-server.js +3 -0
  31. package/bin/webpack.js +3 -0
  32. package/dist/cli/commands/clean.d.ts +1 -0
  33. package/dist/cli/commands/clean.d.ts.map +1 -1
  34. package/dist/cli/commands/clean.js +18 -8
  35. package/dist/cli/commands/clean.js.map +1 -1
  36. package/dist/cli/commands/coverage-finalize.d.ts +16 -0
  37. package/dist/cli/commands/coverage-finalize.d.ts.map +1 -0
  38. package/dist/cli/commands/coverage-finalize.js +41 -0
  39. package/dist/cli/commands/coverage-finalize.js.map +1 -0
  40. package/dist/cli/commands/install.js +1 -1
  41. package/dist/cli/commands/install.js.map +1 -1
  42. package/dist/cli/commands/kendo-ui-license.js +1 -2
  43. package/dist/cli/commands/kendo-ui-license.js.map +1 -1
  44. package/dist/cli/commands/prepare-package.js +2 -2
  45. package/dist/cli/commands/prepare-package.js.map +1 -1
  46. package/dist/cli/commands/registry/coverage-finalize.d.ts +5 -0
  47. package/dist/cli/commands/registry/coverage-finalize.d.ts.map +1 -0
  48. package/dist/cli/commands/registry/coverage-finalize.js +17 -0
  49. package/dist/cli/commands/registry/coverage-finalize.js.map +1 -0
  50. package/dist/cli/commands/review/review.d.ts.map +1 -1
  51. package/dist/cli/commands/review/review.js +8 -7
  52. package/dist/cli/commands/review/review.js.map +1 -1
  53. package/dist/cli/commands/review/rules/require-compatible-launch-darkly-sdk.d.ts +0 -1
  54. package/dist/cli/commands/review/rules/require-compatible-launch-darkly-sdk.d.ts.map +1 -1
  55. package/dist/cli/commands/review/rules/require-compatible-launch-darkly-sdk.js +12 -18
  56. package/dist/cli/commands/review/rules/require-compatible-launch-darkly-sdk.js.map +1 -1
  57. package/dist/cli/commands/review/rules/require-project-version-in-root-node-modules.d.ts.map +1 -1
  58. package/dist/cli/commands/review/rules/require-project-version-in-root-node-modules.js +9 -4
  59. package/dist/cli/commands/review/rules/require-project-version-in-root-node-modules.js.map +1 -1
  60. package/dist/cli/commands/review/types.d.ts +2 -8
  61. package/dist/cli/commands/review/types.d.ts.map +1 -1
  62. package/dist/cli/commands/review/types.js.map +1 -1
  63. package/dist/cli/utils/index.d.ts +0 -3
  64. package/dist/cli/utils/index.d.ts.map +1 -1
  65. package/dist/cli/utils/index.js +0 -3
  66. package/dist/cli/utils/index.js.map +1 -1
  67. package/dist/cypress/config/webpack-config.js +0 -1
  68. package/dist/cypress/config/webpack-config.js.map +1 -1
  69. package/dist/storybook-config/webpack-final.d.ts.map +1 -1
  70. package/dist/storybook-config/webpack-final.js +0 -6
  71. package/dist/storybook-config/webpack-final.js.map +1 -1
  72. package/dist/utils/default-excludes.d.ts +14 -0
  73. package/dist/utils/default-excludes.d.ts.map +1 -0
  74. package/dist/utils/default-excludes.js +23 -0
  75. package/dist/utils/default-excludes.js.map +1 -0
  76. package/dist/utils/find-packages.d.ts.map +1 -1
  77. package/dist/utils/find-packages.js +15 -0
  78. package/dist/utils/find-packages.js.map +1 -1
  79. package/dist/utils/get-coverage-aliases.d.ts +23 -0
  80. package/dist/utils/get-coverage-aliases.d.ts.map +1 -0
  81. package/dist/utils/get-coverage-aliases.js +41 -0
  82. package/dist/utils/get-coverage-aliases.js.map +1 -0
  83. package/dist/utils/get-coverage-source-patterns.d.ts +21 -0
  84. package/dist/utils/get-coverage-source-patterns.d.ts.map +1 -0
  85. package/dist/utils/get-coverage-source-patterns.js +39 -0
  86. package/dist/utils/get-coverage-source-patterns.js.map +1 -0
  87. package/dist/utils/get-jest-config.d.ts.map +1 -1
  88. package/dist/utils/get-jest-config.js +2 -1
  89. package/dist/utils/get-jest-config.js.map +1 -1
  90. package/dist/utils/prettify.js +1 -1
  91. package/dist/utils/prettify.js.map +1 -1
  92. package/dist/webpack/configs/cache-config.d.ts.map +1 -1
  93. package/dist/webpack/configs/cache-config.js +3 -1
  94. package/dist/webpack/configs/cache-config.js.map +1 -1
  95. package/dist/webpack/configs/rules/ts-coverage-rules.d.ts +22 -0
  96. package/dist/webpack/configs/rules/ts-coverage-rules.d.ts.map +1 -0
  97. package/dist/webpack/configs/rules/ts-coverage-rules.js +48 -0
  98. package/dist/webpack/configs/rules/ts-coverage-rules.js.map +1 -0
  99. package/eslint/config.mjs +2 -0
  100. package/package.json +54 -18
  101. package/src/__tests__/postinstall.test.ts +173 -0
  102. package/src/cli/commands/__tests__/clean.test.ts +30 -12
  103. package/src/cli/commands/__tests__/install.test.ts +3 -1
  104. package/src/cli/commands/__tests__/kendo-ui-license.test.ts +4 -4
  105. package/src/cli/commands/__tests__/prepare-package.test.ts +2 -3
  106. package/src/cli/commands/clean.ts +18 -9
  107. package/src/cli/commands/install.ts +1 -1
  108. package/src/cli/commands/kendo-ui-license.ts +1 -1
  109. package/src/cli/commands/prepare-package.ts +1 -1
  110. package/src/cli/commands/review/__mocks__/index.ts +1 -0
  111. package/src/cli/commands/review/__mocks__/mock-package-manager.ts +20 -0
  112. package/src/cli/commands/review/__tests__/review.test.ts +23 -31
  113. package/src/cli/commands/review/review.ts +11 -7
  114. package/src/cli/commands/review/rules/__mocks__/mock-project.ts +2 -1
  115. package/src/cli/commands/review/rules/__tests__/require-compatible-launch-darkly-sdk.test.ts +39 -49
  116. package/src/cli/commands/review/rules/__tests__/require-project-version-in-root-node-modules.test.ts +35 -22
  117. package/src/cli/commands/review/rules/require-compatible-launch-darkly-sdk.ts +13 -28
  118. package/src/cli/commands/review/rules/require-project-version-in-root-node-modules.ts +10 -3
  119. package/src/cli/commands/review/types.ts +3 -12
  120. package/src/cli/utils/index.ts +10 -3
  121. package/src/cypress/config/__tests__/webpack-config.test.ts +0 -9
  122. package/src/cypress/config/webpack-config.ts +0 -1
  123. package/src/postinstall.js +106 -14
  124. package/src/scripts/__tests__/generate-bin-wrappers.test.ts +226 -0
  125. package/src/scripts/generate-bin-wrappers.js +205 -0
  126. package/src/storybook-config/__tests__/webpack-final.test.ts +0 -25
  127. package/src/storybook-config/webpack-final.ts +0 -4
  128. package/src/utils/__tests__/get-jest-config.test.ts +3 -1
  129. package/src/utils/__tests__/get-packages.test.ts +47 -2
  130. package/src/utils/__tests__/prettify.test.ts +1 -1
  131. package/src/utils/find-packages.ts +16 -0
  132. package/src/utils/get-jest-config.ts +4 -1
  133. package/src/utils/prettify.ts +1 -1
  134. package/src/webpack/configs/cache-config.ts +3 -1
@@ -0,0 +1,226 @@
1
+ import fs from 'fs';
2
+ import { vol } from 'memfs';
3
+ import path from 'path';
4
+
5
+ jest.mock('fs', () => require('memfs').fs);
6
+
7
+ const { main } = require('../generate-bin-wrappers');
8
+
9
+ /*
10
+ * Use the real startup directory so path.resolve(__dirname, '..') in the
11
+ * module resolves naturally. The memfs volume mirrors the real directory
12
+ * layout - no path.resolve mock needed.
13
+ */
14
+ const startupDir = path.resolve(__dirname, '..', '..', '..');
15
+ const binDir = path.join(startupDir, 'bin');
16
+ const nodeModulesDir = path.join(startupDir, 'node_modules');
17
+ const startupPackageJson = path.join(startupDir, 'package.json');
18
+
19
+ describe('[startup] generate-bin-wrappers', () => {
20
+ const defaultBin = { startup: './bin/index.js' };
21
+
22
+ beforeEach(() => {
23
+ jest.clearAllMocks();
24
+ jest.spyOn(console, 'log').mockImplementation(() => {});
25
+ });
26
+
27
+ afterEach(() => {
28
+ vol.reset();
29
+ jest.restoreAllMocks();
30
+ });
31
+
32
+ describe(main.name, () => {
33
+ const pkgScriptName = 'foo';
34
+ const pkgScriptPath = './bin/cli.js';
35
+ const pkg = { name: 'bar', version: '1.0.0', bin: { [pkgScriptName]: pkgScriptPath } };
36
+ const pkgJsonPath = path.join(nodeModulesDir, pkg.name, 'package.json');
37
+ const wrapper = path.join(binDir, `${pkgScriptName}.js`);
38
+
39
+ function volFromJSON(overrides: Record<string, string | Record<string, any>> = {}) {
40
+ vol.fromJSON({
41
+ [startupPackageJson]: JSON.stringify({
42
+ dependencies: { [pkg.name]: pkg.version },
43
+ bin: defaultBin,
44
+ }),
45
+ [pkgJsonPath]: JSON.stringify(pkg),
46
+ ...Object.fromEntries(
47
+ Object.entries(overrides).map(([k, v]) => [
48
+ k,
49
+ typeof v === 'string' ? v : JSON.stringify(v),
50
+ ])
51
+ ),
52
+ });
53
+ }
54
+
55
+ beforeEach(() => {
56
+ volFromJSON();
57
+ fs.mkdirSync(binDir, { recursive: true });
58
+ });
59
+
60
+ const subject = () => main();
61
+
62
+ test('generates executable script', () => {
63
+ subject();
64
+
65
+ expect(fs.statSync(wrapper).mode & 0o755).toBe(0o755);
66
+ expect(fs.readFileSync(wrapper, 'utf8')).toContain(
67
+ `require('./_run')('${pkg.name}', '${pkgScriptPath}')`
68
+ );
69
+ });
70
+
71
+ test('adds script to package.json bin field', () => {
72
+ subject();
73
+
74
+ expect(JSON.parse(fs.readFileSync(startupPackageJson, 'utf8')).bin).toEqual(
75
+ expect.objectContaining({
76
+ [pkgScriptName]: `./bin/${pkgScriptName}.js`,
77
+ ...defaultBin,
78
+ })
79
+ );
80
+ });
81
+
82
+ describe('with string-form bin field', () => {
83
+ beforeEach(() => {
84
+ volFromJSON({ [pkgJsonPath]: { name: pkg.name, bin: './cli.js' } });
85
+ });
86
+
87
+ test('uses package name as script name', () => {
88
+ subject();
89
+
90
+ expect(fs.existsSync(path.join(binDir, `${pkg.name}.js`))).toBe(true);
91
+ });
92
+
93
+ describe('with scoped package', () => {
94
+ const scoped = { name: `@scope/${pkg.name}`, bin: './cli.js' };
95
+
96
+ beforeEach(() => volFromJSON({ [pkgJsonPath]: scoped }));
97
+
98
+ test('omits scope from script name', () => {
99
+ subject();
100
+
101
+ expect(fs.existsSync(path.join(binDir, `${pkg.name}.js`))).toBe(true);
102
+ });
103
+ });
104
+ });
105
+
106
+ function itDoesNotGenerateWrapper() {
107
+ test('does not generate wrapper', () => {
108
+ subject();
109
+
110
+ expect(fs.existsSync(wrapper)).toBe(false);
111
+ });
112
+ }
113
+
114
+ describe.each(['@servicetitan/install'])('when dependency is %s', name => {
115
+ beforeEach(() => {
116
+ volFromJSON({
117
+ [startupPackageJson]: { dependencies: { [name]: '1.0.0' } },
118
+ [path.join(nodeModulesDir, name, 'package.json')]: { ...pkg, name },
119
+ });
120
+ });
121
+
122
+ itDoesNotGenerateWrapper();
123
+ });
124
+
125
+ describe('with no dependencies', () => {
126
+ beforeEach(() => {
127
+ volFromJSON({ [startupPackageJson]: {} });
128
+ });
129
+
130
+ test('does nothing', () => {
131
+ expect(() => subject()).not.toThrow();
132
+ });
133
+ });
134
+
135
+ describe('when dependency has no bin field', () => {
136
+ beforeEach(() => {
137
+ fs.writeFileSync(pkgJsonPath, JSON.stringify({ name: pkg.name }));
138
+ });
139
+
140
+ itDoesNotGenerateWrapper();
141
+ });
142
+
143
+ describe('when dependency package.json does not exist', () => {
144
+ beforeEach(() => {
145
+ fs.rmSync(path.join(nodeModulesDir, pkg.name), { recursive: true });
146
+ });
147
+
148
+ itDoesNotGenerateWrapper();
149
+ });
150
+
151
+ describe('when wrapper already exists with same content', () => {
152
+ beforeEach(() => {
153
+ subject();
154
+ });
155
+
156
+ test('does not rewrite wrapped', () => {
157
+ const writeFileSpy = jest.spyOn(fs, 'writeFileSync');
158
+ subject();
159
+
160
+ expect(
161
+ writeFileSpy.mock.calls.filter(([path]) => String(path) === wrapper)
162
+ ).toHaveLength(0);
163
+ });
164
+
165
+ test('does not rewrite package.json', () => {
166
+ const writeFileSpy = jest.spyOn(fs, 'writeFileSync');
167
+ subject();
168
+
169
+ expect(
170
+ writeFileSpy.mock.calls.filter(([path]) => String(path) === startupPackageJson)
171
+ ).toHaveLength(0);
172
+ });
173
+ });
174
+
175
+ describe('when obsolete wrapper exists', () => {
176
+ const obsoleteWrapper = path.join(binDir, 'old-tool.js');
177
+
178
+ beforeEach(() => {
179
+ volFromJSON({
180
+ [obsoleteWrapper]: '// Auto-generated by scripts/generate-bin-wrappers.js\n',
181
+ });
182
+ });
183
+
184
+ test('removes it', () => {
185
+ subject();
186
+
187
+ expect(fs.existsSync(obsoleteWrapper)).toBe(false);
188
+ });
189
+
190
+ describe('when remove fails', () => {
191
+ beforeEach(() => {
192
+ jest.spyOn(fs, 'rmSync').mockImplementation(() => {
193
+ throw new Error('Oops!');
194
+ });
195
+ });
196
+
197
+ test('ignores it', () => {
198
+ expect(() => subject()).not.toThrow();
199
+ });
200
+ });
201
+ });
202
+
203
+ describe('with real dependency resolved via require.resolve', () => {
204
+ const realPkg = { name: 'patch-package', version: '1.0.0', bin: './index.js' };
205
+ const realPkgJson = require.resolve(path.join(realPkg.name, 'package.json'), {
206
+ paths: [startupDir],
207
+ });
208
+
209
+ beforeEach(() => {
210
+ volFromJSON({
211
+ [startupPackageJson]: { dependencies: { [realPkg.name]: realPkg.version } },
212
+ [realPkgJson]: { name: realPkg.name, bin: realPkg.bin },
213
+ });
214
+ });
215
+
216
+ test('generates wrapper from resolved package.json', () => {
217
+ subject();
218
+
219
+ const wrapper = path.join(binDir, `${realPkg.name}.js`);
220
+ expect(fs.readFileSync(wrapper, 'utf8')).toContain(
221
+ `require('./_run')('${realPkg.name}', '${realPkg.bin}')`
222
+ );
223
+ });
224
+ });
225
+ });
226
+ });
@@ -0,0 +1,205 @@
1
+ #!/usr/bin/env node
2
+ /* eslint-disable no-console */
3
+
4
+ /**
5
+ * Generates bin wrapper scripts for all CLI tools exposed by startup's dependencies.
6
+ *
7
+ * pnpm uses strict node_modules isolation - only direct dependencies' bins are linked
8
+ * into a consumer's node_modules/.bin/. Since startup acts as a toolbox package, it
9
+ * needs to re-export its dependencies' bins so consumers get them automatically.
10
+ *
11
+ * Under npm these wrappers are harmless but redundant - npm links the original
12
+ * package's bins directly. Under pnpm, startup's wrappers are the only ones
13
+ * visible to consumers.
14
+ *
15
+ * This script:
16
+ * 1. Reads startup's dependencies from package.json
17
+ * 2. Resolves each dependency's `bin` entries
18
+ * 3. Generates thin wrapper scripts in bin/ (using shared bin/_run.js helper)
19
+ * 4. Removes obsolete wrappers from previous runs
20
+ * 5. Updates the `bin` field in startup's package.json
21
+ *
22
+ * Run: npm run prepare -w packages/startup
23
+ */
24
+
25
+ const fs = require('fs');
26
+ const path = require('path');
27
+
28
+ const GENERATED_MARKER = 'Auto-generated by scripts/generate-bin-wrappers.js';
29
+
30
+ // Packages whose bins should not be exposed to consumers
31
+ const SKIP_PACKAGES = new Set([
32
+ '@servicetitan/install', // consumers use @servicetitan/install directly
33
+ ]);
34
+
35
+ // Bins from transitive dependencies that startup needs to expose to consumers
36
+ const EXTRA_BINS = [
37
+ {
38
+ name: 'kendo-ui-license',
39
+ from: '@progress/kendo-licensing',
40
+ binPath: 'bin/kendo-ui-license.js',
41
+ },
42
+ ];
43
+
44
+ /*
45
+ * Collect bin entries from each dependency's package.json. Skips packages in
46
+ * SKIP_PACKAGES, normalizes string-form bin fields to { name: path } objects,
47
+ * and appends EXTRA_BINS for transitive dependencies that need explicit exposure.
48
+ */
49
+ function discoverBins(startupDirectory, dependencies) {
50
+ const bins = [];
51
+
52
+ for (const packageName of dependencies) {
53
+ if (SKIP_PACKAGES.has(packageName)) {
54
+ continue;
55
+ }
56
+ let packageJson;
57
+ try {
58
+ const packageJsonPath = require.resolve(packageName + '/package.json', {
59
+ paths: [startupDirectory],
60
+ });
61
+ packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
62
+ } catch {
63
+ // Some packages block ./package.json in exports - read directly from node_modules
64
+ try {
65
+ const packageJsonPath = resolveNodeModulesPackageJson(
66
+ packageName,
67
+ startupDirectory
68
+ );
69
+ packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
70
+ } catch {
71
+ continue;
72
+ }
73
+ }
74
+
75
+ if (!packageJson.bin) {
76
+ continue;
77
+ }
78
+
79
+ /*
80
+ * When bin is a string the command name defaults to the package name.
81
+ * Strip the @scope/ prefix for scoped packages, matching npm's behavior.
82
+ */
83
+ const entries =
84
+ typeof packageJson.bin === 'string'
85
+ ? { [packageJson.name.replace(/^@.*\//, '')]: packageJson.bin }
86
+ : packageJson.bin;
87
+
88
+ for (const [name, binPath] of Object.entries(entries)) {
89
+ bins.push({ name, from: packageName, binPath });
90
+ }
91
+ }
92
+
93
+ bins.push(...EXTRA_BINS);
94
+ bins.sort((a, b) => a.name.localeCompare(b.name));
95
+ return bins;
96
+ }
97
+
98
+ /*
99
+ * Delete previously generated wrappers that no longer correspond to a
100
+ * dependency. Identifies generated files by the presence of GENERATED_MARKER
101
+ * and removes any that aren't in the current bin list.
102
+ */
103
+ function removeObsoleteWrappers(binDirectory, bins) {
104
+ const generatedNames = new Set(bins.map(bin => `${bin.name}.js`));
105
+ const removed = [];
106
+ for (const file of fs.readdirSync(binDirectory)) {
107
+ const filePath = path.join(binDirectory, file);
108
+ try {
109
+ const content = fs.readFileSync(filePath, 'utf8');
110
+ if (content.includes(GENERATED_MARKER) && !generatedNames.has(file)) {
111
+ fs.rmSync(filePath);
112
+ removed.push(file);
113
+ }
114
+ } catch {
115
+ continue;
116
+ }
117
+ }
118
+ return removed;
119
+ }
120
+
121
+ /*
122
+ * Write a thin wrapper script for each bin entry. Each wrapper delegates to
123
+ * bin/_run.js which locates and executes the real binary. Skips files whose
124
+ * content is already up to date. Returns the bin entries for package.json
125
+ * and the list of wrappers that were added or updated.
126
+ */
127
+ function generateWrappers(binDirectory, bins) {
128
+ const binEntries = {
129
+ startup: './bin/index.js',
130
+ };
131
+ const added = [];
132
+
133
+ for (const { name, from, binPath } of bins) {
134
+ const wrapperPath = path.join(binDirectory, `${name}.js`);
135
+ const wrapper = `#!/usr/bin/env node
136
+ // ${GENERATED_MARKER} - wraps: ${from} -> ${binPath}
137
+ require('./_run')('${from}', '${binPath}');
138
+ `;
139
+ const existing = fs.existsSync(wrapperPath) && fs.readFileSync(wrapperPath, 'utf8');
140
+ const normalized = existing && existing.replace(/\r\n/g, '\n');
141
+ if (normalized !== wrapper) {
142
+ fs.writeFileSync(wrapperPath, wrapper);
143
+ fs.chmodSync(wrapperPath, 0o755);
144
+ added.push(name);
145
+ }
146
+ binEntries[name] = `./bin/${name}.js`;
147
+ }
148
+
149
+ return { binEntries, added };
150
+ }
151
+
152
+ /**
153
+ * Walk up node_modules directories to find a dependency's package.json,
154
+ * handling both hoisted and nested layouts.
155
+ */
156
+ function resolveNodeModulesPackageJson(packageName, fromDirectory) {
157
+ let directory = fromDirectory;
158
+ while (directory !== path.dirname(directory)) {
159
+ const candidate = path.join(directory, 'node_modules', packageName, 'package.json');
160
+ if (fs.existsSync(candidate)) {
161
+ return candidate;
162
+ }
163
+ directory = path.dirname(directory);
164
+ }
165
+ throw new Error(`Cannot find package.json for ${packageName}`);
166
+ }
167
+
168
+ function main() {
169
+ const startupDirectory = path.resolve(__dirname, '..', '..');
170
+ const packageJsonPath = path.join(startupDirectory, 'package.json');
171
+ const binDirectory = path.join(startupDirectory, 'bin');
172
+
173
+ console.log('Generating bin wrappers...');
174
+
175
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
176
+ const dependencies = Object.keys(packageJson.dependencies ?? {});
177
+
178
+ const bins = discoverBins(startupDirectory, dependencies);
179
+ const removed = removeObsoleteWrappers(binDirectory, bins);
180
+ const { binEntries, added } = generateWrappers(binDirectory, bins);
181
+
182
+ if (JSON.stringify(packageJson.bin) !== JSON.stringify(binEntries)) {
183
+ packageJson.bin = binEntries;
184
+ fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 4) + '\n');
185
+ }
186
+
187
+ for (const name of added) {
188
+ console.log(` added: ${name}`);
189
+ }
190
+ for (const file of removed) {
191
+ console.log(` removed: ${file}`);
192
+ }
193
+ const upToDate = bins.length - added.length;
194
+ console.log(
195
+ ` ${added.length} added/updated, ${removed.length} removed, ${upToDate} up to date`
196
+ );
197
+ }
198
+
199
+ // Only run when executed directly (e.g., `node generate-bin-wrappers.js`), not when imported (e.g., in tests).
200
+ /* istanbul ignore next */
201
+ if (require.main === module) {
202
+ main();
203
+ }
204
+
205
+ module.exports = { main };
@@ -49,31 +49,6 @@ describe(`[startup/storybook-config] ${webpackFinal.name}`, () => {
49
49
  );
50
50
  });
51
51
 
52
- describe('when config has optimization settings', () => {
53
- beforeEach(() => {
54
- config.optimization = { sideEffects: true, splitChunks: { chunks: 'all' } };
55
- });
56
-
57
- describe('when mode is development', () => {
58
- beforeEach(() => (config.mode = 'development'));
59
-
60
- test('disables sideEffects to preserve CSS imports from barrel files', () => {
61
- expect(subject().optimization).toEqual({
62
- ...config.optimization,
63
- sideEffects: false,
64
- });
65
- });
66
- });
67
-
68
- describe('when mode is production', () => {
69
- beforeEach(() => (config.mode = 'production'));
70
-
71
- test('does not change sideEffects', () => {
72
- expect(subject().optimization).toEqual(config.optimization);
73
- });
74
- });
75
- });
76
-
77
52
  test('adds MiniCssExtractPlugin to config', () => {
78
53
  expect(subject().plugins).toEqual([mockMiniCssExtractPlugin]);
79
54
  });
@@ -14,10 +14,6 @@ export function webpackFinal(config: Configuration, overrides: Overrides = {}):
14
14
 
15
15
  return {
16
16
  ...config,
17
- optimization: {
18
- ...config.optimization,
19
- ...(config.mode !== 'production' ? { sideEffects: false } : {}),
20
- },
21
17
  module: {
22
18
  ...config.module,
23
19
  rules: [...transformDefaultRules(config.module?.rules), ...rules],
@@ -27,7 +27,9 @@ describe('[startup] Utils', () => {
27
27
  testEnvironment: 'jsdom',
28
28
  testPathIgnorePatterns: ['\\.yalc', ...destinationFolders],
29
29
  testRunner: 'jest-circus/runner',
30
- transformIgnorePatterns: ['node_modules/(?!(@servicetitan|@react-hook|nanoid|axios)/)'],
30
+ transformIgnorePatterns: [
31
+ 'node_modules/(?!.*node_modules/)(?!(@servicetitan|@react-hook|nanoid|axios)/)',
32
+ ],
31
33
  verbose: true,
32
34
  };
33
35
 
@@ -190,10 +190,55 @@ describe('[startup] Utils', () => {
190
190
  });
191
191
  }
192
192
 
193
- describe('with no packages', () => {
194
- beforeEach(() => vol.fromJSON({ 'package.json': JSON.stringify({}) }));
193
+ describe('when package.json omits packages', () => {
194
+ const packages = {
195
+ foo: {
196
+ 'package.json': JSON.stringify({
197
+ name: 'foo',
198
+ version: '1.0.0',
199
+ }),
200
+ },
201
+ };
202
+
203
+ function volFromJSON(json?: Record<string, any>) {
204
+ vol.fromNestedJSON({
205
+ 'package.json': JSON.stringify({}),
206
+ packages,
207
+ ...json,
208
+ });
209
+ }
210
+
211
+ beforeEach(() => volFromJSON());
195
212
 
196
213
  itReturnsEmptyArray();
214
+
215
+ function itReturnsPackages() {
216
+ test('returns packages', () => {
217
+ expect(subject()).toEqual([
218
+ expect.objectContaining({ name: 'foo', version: '1.0.0' }),
219
+ ]);
220
+ });
221
+ }
222
+
223
+ describe('when pnpm-workspace.yaml contains packages', () => {
224
+ beforeEach(() => {
225
+ volFromJSON({
226
+ 'pnpm-workspace.yaml': 'packages:\n - "packages/*"\n',
227
+ });
228
+ });
229
+
230
+ itReturnsPackages();
231
+ });
232
+
233
+ describe('with lerna.json contains packages', () => {
234
+ beforeEach(() => {
235
+ volFromJSON({
236
+ 'lerna.json': JSON.stringify({ packages: ['packages/*'] }),
237
+ });
238
+ });
239
+
240
+ itReturnsPackages();
241
+ });
197
242
  });
198
243
 
199
244
  describe('with no package.json', () => {
@@ -8,7 +8,7 @@ jest.mock('node:child_process', () => ({
8
8
 
9
9
  describe('[startup] Prettify Utils', () => {
10
10
  const filename = '/path/to/file.ts';
11
- const expectedCommand = `npx prettier -w "${filename}"`;
11
+ const expectedCommand = `prettier -w "${filename}"`;
12
12
 
13
13
  beforeEach(() => {
14
14
  jest.resetAllMocks();
@@ -1,4 +1,6 @@
1
+ import fs from 'fs';
1
2
  import { globSync } from 'glob';
3
+ import yaml from 'js-yaml';
2
4
  import multimatch from 'multimatch';
3
5
  import path from 'path';
4
6
  import { findUp } from './find-up';
@@ -44,6 +46,12 @@ function findWorkspaces() {
44
46
  log.debug('find-packages', `reading: ${packageJson}`);
45
47
  let workspaces = readJsonSafe<{ workspaces: string[] }>(packageJson)?.workspaces;
46
48
 
49
+ if (!workspaces) {
50
+ const pnpmWorkspace = path.resolve(path.join(directory, 'pnpm-workspace.yaml'));
51
+ log.debug('find-packages', `reading: ${pnpmWorkspace}`);
52
+ workspaces = readYamlSafe<{ packages: string[] }>(pnpmWorkspace)?.packages;
53
+ }
54
+
47
55
  if (!workspaces) {
48
56
  const lernaJson = path.resolve(path.join(directory, 'lerna.json'));
49
57
  log.debug('find-packages', `reading: ${lernaJson}`);
@@ -64,3 +72,11 @@ function findWorkspaces() {
64
72
  }
65
73
  });
66
74
  }
75
+
76
+ function readYamlSafe<T>(filePath: string): T | undefined {
77
+ try {
78
+ return yaml.load(fs.readFileSync(filePath, 'utf-8')) as T;
79
+ } catch {
80
+ return undefined;
81
+ }
82
+ }
@@ -22,7 +22,10 @@ function getDefaultJestConfiguration() {
22
22
  testEnvironment: 'jsdom',
23
23
  testPathIgnorePatterns: ['\\.yalc', ...getDestinationFolders()],
24
24
  testRunner: 'jest-circus/runner',
25
- transformIgnorePatterns: ['node_modules/(?!(@servicetitan|@react-hook|nanoid|axios)/)'],
25
+ // Under pnpm, dependencies are nested (e.g. node_modules/.pnpm/…/node_modules/pkg); the extra lookahead skips the inner node_modules.
26
+ transformIgnorePatterns: [
27
+ 'node_modules/(?!.*node_modules/)(?!(@servicetitan|@react-hook|nanoid|axios)/)',
28
+ ],
26
29
  verbose: true,
27
30
  } as Omit<Config.Argv, 'collectCoverageFrom' | 'moduleNameMapper' | 'setupFiles'> & {
28
31
  collectCoverageFrom: string[];
@@ -2,7 +2,7 @@ import { exec, execSync } from 'node:child_process';
2
2
  import { promisify } from 'node:util';
3
3
 
4
4
  function getCommand(filename: string) {
5
- return `npx prettier -w "${filename}"`;
5
+ return `prettier -w "${filename}"`;
6
6
  }
7
7
 
8
8
  function createError(exitCode: number) {
@@ -1,3 +1,4 @@
1
+ import { getPackageManager } from '@servicetitan/install';
1
2
  import fs from 'fs';
2
3
  import path from 'path';
3
4
  import { Configuration } from 'webpack';
@@ -30,8 +31,9 @@ export function cacheConfig(context: Context, _overrides: Overrides): Result {
30
31
  }
31
32
 
32
33
  function findPackageLock() {
34
+ const { lockFileName } = getPackageManager();
33
35
  return findUp(directory => {
34
- const lockFile = path.resolve(path.join(directory), 'package-lock.json');
36
+ const lockFile = path.resolve(path.join(directory), lockFileName);
35
37
  return fs.existsSync(lockFile) ? lockFile : undefined;
36
38
  });
37
39
  }