@servicetitan/startup 37.0.1 → 38.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 (53) hide show
  1. package/dist/cli/commands/review/types.d.ts.map +1 -1
  2. package/dist/cli/commands/review/types.js.map +1 -1
  3. package/dist/cypress/config/vite-config.d.ts.map +1 -1
  4. package/dist/cypress/config/vite-config.js +23 -3
  5. package/dist/cypress/config/vite-config.js.map +1 -1
  6. package/dist/cypress/config/webpack-config.d.ts.map +1 -1
  7. package/dist/cypress/config/webpack-config.js +5 -0
  8. package/dist/cypress/config/webpack-config.js.map +1 -1
  9. package/dist/storybook-config/vite-final.d.ts.map +1 -1
  10. package/dist/storybook-config/vite-final.js +32 -6
  11. package/dist/storybook-config/vite-final.js.map +1 -1
  12. package/dist/storybook-config/webpack-final.js +2 -1
  13. package/dist/storybook-config/webpack-final.js.map +1 -1
  14. package/dist/vite/config/base-config.d.ts.map +1 -1
  15. package/dist/vite/config/base-config.js +2 -0
  16. package/dist/vite/config/base-config.js.map +1 -1
  17. package/dist/vite/config/create-filtering-logger.d.ts.map +1 -1
  18. package/dist/vite/config/create-filtering-logger.js +12 -1
  19. package/dist/vite/config/create-filtering-logger.js.map +1 -1
  20. package/dist/vite/config/create-vite-build-context.d.ts.map +1 -1
  21. package/dist/vite/config/create-vite-build-context.js +4 -1
  22. package/dist/vite/config/create-vite-build-context.js.map +1 -1
  23. package/dist/vite/plugins/dev-server-routing-plugin.d.ts +1 -1
  24. package/dist/vite/plugins/dev-server-routing-plugin.d.ts.map +1 -1
  25. package/dist/vite/plugins/dev-server-routing-plugin.js +3 -2
  26. package/dist/vite/plugins/dev-server-routing-plugin.js.map +1 -1
  27. package/dist/vite/plugins/istanbul-plugin.d.ts.map +1 -1
  28. package/dist/vite/plugins/istanbul-plugin.js +33 -1
  29. package/dist/vite/plugins/istanbul-plugin.js.map +1 -1
  30. package/dist/vite/types.d.ts +4 -1
  31. package/dist/vite/types.d.ts.map +1 -1
  32. package/package.json +12 -12
  33. package/src/cli/commands/__tests__/bundle-vite.test.ts +1 -0
  34. package/src/cli/commands/review/types.ts +1 -3
  35. package/src/cypress/config/__tests__/vite-config.test.ts +21 -11
  36. package/src/cypress/config/__tests__/webpack-config.test.ts +17 -0
  37. package/src/cypress/config/vite-config.ts +18 -3
  38. package/src/cypress/config/webpack-config.ts +2 -0
  39. package/src/storybook-config/__tests__/vite-final.test.ts +65 -11
  40. package/src/storybook-config/__tests__/webpack-final.test.ts +16 -6
  41. package/src/storybook-config/vite-final.ts +23 -2
  42. package/src/storybook-config/webpack-final.ts +4 -2
  43. package/src/vite/config/__tests__/base-config.test.ts +4 -0
  44. package/src/vite/config/__tests__/create-filtering-logger.test.ts +9 -0
  45. package/src/vite/config/__tests__/create-vite-build-context.test.ts +25 -0
  46. package/src/vite/config/base-config.ts +5 -1
  47. package/src/vite/config/create-filtering-logger.ts +13 -2
  48. package/src/vite/config/create-vite-build-context.ts +1 -0
  49. package/src/vite/plugins/__tests__/dev-server-routing-plugin.test.ts +120 -98
  50. package/src/vite/plugins/__tests__/istanbul-plugin.test.ts +42 -1
  51. package/src/vite/plugins/dev-server-routing-plugin.ts +3 -3
  52. package/src/vite/plugins/istanbul-plugin.ts +28 -1
  53. package/src/vite/types.ts +4 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/vite/plugins/istanbul-plugin.ts"],"sourcesContent":["import type { Plugin } from 'vite';\nimport { ViteBuildContext } from '../types';\nimport { importEsm } from './utils';\n\nexport function istanbulPlugin(context: ViteBuildContext): Promise<Plugin> | undefined {\n if (!context.options.codeCoverage) {\n return;\n }\n\n // vite-plugin-istanbul is ESM-only; load it natively so SWC doesn't rewrite to require.\n return importEsm<typeof import('vite-plugin-istanbul')>('vite-plugin-istanbul').then(\n ({ default: istanbul }) =>\n istanbul({\n exclude: ['**/node_modules/**', '**/.yalc/**'],\n requireEnv: false,\n forceBuildInstrument: true,\n })\n );\n}\n"],"names":["istanbulPlugin","context","options","codeCoverage","importEsm","then","default","istanbul","exclude","requireEnv","forceBuildInstrument"],"mappings":";;;;+BAIgBA;;;eAAAA;;;uBAFU;AAEnB,SAASA,eAAeC,OAAyB;IACpD,IAAI,CAACA,QAAQC,OAAO,CAACC,YAAY,EAAE;QAC/B;IACJ;IAEA,wFAAwF;IACxF,OAAOC,IAAAA,gBAAS,EAAwC,wBAAwBC,IAAI,CAChF,CAAC,EAAEC,SAASC,QAAQ,EAAE,GAClBA,SAAS;YACLC,SAAS;gBAAC;gBAAsB;aAAc;YAC9CC,YAAY;YACZC,sBAAsB;QAC1B;AAEZ"}
1
+ {"version":3,"sources":["../../../src/vite/plugins/istanbul-plugin.ts"],"sourcesContent":["import path from 'path';\nimport type { Plugin } from 'vite';\nimport { resolveOutputPath } from '../../core';\nimport { ViteBuildContext } from '../types';\nimport { importEsm } from './utils';\n\nexport function istanbulPlugin(context: ViteBuildContext): Promise<Plugin> | undefined {\n if (!context.options.codeCoverage) {\n return;\n }\n\n // vite-plugin-istanbul is ESM-only; load it natively so SWC doesn't rewrite to require.\n return importEsm<typeof import('vite-plugin-istanbul')>('vite-plugin-istanbul').then(\n ({ default: istanbul }) =>\n istanbul({\n exclude: [\n '**/node_modules/**',\n '**/.yalc/**',\n ...sharedDependenciesExclude(context),\n ],\n requireEnv: false,\n forceBuildInstrument: true,\n })\n );\n}\n\n/*\n * devServerRoutingPlugin serves the prebuilt shared-dependencies bundle (react, mobx, the\n * design system, etc.) straight from disk at dev time. Instrumenting that vendor code breaks\n * Vite's import analysis on the huge instrumented output and provides no useful coverage.\n *\n * vite-plugin-istanbul matches exclude patterns against paths relative to process.cwd()\n * (via test-exclude's default `relativePath` behavior), so the pattern must be cwd-relative\n * rather than absolute.\n */\nfunction sharedDependenciesExclude(context: ViteBuildContext): string[] {\n if (!context.package.isExposeSharedDependencies) {\n return [];\n }\n const sharedContext = {\n ...context,\n build: { ...context.build, emitExposedDependencies: true },\n };\n const outputPath = resolveOutputPath(sharedContext, context.bundlerConfig.outDir);\n return [path.join(path.relative(process.cwd(), outputPath), '**').replace(/\\\\/g, '/')];\n}\n"],"names":["istanbulPlugin","context","options","codeCoverage","importEsm","then","default","istanbul","exclude","sharedDependenciesExclude","requireEnv","forceBuildInstrument","package","isExposeSharedDependencies","sharedContext","build","emitExposedDependencies","outputPath","resolveOutputPath","bundlerConfig","outDir","path","join","relative","process","cwd","replace"],"mappings":";;;;+BAMgBA;;;eAAAA;;;6DANC;sBAEiB;uBAER;;;;;;AAEnB,SAASA,eAAeC,OAAyB;IACpD,IAAI,CAACA,QAAQC,OAAO,CAACC,YAAY,EAAE;QAC/B;IACJ;IAEA,wFAAwF;IACxF,OAAOC,IAAAA,gBAAS,EAAwC,wBAAwBC,IAAI,CAChF,CAAC,EAAEC,SAASC,QAAQ,EAAE,GAClBA,SAAS;YACLC,SAAS;gBACL;gBACA;mBACGC,0BAA0BR;aAChC;YACDS,YAAY;YACZC,sBAAsB;QAC1B;AAEZ;AAEA;;;;;;;;CAQC,GACD,SAASF,0BAA0BR,OAAyB;IACxD,IAAI,CAACA,QAAQW,OAAO,CAACC,0BAA0B,EAAE;QAC7C,OAAO,EAAE;IACb;IACA,MAAMC,gBAAgB;QAClB,GAAGb,OAAO;QACVc,OAAO;YAAE,GAAGd,QAAQc,KAAK;YAAEC,yBAAyB;QAAK;IAC7D;IACA,MAAMC,aAAaC,IAAAA,uBAAiB,EAACJ,eAAeb,QAAQkB,aAAa,CAACC,MAAM;IAChF,OAAO;QAACC,aAAI,CAACC,IAAI,CAACD,aAAI,CAACE,QAAQ,CAACC,QAAQC,GAAG,IAAIR,aAAa,MAAMS,OAAO,CAAC,OAAO;KAAK;AAC1F"}
@@ -1,4 +1,7 @@
1
1
  import { BuildContext } from '../core';
2
2
  import { ViteBundlerConfig } from '../utils';
3
- export type ViteBuildContext = BuildContext<Required<Omit<ViteBundlerConfig, 'port'>> & Pick<ViteBundlerConfig, 'port'>>;
3
+ export type ViteBuildContext = BuildContext<(Required<Omit<ViteBundlerConfig, 'port'>> & Pick<ViteBundlerConfig, 'port'>) & {
4
+ /** The resolved `build.outDir` override, if the package configures one. */
5
+ outDir?: string;
6
+ }>;
4
7
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/vite/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAE7C,MAAM,MAAM,gBAAgB,GAAG,YAAY,CACvC,QAAQ,CAAC,IAAI,CAAC,iBAAiB,EAAE,MAAM,CAAC,CAAC,GAAG,IAAI,CAAC,iBAAiB,EAAE,MAAM,CAAC,CAC9E,CAAC"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/vite/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAE7C,MAAM,MAAM,gBAAgB,GAAG,YAAY,CACvC,CAAC,QAAQ,CAAC,IAAI,CAAC,iBAAiB,EAAE,MAAM,CAAC,CAAC,GAAG,IAAI,CAAC,iBAAiB,EAAE,MAAM,CAAC,CAAC,GAAG;IAC5E,2EAA2E;IAC3E,MAAM,CAAC,EAAE,MAAM,CAAC;CACnB,CACJ,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@servicetitan/startup",
3
- "version": "37.0.1",
3
+ "version": "38.0.0",
4
4
  "description": "CLI to create multi-package Lerna projects with TypeScript and React",
5
5
  "homepage": "https://docs.st.dev/docs/frontend/uikit/startup",
6
6
  "repository": {
@@ -103,10 +103,10 @@
103
103
  "@jest/core": "~30.4.2",
104
104
  "@jest/types": "~30.4.1",
105
105
  "@jsdevtools/coverage-istanbul-loader": "^3.0.5",
106
- "@servicetitan/eslint-config": "37.0.1",
107
- "@servicetitan/install": "37.0.1",
108
- "@servicetitan/startup-utils": "37.0.1",
109
- "@servicetitan/stylelint-config": "37.0.1",
106
+ "@servicetitan/eslint-config": "38.0.0",
107
+ "@servicetitan/install": "38.0.0",
108
+ "@servicetitan/startup-utils": "38.0.0",
109
+ "@servicetitan/stylelint-config": "38.0.0",
110
110
  "@svgr/webpack": "^8.1.0",
111
111
  "@swc/cli": "^0.8.1",
112
112
  "@swc/core": "1.15.43",
@@ -120,7 +120,7 @@
120
120
  "css-minimizer-webpack-plugin": "^8.0.0",
121
121
  "debug": "^4.4.3",
122
122
  "deepmerge": "~4.3.1",
123
- "es-module-lexer": "^2.1.0",
123
+ "es-module-lexer": "^2.3.0",
124
124
  "eslint": "~9.39.2",
125
125
  "eta": "^4.6.0",
126
126
  "execa": "~5.1.1",
@@ -135,7 +135,7 @@
135
135
  "js-yaml": "~4.2.0",
136
136
  "json5": "^2.2.3",
137
137
  "lerna": "~9.0.7",
138
- "less": "~4.6.6",
138
+ "less": "~4.6.7",
139
139
  "less-loader": "~13.0.0",
140
140
  "less-plugin-npm-import": "~2.1.0",
141
141
  "lodash.kebabcase": "^4.1.1",
@@ -146,14 +146,14 @@
146
146
  "multimatch": "~8.0.0",
147
147
  "patch-package": "^8.0.1",
148
148
  "portfinder": "~1.0.38",
149
- "postcss": "~8.5.15",
150
- "prettier": "~3.8.4",
149
+ "postcss": "~8.5.16",
150
+ "prettier": "~3.9.4",
151
151
  "sass": "~1.101.0",
152
152
  "sass-loader": "~17.0.0",
153
153
  "semver": "~7.8.5",
154
154
  "source-map-loader": "~5.0.0",
155
155
  "style-loader": "~4.0.0",
156
- "stylelint": "~17.13.0",
156
+ "stylelint": "~17.14.0",
157
157
  "swc-loader": "^0.2.7",
158
158
  "terminal-link": "^5.0.0",
159
159
  "terser-webpack-plugin": "^5.6.1",
@@ -161,7 +161,7 @@
161
161
  "ts-node": "~10.9.2",
162
162
  "typed-css-modules": "~0.9.1",
163
163
  "typescript": "6.0.3",
164
- "vite": "^8.0.16",
164
+ "vite": "^8.1.4",
165
165
  "vite-bundle-analyzer": "^1.3.8",
166
166
  "vite-plugin-istanbul": "^9.0.1",
167
167
  "vite-plugin-svgr": "^5.2.0",
@@ -197,5 +197,5 @@
197
197
  "cli": {
198
198
  "webpack": false
199
199
  },
200
- "gitHead": "9a2e5579f88cf9895fd2948ce5d5810e976d20b0"
200
+ "gitHead": "a96a725030a0c422257567b1f06025bca8ca53b9"
201
201
  }
@@ -136,6 +136,7 @@ describe(BundleVite.name, () => {
136
136
  beforeEach(() => (args.watch = true));
137
137
 
138
138
  test('throws error', async () => {
139
+ jest.spyOn(process.stdout, 'write').mockImplementationOnce(jest.fn()); // suppress error output
139
140
  await expect(subject()).rejects.toThrow(
140
141
  'cannot bundle exposed dependencies in watch mode'
141
142
  );
@@ -71,8 +71,6 @@ export interface ReviewConfiguration {
71
71
  }
72
72
 
73
73
  export type RuleConfiguration =
74
- | Level
75
- | [Level]
76
- | [Level, { exclude?: string[] | Record<string, string[]> }];
74
+ Level | [Level] | [Level, { exclude?: string[] | Record<string, string[]> }];
77
75
 
78
76
  export type Level = 'warn' | 'error' | 'off';
@@ -1,6 +1,6 @@
1
1
  import type { InlineConfig } from 'vite';
2
2
  import { createCssConfig } from '../../../vite/config/create-css-config';
3
- import { istanbulPlugin, svgrPlugin } from '../../../vite/plugins';
3
+ import { ignoreModulesPlugin, istanbulPlugin, svgrPlugin } from '../../../vite/plugins';
4
4
  import { viteConfig } from '../vite-config';
5
5
 
6
6
  jest.mock('vite', () => ({
@@ -11,8 +11,9 @@ jest.mock('../../../vite/plugins');
11
11
 
12
12
  describe(`[startup/cypress-config] ${viteConfig.name}`, () => {
13
13
  const cssConfig: any = { modules: { localsConvention: 'camelCaseOnly' } };
14
- const svgr: any = { name: 'svgr' };
15
- const istanbul: any = { name: 'istanbul' };
14
+ const mockSvgrPlugin: any = { name: 'vite-plugin-svgr' };
15
+ const mockIstanbulPlugin: any = { name: 'istanbul' };
16
+ const mockIgnoreModulesPlugin: any = { name: 'ignore-modules' };
16
17
 
17
18
  let overrides: InlineConfig | undefined;
18
19
 
@@ -21,8 +22,9 @@ describe(`[startup/cypress-config] ${viteConfig.name}`, () => {
21
22
  beforeEach(() => {
22
23
  jest.clearAllMocks();
23
24
  jest.mocked(createCssConfig).mockReturnValue(cssConfig);
24
- jest.mocked(svgrPlugin).mockReturnValue(svgr);
25
- jest.mocked(istanbulPlugin).mockReturnValue(istanbul);
25
+ jest.mocked(svgrPlugin).mockReturnValue(mockSvgrPlugin);
26
+ jest.mocked(istanbulPlugin).mockReturnValue(mockIstanbulPlugin);
27
+ jest.mocked(ignoreModulesPlugin).mockReturnValue(mockIgnoreModulesPlugin);
26
28
  overrides = undefined;
27
29
  });
28
30
 
@@ -36,8 +38,20 @@ describe(`[startup/cypress-config] ${viteConfig.name}`, () => {
36
38
  expect(subject().css).toBe(cssConfig);
37
39
  });
38
40
 
39
- test('includes the SVG plugin', () => {
40
- expect(subject().plugins).toContain(svgr);
41
+ test('adds the svgr plugin', () => {
42
+ expect(subject().plugins).toContain(mockSvgrPlugin);
43
+ });
44
+
45
+ test('adds the istanbul plugin', () => {
46
+ expect(subject().plugins).toContain(mockIstanbulPlugin);
47
+ });
48
+
49
+ test('adds the ignore-modules plugin', () => {
50
+ expect(subject().plugins).toContain(mockIgnoreModulesPlugin);
51
+ });
52
+
53
+ test('registers the ignore modules plugin for the dependency pre-bundler', () => {
54
+ expect(subject().optimizeDeps?.rolldownOptions?.plugins).toEqual([mockIgnoreModulesPlugin]);
41
55
  });
42
56
 
43
57
  test('enables code coverage', () => {
@@ -48,10 +62,6 @@ describe(`[startup/cypress-config] ${viteConfig.name}`, () => {
48
62
  );
49
63
  });
50
64
 
51
- test('includes the coverage plugin', () => {
52
- expect(subject().plugins).toContain(istanbul);
53
- });
54
-
55
65
  test('dedupes React', () => {
56
66
  expect(subject().resolve?.dedupe).toEqual(['react', 'react-dom']);
57
67
  });
@@ -3,12 +3,17 @@ import { getSwcOptions } from '../../../cli/tasks';
3
3
  import { TSConfig } from '../../../cli/utils';
4
4
  import { getBaseTsConfig } from '../../../utils';
5
5
  import { JS_RULE_EXCLUDE, rulesConfig } from '../../../webpack/configs';
6
+ import { filterWarningsPlugin, ignorePlugin } from '../../../webpack/configs/plugins';
6
7
  import { Overrides } from '../../../webpack/types';
7
8
  import { webpackConfig } from '../webpack-config';
8
9
 
9
10
  jest.mock('@servicetitan/install');
10
11
  jest.mock('fs', () => fs);
11
12
  jest.mock('../../../webpack/configs');
13
+ jest.mock('../../../webpack/configs/plugins', () => ({
14
+ filterWarningsPlugin: jest.fn(),
15
+ ignorePlugin: jest.fn(),
16
+ }));
12
17
  jest.mock('../../../cli/tasks');
13
18
 
14
19
  describe(`[startup/cypress-config] ${webpackConfig.name}`, () => {
@@ -19,12 +24,16 @@ describe(`[startup/cypress-config] ${webpackConfig.name}`, () => {
19
24
  const mockSwcOptions: any = { jsc: 'foobar' };
20
25
  const mockTsConfig = { compilerOptions: { target: 'es2020' } };
21
26
  const mockFallbackTsConfig = { compilerOptions: { target: 'es2018' } };
27
+ const mockIgnorePlugin: any = { name: 'IgnorePlugin' };
28
+ const mockFilterWarningsPlugin = { name: 'FilterWarningsPlugin' };
22
29
  const fallbackConfig = getBaseTsConfig();
23
30
 
24
31
  let overrides: Overrides | undefined;
25
32
 
26
33
  beforeEach(() => {
27
34
  jest.clearAllMocks();
35
+ jest.mocked(ignorePlugin).mockReturnValue(mockIgnorePlugin);
36
+ jest.mocked(filterWarningsPlugin).mockReturnValue(mockFilterWarningsPlugin);
28
37
  jest.mocked(rulesConfig).mockReturnValue({ rules: mockRules });
29
38
  jest.mocked(getSwcOptions).mockReturnValue(mockSwcOptions);
30
39
  vol.fromJSON({
@@ -56,6 +65,14 @@ describe(`[startup/cypress-config] ${webpackConfig.name}`, () => {
56
65
  );
57
66
  });
58
67
 
68
+ test('adds the ignore plugin', () => {
69
+ expect(subject().plugins).toContain(mockIgnorePlugin);
70
+ });
71
+
72
+ test('adds the filter warnings plugin', () => {
73
+ expect(subject().plugins).toContain(mockFilterWarningsPlugin);
74
+ });
75
+
59
76
  test('includes all rules from rulesConfig', () => {
60
77
  const result = subject();
61
78
 
@@ -1,6 +1,6 @@
1
1
  import { type InlineConfig, mergeConfig } from 'vite';
2
2
  import { createCssConfig } from '../../vite/config/create-css-config';
3
- import { istanbulPlugin, svgrPlugin } from '../../vite/plugins';
3
+ import { ignoreModulesPlugin, istanbulPlugin, svgrPlugin } from '../../vite/plugins';
4
4
  import type { ViteBuildContext } from '../../vite/types';
5
5
 
6
6
  /*
@@ -17,8 +17,22 @@ export function viteConfig(overrides?: InlineConfig): InlineConfig {
17
17
  const config: InlineConfig = {
18
18
  css: createCssConfig(false),
19
19
  resolve: { dedupe: REACT_DEDUPE },
20
- optimizeDeps: { include: REACT_OPTIMIZE },
21
- plugins: [svgrPlugin(context), istanbulPlugin(context)],
20
+ optimizeDeps: {
21
+ include: REACT_OPTIMIZE /*
22
+ * Top-level plugins don't run during dependency pre-bundling, so
23
+ * register ignoreModulesPlugin again here. Without this, missing
24
+ * optional peer dependencies inside pre-bundled packages throw at
25
+ * runtime instead of resolving to an empty module.
26
+ */,
27
+ rolldownOptions: {
28
+ plugins: [ignoreModulesPlugin({ package: { dependencies: {} } })],
29
+ },
30
+ },
31
+ plugins: [
32
+ svgrPlugin(context),
33
+ istanbulPlugin(context),
34
+ ignoreModulesPlugin({ package: { dependencies: {} } }),
35
+ ],
22
36
  };
23
37
 
24
38
  return mergeConfig(config, overrides ?? {});
@@ -33,5 +47,6 @@ function getContext(): ViteBuildContext {
33
47
  return {
34
48
  options: { codeCoverage: true },
35
49
  bundlerConfig: { pluginOverrides: {} },
50
+ package: {},
36
51
  } as ViteBuildContext;
37
52
  }
@@ -6,6 +6,7 @@ import { getSwcOptions } from '../../cli/tasks';
6
6
  import { TSConfig } from '../../cli/utils';
7
7
  import { getTsConfigWithFallback, log } from '../../utils';
8
8
  import { JS_RULE_EXCLUDE, rulesConfig } from '../../webpack/configs';
9
+ import { filterWarningsPlugin, ignorePlugin } from '../../webpack/configs/plugins';
9
10
  import { Overrides, WebpackBuildContext } from '../../webpack/types';
10
11
 
11
12
  export function webpackConfig(overrides?: Overrides): Configuration {
@@ -19,6 +20,7 @@ export function webpackConfig(overrides?: Overrides): Configuration {
19
20
  module: {
20
21
  rules,
21
22
  },
23
+ plugins: [ignorePlugin(context, {}), filterWarningsPlugin(context, {})],
22
24
  resolve: {
23
25
  extensions: ['.ts', '.tsx', '.js', '.jsx'],
24
26
  },
@@ -1,26 +1,46 @@
1
1
  import type { InlineConfig } from 'vite';
2
2
  import { createCssConfig } from '../../vite/config/create-css-config';
3
- import { svgrPlugin } from '../../vite/plugins/svgr-plugin';
3
+ import { createFilteringLogger } from '../../vite/config/create-filtering-logger';
4
+ import { ignoreModulesPlugin, svgrPlugin } from '../../vite/plugins';
4
5
  import { viteFinal } from '../vite-final';
5
6
 
6
- jest.mock('../../vite/plugins/svgr-plugin', () => ({ svgrPlugin: jest.fn() }));
7
+ jest.mock('../../vite/plugins');
8
+ jest.mock('../../vite/config/create-filtering-logger');
7
9
 
8
10
  describe(`[startup/storybook-config] ${viteFinal.name}`, () => {
9
- const mockSvgrPlugin = { name: 'vite-plugin-svgr' } as any;
11
+ const mockSvgrPlugin: any = { name: 'vite-plugin-svgr' };
12
+ const mockIgnoreModulesPlugin: any = { name: 'ignore-modules' };
13
+ const mockFilteringLogger: any = { name: 'filteringLoggeer' };
10
14
  let config: InlineConfig;
11
15
  let configType: 'DEVELOPMENT' | 'PRODUCTION' | undefined;
12
16
 
13
17
  beforeEach(() => {
14
18
  jest.clearAllMocks();
15
19
  jest.mocked(svgrPlugin).mockReturnValue(mockSvgrPlugin);
20
+ jest.mocked(ignoreModulesPlugin).mockReturnValue(mockIgnoreModulesPlugin);
21
+ jest.mocked(createFilteringLogger).mockReturnValue(mockFilteringLogger);
16
22
  config = {};
17
23
  configType = undefined;
18
24
  });
19
25
 
20
26
  const subject = () => viteFinal(config, { configType });
21
27
 
22
- test('adds svgr plugin', () => {
23
- expect(subject()).toEqual(expect.objectContaining({ plugins: [mockSvgrPlugin] }));
28
+ test('adds the svgr plugin', () => {
29
+ expect(subject().plugins).toContain(mockSvgrPlugin);
30
+ });
31
+
32
+ test('adds the ignore-modules plugin', () => {
33
+ expect(subject().plugins).toContain(mockIgnoreModulesPlugin);
34
+ });
35
+
36
+ test('adds the filtering logger', () => {
37
+ expect(subject().customLogger).toBe(mockFilteringLogger);
38
+ });
39
+
40
+ test('registers the ignore-modules plugin for the dependency pre-bundler', () => {
41
+ expect(subject().optimizeDeps!.rolldownOptions).toEqual({
42
+ plugins: [mockIgnoreModulesPlugin],
43
+ });
24
44
  });
25
45
 
26
46
  test('dedupes react', () => {
@@ -30,11 +50,11 @@ describe(`[startup/storybook-config] ${viteFinal.name}`, () => {
30
50
  });
31
51
 
32
52
  test('pre-bundles react', () => {
33
- expect(subject()).toEqual(
34
- expect.objectContaining({
35
- optimizeDeps: { include: ['react', 'react-dom', 'react/jsx-runtime'] },
36
- })
37
- );
53
+ expect(subject().optimizeDeps!.include).toEqual([
54
+ 'react',
55
+ 'react-dom',
56
+ 'react/jsx-runtime',
57
+ ]);
38
58
  });
39
59
 
40
60
  describe.each([
@@ -90,7 +110,9 @@ describe(`[startup/storybook-config] ${viteFinal.name}`, () => {
90
110
 
91
111
  test('preserves existing plugins', () => {
92
112
  expect(subject()).toEqual(
93
- expect.objectContaining({ plugins: [existingPlugin, mockSvgrPlugin] })
113
+ expect.objectContaining({
114
+ plugins: [existingPlugin, mockSvgrPlugin, mockIgnoreModulesPlugin],
115
+ })
94
116
  );
95
117
  });
96
118
  });
@@ -119,4 +141,36 @@ describe(`[startup/storybook-config] ${viteFinal.name}`, () => {
119
141
  ]);
120
142
  });
121
143
  });
144
+
145
+ describe('when config already contains optimizeDeps.rolldownOptions', () => {
146
+ const existingPlugin = { name: 'existing' };
147
+
148
+ beforeEach(() => {
149
+ config = {
150
+ optimizeDeps: { rolldownOptions: { plugins: [existingPlugin] } },
151
+ };
152
+ });
153
+
154
+ test('preserves existing rolldownOptions plugins', () => {
155
+ expect(subject().optimizeDeps!.rolldownOptions).toEqual({
156
+ plugins: [existingPlugin, mockIgnoreModulesPlugin],
157
+ });
158
+ });
159
+ });
160
+
161
+ describe('when config already contains a single (non-array) rolldownOptions plugin', () => {
162
+ const existingPlugin = { name: 'existing' };
163
+
164
+ beforeEach(() => {
165
+ config = {
166
+ optimizeDeps: { rolldownOptions: { plugins: existingPlugin } },
167
+ };
168
+ });
169
+
170
+ test('preserves the existing rolldownOptions plugin', () => {
171
+ expect(subject().optimizeDeps!.rolldownOptions).toEqual({
172
+ plugins: [existingPlugin, mockIgnoreModulesPlugin],
173
+ });
174
+ });
175
+ });
122
176
  });
@@ -1,6 +1,6 @@
1
1
  import type { Configuration } from 'webpack';
2
2
  import { rulesConfig } from '../../webpack/configs';
3
- import { miniCssExtractPlugin } from '../../webpack/configs/plugins';
3
+ import { ignorePlugin, miniCssExtractPlugin } from '../../webpack/configs/plugins';
4
4
  import { Overrides } from '../../webpack/types';
5
5
  import { webpackFinal } from '../webpack-final';
6
6
 
@@ -19,6 +19,7 @@ describe(`[startup/storybook-config] ${webpackFinal.name}`, () => {
19
19
  { test: /\.css$/, use: ['css-loader'] },
20
20
  ];
21
21
  const mockMiniCssExtractPlugin: any = { name: 'MiniCssExtractPlugin' };
22
+ const mockIgnorePlugin: any = { name: 'IgnorePlugin' };
22
23
  let config: Configuration;
23
24
  let overrides: Overrides | undefined;
24
25
 
@@ -26,6 +27,7 @@ describe(`[startup/storybook-config] ${webpackFinal.name}`, () => {
26
27
  jest.clearAllMocks();
27
28
  jest.mocked(rulesConfig).mockReturnValue({ rules: mockRules });
28
29
  jest.mocked(miniCssExtractPlugin).mockReturnValue(mockMiniCssExtractPlugin);
30
+ jest.mocked(ignorePlugin).mockReturnValue(mockIgnorePlugin);
29
31
  config = {};
30
32
  overrides = undefined;
31
33
  });
@@ -52,7 +54,11 @@ describe(`[startup/storybook-config] ${webpackFinal.name}`, () => {
52
54
  });
53
55
 
54
56
  test('adds MiniCssExtractPlugin to config', () => {
55
- expect(subject().plugins).toEqual([mockMiniCssExtractPlugin]);
57
+ expect(subject().plugins).toEqual(expect.arrayContaining([mockMiniCssExtractPlugin]));
58
+ });
59
+
60
+ test('adds IgnorePlugin to config', () => {
61
+ expect(subject().plugins).toEqual(expect.arrayContaining([mockIgnorePlugin]));
56
62
  });
57
63
 
58
64
  describe('when config contains rules', () => {
@@ -73,15 +79,19 @@ describe(`[startup/storybook-config] ${webpackFinal.name}`, () => {
73
79
 
74
80
  beforeEach(() => (config.plugins = [configPlugin]));
75
81
 
76
- test('appends MiniCssExtractPlugin to config', () => {
77
- expect(subject().plugins).toEqual([configPlugin, mockMiniCssExtractPlugin]);
82
+ test('appends default plugins to config', () => {
83
+ expect(subject().plugins).toEqual([
84
+ configPlugin,
85
+ mockMiniCssExtractPlugin,
86
+ mockIgnorePlugin,
87
+ ]);
78
88
  });
79
89
 
80
90
  describe('when MiniCssExtractPlugin is disabled', () => {
81
91
  beforeEach(() => jest.mocked(miniCssExtractPlugin).mockReturnValue(undefined));
82
92
 
83
- test('returns only configured plugins', () => {
84
- expect(subject().plugins).toEqual(config.plugins);
93
+ test('omits plugin', () => {
94
+ expect(subject().plugins).toEqual([configPlugin, mockIgnorePlugin]);
85
95
  });
86
96
  });
87
97
  });
@@ -2,7 +2,8 @@ import { inspect } from 'node:util';
2
2
  import type { InlineConfig } from 'vite';
3
3
  import { log } from '../utils';
4
4
  import { createCssConfig } from '../vite/config/create-css-config';
5
- import { svgrPlugin } from '../vite/plugins/svgr-plugin';
5
+ import { createFilteringLogger } from '../vite/config/create-filtering-logger';
6
+ import { ignoreModulesPlugin, svgrPlugin } from '../vite/plugins';
6
7
  import type { ViteBuildContext } from '../vite/types';
7
8
 
8
9
  interface ViteFinalOptions {
@@ -27,7 +28,11 @@ export function viteFinal(
27
28
  ): InlineConfig {
28
29
  const isProduction = configType === 'PRODUCTION';
29
30
  const css = createCssConfig(isProduction);
30
- const plugins = [svgrPlugin(getContext())];
31
+ const plugins = [
32
+ svgrPlugin(getContext()),
33
+ ignoreModulesPlugin({ package: { dependencies: {} } }),
34
+ ];
35
+ const existingRolldownPlugins = config.optimizeDeps?.rolldownOptions?.plugins;
31
36
 
32
37
  const merged: InlineConfig = {
33
38
  ...config,
@@ -46,8 +51,24 @@ export function viteFinal(
46
51
  optimizeDeps: {
47
52
  ...config.optimizeDeps,
48
53
  include: [...new Set([...(config.optimizeDeps?.include ?? []), ...REACT_OPTIMIZE])],
54
+ /*
55
+ * Top-level plugins don't run during dependency pre-bundling, so
56
+ * register ignoreModulesPlugin again here. Without this, missing
57
+ * optional peer dependencies inside pre-bundled packages throw at
58
+ * runtime instead of resolving to an empty module.
59
+ */
60
+ rolldownOptions: {
61
+ ...config.optimizeDeps?.rolldownOptions,
62
+ plugins: [
63
+ ...(Array.isArray(existingRolldownPlugins)
64
+ ? existingRolldownPlugins
65
+ : [existingRolldownPlugins].filter(Boolean)),
66
+ ignoreModulesPlugin({ package: { dependencies: {} } }),
67
+ ],
68
+ },
49
69
  },
50
70
  plugins: [...(config.plugins ?? []), ...plugins],
71
+ customLogger: createFilteringLogger(),
51
72
  };
52
73
 
53
74
  /* istanbul ignore next: debug only */
@@ -3,7 +3,7 @@ import { inspect } from 'node:util';
3
3
  import type { Configuration } from 'webpack';
4
4
  import { log } from '../utils';
5
5
  import { rulesConfig } from '../webpack/configs';
6
- import { miniCssExtractPlugin } from '../webpack/configs/plugins';
6
+ import { ignorePlugin, miniCssExtractPlugin } from '../webpack/configs/plugins';
7
7
  import { Overrides, WebpackBuildContext } from '../webpack/types';
8
8
 
9
9
  export function webpackFinal(config: Configuration, overrides: Overrides = {}): Configuration {
@@ -48,7 +48,9 @@ function getContext(config: Configuration): WebpackBuildContext {
48
48
  }
49
49
 
50
50
  function getPlugins(context: WebpackBuildContext, overrides: Overrides) {
51
- return [miniCssExtractPlugin(context, overrides)].filter(plugin => !!plugin);
51
+ return [miniCssExtractPlugin(context, overrides), ignorePlugin(context, {})].filter(
52
+ plugin => !!plugin
53
+ );
52
54
  }
53
55
 
54
56
  function getRules(context: WebpackBuildContext, overrides: Overrides) {
@@ -119,6 +119,10 @@ describe(createBaseConfig.name, () => {
119
119
  expect(subject().server!.port).toBe(8080);
120
120
  });
121
121
 
122
+ test('binds dev server to 127.0.0.1', () => {
123
+ expect(subject().server!.host).toBe('127.0.0.1');
124
+ });
125
+
122
126
  test('sets development mode', () => {
123
127
  expect(subject().mode).toBe(bundleConstants.mode.development);
124
128
  });
@@ -47,5 +47,14 @@ describe(createFilteringLogger.name, () => {
47
47
 
48
48
  itSuppressesMessage();
49
49
  });
50
+
51
+ describe('when message is an ignored-module import warning', () => {
52
+ beforeEach(() => {
53
+ message =
54
+ "[IMPORT_IS_UNDEFINED] Import `DefaultPortalContext` will always be undefined because there is no matching export in '\\0IGNORE_EMPTY:@servicetitan/design-system'";
55
+ });
56
+
57
+ itSuppressesMessage();
58
+ });
50
59
  });
51
60
  });
@@ -29,6 +29,7 @@ describe(createViteBuildContext.name, () => {
29
29
  omitDefault: [],
30
30
  pluginOverrides: { html: {}, svgr: {} },
31
31
  minify: {},
32
+ outDir: undefined,
32
33
  });
33
34
  });
34
35
 
@@ -86,4 +87,28 @@ describe(createViteBuildContext.name, () => {
86
87
  });
87
88
  });
88
89
  });
90
+
91
+ describe('when package.json contains build.outDir', () => {
92
+ const pkgJsonOutDir = 'pkg-dist';
93
+
94
+ beforeEach(() => {
95
+ pkgJsonConfig = { config: { build: { outDir: pkgJsonOutDir } } };
96
+ });
97
+
98
+ test('configures outDir', () => {
99
+ expect(subject().bundlerConfig.outDir).toBe(pkgJsonConfig.config?.build?.outDir);
100
+ });
101
+
102
+ describe('when file config also contains build.outDir', () => {
103
+ const fileOutDir = 'file-dist';
104
+
105
+ beforeEach(() => {
106
+ fileConfig = { config: { build: { outDir: fileOutDir } } };
107
+ });
108
+
109
+ test('file config takes precedence', () => {
110
+ expect(subject().bundlerConfig.outDir).toBe(fileOutDir);
111
+ });
112
+ });
113
+ });
89
114
  });
@@ -83,7 +83,11 @@ export function createBaseConfig(context: ViteBuildContext): InlineConfig {
83
83
  let config: InlineConfig = {
84
84
  cacheDir: `node_modules/.vite/${getBundleType(context) ?? 'host'}`,
85
85
  customLogger: createFilteringLogger(),
86
- server: { port: context.bundlerConfig.port ?? (isWebComponent ? 8888 : 8080) },
86
+ server: {
87
+ // Windows machines can fail to connect to "localhost"; bind to a fixed address instead.
88
+ host: '127.0.0.1',
89
+ port: context.bundlerConfig.port ?? (isWebComponent ? 8888 : 8080),
90
+ },
87
91
  mode: isProduction ? bundleConstants.mode.production : bundleConstants.mode.development,
88
92
  configFile: false,
89
93
  oxc: { legalComments: 'none' },
@@ -11,13 +11,24 @@ const SHARED_ASSET_WARNINGS = [
11
11
  ];
12
12
 
13
13
  /*
14
- * Wraps Vite's logger to drop the shared-asset HTML warnings. Everything else passes through.
14
+ * Expected when code feature-detects an optional peer dependency (e.g. `Package.Export
15
+ * === undefined`) and ignoreModulesPlugin has substituted an empty module for it. Matched
16
+ * on the plugin's IGNORE_EMPTY marker so a genuinely undefined export elsewhere still warns.
17
+ */
18
+ const IGNORED_MODULE_WARNINGS = [
19
+ /Import `\w+` will always be undefined because there is no matching export in '\\0IGNORE_EMPTY:[^']+'/,
20
+ ];
21
+
22
+ const SUPPRESSED_WARNINGS = [...SHARED_ASSET_WARNINGS, ...IGNORED_MODULE_WARNINGS];
23
+
24
+ /*
25
+ * Wraps Vite's logger to drop expected warnings. Everything else passes through.
15
26
  */
16
27
  export function createFilteringLogger(): Logger {
17
28
  const logger = createLogger();
18
29
  const { warn, warnOnce } = logger;
19
30
  const isSuppressed = (message: string) =>
20
- SHARED_ASSET_WARNINGS.some(pattern => pattern.test(message));
31
+ SUPPRESSED_WARNINGS.some(pattern => pattern.test(message));
21
32
 
22
33
  logger.warn = (message, options) => {
23
34
  if (!isSuppressed(message)) {
@@ -31,6 +31,7 @@ function createBundlerConfig(
31
31
  svgr: { ...pkgPluginOverrides.svgr, ...filePluginOverrides.svgr },
32
32
  },
33
33
  minify: { ...pkgMinify, ...fileMinify },
34
+ outDir: fileConfig.config?.build?.outDir ?? packageJsonConfig.config?.build?.outDir,
34
35
  };
35
36
  }
36
37