@servicetitan/startup 23.0.0 → 23.2.0

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 (99) hide show
  1. package/dist/cli/commands/build.d.ts +1 -0
  2. package/dist/cli/commands/build.d.ts.map +1 -1
  3. package/dist/cli/commands/build.js +1 -0
  4. package/dist/cli/commands/build.js.map +1 -1
  5. package/dist/cli/commands/bundle-package.d.ts +1 -0
  6. package/dist/cli/commands/bundle-package.d.ts.map +1 -1
  7. package/dist/cli/commands/bundle-package.js +1 -0
  8. package/dist/cli/commands/bundle-package.js.map +1 -1
  9. package/dist/cli/commands/init.d.ts +8 -1
  10. package/dist/cli/commands/init.d.ts.map +1 -1
  11. package/dist/cli/commands/init.js +25 -3
  12. package/dist/cli/commands/init.js.map +1 -1
  13. package/dist/cli/commands/start.d.ts +1 -0
  14. package/dist/cli/commands/start.d.ts.map +1 -1
  15. package/dist/cli/commands/start.js +1 -0
  16. package/dist/cli/commands/start.js.map +1 -1
  17. package/dist/cli/utils/bundle.d.ts +1 -0
  18. package/dist/cli/utils/bundle.d.ts.map +1 -1
  19. package/dist/cli/utils/bundle.js +11 -5
  20. package/dist/cli/utils/bundle.js.map +1 -1
  21. package/dist/utils/get-configuration.d.ts +10 -6
  22. package/dist/utils/get-configuration.d.ts.map +1 -1
  23. package/dist/utils/get-configuration.js +47 -38
  24. package/dist/utils/get-configuration.js.map +1 -1
  25. package/dist/utils/get-jest-config.js +1 -1
  26. package/dist/utils/get-jest-config.js.map +1 -1
  27. package/dist/webpack/configs/plugins/ignore-plugin/check-resource.d.ts +2 -0
  28. package/dist/webpack/configs/plugins/ignore-plugin/check-resource.d.ts.map +1 -0
  29. package/dist/webpack/configs/plugins/ignore-plugin/check-resource.js +17 -0
  30. package/dist/webpack/configs/plugins/ignore-plugin/check-resource.js.map +1 -0
  31. package/dist/webpack/configs/plugins/ignore-plugin/ignore-plugin.d.ts +4 -0
  32. package/dist/webpack/configs/plugins/ignore-plugin/ignore-plugin.d.ts.map +1 -0
  33. package/dist/webpack/configs/plugins/ignore-plugin/ignore-plugin.js +15 -0
  34. package/dist/webpack/configs/plugins/ignore-plugin/ignore-plugin.js.map +1 -0
  35. package/dist/webpack/configs/plugins/ignore-plugin/index.d.ts +2 -0
  36. package/dist/webpack/configs/plugins/ignore-plugin/index.d.ts.map +1 -0
  37. package/dist/webpack/configs/plugins/ignore-plugin/index.js +18 -0
  38. package/dist/webpack/configs/plugins/ignore-plugin/index.js.map +1 -0
  39. package/dist/webpack/configs/plugins/ignore-plugin/is-optional-peer-dependency.d.ts +7 -0
  40. package/dist/webpack/configs/plugins/ignore-plugin/is-optional-peer-dependency.d.ts.map +1 -0
  41. package/dist/webpack/configs/plugins/ignore-plugin/is-optional-peer-dependency.js +8 -0
  42. package/dist/webpack/configs/plugins/ignore-plugin/is-optional-peer-dependency.js.map +1 -0
  43. package/dist/webpack/configs/plugins/index.d.ts +1 -0
  44. package/dist/webpack/configs/plugins/index.d.ts.map +1 -1
  45. package/dist/webpack/configs/plugins/index.js +1 -0
  46. package/dist/webpack/configs/plugins/index.js.map +1 -1
  47. package/dist/webpack/configs/plugins/virtual-modules-plugin.js +4 -3
  48. package/dist/webpack/configs/plugins/virtual-modules-plugin.js.map +1 -1
  49. package/dist/webpack/configs/plugins-config.d.ts.map +1 -1
  50. package/dist/webpack/configs/plugins-config.js +1 -0
  51. package/dist/webpack/configs/plugins-config.js.map +1 -1
  52. package/dist/webpack/configs/rules/tsx-rules.d.ts +1 -1
  53. package/dist/webpack/configs/rules/tsx-rules.d.ts.map +1 -1
  54. package/dist/webpack/configs/rules/tsx-rules.js +10 -8
  55. package/dist/webpack/configs/rules/tsx-rules.js.map +1 -1
  56. package/dist/webpack/configs/types.d.ts +2 -0
  57. package/dist/webpack/configs/types.d.ts.map +1 -1
  58. package/dist/webpack/create-webpack-config.d.ts.map +1 -1
  59. package/dist/webpack/create-webpack-config.js +2 -2
  60. package/dist/webpack/create-webpack-config.js.map +1 -1
  61. package/dist/webpack/types.d.ts +3 -2
  62. package/dist/webpack/types.d.ts.map +1 -1
  63. package/package.json +10 -8
  64. package/src/cli/commands/__tests__/build.test.ts +14 -0
  65. package/src/cli/commands/__tests__/init.test.ts +48 -17
  66. package/src/cli/commands/__tests__/start.test.ts +14 -0
  67. package/src/cli/commands/build.ts +2 -0
  68. package/src/cli/commands/bundle-package.ts +2 -0
  69. package/src/cli/commands/init.ts +26 -12
  70. package/src/cli/commands/start.ts +2 -0
  71. package/src/cli/utils/bundle.ts +14 -5
  72. package/src/utils/__tests__/get-configuration.test.ts +17 -5
  73. package/src/utils/__tests__/get-jest-config.test.ts +1 -1
  74. package/src/utils/get-configuration.ts +54 -43
  75. package/src/utils/get-jest-config.ts +1 -1
  76. package/src/webpack/__tests__/create-webpack-config-web-component.test.ts +34 -11
  77. package/src/webpack/__tests__/create-webpack-config.test.ts +46 -12
  78. package/src/webpack/configs/plugins/ignore-plugin/__tests__/check-resource.test.ts +37 -0
  79. package/src/webpack/configs/plugins/ignore-plugin/__tests__/is-optional-peer-dependency.test.ts +30 -0
  80. package/src/webpack/configs/plugins/ignore-plugin/check-resource.ts +12 -0
  81. package/src/webpack/configs/plugins/ignore-plugin/ignore-plugin.ts +13 -0
  82. package/src/webpack/configs/plugins/ignore-plugin/index.ts +1 -0
  83. package/src/webpack/configs/plugins/ignore-plugin/is-optional-peer-dependency.ts +10 -0
  84. package/src/webpack/configs/plugins/index.ts +1 -0
  85. package/src/webpack/configs/plugins/virtual-modules-plugin.ts +4 -3
  86. package/src/webpack/configs/plugins-config.ts +2 -0
  87. package/src/webpack/configs/rules/tsx-rules.ts +13 -9
  88. package/src/webpack/configs/types.ts +2 -0
  89. package/src/webpack/create-webpack-config.ts +5 -2
  90. package/src/webpack/types.ts +3 -2
  91. package/template/package.json +3 -3
  92. package/template/packages/application/package.json +9 -3
  93. package/template/packages/application/src/__tests__/app.test.tsx +1 -1
  94. package/template/packages/application/src/app.tsx +4 -3
  95. package/template-react18/packages/application/package.json +35 -0
  96. package/template-react18/packages/application/src/index.tsx +9 -0
  97. package/template-react18/packages/feature-a/package.json +19 -0
  98. package/template-react18/packages/feature-b/package.json +19 -0
  99. package/template-react18/packages/feature-c/package.json +19 -0
@@ -57,11 +57,15 @@ export enum CommandName {
57
57
  }
58
58
  /* eslint-enable @typescript-eslint/naming-convention */
59
59
 
60
+ interface WebComponentOptions {
61
+ legacyRoot: boolean;
62
+ }
63
+
60
64
  type Configuration = {
61
65
  'legacy'?: boolean;
62
66
  'lint'?: { eslint: ESLintConfiguration; stylelint: StylelintConfiguration };
63
67
  'test'?: JestConfiguration;
64
- 'web-component'?: boolean;
68
+ 'web-component'?: boolean | WebComponentOptions;
65
69
  'webpack'?: false | WebpackConfiguration;
66
70
  } & {
67
71
  [key in CommandName]: NodeConfiguration;
@@ -75,38 +79,6 @@ export function getConfigurationSafe(location = './'): Configuration {
75
79
  return readJsonSafe(path.join(location, 'package.json'))?.cli ?? {};
76
80
  }
77
81
 
78
- export function isBundle(location?: string) {
79
- return getConfiguration(location).webpack !== false;
80
- }
81
-
82
- export function isCustomStyleRules() {
83
- const configuration = getConfiguration();
84
-
85
- if (typeof configuration.webpack !== 'object') {
86
- return false;
87
- }
88
-
89
- return configuration.webpack['custom-style-rules'] === true;
90
- }
91
-
92
- export function isExposeSharedDependencies() {
93
- const configuration = getConfiguration();
94
-
95
- if (typeof configuration.webpack !== 'object') {
96
- return false;
97
- }
98
-
99
- return configuration.webpack['expose-shared-dependencies'] === true;
100
- }
101
-
102
- function pick<T, K extends keyof T>(target: T, keys: readonly K[]) {
103
- return Object.fromEntries(
104
- keys
105
- .filter(key => Object.prototype.hasOwnProperty.call(target, key))
106
- .map(key => [key, target[key]])
107
- ) as Pick<T, K>;
108
- }
109
-
110
82
  export function getDevServerConfiguration() {
111
83
  const configuration = getConfiguration();
112
84
 
@@ -130,24 +102,49 @@ export function getDevServerConfiguration() {
130
102
  >;
131
103
  }
132
104
 
133
- export function isWebComponent() {
134
- return getConfiguration()['web-component'] === true;
135
- }
136
-
137
- export function isLegacy(location?: string) {
138
- return getConfiguration(location).legacy === true;
139
- }
140
-
141
105
  export function getESLintConfiguration() {
142
106
  return getConfiguration().lint?.eslint ?? {};
143
107
  }
144
108
 
109
+ export function getJestConfiguration() {
110
+ return getConfiguration().test ?? {};
111
+ }
112
+
145
113
  export function getStylelintConfiguration() {
146
114
  return getConfiguration().lint?.stylelint ?? {};
147
115
  }
148
116
 
149
- export function getJestConfiguration() {
150
- return getConfiguration().test ?? {};
117
+ export function isBundle(location?: string) {
118
+ return getConfiguration(location).webpack !== false;
119
+ }
120
+
121
+ export function isCustomStyleRules() {
122
+ const configuration = getConfiguration();
123
+
124
+ if (typeof configuration.webpack !== 'object') {
125
+ return false;
126
+ }
127
+
128
+ return configuration.webpack['custom-style-rules'] === true;
129
+ }
130
+
131
+ export function isExposeSharedDependencies() {
132
+ const configuration = getConfiguration();
133
+
134
+ if (typeof configuration.webpack !== 'object') {
135
+ return false;
136
+ }
137
+
138
+ return configuration.webpack['expose-shared-dependencies'] === true;
139
+ }
140
+
141
+ export function isLegacy(location?: string) {
142
+ return getConfiguration(location).legacy === true;
143
+ }
144
+
145
+ export function isLegacyRoot() {
146
+ const config = getConfiguration()['web-component'];
147
+ return typeof config === 'object' && config.legacyRoot === true;
151
148
  }
152
149
 
153
150
  export function isStyleCheckDisabled() {
@@ -159,3 +156,17 @@ export function isStyleCheckDisabled() {
159
156
 
160
157
  return configuration.webpack?.['disable-style-check'] === true;
161
158
  }
159
+
160
+ export function isWebComponent() {
161
+ const config = getConfiguration()['web-component'];
162
+ return config === true || typeof config === 'object';
163
+ }
164
+
165
+ function pick<T extends object, K extends keyof T>(target: T, keys: readonly K[]) {
166
+ return keys.reduce((result, key) => {
167
+ if (Object.hasOwn(target, key)) {
168
+ result[key] = target[key];
169
+ }
170
+ return result;
171
+ }, {} as Pick<T, K>);
172
+ }
@@ -28,7 +28,7 @@ const getJestConfigBase = ({
28
28
  verbose: true,
29
29
  testEnvironment: 'jsdom',
30
30
  testRunner: 'jest-circus/runner',
31
- transformIgnorePatterns: ['node_modules/(?!(@servicetitan|@react-hook|nanoid)/)'],
31
+ transformIgnorePatterns: ['node_modules/(?!(@servicetitan|@react-hook|nanoid|axios)/)'],
32
32
  modulePathIgnorePatterns: ['<rootDir>/.*/__mocks__'],
33
33
  transform,
34
34
  moduleNameMapper,
@@ -7,7 +7,14 @@ import WebpackAssetsManifest from 'webpack-assets-manifest';
7
7
  import VirtualModulesPlugin from 'webpack-virtual-modules';
8
8
 
9
9
  import { webpackDevConfigFileName, webpackProdConfigFileName } from '../../cli/utils';
10
- import { getFolders, getPackageData, getPackages, getTsConfig, isWebComponent } from '../../utils';
10
+ import {
11
+ getFolders,
12
+ getPackageData,
13
+ getPackages,
14
+ getTsConfig,
15
+ isLegacyRoot,
16
+ isWebComponent,
17
+ } from '../../utils';
11
18
  import { webComponentStyleRules } from '../__mocks__';
12
19
  import { generateMetadata } from '../configs/utils';
13
20
  import { featureCohort, getCallerFile, splitByEntry } from '../utils';
@@ -33,6 +40,7 @@ jest.mock('../../utils', () => ({
33
40
  getPackageData: jest.fn(),
34
41
  getPackages: jest.fn(),
35
42
  getTsConfig: jest.fn(),
43
+ isLegacyRoot: jest.fn(),
36
44
  isWebComponent: jest.fn(),
37
45
  }));
38
46
  jest.mock('../configs/utils/generate-metadata');
@@ -56,12 +64,13 @@ describe(`[startup] ${createWebpackConfig.name}`, () => {
56
64
  ].join('\n');
57
65
 
58
66
  const indexPath = () => path.join(process.cwd(), `${source}/index.ts`);
59
- const indexCode = [
60
- `import { register } from '@servicetitan/web-components';`,
61
- `import { App } from './app';`,
62
- `require('./design-system.css');`,
63
- `register(App, false);`,
64
- ].join('\n');
67
+ const indexCode = ({ legacyRoot = false }: { legacyRoot?: boolean } = {}) =>
68
+ [
69
+ `import { register } from '@servicetitan/web-components';`,
70
+ `import { App } from './app';`,
71
+ `require('./design-system.css');`,
72
+ `register(App, false, { legacyRoot: ${legacyRoot} });`,
73
+ ].join('\n');
65
74
 
66
75
  let overrides: Parameters<typeof createWebpackConfig>[0];
67
76
  let options: NonNullable<Parameters<typeof createWebpackConfig>[1]>;
@@ -86,6 +95,7 @@ describe(`[startup] ${createWebpackConfig.name}`, () => {
86
95
  jest.mocked(getPackageData).mockReturnValue({ dependencies: {}, sharedDependencies: {} });
87
96
  jest.mocked(getPackages).mockReturnValue([]);
88
97
  jest.mocked(getTsConfig).mockReturnValue(tsConfig);
98
+ jest.mocked(isLegacyRoot).mockReturnValue(false);
89
99
 
90
100
  vol.fromJSON({ [packageJson]: JSON.stringify({ name: packageName }) });
91
101
  });
@@ -216,11 +226,24 @@ describe(`[startup] ${createWebpackConfig.name}`, () => {
216
226
  expect(subject().plugins).toContainEqual(
217
227
  new VirtualModulesPlugin({
218
228
  [designSystemPath()]: designSystemCode,
219
- [indexPath()]: indexCode,
229
+ [indexPath()]: indexCode(),
220
230
  })
221
231
  );
222
232
  });
223
233
 
234
+ describe('with {legacyRoot: true}', () => {
235
+ beforeEach(() => jest.mocked(isLegacyRoot).mockReturnValue(true));
236
+
237
+ test('registers component with {legacyRoot: true}', () => {
238
+ expect(subject().plugins).toContainEqual(
239
+ new VirtualModulesPlugin({
240
+ [designSystemPath()]: designSystemCode,
241
+ [indexPath()]: indexCode({ legacyRoot: true }),
242
+ })
243
+ );
244
+ });
245
+ });
246
+
224
247
  describe('when design-system.css exists', () => {
225
248
  beforeEach(() => {
226
249
  vol.fromJSON({
@@ -232,7 +255,7 @@ describe(`[startup] ${createWebpackConfig.name}`, () => {
232
255
  test('omits design system virtual module', () => {
233
256
  expect(subject().plugins).toContainEqual(
234
257
  new VirtualModulesPlugin({
235
- [indexPath()]: indexCode,
258
+ [indexPath()]: indexCode(),
236
259
  })
237
260
  );
238
261
  });
@@ -245,9 +268,9 @@ describe(`[startup] ${createWebpackConfig.name}`, () => {
245
268
  expect(subject().plugins).toContainEqual(
246
269
  new VirtualModulesPlugin({
247
270
  [designSystemPath()]: designSystemCode,
248
- [indexPath()]: indexCode
271
+ [indexPath()]: indexCode()
249
272
  .replace("require('./design-system.css');\n", '')
250
- .replace('register(App, false)', 'register(App, true)'),
273
+ .replace('register(App, false', 'register(App, true'),
251
274
  })
252
275
  );
253
276
  });
@@ -5,7 +5,7 @@ import { fs, vol } from 'memfs';
5
5
  import MomentLocalesPlugin from 'moment-locales-webpack-plugin';
6
6
  import os from 'os';
7
7
  import path from 'path';
8
- import { ProvidePlugin } from 'webpack';
8
+ import { IgnorePlugin, ProvidePlugin } from 'webpack';
9
9
  const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
10
10
  const FilterWarningsPlugin = require('webpack-filter-warnings-plugin');
11
11
  import { argv } from 'yargs';
@@ -37,6 +37,7 @@ jest.mock('mini-css-extract-plugin', () =>
37
37
  jest.mock('moment-locales-webpack-plugin', () => jest.fn());
38
38
  jest.mock('webpack', () => ({
39
39
  ...jest.requireActual('webpack'),
40
+ IgnorePlugin: jest.fn(),
40
41
  ProvidePlugin: jest.fn(),
41
42
  }));
42
43
  jest.mock('webpack-assets-manifest', () => jest.fn());
@@ -86,7 +87,8 @@ describe(`[startup] ${createWebpackConfig.name}`, () => {
86
87
  }
87
88
 
88
89
  function setOptions(props: typeof options) {
89
- Object.assign((options ??= {}), props);
90
+ options ??= {};
91
+ Object.assign(options, props);
90
92
  }
91
93
 
92
94
  beforeEach(() => {
@@ -108,13 +110,14 @@ describe(`[startup] ${createWebpackConfig.name}`, () => {
108
110
  mockPlugIn('ForkTsCheckerWebpackPlugin')
109
111
  );
110
112
  jest.mocked(HtmlWebpackPlugin).mockImplementation(mockPlugIn('HtmlWebpackPlugin'));
113
+ jest.mocked(IgnorePlugin).mockImplementation(mockPlugIn('IgnorePlugin'));
111
114
  jest.mocked(MomentLocalesPlugin).mockImplementation(mockPlugIn('MomentLocalesPlugin'));
112
115
  jest.mocked(ProvidePlugin).mockImplementation(mockPlugIn('ProvidePlugin'));
113
116
 
114
- jest.mocked(getFolders).mockImplementation((location?: string) => ({
115
- source: `${location ? `${location.replace(/\W/g, '')}-` : ''}${source}`,
116
- destination,
117
- }));
117
+ jest.mocked(getFolders).mockImplementation((location?: string) => {
118
+ const prefix = location ? `${location.replace(/\W/g, '')}-` : '';
119
+ return { source: `${prefix}${source}`, destination };
120
+ });
118
121
  jest.mocked(getPackageData).mockReturnValue({ dependencies, sharedDependencies });
119
122
  jest.mocked(getPackageDependencyVersion).mockImplementation(
120
123
  (_, defaultVersion) => defaultVersion
@@ -231,7 +234,19 @@ describe(`[startup] ${createWebpackConfig.name}`, () => {
231
234
  });
232
235
  });
233
236
 
234
- function itConfiguresTsLoader() {
237
+ function itConfiguresCodeCoverage() {
238
+ test('configures ".tsx" rules to use coverage-istanbul-loader', () => {
239
+ expect(subject().module?.rules).toEqual(
240
+ expect.arrayContaining([
241
+ expect.objectContaining({
242
+ test: /\.tsx?$/,
243
+ use: expect.arrayContaining(['@jsdevtools/coverage-istanbul-loader']),
244
+ }),
245
+ ])
246
+ );
247
+ });
248
+ }
249
+ function itConfiguresESBuildLoader() {
235
250
  test('configures ".tsx" rules to use esbuild-loader', () => {
236
251
  expect(subject().module?.rules).toContainEqual({
237
252
  test: /\.tsx?$/,
@@ -265,16 +280,26 @@ describe(`[startup] ${createWebpackConfig.name}`, () => {
265
280
  expect(subject().plugins).toContainEqual(new ProvidePlugin({ React: 'react' }));
266
281
  });
267
282
  }
283
+ describe('with --code-coverage command line argument', () => {
284
+ beforeEach(() => Object.assign(argv, { 'code-coverage': true }));
285
+ itConfiguresCodeCoverage();
286
+ });
287
+ describe('with codeCoverage option', () => {
288
+ beforeEach(() => {
289
+ options = { codeCoverage: true };
290
+ });
291
+ itConfiguresCodeCoverage();
292
+ });
268
293
  describe('with --esbuild command line argument', () => {
269
294
  beforeEach(() => Object.assign(argv, { esbuild: true }));
270
- itConfiguresTsLoader();
295
+ itConfiguresESBuildLoader();
271
296
  itConfiguresReactToLoadAutomatically();
272
297
  });
273
298
  describe('with esbuild option', () => {
274
299
  beforeEach(() => {
275
300
  options = { esbuild: true };
276
301
  });
277
- itConfiguresTsLoader();
302
+ itConfiguresESBuildLoader();
278
303
  itConfiguresReactToLoadAutomatically();
279
304
  });
280
305
  describe('with experimental-bundlers option', () => {
@@ -291,8 +316,8 @@ describe(`[startup] ${createWebpackConfig.name}`, () => {
291
316
  });
292
317
 
293
318
  test('configures ".css" rule to exclude .module.css files', () => {
294
- const cssRule: any = subject().module?.rules?.find(({ test }: { test: RegExp }) =>
295
- '.css'.match(test)
319
+ const cssRule: any = subject().module?.rules?.find(({ test: regexp }: { test: RegExp }) =>
320
+ regexp.test('.css')
296
321
  );
297
322
  expect(
298
323
  ['design-system.css', 'foo.module.css', 'foo.css'].filter(path => cssRule.exclude(path))
@@ -344,6 +369,12 @@ describe(`[startup] ${createWebpackConfig.name}`, () => {
344
369
  );
345
370
  });
346
371
 
372
+ test('configures IgnorePlugin plugin', () => {
373
+ expect(subject().plugins).toContainEqual(
374
+ new IgnorePlugin({ checkResource: expect.any(Function) })
375
+ );
376
+ });
377
+
347
378
  describe('when buildStat option is set to true', () => {
348
379
  beforeEach(() => {
349
380
  setOptions({ buildStat: true });
@@ -428,7 +459,10 @@ describe(`[startup] ${createWebpackConfig.name}`, () => {
428
459
  });
429
460
 
430
461
  describe('when mode is "production"', () => {
431
- beforeEach(() => Object.assign((overrides.configuration ??= {}), { mode: 'production' }));
462
+ beforeEach(() => {
463
+ overrides.configuration ??= {};
464
+ Object.assign(overrides.configuration, { mode: 'production' });
465
+ });
432
466
 
433
467
  test('changes "devtool" to "source-map"', () => {
434
468
  expect(subject().devtool).toEqual('source-map');
@@ -0,0 +1,37 @@
1
+ import { checkResource } from '../check-resource';
2
+ import { isOptionalPeerDependency } from '../is-optional-peer-dependency';
3
+
4
+ jest.mock('../is-optional-peer-dependency');
5
+
6
+ describe(`${checkResource.name}`, () => {
7
+ let context: string;
8
+ let resource: string;
9
+
10
+ const subject = () => checkResource(resource, context);
11
+
12
+ function itReturns(value: boolean) {
13
+ test(`returns ${value}`, () => expect(subject()).toBe(value));
14
+ }
15
+
16
+ describe('when resource is not an optional peer dependency', () => {
17
+ beforeEach(() => jest.mocked(isOptionalPeerDependency).mockReturnValue(false));
18
+
19
+ itReturns(false);
20
+ });
21
+
22
+ describe('when resource is optional peer dependency', () => {
23
+ beforeEach(() => jest.mocked(isOptionalPeerDependency).mockReturnValue(true));
24
+
25
+ describe('when resource is present', () => {
26
+ beforeEach(() => (resource = 'fs'));
27
+
28
+ itReturns(false);
29
+ });
30
+
31
+ describe('when resource is not present', () => {
32
+ beforeEach(() => (resource = 'foo'));
33
+
34
+ itReturns(true);
35
+ });
36
+ });
37
+ });
@@ -0,0 +1,30 @@
1
+ import { isOptionalPeerDependency } from '../is-optional-peer-dependency';
2
+
3
+ describe(`${isOptionalPeerDependency.name}`, () => {
4
+ const subject = (...args: Parameters<typeof isOptionalPeerDependency>) =>
5
+ isOptionalPeerDependency(...args);
6
+
7
+ describe.each([
8
+ {
9
+ resource: 'react-dom/client',
10
+ contexts: ['@servicetitan/web-components', '@servicetitan/ko-bridge'],
11
+ },
12
+ ])('with dependency "$resource"', ({ resource, contexts }) => {
13
+ contexts.forEach(context => {
14
+ test(`with context ${context}, returns true`, () => {
15
+ expect(subject({ context, resource })).toBe(true);
16
+ });
17
+ });
18
+ });
19
+
20
+ describe.each([
21
+ { resource: 'react-dom', contexts: ['@servicetitan/web-components'] },
22
+ { resource: 'react-dom/client', contexts: ['@servicetitan/foo'] },
23
+ ])('with dependency "$resource"', ({ resource, contexts }) => {
24
+ contexts.forEach(context => {
25
+ test(`with context ${context}, returns false`, () => {
26
+ expect(subject({ context, resource })).toBe(false);
27
+ });
28
+ });
29
+ });
30
+ });
@@ -0,0 +1,12 @@
1
+ import { isOptionalPeerDependency } from './is-optional-peer-dependency';
2
+
3
+ export function checkResource(resource: string, context: string) {
4
+ if (isOptionalPeerDependency({ resource, context })) {
5
+ try {
6
+ require.resolve(resource);
7
+ } catch (e) {
8
+ return true;
9
+ }
10
+ }
11
+ return false;
12
+ }
@@ -0,0 +1,13 @@
1
+ import { IgnorePlugin } from 'webpack';
2
+
3
+ import { Context, Overrides } from '../../types';
4
+ import { checkResource } from './check-resource';
5
+
6
+ export function ignorePlugin(_context: Context, _: Overrides) {
7
+ /**
8
+ * Ignore optional peer dependencies
9
+ * @see {@link: file://./../../../../../../web-components/src/render.ts}
10
+ * @see {@link: file://./../../../../../../ko-bridge/src/ko-binding-handlers.tsx}
11
+ */
12
+ return new IgnorePlugin({ checkResource });
13
+ }
@@ -0,0 +1 @@
1
+ export * from './ignore-plugin';
@@ -0,0 +1,10 @@
1
+ interface Dependency {
2
+ context: string;
3
+ resource: string;
4
+ }
5
+
6
+ export function isOptionalPeerDependency({ context, resource }: Dependency) {
7
+ return (
8
+ resource === 'react-dom/client' && /@servicetitan\/(ko-bridge|web-components)/.test(context)
9
+ );
10
+ }
@@ -4,6 +4,7 @@ export * from './define-exposed-dependencies-plugin';
4
4
  export * from './define-web-component-name-plugin';
5
5
  export * from './filter-warnings-plugin';
6
6
  export * from './html-plugin';
7
+ export * from './ignore-plugin';
7
8
  export * from './mini-css-extract-plugin';
8
9
  export * from './moment-locales-plugin';
9
10
  export * from './provide-react-plugin';
@@ -31,12 +31,13 @@ function designSystemCode() {
31
31
  ].join('\n');
32
32
  }
33
33
 
34
- function indexCode({ embed }: Context) {
34
+ function indexCode({ embed, isLegacyRoot }: Context) {
35
+ const options = `{ legacyRoot: ${isLegacyRoot} }`;
35
36
  return [
36
37
  `import { register } from '@servicetitan/web-components';`,
37
38
  `import { App } from './app';`,
38
39
  ...(embed
39
- ? [`register(App, true);`]
40
- : [`require('./design-system.css');`, `register(App, false);`]),
40
+ ? [`register(App, true, ${options});`]
41
+ : [`require('./design-system.css');`, `register(App, false, ${options});`]),
41
42
  ].join('\n');
42
43
  }
@@ -6,6 +6,7 @@ import {
6
6
  defineWebComponentNamePlugin,
7
7
  filterWarningsPlugin,
8
8
  htmlPlugin,
9
+ ignorePlugin,
9
10
  miniCssExtractPlugin,
10
11
  momentLocalesPlugin,
11
12
  provideReactPlugin,
@@ -25,6 +26,7 @@ export function pluginsConfig(context: Context, overrides: Overrides): Result {
25
26
  defineWebComponentNamePlugin,
26
27
  filterWarningsPlugin,
27
28
  htmlPlugin,
29
+ ignorePlugin,
28
30
  miniCssExtractPlugin,
29
31
  momentLocalesPlugin,
30
32
  tsCheckerPlugin,
@@ -1,7 +1,7 @@
1
- import { RuleSetRule } from 'webpack';
1
+ import { RuleSetRule, RuleSetUseItem } from 'webpack';
2
2
  import { Context } from '../types';
3
3
 
4
- export function tsxRules({ esbuild, experimentalBundlers }: Context): RuleSetRule[] {
4
+ export function tsxRules({ codeCoverage, esbuild, experimentalBundlers }: Context): RuleSetRule[] {
5
5
  let loader: Record<string, any> = {
6
6
  loader: 'ts-loader',
7
7
  options: { transpileOnly: true },
@@ -34,11 +34,15 @@ export function tsxRules({ esbuild, experimentalBundlers }: Context): RuleSetRul
34
34
  };
35
35
  }
36
36
 
37
- return [
38
- {
39
- test: /\.tsx?$/,
40
- exclude: /node_modules/,
41
- use: [loader],
42
- },
43
- ];
37
+ const rule = {
38
+ test: /\.tsx?$/,
39
+ exclude: /node_modules/,
40
+ use: [loader] as RuleSetUseItem[],
41
+ };
42
+
43
+ if (codeCoverage) {
44
+ rule.use.unshift('@jsdevtools/coverage-istanbul-loader');
45
+ }
46
+
47
+ return [rule];
44
48
  }
@@ -3,11 +3,13 @@ import { Options } from '../types';
3
3
  export { Overrides } from '../types';
4
4
 
5
5
  export interface Context extends Options {
6
+ codeCoverage: boolean;
6
7
  destination: string;
7
8
  esbuild: boolean;
8
9
  experimentalBundlers: boolean;
9
10
  isCustomStyleRules: boolean;
10
11
  isExposeSharedDependencies: boolean;
12
+ isLegacyRoot: boolean;
11
13
  isProduction: boolean;
12
14
  isWebComponent: boolean;
13
15
  packageData: ReturnType<typeof getPackageData>;
@@ -8,6 +8,7 @@ import {
8
8
  getPackageData,
9
9
  isCustomStyleRules,
10
10
  isExposeSharedDependencies,
11
+ isLegacyRoot,
11
12
  isWebComponent,
12
13
  loadSharedDependencies,
13
14
  log,
@@ -41,12 +42,14 @@ export function createWebpackConfig(overrides: Overrides, options: Options = {})
41
42
  );
42
43
 
43
44
  const context: Context = {
45
+ codeCoverage: options.codeCoverage ?? !!(argv as Arguments)['code-coverage'],
44
46
  destination,
45
- esbuild: options.esbuild ?? !!(argv as Arguments).esbuild ?? false,
47
+ esbuild: options.esbuild ?? !!(argv as Arguments).esbuild,
46
48
  experimentalBundlers:
47
- options.experimentalBundlers ?? !!(argv as Arguments)['experimental-bundlers'] ?? false,
49
+ options.experimentalBundlers ?? !!(argv as Arguments)['experimental-bundlers'],
48
50
  isCustomStyleRules: isCustomStyleRules(),
49
51
  isExposeSharedDependencies: isExposeSharedDependencies(),
52
+ isLegacyRoot: isLegacyRoot(),
50
53
  isProduction: configuration.mode === 'production',
51
54
  isWebComponent: isWebComponent(),
52
55
  name: '',
@@ -4,11 +4,12 @@ import { PluginOptions as MiniCssExtractPlugInOptions } from 'mini-css-extract-p
4
4
  import { Configuration } from 'webpack';
5
5
 
6
6
  export interface Options {
7
- embed?: boolean;
8
7
  buildStat?: boolean;
9
- name?: string;
8
+ codeCoverage?: boolean;
9
+ embed?: boolean;
10
10
  esbuild?: boolean;
11
11
  experimentalBundlers?: boolean;
12
+ name?: string;
12
13
  }
13
14
 
14
15
  export interface Overrides {
@@ -4,8 +4,8 @@
4
4
  "description": "Multi-package web application template",
5
5
  "private": true,
6
6
  "engines": {
7
- "node": ">=16",
8
- "npm": ">=7"
7
+ "node": ">=18",
8
+ "npm": ">=10"
9
9
  },
10
10
  "scripts": {
11
11
  "bootstrap": "npx --yes @servicetitan/startup install",
@@ -17,7 +17,7 @@
17
17
  "test": "startup test"
18
18
  },
19
19
  "devDependencies": {
20
- "@servicetitan/startup": ">=22.11.0"
20
+ "@servicetitan/startup": ">=22.21.0"
21
21
  },
22
22
  "workspaces": [
23
23
  "packages/*"
@@ -7,12 +7,18 @@
7
7
  "typings": "./dist/index.d.ts",
8
8
  "scripts": {},
9
9
  "dependencies": {
10
- "@servicetitan/design-system": "^12.10.0",
11
- "@servicetitan/link-item": "^23.1.0",
12
- "@servicetitan/react-ioc": "^22.15.0",
10
+ "@servicetitan/design-system": "^13.4.3",
11
+ "@servicetitan/hash-browser-router": "^23.1.0",
12
+ "@servicetitan/link-item": "^25.9.0",
13
+ "@servicetitan/log-service": "^23.1.0",
14
+ "@servicetitan/react-ioc": "^23.1.0",
15
+ "@servicetitan/web-components": "^23.1.0",
16
+ "axios": "^0.27.2",
13
17
  "feature-a": "^0.0.0",
14
18
  "feature-b": "^0.0.0",
15
19
  "feature-c": "^0.0.0",
20
+ "history": "~4.10.1",
21
+ "mobx": "~6.10.0",
16
22
  "react": "^17.0.2",
17
23
  "react-dom": "^17.0.2",
18
24
  "react-router-dom": "^5.3.0"
@@ -18,7 +18,7 @@ describe(`${App.name}`, () => {
18
18
 
19
19
  const routes = Object.entries({
20
20
  MainPage: '/',
21
- SecondPage: '/second-page',
21
+ SecondPage: '/#/second-page',
22
22
  }).map(([component, path]) => ({ component, path }));
23
23
 
24
24
  describe.each(routes)('when location is $path', ({ component, path }) => {