@corva/dc-platform-shared 0.0.0-58393c2 → 0.0.0-59520c5

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 (79) hide show
  1. package/CLI/components/App/App.js +1 -1
  2. package/CLI/components/App/components/AppBar/AppBar.js +1 -1
  3. package/CLI/constants/index.js +1 -1
  4. package/CLI/context/auth.js +1 -1
  5. package/CLI/hocs/effects/useProvideAuth/api.js +1 -1
  6. package/CLI/hocs/effects/useProvideAuth/constants.js +1 -1
  7. package/CLI/hocs/effects/useProvideAuth/useProvideAuth.js +1 -1
  8. package/CLI/hocs/utils.js +1 -1
  9. package/CLI/hocs/withAppDataProviderHOC.js +1 -1
  10. package/CLI/hocs/withCLIAppSettingsHOC.js +1 -1
  11. package/CLI/hocs/withCorvaMagicHOC.js +1 -1
  12. package/CLI/hocs/withDevCenterRouterHOC.js +1 -1
  13. package/CLI/hocs/withGridLayoutHOC/withGridLayoutHOC.js +1 -1
  14. package/CLI/hocs/withPDFLayoutHOC.js +1 -1
  15. package/CLI/hocs/withReactRouterHOC.js +1 -1
  16. package/CLI/screens/LoginScreen/LoginScreen.js +1 -1
  17. package/CLI/screens/LoginScreen/components/Auth.js +1 -1
  18. package/CLI/screens/LoginScreen/components/AuthStep.js +1 -1
  19. package/CLI/screens/LoginScreen/components/EmailStep.js +1 -1
  20. package/CLI/screens/LoginScreen/components/SSOButton.js +1 -1
  21. package/CLI/screens/OAuthCallbackScreen/index.js +1 -1
  22. package/cjs/devcenter/webpack/DevServerEntryPoint.js +1 -1
  23. package/cjs/devcenter/webpack/DevServerIsolatedAppEntryPoint.js +1 -1
  24. package/cjs/devcenter/webpack/MapboxGlDevSetupEntryPoint.js +1 -0
  25. package/cjs/devcenter/webpack/constants.js +1 -1
  26. package/cjs/devcenter/webpack/createAdaptiveRoot.js +1 -0
  27. package/cjs/devcenter/webpack/plugins/checkCorvaUIVersion.js +11 -1
  28. package/cjs/devcenter/webpack/utils/middlewares.js +1 -1
  29. package/cjs/devcenter/webpack/utils/serializeDotEnv.js +1 -1
  30. package/cjs/devcenter/webpack/validators/dependencies.js +1 -1
  31. package/cjs/devcenter/webpack/validators/index.js +1 -1
  32. package/cjs/devcenter/webpack/webpack.config.common.js +1 -1
  33. package/cjs/devcenter/webpack/webpack.config.dev.js +1 -1
  34. package/cjs/devcenter/webpack/webpack.config.prod.js +1 -1
  35. package/clients/auth0/index.js +1 -1
  36. package/components/GridLockButton/index.js +1 -1
  37. package/components/Login/components/Chip.js +1 -1
  38. package/components/Login/components/EmailField.js +1 -1
  39. package/components/Login/components/HiddenEmailField.js +1 -1
  40. package/components/Login/components/Login.js +1 -1
  41. package/components/Login/components/OrDivider.js +1 -1
  42. package/components/Login/components/PasswordField.js +1 -1
  43. package/components/Login/components/StepContent.js +1 -1
  44. package/components/Login/components/StepFooter.js +1 -1
  45. package/components/Login/components/StepHeader.js +1 -1
  46. package/components/Login/components/VerificationStep.js +1 -1
  47. package/ext-esm/auth0-js/dist/auth0.min.esm.js +1 -1
  48. package/package.json +1 -1
  49. package/utils/devcenter.js +1 -1
  50. package/utils/recaptcha.js +1 -1
  51. package/vite/corva-vite-config.mjs +843 -0
  52. package/vite/unscoped-css.mjs +63 -0
  53. package/_virtual/_commonjsHelpers.js +0 -1
  54. package/_virtual/arrayLikeToArray.js +0 -1
  55. package/_virtual/arrayWithHoles.js +0 -1
  56. package/_virtual/arrayWithoutHoles.js +0 -1
  57. package/_virtual/iterableToArray.js +0 -1
  58. package/_virtual/iterableToArrayLimit.js +0 -1
  59. package/_virtual/nonIterableRest.js +0 -1
  60. package/_virtual/nonIterableSpread.js +0 -1
  61. package/_virtual/regeneratorRuntime.js +0 -1
  62. package/_virtual/typeof.js +0 -1
  63. package/_virtual/unsupportedIterableToArray.js +0 -1
  64. package/ext-cjs/@babel/runtime/helpers/arrayLikeToArray.js +0 -1
  65. package/ext-cjs/@babel/runtime/helpers/arrayWithHoles.js +0 -1
  66. package/ext-cjs/@babel/runtime/helpers/arrayWithoutHoles.js +0 -1
  67. package/ext-cjs/@babel/runtime/helpers/asyncToGenerator.js +0 -1
  68. package/ext-cjs/@babel/runtime/helpers/defineProperty.js +0 -1
  69. package/ext-cjs/@babel/runtime/helpers/iterableToArray.js +0 -1
  70. package/ext-cjs/@babel/runtime/helpers/iterableToArrayLimit.js +0 -1
  71. package/ext-cjs/@babel/runtime/helpers/nonIterableRest.js +0 -1
  72. package/ext-cjs/@babel/runtime/helpers/nonIterableSpread.js +0 -1
  73. package/ext-cjs/@babel/runtime/helpers/regeneratorRuntime.js +0 -1
  74. package/ext-cjs/@babel/runtime/helpers/slicedToArray.js +0 -1
  75. package/ext-cjs/@babel/runtime/helpers/taggedTemplateLiteral.js +0 -1
  76. package/ext-cjs/@babel/runtime/helpers/toConsumableArray.js +0 -1
  77. package/ext-cjs/@babel/runtime/helpers/typeof.js +0 -1
  78. package/ext-cjs/@babel/runtime/helpers/unsupportedIterableToArray.js +0 -1
  79. package/ext-cjs/@babel/runtime/regenerator/index.js +0 -1
@@ -0,0 +1,843 @@
1
+ // The Vite equivalent of getWebpackConfig: one shared build for every DC app, extended per app
2
+ // through `overrides`. Ships unbundled - it is a Node-side config Vite loads as ESM, so rollup
3
+ // must not touch it.
4
+ import { randomUUID } from 'node:crypto';
5
+ import { builtinModules, createRequire } from 'node:module';
6
+ import { existsSync, readdirSync, readFileSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
7
+ import { join, relative, resolve } from 'node:path';
8
+
9
+ import dotenv from 'dotenv';
10
+ import { expand } from 'dotenv-expand';
11
+ import react from '@vitejs/plugin-react';
12
+ import license from 'rollup-plugin-license';
13
+ import { viteStaticCopy } from 'vite-plugin-static-copy';
14
+ import presetEnv from 'postcss-preset-env';
15
+ import { defineConfig, mergeConfig } from 'vitest/config';
16
+
17
+ import { rejectUnscopedCssPlugin } from './unscoped-css.mjs';
18
+
19
+ const require = createRequire(import.meta.url);
20
+
21
+ const {
22
+ CORVA_API_ENVS,
23
+ CORVA_API_ENV_VARIABLES,
24
+ CORVA_GLOBAL_DEPENDENCIES,
25
+ } = require('../cjs/devcenter/webpack/constants.js');
26
+ const validateDependencies = require('../cjs/devcenter/webpack/validators/index.js');
27
+ const { updateDotEnv } = require('../cjs/devcenter/webpack/utils/serializeDotEnv.js');
28
+
29
+ // this file ships unbundled at <package>/vite/, so the package root is one level up and
30
+ // @corva/ui sits next to it in the app's node_modules/@corva
31
+ const PLATFORM_SHARED_DIR = join(import.meta.dirname, '..');
32
+ // NOTE: These imports are always bundled, never taken from the platform globals
33
+ const SKIP_IN_EXTERNALS = ['react/jsx-runtime', 'highcharts/'];
34
+
35
+ // webpack's css-loader appended a per-build uuid to every local class name, so two DC apps sharing a
36
+ // dashboard could not overwrite each other's styles. Vite hashes path + name + source instead, which
37
+ // is identical for two apps scaffolded from the same template.
38
+ const BUILD_ID = randomUUID().slice(-5);
39
+
40
+ const isLegalIdentifier = value => /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(value);
41
+
42
+ const toLegalIdentifier = value => {
43
+ const identifier = value.replace(/[^A-Za-z0-9_$]/g, '_');
44
+
45
+ return isLegalIdentifier(identifier) ? identifier : `_${identifier}`;
46
+ };
47
+
48
+ const toGlobalSegmentAccess = segment =>
49
+ /^\d+$/.test(segment) ? `[${segment}]` : `[${JSON.stringify(segment)}]`;
50
+
51
+ const getSegmentedGlobalAssignmentPrefix = appIdentifier => {
52
+ const [rootSegment, ...segments] = appIdentifier.split('.');
53
+ const rootAccessor = `this[${JSON.stringify(rootSegment)}]`;
54
+
55
+ if (segments.length === 0) {
56
+ return `${rootAccessor}=`;
57
+ }
58
+
59
+ let currentAccessor = rootAccessor;
60
+ const initializers = [`${rootAccessor}=${rootAccessor}||{}`];
61
+
62
+ segments.slice(0, -1).forEach(segment => {
63
+ currentAccessor += toGlobalSegmentAccess(segment);
64
+ initializers.push(`${currentAccessor}=${currentAccessor}||{}`);
65
+ });
66
+
67
+ currentAccessor += toGlobalSegmentAccess(segments[segments.length - 1]);
68
+
69
+ return `${initializers.join(',')},${currentAccessor}=`;
70
+ };
71
+
72
+ const getGlobalAssignmentPrefixes = globalName =>
73
+ [
74
+ ...new Set([
75
+ isLegalIdentifier(globalName) ? `var ${globalName}=` : '',
76
+ isLegalIdentifier(globalName)
77
+ ? `this.${globalName}=`
78
+ : `this[${JSON.stringify(globalName)}]=`,
79
+ `this[${JSON.stringify(globalName)}]=`,
80
+ getSegmentedGlobalAssignmentPrefix(globalName),
81
+ ]),
82
+ ].filter(Boolean);
83
+
84
+ const normalizeIifeGlobalAssignment = (jsContent, appIdentifier, emittedGlobalName) => {
85
+ const normalizedAssignment = `this[${JSON.stringify(appIdentifier)}]=`;
86
+
87
+ if (jsContent.includes(normalizedAssignment)) {
88
+ return jsContent;
89
+ }
90
+
91
+ const assignmentPrefix = getGlobalAssignmentPrefixes(emittedGlobalName).find(prefix =>
92
+ jsContent.includes(prefix),
93
+ );
94
+
95
+ if (assignmentPrefix) {
96
+ return jsContent.replace(assignmentPrefix, normalizedAssignment);
97
+ }
98
+
99
+ // Rolldown doesn't emit a global assignment for IIFE output — inject one.
100
+ // Terser rewrites `(function(){})()` → `!function(){}()` when the result is discarded.
101
+ // Replacing `!` with the assignment (`this["name"]=function(){}()`) is valid JS
102
+ // because after `=` the `function` keyword is an expression, not a declaration.
103
+ if (jsContent.trimStart().startsWith('!function')) {
104
+ return jsContent.replace('!function', `${normalizedAssignment}function`);
105
+ }
106
+
107
+ return normalizedAssignment + jsContent;
108
+ };
109
+
110
+ // webpack's `output.publicPath: 'auto'` read the bundle's own <script> src once, at bundle
111
+ // init, and served every emitted asset from next to it. Same expression, same moment.
112
+ const ASSET_BASE_EXPR =
113
+ '(function(){var d=document,s=d.currentScript;if(!s||s.tagName.toUpperCase()!=="SCRIPT"){' +
114
+ 'var a=d.getElementsByTagName("script");s=a[a.length-1];}' +
115
+ 'return((s&&s.src)||d.baseURI).replace(/[^/]*(?:[?#].*)?$/,"");})()';
116
+
117
+ // url() in the extracted CSS carries this marker instead of a path (see renderBuiltUrl); the
118
+ // marker is spliced out into a concatenation with the base, so the injected stylesheet points
119
+ // at the CDN directory rather than at the host document's.
120
+ const ASSET_MARKER = '__CORVA_ASSET__';
121
+
122
+ const createCssInjection = cssContent =>
123
+ `(function(){var B=${ASSET_BASE_EXPR};var s=document.createElement('style');s.textContent=${JSON.stringify(
124
+ cssContent,
125
+ ).replaceAll(ASSET_MARKER, '"+B+"')};document.head.appendChild(s)})();`;
126
+
127
+ // The platform loads a single file, so the emitted stylesheets are folded back into it.
128
+ const finalizeJsBundle = ({ outDir, cssFileNames, appIdentifier, emittedGlobalName }) => {
129
+ const jsPath = join(outDir, 'app.js');
130
+ const css = cssFileNames
131
+ .map(fileName => readFileSync(join(outDir, fileName), 'utf-8'))
132
+ .join('');
133
+ // normalise first: the fallbacks read the head of the bundle, so a stylesheet
134
+ // sitting in front of it would take the global assignment for itself
135
+ const jsContent = normalizeIifeGlobalAssignment(
136
+ readFileSync(jsPath, 'utf-8'),
137
+ appIdentifier,
138
+ emittedGlobalName,
139
+ );
140
+
141
+ writeFileSync(jsPath, css ? createCssInjection(css) + jsContent : jsContent);
142
+
143
+ cssFileNames.forEach(fileName => unlinkSync(join(outDir, fileName)));
144
+ };
145
+
146
+ // webpack emitted binaries as files (`asset/resource`, publicPath resolved at runtime) and
147
+ // inlined only SVGs under svg-url-loader's 10 KB limit. Vite's library mode inlines every asset
148
+ // whatever `assetsInlineLimit` says — `?no-inline` is the one thing it still honours, so the
149
+ // same split is restored by tagging the id.
150
+ const EMITTED_ASSET_RE = /\.(png|jpe?g|gif|webp|avif|woff2?|eot|ttf|otf|mp4|webm|ogg|mp3|wav|glb|gltf)$/i;
151
+ const SVG_INLINE_LIMIT = 10000;
152
+
153
+ const emitAssetsAsFilesPlugin = () => {
154
+ const resolving = new Set();
155
+
156
+ return {
157
+ name: 'corva-emit-assets-as-files',
158
+ // before vite:resolve, which would otherwise resolve the asset id first and never let this
159
+ // hook see it
160
+ enforce: 'pre',
161
+ async resolveId(source, importer, options) {
162
+ if (source.includes('?') || (!EMITTED_ASSET_RE.test(source) && !/\.svg$/i.test(source))) {
163
+ return null;
164
+ }
165
+
166
+ const key = `${importer}\u0000${source}`;
167
+
168
+ if (resolving.has(key)) {
169
+ return null;
170
+ }
171
+
172
+ resolving.add(key);
173
+
174
+ try {
175
+ const resolved = await this.resolve(source, importer, options);
176
+
177
+ if (!resolved || resolved.external) {
178
+ return null;
179
+ }
180
+
181
+ if (/\.svg$/i.test(resolved.id) && statSync(resolved.id).size < SVG_INLINE_LIMIT) {
182
+ return null;
183
+ }
184
+
185
+ return `${resolved.id}?no-inline`;
186
+ } finally {
187
+ resolving.delete(key);
188
+ }
189
+ },
190
+ };
191
+ };
192
+
193
+ const getCorvaApiEnvVariables = () => {
194
+ const apiEnv = process.env.CORVA_API_ENV || CORVA_API_ENVS.production;
195
+
196
+ if (Object.hasOwn(CORVA_API_ENVS, apiEnv)) {
197
+ return CORVA_API_ENV_VARIABLES[apiEnv];
198
+ }
199
+
200
+ console.error(`CORVA_API_ENV should be one of [${Object.keys(CORVA_API_ENVS).join(', ')}]`);
201
+
202
+ return process.exit(1);
203
+ };
204
+
205
+ // react-refresh can only swap React components, and a hot update of anything else reaches the
206
+ // screen only when a component happens to re-read it while rendering. Measured on this app: all 7
207
+ // component edits applied live, while all 7 edits to a module behind a memo boundary or consumed
208
+ // while the app boots (a template default, a colour table, getStateColor) left the old value on
209
+ // screen with `hmr update` in the terminal and no hint that it was stale. Nothing in the module
210
+ // graph tells the two apart, so the honest rule is: components fast-refresh, every other module
211
+ // under src reloads the page. A developer never has to guess whether an edit arrived.
212
+ const COMPONENT_FILE_RE = /\.[jt]sx$/;
213
+ const SCRIPT_FILE_RE = /\.[cm]?[jt]s$/;
214
+
215
+ const reloadOnNonComponentEditPlugin = pwd => ({
216
+ name: 'corva-reload-on-non-component-edit',
217
+ handleHotUpdate({ file, server }) {
218
+ if (!file.startsWith(join(pwd, 'src')) || COMPONENT_FILE_RE.test(file) || !SCRIPT_FILE_RE.test(file)) {
219
+ return undefined;
220
+ }
221
+
222
+ server.config.logger.info(`page reload ${relative(pwd, file)} (not a component)`, { timestamp: true });
223
+ server.ws.send({ type: 'full-reload' });
224
+
225
+ return [];
226
+ },
227
+ });
228
+
229
+ // webpack tapped this after every compile: it warns when the installed @corva/ui is behind the
230
+ // one published on npm. It is written as a webpack plugin, so it gets the single hook it taps,
231
+ // and it is required here rather than at the top of the file because it reads @corva/ui's
232
+ // package.json off the cwd as it loads, which has nothing to do with a build or a test run.
233
+ const corvaUiVersionPlugin = () => ({
234
+ name: 'corva-ui-version',
235
+ configureServer() {
236
+ const checkCorvaUiVersion = require('../cjs/devcenter/webpack/plugins/checkCorvaUIVersion.js');
237
+ let warnWhenOutdated;
238
+
239
+ checkCorvaUiVersion({
240
+ hooks: { afterCompile: { tapPromise: (name, hook) => { warnWhenOutdated = hook; } } },
241
+ });
242
+
243
+ warnWhenOutdated({ getLogger: () => console });
244
+ },
245
+ });
246
+
247
+ // webpack's dev server ran react-error-overlay (through @pmmmwh/react-refresh-webpack-plugin),
248
+ // which put every uncaught error and rejection on the screen. Vite's overlay only shows what
249
+ // its own pipeline throws, so a platform call rejecting mid-render left the DevCenter sitting
250
+ // on its loader with nothing but a console entry.
251
+ // The auth branch is the one rejection that must not stop at an overlay: @corva/ui redirects to
252
+ // /login itself for anything that goes through apiCore, but the DevCenter's first call is
253
+ // jsonApi.getCurrentUser, whose 401 leaves useProvideAuth through an uncaught rejection, so a
254
+ // session that is missing or expired never reaches the login screen.
255
+ const RUNTIME_ERRORS_ID = 'virtual:corva-dev-runtime-errors';
256
+
257
+ const AUTH_RECOVERY_MODULE = '@corva/ui/clients/api/AuthRecovery';
258
+
259
+ const RUNTIME_ERRORS_SOURCE = `
260
+ import { AuthRecovery } from '${AUTH_RECOVERY_MODULE}';
261
+
262
+ const show = error => {
263
+ document.querySelectorAll('vite-error-overlay').forEach(overlay => overlay.close());
264
+ document.body.appendChild(
265
+ new (customElements.get('vite-error-overlay'))({
266
+ message: error?.message ?? String(error),
267
+ stack: error?.stack ?? '',
268
+ }),
269
+ );
270
+ };
271
+
272
+ // Browsers fire this when a ResizeObserver callback schedules another layout pass. It is a
273
+ // notice, not a failure, and webpack-dev-server's overlay filtered it out too; showing it puts a
274
+ // full-screen overlay over a working app.
275
+ const isBenign = error => /ResizeObserver loop/.test(error?.message ?? String(error ?? ''));
276
+
277
+ window.addEventListener('error', event => {
278
+ const error = event.error ?? event.message;
279
+
280
+ if (!isBenign(error)) show(error);
281
+ });
282
+
283
+ window.addEventListener('unhandledrejection', event => {
284
+ const authRecovery = AuthRecovery.getInstance();
285
+
286
+ if (authRecovery.isRefreshableAuthError(event.reason)) {
287
+ authRecovery.handleAuthFailure(event.reason);
288
+ return;
289
+ }
290
+
291
+ if (!isBenign(event.reason)) show(event.reason);
292
+ });
293
+ `;
294
+
295
+ const runtimeErrorsPlugin = () => ({
296
+ name: 'corva-dev-runtime-errors',
297
+ resolveId(id) {
298
+ return id === RUNTIME_ERRORS_ID ? `\0${RUNTIME_ERRORS_ID}` : null;
299
+ },
300
+ load(id) {
301
+ return id === `\0${RUNTIME_ERRORS_ID}` ? RUNTIME_ERRORS_SOURCE : null;
302
+ },
303
+ transformIndexHtml: () => [
304
+ {
305
+ tag: 'script',
306
+ attrs: { type: 'module', src: `/@id/__x00__${RUNTIME_ERRORS_ID}` },
307
+ injectTo: 'body',
308
+ },
309
+ ],
310
+ });
311
+
312
+ // webpack's dev config added this entry point whenever the app depends on
313
+ // mapbox-gl: it reads the access token off @corva/ui and publishes the
314
+ // configured instance as window['mapbox-gl'].
315
+ // A virtual module rather than a direct node_modules URL: mapbox-gl's dist is
316
+ // CJS, so it only imports cleanly once Vite's resolver and dep optimizer see it.
317
+ const MAPBOX_DEV_SETUP_ID = 'virtual:corva-mapbox-dev-setup';
318
+
319
+ const mapboxDevSetupPlugin = () => ({
320
+ name: 'corva-mapbox-dev-setup',
321
+ resolveId(id) {
322
+ return id === MAPBOX_DEV_SETUP_ID ? `\0${MAPBOX_DEV_SETUP_ID}` : null;
323
+ },
324
+ load(id) {
325
+ return id === `\0${MAPBOX_DEV_SETUP_ID}`
326
+ ? "import '@corva/dc-platform-shared/cjs/devcenter/webpack/MapboxGlDevSetupEntryPoint.js';"
327
+ : null;
328
+ },
329
+ transformIndexHtml: () => [
330
+ {
331
+ tag: 'script',
332
+ attrs: { type: 'module', src: `/@id/__x00__${MAPBOX_DEV_SETUP_ID}` },
333
+ injectTo: 'body',
334
+ },
335
+ ],
336
+ });
337
+
338
+ // The DevCenter dev page edits the app's .env through this endpoint.
339
+ const dotEnvEndpointPlugin = appDirectory => ({
340
+ name: 'corva-dotenv-endpoint',
341
+ configureServer(server) {
342
+ server.middlewares.use('/dotenv', (req, res, next) => {
343
+ if (req.method !== 'PATCH') {
344
+ return next();
345
+ }
346
+
347
+ const envPath = resolve(appDirectory, '.env');
348
+ const chunks = [];
349
+
350
+ req.on('data', chunk => chunks.push(chunk));
351
+ req.on('end', () => {
352
+ let update;
353
+
354
+ // webpack's dev server parsed the body with express.json(), which answers
355
+ // 400 on a malformed one; parsing it here would take the server down
356
+ try {
357
+ update = JSON.parse(Buffer.concat(chunks));
358
+ } catch {
359
+ res.statusCode = 400;
360
+ res.end();
361
+
362
+ return;
363
+ }
364
+
365
+ const currentEnv = existsSync(envPath) ? readFileSync(envPath, 'utf-8') : '';
366
+
367
+ writeFileSync(envPath, updateDotEnv(currentEnv, update));
368
+ res.statusCode = 200;
369
+ res.end();
370
+ });
371
+ });
372
+ },
373
+ });
374
+
375
+ // Vitest runs its dependency optimizer with `noDiscovery: true`, so a dependency is pre-bundled
376
+ // only if it is listed. Unlisted, every test file re-evaluates that dependency's entire module
377
+ // graph in its own isolated context — @corva/ui alone is 2022 ESM files — and a 169-file suite
378
+ // takes 105 s where jest took 21 s (jest required one CJS module tree per worker). Pre-bundling
379
+ // the dependencies the app imports brings the same suite to 23 s.
380
+ const BARE_IMPORT_RE =
381
+ /(?:import|export)[^'"\n]*?from\s*['"]([^'"]+)['"]|\bimport\(\s*['"]([^'"]+)['"]\s*\)|\brequire\(\s*['"]([^'"]+)['"]\s*\)/g;
382
+ const NAMESPACE_IMPORT_RE = /import\s*\*\s*as\s+([A-Za-z_$][\w$]*)\s*from\s*['"]([^'"]+)['"]/g;
383
+ // react and the test renderers stay external whatever their package says: a copy inside the
384
+ // pre-bundle is a second React instance, and the two cannot see each other's hooks or renderer
385
+ // registry ("Could not auto-detect a React renderer")
386
+ const TEST_EXTERNAL_DEP_RE = /^(react|react-dom|react-test-renderer)($|\/)|^@testing-library\//;
387
+
388
+ const packageRoot = specifier =>
389
+ (specifier.startsWith('@') ? specifier.split('/').slice(0, 2) : [specifier.split('/')[0]]).join('/');
390
+
391
+ const readPackageJson = (pwd, specifier) => {
392
+ const manifest = join(pwd, 'node_modules', packageRoot(specifier), 'package.json');
393
+
394
+ return existsSync(manifest) ? JSON.parse(readFileSync(manifest, 'utf-8')) : null;
395
+ };
396
+
397
+ // Only a dependency vite pre-bundles as ESM can be included: a CJS one reaches the app through
398
+ // vite's interop, which changes what a `vi.mock` factory has to return and whether `new Dep()`
399
+ // still constructs. This is the same decision vite records as `needsInterop`, read off the
400
+ // package the way vite resolves its entry: `exports`/`type` win, then `module`, and a package
401
+ // with a real `main` and nothing else is CJS.
402
+ const isEsmPackage = (pwd, specifier) => {
403
+ const packageJson = readPackageJson(pwd, specifier);
404
+
405
+ if (!packageJson) {
406
+ return false;
407
+ }
408
+
409
+ if (packageJson.type === 'module' || JSON.stringify(packageJson.exports || {}).includes('"import"')) {
410
+ return true;
411
+ }
412
+
413
+ const main = packageJson.main && join(pwd, 'node_modules', packageRoot(specifier), packageJson.main);
414
+
415
+ return Boolean(packageJson.module) && !(main && (existsSync(main) || existsSync(`${main}.js`) || existsSync(join(main, 'index.js'))));
416
+ };
417
+
418
+ // the optimizer needs an entry file to bundle; 85 of @corva/ui's component folders ship only
419
+ // index.d.ts, and `@corva/ui`'s own `main` points at a directory that is not published
420
+ const hasEntryFile = (pwd, appRequire, specifier) => {
421
+ try {
422
+ return /\.(js|mjs|cjs)$/.test(appRequire.resolve(specifier));
423
+ } catch {
424
+ const base = join(pwd, 'node_modules', specifier);
425
+
426
+ return existsSync(`${base}.js`) || existsSync(join(base, 'index.js'));
427
+ }
428
+ };
429
+
430
+ const scanAppImports = pwd => {
431
+ const specifiers = new Set();
432
+ const spied = new Set();
433
+
434
+ const collect = dir => {
435
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
436
+ const full = join(dir, entry.name);
437
+
438
+ if (entry.isDirectory()) {
439
+ collect(full);
440
+ } else if (/\.(js|jsx|ts|tsx|mjs|cjs)$/.test(entry.name)) {
441
+ const code = readFileSync(full, 'utf-8');
442
+
443
+ for (const match of code.matchAll(BARE_IMPORT_RE)) {
444
+ specifiers.add(match[1] || match[2] || match[3]);
445
+ }
446
+
447
+ // `import * as utils from 'x'` + `vi.spyOn(utils, 'y')`: a pre-bundled module hands the
448
+ // test a real, sealed ESM namespace and the spy throws
449
+ for (const match of code.matchAll(NAMESPACE_IMPORT_RE)) {
450
+ if (new RegExp(`spyOn\\(\\s*${match[1]}\\s*,`).test(code)) {
451
+ spied.add(match[2]);
452
+ }
453
+ }
454
+ }
455
+ }
456
+ };
457
+
458
+ collect(join(pwd, 'src'));
459
+
460
+ return { specifiers: [...specifiers], spied };
461
+ };
462
+
463
+ // The dev server pre-bundles whatever it discovers while serving a request, and the app's entry
464
+ // is behind a dynamic import in the DevCenter harness, so discovery only starts once the browser
465
+ // asks for the app: it then bundles the dependencies mid-session and force-reloads the page
466
+ // ("optimized dependencies changed. reloading"). Listing them up front makes the first load one
467
+ // pass instead of two — 4.6 s to a rendered app becomes 2.0 s on a 169-file app, cold cache.
468
+ // Interop does not matter here the way it does for tests, so this list is everything importable.
469
+ const getDevOptimizeDeps = (pwd, { specifiers }) => {
470
+ const appRequire = createRequire(join(pwd, 'package.json'));
471
+
472
+ return specifiers
473
+ .filter(specifier => /^@?[\w.-]/.test(specifier) && !specifier.startsWith('@/'))
474
+ .filter(specifier => !builtinModules.includes(specifier.replace(/^node:/, '')))
475
+ .filter(specifier => hasEntryFile(pwd, appRequire, specifier))
476
+ .sort();
477
+ };
478
+
479
+ const getTestOptimizeDeps = (pwd, { specifiers, spied }) => {
480
+ const appRequire = createRequire(join(pwd, 'package.json'));
481
+
482
+ return specifiers
483
+ .filter(specifier => /^@?[\w.-]/.test(specifier) && !specifier.startsWith('@/'))
484
+ .filter(specifier => !builtinModules.includes(specifier.replace(/^node:/, '')))
485
+ .filter(specifier => !TEST_EXTERNAL_DEP_RE.test(specifier))
486
+ .filter(specifier => !spied.has(specifier))
487
+ .filter(specifier => isEsmPackage(pwd, specifier) && hasEntryFile(pwd, appRequire, specifier))
488
+ .sort();
489
+ };
490
+
491
+ const DEP_CSS_ID = '\0corva-test-dep-css';
492
+ const NODE_BUILTIN_PREFIX = '\0corva-test-node-builtin:';
493
+
494
+ const testDepBundlePlugin = pwd => {
495
+ const appRequire = createRequire(join(pwd, 'package.json'));
496
+
497
+ return {
498
+ name: 'corva-test-dep-bundle',
499
+ resolveId(source, importer) {
500
+ // the shim below imports the real builtin
501
+ if (importer?.startsWith(NODE_BUILTIN_PREFIX)) {
502
+ return { id: source, external: true };
503
+ }
504
+
505
+ // React 17 publishes no exports map, so Node cannot resolve a bare `react/jsx-runtime`
506
+ // left in a pre-bundled chunk
507
+ if (source === 'react/jsx-runtime') {
508
+ return appRequire.resolve(source);
509
+ }
510
+
511
+ // jest mapped every stylesheet to a stub object; Node cannot load a .css file at all
512
+ if (/\.(css|scss|sass|less)(\?|$)/.test(source)) {
513
+ return DEP_CSS_ID;
514
+ }
515
+
516
+ // these tests run in Node, so a dependency that reads a builtin (debug -> tty,
517
+ // fflate -> worker_threads) gets the real one instead of the browser stub that throws
518
+ const builtin = source.replace(/^node:/, '');
519
+
520
+ return builtinModules.includes(builtin) ? `${NODE_BUILTIN_PREFIX}${builtin}` : null;
521
+ },
522
+ load(id) {
523
+ if (id === DEP_CSS_ID) {
524
+ return 'export default {};';
525
+ }
526
+
527
+ if (id.startsWith(NODE_BUILTIN_PREFIX)) {
528
+ const builtin = id.slice(NODE_BUILTIN_PREFIX.length);
529
+
530
+ return `export * from 'node:${builtin}';\nexport { default } from 'node:${builtin}';`;
531
+ }
532
+
533
+ return null;
534
+ },
535
+ };
536
+ };
537
+
538
+ export const createViteConfig = ({ pwd, overrides = {} }) => {
539
+ expand(dotenv.config({ path: resolve(pwd, '.env'), quiet: true }));
540
+
541
+ const packageJson = JSON.parse(readFileSync(resolve(pwd, 'package.json'), 'utf-8'));
542
+ const manifestJson = JSON.parse(readFileSync(resolve(pwd, 'manifest.json'), 'utf-8'));
543
+
544
+ const appKey = manifestJson.application.key;
545
+ const version = packageJson.version;
546
+ const appIdentifier = `${appKey.replace(/\./g, '_')}-${version}`;
547
+ const appIifeName = toLegalIdentifier(appIdentifier);
548
+
549
+ const globalDependencies = Object.keys(CORVA_GLOBAL_DEPENDENCIES);
550
+ const externalPatterns = globalDependencies.map(
551
+ libName => new RegExp(`(^${libName}$|^${libName}/)([^]+)?`),
552
+ );
553
+
554
+ const isExternal = source =>
555
+ // a stylesheet cannot be read off a window global: webpack bundled `mapbox-gl/dist/mapbox-gl.css`
556
+ // and so does this, and `this["mapbox-gl"].dist["mapbox-gl.css"]` is not even valid JS
557
+ !source.endsWith('.css') &&
558
+ !SKIP_IN_EXTERNALS.some(skipped => source.startsWith(skipped)) &&
559
+ externalPatterns.some(pattern => pattern.test(source));
560
+
561
+ // Package names are not valid JS identifiers, so every global is read off `this`.
562
+ // The name has to match on a package boundary, as isExternal does: matching on a
563
+ // bare prefix maps react-dom/client onto react and emits `this["react"]-dom.client`.
564
+ const toGlobal = id => {
565
+ const dep = globalDependencies.find(libName => id === libName || id.startsWith(`${libName}/`));
566
+
567
+ if (!dep) {
568
+ throw new Error(`Unknown dependency: ${id}`);
569
+ }
570
+
571
+ if (dep === id) {
572
+ return `this["${dep}"].default || this["${dep}"]`;
573
+ }
574
+
575
+ return `this["${dep}"]${id
576
+ .slice(dep.length)
577
+ .split('/')
578
+ .filter(Boolean)
579
+ .map(segment => `.${segment}`)
580
+ .join('')}`;
581
+ };
582
+
583
+ const getAlias = () => {
584
+ // jest resolved '~' to src for both templates; keep it in the build too
585
+ const defaultAlias = { '~': resolve(pwd, 'src') };
586
+ const tsconfigPath = resolve(pwd, 'tsconfig.json');
587
+
588
+ if (!existsSync(tsconfigPath)) {
589
+ return defaultAlias;
590
+ }
591
+
592
+ // tsconfig.json is JSONC, which JSON.parse rejects; a comment in it must not
593
+ // take the build down
594
+ let compilerOptions;
595
+
596
+ try {
597
+ ({ compilerOptions } = JSON.parse(readFileSync(tsconfigPath, 'utf-8')));
598
+ } catch {
599
+ console.warn(`Could not read compilerOptions.paths from ${tsconfigPath}; only '~' is aliased.`);
600
+
601
+ return defaultAlias;
602
+ }
603
+
604
+ // tsc resolves every `paths` value against `baseUrl`, not against the tsconfig's own directory
605
+ const pathsRoot = resolve(pwd, compilerOptions?.baseUrl || '.');
606
+
607
+ return {
608
+ ...defaultAlias,
609
+ ...Object.fromEntries(
610
+ Object.entries(compilerOptions?.paths || {})
611
+ // `"react": ["./node_modules/@types/react"]` tells tsc where the types live; as a runtime
612
+ // alias it points the bundler at a package with no code, and every `react/...` subpath
613
+ // then fails to resolve
614
+ .filter(([, [value]]) => !/(^|\/)@types\//.test(value))
615
+ .map(([key, [value]]) => (
616
+ key.endsWith('/*')
617
+ // keep the `/*` boundary: a bare `@` prefix-matches @types/react, @corva/ui and
618
+ // every other scoped package and rewrites them into src/
619
+ ? [`${key.slice(0, -1)}`, `${resolve(pathsRoot, value.replace('/*', ''))}/`]
620
+ : [key, resolve(pathsRoot, value)]
621
+ )),
622
+ ),
623
+ };
624
+ };
625
+
626
+ const appImports = scanAppImports(pwd);
627
+
628
+ const resolveConfig = ({ command }) => {
629
+ validateDependencies({ packageJson });
630
+
631
+ const config = {
632
+ root: pwd,
633
+ // vite's default asset list has no 3D model types, so without this it tries to parse a .glb
634
+ // as a module and fails with `stream did not contain valid UTF-8`
635
+ assetsInclude: ['**/*.glb', '**/*.gltf'],
636
+ optimizeDeps: {
637
+ // the harness module's own import belongs in this pass: discovered later, it becomes a
638
+ // second pre-bundled chunk in which AuthRecovery and apiCore's circular import
639
+ // initialises the wrong way round and apiCore's top-level getInstance() throws
640
+ include: [...getDevOptimizeDeps(pwd, appImports), AUTH_RECOVERY_MODULE],
641
+ },
642
+ server: {
643
+ host: process.env.HOST || 'app.local.corva.ai',
644
+ port: Number(process.env.PORT) || 8080,
645
+ // the local-debug flow loads this bundle into the platform from another origin, which
646
+ // webpack allowed with `allowedHosts: 'all'` plus a permissive CORS header. Without the
647
+ // same here the HMR socket is refused and the bundle stops live-reloading.
648
+ allowedHosts: true,
649
+ cors: true,
650
+ },
651
+ css: {
652
+ modules: {
653
+ localsConvention: 'camelCase',
654
+ generateScopedName: `[name]--[local]__[hash:base64:10]__${BUILD_ID}`,
655
+ },
656
+ // webpack's postcss-loader ran preset-env with no options, i.e. stage 2; a narrower
657
+ // stage silently stops transpiling CSS features the app already relies on
658
+ postcss: { plugins: [presetEnv()] },
659
+ },
660
+ plugins: [react(), dotEnvEndpointPlugin(pwd), rejectUnscopedCssPlugin(pwd)],
661
+ // webpack ran with `resolve.symlinks: false`; a yarn-linked @corva/ui has to keep
662
+ // resolving through the link, not through its real path
663
+ resolve: { alias: getAlias(), preserveSymlinks: true },
664
+ test: {
665
+ watch: false,
666
+ globals: true,
667
+ // jest mapped every CSS module through identity-obj-proxy, so `styles.foo` was the string
668
+ // 'foo' and a test could assert on `.foo`. vitest 4's default strategy hands back
669
+ // `_foo_<hash>` instead, which silently breaks those assertions.
670
+ css: { modules: { classNameStrategy: 'non-scoped' } },
671
+ // vitest transforms and imports a file's module graph the first time a worker touches it,
672
+ // where babel-jest served it from its cache. Under a full parallel run that first touch can
673
+ // outlast vitest's 5s default and fail a test that does no async work at all - it passes
674
+ // alone on a dev machine and times out on a CI runner.
675
+ testTimeout: 20000,
676
+ // jest ran with resetMocks: true; vitest defaults it to false
677
+ mockReset: true,
678
+ // measured on a 169-file app before the pre-bundle below: threads 105 s, the default
679
+ // forks pool 114 s, vmThreads 950 s
680
+ pool: 'threads',
681
+ deps: {
682
+ optimizer: {
683
+ client: {
684
+ enabled: true,
685
+ include: getTestOptimizeDeps(pwd, appImports),
686
+ rolldownOptions: { plugins: [testDepBundlePlugin(pwd)] },
687
+ },
688
+ },
689
+ },
690
+ environment: 'jsdom',
691
+ setupFiles: ['./vitest.setup.js'],
692
+ globalSetup: ['./vitest.global-setup.js'],
693
+ coverage: {
694
+ provider: 'v8',
695
+ include: ['src/**/*.{js,jsx,ts,tsx}'],
696
+ thresholds: { branches: 60, functions: 60, lines: 60, statements: 60 },
697
+ },
698
+ },
699
+ };
700
+
701
+ if (command === 'serve') {
702
+ return {
703
+ ...config,
704
+ define: {
705
+ // webpack's node.global shim made `global` the real global object; `{}` would hand
706
+ // every `global.x` read a fresh empty object and make `global.x = …` a syntax error
707
+ global: 'globalThis',
708
+ // Webpack's EnvironmentPlugin exposed the whole environment, so apps read
709
+ // their own REACT_APP_* vars straight off process.env.
710
+ 'process.env': {
711
+ ...process.env,
712
+ DEV_CENTER_APP_PATH: '',
713
+ NODE_ENV: 'development',
714
+ REACT_APP_ENVIRONMENT: process.env.REACT_APP_ENVIRONMENT || 'development',
715
+ ...getCorvaApiEnvVariables(),
716
+ },
717
+ },
718
+ plugins: [
719
+ ...config.plugins,
720
+ ...(packageJson.dependencies?.['mapbox-gl'] ? [mapboxDevSetupPlugin()] : []),
721
+ runtimeErrorsPlugin(),
722
+ corvaUiVersionPlugin(),
723
+ reloadOnNonComponentEditPlugin(pwd),
724
+ // webpack's dev config copied @corva/ui's and @corva/dc-platform-shared's assets
725
+ // into static/media, where both packages request them by hash name (the loader gif
726
+ // of every DevCenter screen is one). stripBase drops the node_modules path the
727
+ // plugin would otherwise keep, which put them out of the platform's reach.
728
+ viteStaticCopy({
729
+ watch: { reloadPageOnChange: false },
730
+ targets: [
731
+ {
732
+ src: `${PLATFORM_SHARED_DIR}/assets/*`,
733
+ dest: 'static/media',
734
+ rename: { stripBase: true },
735
+ },
736
+ {
737
+ src: `${join(PLATFORM_SHARED_DIR, '../ui/assets')}/*`,
738
+ dest: 'static/media',
739
+ rename: { stripBase: true },
740
+ },
741
+ ],
742
+ }),
743
+ ],
744
+ };
745
+ }
746
+
747
+ return {
748
+ ...config,
749
+ define: {
750
+ // webpack's prod config ran `EnvironmentPlugin(process.env)`: one textual replacement
751
+ // per key, so only the keys the source actually reads reach the bundle. Without it an
752
+ // app reading e.g. process.env.CORVA_API_ENV silently compiles to undefined.
753
+ ...Object.fromEntries(
754
+ Object.entries(process.env)
755
+ .filter(([key]) => /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key))
756
+ .map(([key, value]) => [`process.env.${key}`, JSON.stringify(value)]),
757
+ ),
758
+ 'process.env': { NODE_ENV: 'production' },
759
+ 'process.env.NODE_ENV': '"production"',
760
+ global: 'globalThis',
761
+ },
762
+ plugins: [
763
+ ...config.plugins,
764
+ emitAssetsAsFilesPlugin(),
765
+ viteStaticCopy({ targets: [{ src: 'manifest.json', dest: '.' }] }),
766
+ {
767
+ name: 'corva-single-file-bundle',
768
+ writeBundle(_options, bundle) {
769
+ finalizeJsBundle({
770
+ outDir: resolve(pwd, 'dist'),
771
+ cssFileNames: Object.keys(bundle).filter(fileName => fileName.endsWith('.css')),
772
+ appIdentifier,
773
+ emittedGlobalName: appIifeName,
774
+ });
775
+ },
776
+ },
777
+ ],
778
+ // an asset URL has to be resolved at runtime, from the bundle's own location: the same
779
+ // app.js is served from a different package directory for every version
780
+ experimental: {
781
+ renderBuiltUrl: (filename, { hostType }) =>
782
+ (hostType === 'css'
783
+ ? `${ASSET_MARKER}${filename}`
784
+ : { runtime: `__CORVA_ASSET_BASE__+${JSON.stringify(filename)}` }),
785
+ },
786
+ build: {
787
+ outDir: 'dist',
788
+ emptyOutDir: true,
789
+ copyPublicDir: false,
790
+ sourcemap: false,
791
+ cssCodeSplit: false,
792
+ minify: 'terser',
793
+ terserOptions: {
794
+ format: { comments: false },
795
+ },
796
+ // lib mode plus named exports keeps the IIFE compatible with the platform
797
+ // loader, which reads `this["appKey-version"].default.component`.
798
+ lib: {
799
+ entry: resolve(pwd, 'src/index.js'),
800
+ name: appIifeName,
801
+ fileName: () => 'app.js',
802
+ formats: ['iife'],
803
+ },
804
+ rolldownOptions: {
805
+ external: isExternal,
806
+ moduleTypes: { '.cjs': 'js' },
807
+ output: {
808
+ format: 'iife',
809
+ name: appIifeName,
810
+ entryFileNames: 'app.js',
811
+ // flat next to app.js, the way webpack's default assetModuleFilename emitted them
812
+ assetFileNames: '[hash][extname]',
813
+ globals: toGlobal,
814
+ exports: 'named',
815
+ // Rolldown doesn't rewrite `require()` inside bundled CJS modules (e.g.
816
+ // react-is) to their external global references. This shim resolves them
817
+ // to the globals the IIFE receives as params.
818
+ intro:
819
+ `var require=function(id){var g=globalThis[id];return g&&g.default||g;};` +
820
+ `var __CORVA_ASSET_BASE__=${ASSET_BASE_EXPR};`,
821
+ },
822
+ plugins: [
823
+ {
824
+ // Rolldown rejects `typeof require` as a define key (not an identifier),
825
+ // so the CJS-detection branches are stripped by transform instead.
826
+ name: 'replace-typeof-require',
827
+ transform(code) {
828
+ if (!code.includes('typeof require')) return null;
829
+
830
+ return { code: code.replaceAll('typeof require', '"undefined"'), map: null };
831
+ },
832
+ },
833
+ license({
834
+ thirdParty: { output: resolve(pwd, 'dist/app.js.LICENSE.txt') },
835
+ }),
836
+ ],
837
+ },
838
+ },
839
+ };
840
+ };
841
+
842
+ return defineConfig(env => mergeConfig(resolveConfig(env), overrides));
843
+ };