@servicetitan/startup 25.0.0 → 26.0.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 (52) hide show
  1. package/dist/cli/commands/start.d.ts.map +1 -1
  2. package/dist/cli/commands/start.js +1 -0
  3. package/dist/cli/commands/start.js.map +1 -1
  4. package/dist/cli/utils/bundle.d.ts.map +1 -1
  5. package/dist/cli/utils/bundle.js +54 -48
  6. package/dist/cli/utils/bundle.js.map +1 -1
  7. package/dist/utils/get-configuration.d.ts +3 -2
  8. package/dist/utils/get-configuration.d.ts.map +1 -1
  9. package/dist/utils/get-configuration.js +2 -28
  10. package/dist/utils/get-configuration.js.map +1 -1
  11. package/dist/utils/get-package-data.d.ts +1 -0
  12. package/dist/utils/get-package-data.d.ts.map +1 -1
  13. package/dist/utils/get-package-data.js +1 -0
  14. package/dist/utils/get-package-data.js.map +1 -1
  15. package/dist/utils/index.d.ts +4 -3
  16. package/dist/utils/index.d.ts.map +1 -1
  17. package/dist/utils/index.js +4 -3
  18. package/dist/utils/index.js.map +1 -1
  19. package/dist/utils/log.js +1 -1
  20. package/dist/utils/log.js.map +1 -1
  21. package/dist/utils/pick.d.ts +2 -0
  22. package/dist/utils/pick.d.ts.map +1 -0
  23. package/dist/utils/pick.js +13 -0
  24. package/dist/utils/pick.js.map +1 -0
  25. package/dist/webpack/configs/dev-server-config.d.ts +1 -1
  26. package/dist/webpack/configs/dev-server-config.d.ts.map +1 -1
  27. package/dist/webpack/configs/dev-server-config.js +46 -5
  28. package/dist/webpack/configs/dev-server-config.js.map +1 -1
  29. package/dist/webpack/configs/plugins/bundle-analyser-plugin.d.ts +1 -1
  30. package/dist/webpack/configs/plugins/bundle-analyser-plugin.d.ts.map +1 -1
  31. package/dist/webpack/configs/plugins/bundle-analyser-plugin.js +28 -3
  32. package/dist/webpack/configs/plugins/bundle-analyser-plugin.js.map +1 -1
  33. package/package.json +11 -12
  34. package/src/cli/commands/__tests__/start.test.ts +5 -5
  35. package/src/cli/commands/__tests__/styles-check.test.ts +5 -1
  36. package/src/cli/commands/start.ts +1 -0
  37. package/src/cli/utils/__tests__/bundle.test.ts +19 -7
  38. package/src/cli/utils/bundle.ts +54 -55
  39. package/src/utils/__tests__/get-configuration.test.ts +0 -60
  40. package/src/utils/__tests__/get-package-data.test.ts +5 -1
  41. package/src/utils/__tests__/log.test.ts +2 -2
  42. package/src/utils/get-configuration.ts +3 -34
  43. package/src/utils/get-package-data.ts +2 -0
  44. package/src/utils/index.ts +4 -3
  45. package/src/utils/log.ts +1 -1
  46. package/src/utils/pick.ts +8 -0
  47. package/src/webpack/__tests__/create-webpack-config-shared-dependencies.test.ts +5 -1
  48. package/src/webpack/__tests__/create-webpack-config-web-component.test.ts +5 -1
  49. package/src/webpack/__tests__/create-webpack-config.test.ts +148 -20
  50. package/src/webpack/configs/dev-server-config.ts +61 -12
  51. package/src/webpack/configs/plugins/bundle-analyser-plugin.ts +32 -6
  52. package/src/webpack/configs/utils/__tests__/generate-metadata.test.ts +1 -1
@@ -6,6 +6,7 @@ import MomentLocalesPlugin from 'moment-locales-webpack-plugin';
6
6
  import os from 'os';
7
7
  import path from 'path';
8
8
  import { IgnorePlugin, ProvidePlugin } from 'webpack';
9
+ import WebpackDevServer from 'webpack-dev-server';
9
10
  const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
10
11
  const FilterWarningsPlugin = require('webpack-filter-warnings-plugin');
11
12
  import { argv } from 'yargs';
@@ -13,6 +14,8 @@ import { argv } from 'yargs';
13
14
  import {
14
15
  Package,
15
16
  PackageType,
17
+ WebpackConfiguration,
18
+ getConfiguration,
16
19
  getFolders,
17
20
  getPackageData,
18
21
  getPackageDependencyVersion,
@@ -21,6 +24,7 @@ import {
21
24
  isCustomStyleRules,
22
25
  isWebComponent,
23
26
  loadSharedDependencies,
27
+ log,
24
28
  } from '../../utils';
25
29
  import { createPackage } from '../../__mocks__';
26
30
  import { fileRules, productionStyleRules, styleRules } from '../__mocks__';
@@ -47,6 +51,7 @@ jest.mock('webpack-virtual-modules', () => jest.fn());
47
51
  jest.mock('yargs', () => ({ argv: {} }));
48
52
  jest.mock('../../utils', () => ({
49
53
  ...jest.requireActual('../../utils'),
54
+ getConfiguration: jest.fn(),
50
55
  getFolders: jest.fn(),
51
56
  getPackageData: jest.fn(),
52
57
  getPackageDependencyVersion: jest.fn(),
@@ -56,7 +61,7 @@ jest.mock('../../utils', () => ({
56
61
  isExposeSharedDependencies: jest.fn(),
57
62
  isWebComponent: jest.fn(),
58
63
  loadSharedDependencies: jest.fn(),
59
- log: { debug: jest.fn(), info: jest.fn() },
64
+ log: { debug: jest.fn(), info: jest.fn(), warning: jest.fn() },
60
65
  }));
61
66
  jest.mock('../utils', () => ({
62
67
  ...jest.requireActual('../utils'),
@@ -81,6 +86,7 @@ describe(`[startup] ${createWebpackConfig.name}`, () => {
81
86
  let packages: Package[];
82
87
  let overrides: Parameters<typeof createWebpackConfig>[0];
83
88
  let options: Parameters<typeof createWebpackConfig>[1];
89
+ let configuration: { webpack?: WebpackConfiguration };
84
90
 
85
91
  function mockPlugIn(name: string) {
86
92
  return (options: Record<string, any>): any => ({ [name]: options });
@@ -101,6 +107,7 @@ describe(`[startup] ${createWebpackConfig.name}`, () => {
101
107
  },
102
108
  };
103
109
  options = undefined;
110
+ configuration = {};
104
111
 
105
112
  jest.resetAllMocks();
106
113
  jest.mocked(MiniCssExtractPlugin).mockImplementation(mockPlugIn('MiniCssExtractPlugin'));
@@ -114,11 +121,16 @@ describe(`[startup] ${createWebpackConfig.name}`, () => {
114
121
  jest.mocked(MomentLocalesPlugin).mockImplementation(mockPlugIn('MomentLocalesPlugin'));
115
122
  jest.mocked(ProvidePlugin).mockImplementation(mockPlugIn('ProvidePlugin'));
116
123
 
124
+ jest.mocked(getConfiguration).mockImplementation(() => configuration as any);
117
125
  jest.mocked(getFolders).mockImplementation((location?: string) => {
118
126
  const prefix = location ? `${location.replace(/\W/g, '')}-` : '';
119
127
  return { source: `${prefix}${source}`, destination };
120
128
  });
121
- jest.mocked(getPackageData).mockReturnValue({ dependencies, sharedDependencies });
129
+ jest.mocked(getPackageData).mockReturnValue({
130
+ name: packageName,
131
+ dependencies,
132
+ sharedDependencies,
133
+ });
122
134
  jest.mocked(getPackageDependencyVersion).mockImplementation(
123
135
  (_, defaultVersion) => defaultVersion
124
136
  );
@@ -181,16 +193,100 @@ describe(`[startup] ${createWebpackConfig.name}`, () => {
181
193
  expect(subject().amd).toEqual(false);
182
194
  });
183
195
 
196
+ const defaultDefServerConfig = {
197
+ hot: false,
198
+ port: 8080,
199
+ historyApiFallback: true,
200
+ devMiddleware: { stats: expect.anything(), writeToDisk: true },
201
+ headers: {
202
+ 'Access-Control-Allow-Origin': '*',
203
+ 'Access-Control-Allow-Headers': 'Origin, X-Requested-With, Content-Type, Accept',
204
+ },
205
+ static: '.',
206
+ };
207
+
184
208
  test('configures "devServer"', () => {
185
- expect(subject().devServer).toEqual({
186
- port: 8080,
187
- historyApiFallback: true,
188
- writeToDisk: true,
189
- headers: {
190
- 'Access-Control-Allow-Origin': '*',
191
- 'Access-Control-Allow-Headers': 'Origin, X-Requested-With, Content-Type, Accept',
192
- },
193
- stats: expect.anything(),
209
+ expect(subject().devServer).toEqual(defaultDefServerConfig);
210
+ });
211
+
212
+ describe('with custom devServer options', () => {
213
+ const options: WebpackDevServer.Configuration = {
214
+ // allowed options
215
+ headers: { foo: 'bar' },
216
+ port: 9000,
217
+ proxy: [{ context: ['/foo'] }],
218
+ static: 'foo',
219
+ // disallowed options
220
+ compress: true,
221
+ client: { logging: 'verbose', overlay: false },
222
+ };
223
+
224
+ beforeEach(() => (configuration = { webpack: options }));
225
+
226
+ test('adds allowed options to "devServer" configuration', () => {
227
+ expect(subject().devServer).toEqual({
228
+ ...defaultDefServerConfig,
229
+ headers: options.headers,
230
+ port: options.port,
231
+ proxy: options.proxy,
232
+ static: options.static,
233
+ });
234
+ });
235
+
236
+ describe('with "devServer" option', () => {
237
+ beforeEach(() => (configuration.webpack = { devServer: options }));
238
+
239
+ test('adds all options to "devServer" configuration', () => {
240
+ expect(subject().devServer).toEqual({
241
+ ...defaultDefServerConfig,
242
+ ...options,
243
+ });
244
+ });
245
+ });
246
+
247
+ describe('when "proxy" is a path', () => {
248
+ const proxy = 'proxy.js';
249
+ const proxyConfig = { context: ['/bar'] };
250
+
251
+ beforeEach(() => (configuration.webpack!.proxy = proxy));
252
+
253
+ describe('when file does not exist', () => {
254
+ test('drops the proxy option', () => {
255
+ expect(subject().devServer!.proxy).toBeUndefined();
256
+ });
257
+ });
258
+
259
+ describe('when file exists', () => {
260
+ beforeEach(() => {
261
+ vol.fromJSON({ [proxy]: JSON.stringify(proxyConfig) });
262
+
263
+ // Have to mock proxy file because our fs is virtual, but require() is not
264
+ jest.doMock(path.resolve(proxy), () => proxyConfig, {
265
+ virtual: true,
266
+ });
267
+ });
268
+
269
+ test('loads proxy option from the file', () => {
270
+ expect(subject().devServer!.proxy).toEqual(proxyConfig);
271
+ });
272
+ });
273
+ });
274
+
275
+ describe('when options omits "static"', () => {
276
+ beforeEach(() => delete configuration.webpack!.static);
277
+
278
+ describe('when options includes "contentBase"', () => {
279
+ const contentBase = 'src/public';
280
+
281
+ beforeEach(() => (configuration.webpack!.contentBase = contentBase));
282
+
283
+ test('moves "contentBase" value to "static" and logs deprecation warning', () => {
284
+ expect(subject().devServer!.static).toEqual(contentBase);
285
+ expect(log.warning).toHaveBeenCalledWith(
286
+ expect.stringMatching('DEPRECATION WARNING')
287
+ );
288
+ });
289
+ });
194
290
  });
195
291
  });
196
292
 
@@ -210,11 +306,11 @@ describe(`[startup] ${createWebpackConfig.name}`, () => {
210
306
  ];
211
307
  });
212
308
 
213
- test('configures "devServer.watchOptions" to ignore TSC packages', () => {
309
+ test('configures "watchOptions" to ignore TSC packages', () => {
214
310
  const ignored = packages
215
311
  .filter(({ type }) => type === PackageType.TSC)
216
312
  .map(({ location }) => location);
217
- expect(subject().devServer?.watchOptions).toEqual({ ignored });
313
+ expect(subject().watchOptions).toEqual({ ignored });
218
314
  });
219
315
  });
220
316
 
@@ -381,29 +477,61 @@ describe(`[startup] ${createWebpackConfig.name}`, () => {
381
477
  jest.useFakeTimers(); // because filename includes timestamp
382
478
  });
383
479
 
384
- const reportFilename = (type: string) =>
385
- path.join(os.tmpdir(), `report-${type}-bundle-${Date.now()}.html`);
480
+ function reportFilename(type?: string) {
481
+ return path.join(
482
+ os.tmpdir(),
483
+ `${packageName}${type ? `-${type}` : ''}-bundle-${Date.now()}.html`
484
+ );
485
+ }
486
+
487
+ function reportTitle(type?: string) {
488
+ const date = new Date();
489
+
490
+ const timestamp = `${date.getFullYear()}-${
491
+ date.getMonth() + 1
492
+ }-${date.getDate()} ${date.getHours()}:${date.getMinutes()}`;
493
+
494
+ return `${packageName.replace('@servicetitan/', '')}${
495
+ type ? ` (${type})` : ''
496
+ } ${timestamp}`;
497
+ }
386
498
 
387
499
  test('configures BundleAnalyzerPlugin plugin', () => {
388
500
  expect(subject().plugins).toContainEqual(
389
501
  new BundleAnalyzerPlugin({
390
502
  analyzerMode: 'static',
391
- reportFilename: reportFilename('full'),
503
+ reportFilename: reportFilename(),
504
+ reportTitle: reportTitle(),
392
505
  })
393
506
  );
394
507
  });
395
508
 
396
- describe('when embed option is set to true', () => {
397
- beforeEach(() => setOptions({ embed: true }));
509
+ describe('when package is web component', () => {
510
+ beforeEach(() => jest.mocked(isWebComponent).mockReturnValue(true));
398
511
 
399
- test('changes BundleAnalyzerPlugin reportFilename from "full" to "light"', () => {
512
+ test('adds "full" to reportFilename and reportTitle', () => {
400
513
  expect(subject().plugins).toContainEqual(
401
514
  new BundleAnalyzerPlugin({
402
515
  analyzerMode: 'static',
403
- reportFilename: reportFilename('light'),
516
+ reportFilename: reportFilename('full'),
517
+ reportTitle: reportTitle('full'),
404
518
  })
405
519
  );
406
520
  });
521
+
522
+ describe('when embed option is set to true', () => {
523
+ beforeEach(() => setOptions({ embed: true }));
524
+
525
+ test('adds "light" to reportFilename and reportTitle', () => {
526
+ expect(subject().plugins).toContainEqual(
527
+ new BundleAnalyzerPlugin({
528
+ analyzerMode: 'static',
529
+ reportFilename: reportFilename('light'),
530
+ reportTitle: reportTitle('light'),
531
+ })
532
+ );
533
+ });
534
+ });
407
535
  });
408
536
  });
409
537
 
@@ -1,36 +1,85 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
1
3
  import { Configuration } from 'webpack';
2
- import { PackageType, getDevServerConfiguration, getPackageName, getPackages } from '../../utils';
4
+ import {
5
+ PackageType,
6
+ allowedWebpackDevServerOptions,
7
+ getConfiguration,
8
+ getPackageName,
9
+ getPackages,
10
+ log,
11
+ pick,
12
+ } from '../../utils';
3
13
  import { statsConfig } from './stats-config';
4
14
  import { Context, Overrides } from './types';
5
15
 
6
- type Config = Configuration['devServer'];
7
- type Result = Pick<Configuration, 'devServer'> | undefined;
16
+ type DevServerConfig = NonNullable<Configuration['devServer']>;
17
+ type WatchOptionsConfig = NonNullable<Configuration['watchOptions']>;
18
+ type Result = Pick<Configuration, 'devServer' | 'watchOptions'> | undefined;
8
19
 
9
20
  export function devServerConfig(context: Context, overrides: Overrides): Result {
10
21
  if (context.isProduction) {
11
22
  return;
12
23
  }
13
24
 
14
- const devServer: Config = {
25
+ const watchOptions = watchOptionsConfig();
26
+ const devServer = {
27
+ hot: false,
15
28
  port: 8080,
16
29
  historyApiFallback: true,
17
- writeToDisk: true,
30
+ devMiddleware: { writeToDisk: true, ...statsConfig(context, overrides) },
18
31
  headers: {
19
32
  'Access-Control-Allow-Origin': '*',
20
33
  'Access-Control-Allow-Headers': 'Origin, X-Requested-With, Content-Type, Accept',
21
34
  },
22
- ...watchOptionsConfig(),
23
- ...statsConfig(context, overrides),
24
- ...getDevServerConfiguration(),
35
+ ...getDevServerConfig(),
25
36
  };
26
37
 
27
- return { devServer };
38
+ /* istanbul ignore next: debug only */
39
+ log.debug('dev-server-config', () => JSON.stringify({ devServer, watchOptions }, null, 2));
40
+ return { devServer, watchOptions };
28
41
  }
29
42
 
30
- function watchOptionsConfig() {
31
- const watchIgnore = getPackages({ scope: getPackageName() })
43
+ function getDevServerConfig() {
44
+ const webpack = getConfiguration().webpack || {}; // eslint-disable-line @typescript-eslint/prefer-nullish-coalescing
45
+ /* istanbul ignore next: debug only */
46
+ log.debug('dev-server-config', () => JSON.stringify({ webpack }, null, 2));
47
+
48
+ const result = pick(webpack, allowedWebpackDevServerOptions);
49
+ if (typeof result.proxy === 'string') {
50
+ if (fs.existsSync(result.proxy)) {
51
+ result.proxy = require(path.resolve(result.proxy));
52
+ } else {
53
+ delete result.proxy;
54
+ }
55
+ }
56
+
57
+ if (result.static === undefined) {
58
+ const contentBase = webpack.contentBase;
59
+ if (contentBase !== undefined) {
60
+ log.warning(CONTENT_BASE_DEPRECATION_WARNING);
61
+ if (typeof contentBase !== 'number') {
62
+ result.static = contentBase;
63
+ }
64
+ } else {
65
+ result.static = '.';
66
+ }
67
+ }
68
+
69
+ return { ...result, ...webpack.devServer } as Partial<DevServerConfig>;
70
+ }
71
+
72
+ function watchOptionsConfig(): WatchOptionsConfig {
73
+ const ignored = getPackages({ scope: getPackageName() })
32
74
  .filter(({ type }) => type === PackageType.TSC)
33
75
  .map(({ location }) => location);
34
76
 
35
- return watchIgnore.length ? { watchOptions: { ignored: watchIgnore } } : undefined;
77
+ return { ignored };
36
78
  }
79
+
80
+ const CONTENT_BASE_DEPRECATION_WARNING = `
81
+ DEPRECATION WARNING: webpack.contentBase in package.json is deprecated.
82
+ Use webpack.static instead.
83
+ `
84
+ .replace(/\n\s*/g, ' ')
85
+ .trim();
@@ -3,16 +3,42 @@ import os from 'os';
3
3
  const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
4
4
  import { Context, Overrides } from '../types';
5
5
 
6
- export function bundleAnalyzerPlugin({ buildStat, embed }: Context, _: Overrides) {
7
- if (!buildStat) {
6
+ export function bundleAnalyzerPlugin(context: Context, _: Overrides) {
7
+ if (!context.buildStat) {
8
8
  return;
9
9
  }
10
10
 
11
11
  return new BundleAnalyzerPlugin({
12
12
  analyzerMode: 'static',
13
- reportFilename: path.join(
14
- os.tmpdir(),
15
- `report-${embed ? 'light' : 'full'}-bundle-${Date.now()}.html`
16
- ),
13
+ reportFilename: path.join(os.tmpdir(), getReportFileName(context)),
14
+ reportTitle: getReportTitle(context),
17
15
  });
18
16
  }
17
+
18
+ function getBundleType({ embed, isWebComponent }: Context) {
19
+ if (isWebComponent) {
20
+ return embed ? 'light' : 'full';
21
+ }
22
+ }
23
+
24
+ function getReportFileName(context: Context) {
25
+ const type = getBundleType(context);
26
+ const qualifier = type ? `-${type}` : '';
27
+ return `${context.packageData.name}${qualifier}-bundle-${Date.now()}.html`;
28
+ }
29
+
30
+ function getReportTitle(context: Context) {
31
+ const unscopedName = context.packageData.name.replace(/^@[^/]+\//, '');
32
+ const type = getBundleType(context);
33
+ const qualifier = type ? ` (${type})` : '';
34
+ return `${unscopedName}${qualifier} ${formatDate(new Date())}`;
35
+ }
36
+
37
+ function formatDate(date: Date) {
38
+ const yyyy = date.getFullYear();
39
+ const mm = date.getMonth() + 1;
40
+ const dd = date.getDate();
41
+ const HH = date.getHours();
42
+ const MM = date.getMinutes();
43
+ return `${yyyy}-${mm}-${dd} ${HH}:${MM}`;
44
+ }
@@ -34,7 +34,7 @@ describe(`[startup] ${generateMetadata.name}`, () => {
34
34
  context = {
35
35
  destination,
36
36
  name: pkg.name,
37
- packageData: { dependencies: { foo: '1.0.1' } },
37
+ packageData: { name: pkg.name, dependencies: { foo: '1.0.1' } },
38
38
  sharedDependencies: { react: 'SharedDependencies.React' },
39
39
  };
40
40
  });