@corva/create-app 0.123.0 → 0.125.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 (45) hide show
  1. package/README.md +9 -1
  2. package/common/ui/config/vite/corva-vite-config.mjs +551 -0
  3. package/common/ui/devApp.js +1 -0
  4. package/common/ui/index.html +17 -0
  5. package/common/ui/isolatedApp.js +1 -0
  6. package/common/ui/public/DevCenterIsolatedAppPage.html +26 -0
  7. package/common/ui/public/favicon.ico +0 -0
  8. package/common/ui/vite.config.mjs +4 -0
  9. package/common/ui/vitest.global-setup.js +4 -0
  10. package/common/ui/vitest.setup.js +26 -0
  11. package/lib/commands/create.js +4 -0
  12. package/lib/constants/package.js +26 -57
  13. package/lib/flows/steps/zip-file-list-resolve.js +9 -0
  14. package/lib/helpers/utils.js +2 -2
  15. package/package.json +1 -1
  16. package/template_extensions/corva/.eslintrc +4 -0
  17. package/template_extensions/corva/.github/workflows/feat-fix.yml +1 -0
  18. package/templates/ui/javascript/.eslintrc +7 -2
  19. package/templates/ui/javascript/AGENTS.md +13 -11
  20. package/templates/ui/javascript/src/{App.completion.js → App.completion.jsx} +3 -3
  21. package/templates/ui/javascript/src/{App.drilling.js → App.drilling.jsx} +3 -3
  22. package/templates/ui/javascript/src/{AppSettings.js → AppSettings.jsx} +1 -1
  23. package/templates/ui/javascript/src/__tests__/{AppSettings.test.js → AppSettings.test.jsx} +1 -1
  24. package/templates/ui/typescript/.eslintrc +4 -0
  25. package/templates/ui/typescript/AGENTS.md +11 -8
  26. package/templates/ui/typescript/src/App.completion.tsx +1 -1
  27. package/templates/ui/typescript/src/App.drilling.tsx +1 -1
  28. package/templates/ui/typescript/src/__tests__/AppSettings.test.tsx +1 -1
  29. package/templates/ui/typescript/tsconfig.json +17 -4
  30. package/templates/ui/javascript/config/jest/babelTransform.js +0 -16
  31. package/templates/ui/javascript/config/jest/cssTransform.js +0 -16
  32. package/templates/ui/javascript/config/jest/fileTransform.js +0 -48
  33. package/templates/ui/javascript/config/jest/globalSetup.js +0 -5
  34. package/templates/ui/javascript/config/jest/setupTests.js +0 -33
  35. package/templates/ui/javascript/config-overrides.js +0 -10
  36. package/templates/ui/typescript/config/jest/babelTransform.js +0 -16
  37. package/templates/ui/typescript/config/jest/cssTransform.js +0 -16
  38. package/templates/ui/typescript/config/jest/fileTransform.js +0 -48
  39. package/templates/ui/typescript/config/jest/globalSetup.js +0 -5
  40. package/templates/ui/typescript/config/jest/setupTests.js +0 -33
  41. package/templates/ui/typescript/config-overrides.js +0 -10
  42. /package/templates/ui/javascript/src/{App.scss → App.module.scss} +0 -0
  43. /package/templates/ui/javascript/src/__tests__/{App.test.js → App.test.jsx} +0 -0
  44. /package/templates/ui/javascript/src/__tests__/{TestsExample.test.js → TestsExample.test.jsx} +0 -0
  45. /package/templates/ui/typescript/src/{App.scss → App.module.scss} +0 -0
package/README.md CHANGED
@@ -114,8 +114,16 @@ For `frontend` apps:
114
114
  - `package.json`
115
115
  - `yarn.lock`
116
116
  - `.npmrc`
117
- - `config-overrides.js`
117
+ - `config-overrides.js` (webpack apps)
118
+ - `vite.config.mjs`
119
+ - `index.html`
120
+ - `devApp.js`
121
+ - `isolatedApp.js`
122
+ - `vitest.setup.js`
123
+ - `vitest.global-setup.js`
118
124
  - all files under `src`
125
+ - all files under `config`
126
+ - all files under `public`
119
127
  - `tsconfig.json`
120
128
 
121
129
  For apps that written in `node`:
@@ -0,0 +1,551 @@
1
+ // Ported from corva-monorepo-frontend tools/vite-config. Moves to
2
+ // @corva/dc-platform-shared once the standalone flow is proven, which is why
3
+ // nothing here imports from the app's own source.
4
+ import { createRequire } from 'node:module';
5
+ import { existsSync, readFileSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
6
+ import { join, relative, resolve } from 'node:path';
7
+
8
+ import dotenv from 'dotenv';
9
+ import { expand } from 'dotenv-expand';
10
+ import react from '@vitejs/plugin-react';
11
+ import license from 'rollup-plugin-license';
12
+ import { viteStaticCopy } from 'vite-plugin-static-copy';
13
+ import presetEnv from 'postcss-preset-env';
14
+ import { defineConfig, mergeConfig } from 'vitest/config';
15
+
16
+ const require = createRequire(import.meta.url);
17
+
18
+ const {
19
+ CORVA_API_ENVS,
20
+ CORVA_API_ENV_VARIABLES,
21
+ CORVA_GLOBAL_DEPENDENCIES,
22
+ } = require('@corva/dc-platform-shared/cjs/devcenter/webpack/constants.js');
23
+ const validateDependencies = require('@corva/dc-platform-shared/cjs/devcenter/webpack/validators/index.js');
24
+ const { updateDotEnv } = require('@corva/dc-platform-shared/cjs/devcenter/webpack/utils/serializeDotEnv.js');
25
+
26
+ const PLATFORM_SHARED_DIR = join(
27
+ require.resolve('@corva/dc-platform-shared/package.json'),
28
+ '..',
29
+ );
30
+ // NOTE: These imports are always bundled, never taken from the platform globals
31
+ const SKIP_IN_EXTERNALS = ['react/jsx-runtime', 'highcharts/'];
32
+
33
+ const isLegalIdentifier = value => /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(value);
34
+
35
+ const toLegalIdentifier = value => {
36
+ const identifier = value.replace(/[^A-Za-z0-9_$]/g, '_');
37
+
38
+ return isLegalIdentifier(identifier) ? identifier : `_${identifier}`;
39
+ };
40
+
41
+ const toGlobalSegmentAccess = segment =>
42
+ /^\d+$/.test(segment) ? `[${segment}]` : `[${JSON.stringify(segment)}]`;
43
+
44
+ const getSegmentedGlobalAssignmentPrefix = appIdentifier => {
45
+ const [rootSegment, ...segments] = appIdentifier.split('.');
46
+ const rootAccessor = `this[${JSON.stringify(rootSegment)}]`;
47
+
48
+ if (segments.length === 0) {
49
+ return `${rootAccessor}=`;
50
+ }
51
+
52
+ let currentAccessor = rootAccessor;
53
+ const initializers = [`${rootAccessor}=${rootAccessor}||{}`];
54
+
55
+ segments.slice(0, -1).forEach(segment => {
56
+ currentAccessor += toGlobalSegmentAccess(segment);
57
+ initializers.push(`${currentAccessor}=${currentAccessor}||{}`);
58
+ });
59
+
60
+ currentAccessor += toGlobalSegmentAccess(segments[segments.length - 1]);
61
+
62
+ return `${initializers.join(',')},${currentAccessor}=`;
63
+ };
64
+
65
+ const getGlobalAssignmentPrefixes = globalName =>
66
+ [
67
+ ...new Set([
68
+ isLegalIdentifier(globalName) ? `var ${globalName}=` : '',
69
+ isLegalIdentifier(globalName)
70
+ ? `this.${globalName}=`
71
+ : `this[${JSON.stringify(globalName)}]=`,
72
+ `this[${JSON.stringify(globalName)}]=`,
73
+ getSegmentedGlobalAssignmentPrefix(globalName),
74
+ ]),
75
+ ].filter(Boolean);
76
+
77
+ const normalizeIifeGlobalAssignment = (jsContent, appIdentifier, emittedGlobalName) => {
78
+ const normalizedAssignment = `this[${JSON.stringify(appIdentifier)}]=`;
79
+
80
+ if (jsContent.includes(normalizedAssignment)) {
81
+ return jsContent;
82
+ }
83
+
84
+ const assignmentPrefix = getGlobalAssignmentPrefixes(emittedGlobalName).find(prefix =>
85
+ jsContent.includes(prefix),
86
+ );
87
+
88
+ if (assignmentPrefix) {
89
+ return jsContent.replace(assignmentPrefix, normalizedAssignment);
90
+ }
91
+
92
+ // Rolldown doesn't emit a global assignment for IIFE output — inject one.
93
+ // Terser rewrites `(function(){})()` → `!function(){}()` when the result is discarded.
94
+ // Replacing `!` with the assignment (`this["name"]=function(){}()`) is valid JS
95
+ // because after `=` the `function` keyword is an expression, not a declaration.
96
+ if (jsContent.trimStart().startsWith('!function')) {
97
+ return jsContent.replace('!function', `${normalizedAssignment}function`);
98
+ }
99
+
100
+ return normalizedAssignment + jsContent;
101
+ };
102
+
103
+ // webpack's `output.publicPath: 'auto'` read the bundle's own <script> src once, at bundle
104
+ // init, and served every emitted asset from next to it. Same expression, same moment.
105
+ const ASSET_BASE_EXPR =
106
+ '(function(){var d=document,s=d.currentScript;if(!s||s.tagName.toUpperCase()!=="SCRIPT"){' +
107
+ 'var a=d.getElementsByTagName("script");s=a[a.length-1];}' +
108
+ 'return((s&&s.src)||d.baseURI).replace(/[^/]*(?:[?#].*)?$/,"");})()';
109
+
110
+ // url() in the extracted CSS carries this marker instead of a path (see renderBuiltUrl); the
111
+ // marker is spliced out into a concatenation with the base, so the injected stylesheet points
112
+ // at the CDN directory rather than at the host document's.
113
+ const ASSET_MARKER = '__CORVA_ASSET__';
114
+
115
+ const createCssInjection = cssContent =>
116
+ `(function(){var B=${ASSET_BASE_EXPR};var s=document.createElement('style');s.textContent=${JSON.stringify(
117
+ cssContent,
118
+ ).replaceAll(ASSET_MARKER, '"+B+"')};document.head.appendChild(s)})();`;
119
+
120
+ // The platform loads a single file, so the emitted stylesheets are folded back into it.
121
+ const finalizeJsBundle = ({ outDir, cssFileNames, appIdentifier, emittedGlobalName }) => {
122
+ const jsPath = join(outDir, 'app.js');
123
+ const css = cssFileNames
124
+ .map(fileName => readFileSync(join(outDir, fileName), 'utf-8'))
125
+ .join('');
126
+ // normalise first: the fallbacks read the head of the bundle, so a stylesheet
127
+ // sitting in front of it would take the global assignment for itself
128
+ const jsContent = normalizeIifeGlobalAssignment(
129
+ readFileSync(jsPath, 'utf-8'),
130
+ appIdentifier,
131
+ emittedGlobalName,
132
+ );
133
+
134
+ writeFileSync(jsPath, css ? createCssInjection(css) + jsContent : jsContent);
135
+
136
+ cssFileNames.forEach(fileName => unlinkSync(join(outDir, fileName)));
137
+ };
138
+
139
+ // webpack emitted binaries as files (`asset/resource`, publicPath resolved at runtime) and
140
+ // inlined only SVGs under svg-url-loader's 10 KB limit. Vite's library mode inlines every asset
141
+ // whatever `assetsInlineLimit` says — `?no-inline` is the one thing it still honours, so the
142
+ // same split is restored by tagging the id.
143
+ const EMITTED_ASSET_RE = /\.(png|jpe?g|gif|webp|avif|woff2?|eot|ttf|otf|mp4|webm|ogg|mp3|wav)$/i;
144
+ const SVG_INLINE_LIMIT = 10000;
145
+
146
+ const emitAssetsAsFilesPlugin = () => {
147
+ const resolving = new Set();
148
+
149
+ return {
150
+ name: 'corva-emit-assets-as-files',
151
+ // before vite:resolve, which would otherwise resolve the asset id first and never let this
152
+ // hook see it
153
+ enforce: 'pre',
154
+ async resolveId(source, importer, options) {
155
+ if (source.includes('?') || (!EMITTED_ASSET_RE.test(source) && !/\.svg$/i.test(source))) {
156
+ return null;
157
+ }
158
+
159
+ const key = `${importer}\u0000${source}`;
160
+
161
+ if (resolving.has(key)) {
162
+ return null;
163
+ }
164
+
165
+ resolving.add(key);
166
+
167
+ try {
168
+ const resolved = await this.resolve(source, importer, options);
169
+
170
+ if (!resolved || resolved.external) {
171
+ return null;
172
+ }
173
+
174
+ if (/\.svg$/i.test(resolved.id) && statSync(resolved.id).size < SVG_INLINE_LIMIT) {
175
+ return null;
176
+ }
177
+
178
+ return `${resolved.id}?no-inline`;
179
+ } finally {
180
+ resolving.delete(key);
181
+ }
182
+ },
183
+ };
184
+ };
185
+
186
+ // Every DC app on a dashboard renders into the same document, which is why webpack's css-loader
187
+ // ran with `modules` on for every stylesheet: nothing an app shipped could leak onto its
188
+ // neighbours. Vite decides by filename, so a plain `.css` would quietly become global. Refuse it
189
+ // instead — the selectors that really have to be global still go through `:global(...)`, exactly
190
+ // as they did under webpack.
191
+ const UNSCOPED_CSS_RE = /(?<!\.module)\.(css|scss|sass)(\?|$)/;
192
+ const STYLESHEET_IMPORTER_RE = /\.(css|scss|sass)(\?|$)/;
193
+
194
+ const rejectUnscopedCssPlugin = pwd => {
195
+ const resolving = new Set();
196
+
197
+ return {
198
+ name: 'corva-reject-unscoped-css',
199
+ enforce: 'pre',
200
+ async resolveId(source, importer, options) {
201
+ // no importer: an entry. Stylesheet importer: a sass/postcss partial, which is not a module
202
+ // in either build system.
203
+ if (!importer || !UNSCOPED_CSS_RE.test(source) || STYLESHEET_IMPORTER_RE.test(importer)) {
204
+ return null;
205
+ }
206
+
207
+ const key = `${importer}\u0000${source}`;
208
+
209
+ if (resolving.has(key)) {
210
+ return null;
211
+ }
212
+
213
+ resolving.add(key);
214
+
215
+ let resolved;
216
+
217
+ try {
218
+ resolved = await this.resolve(source, importer, options);
219
+ } finally {
220
+ resolving.delete(key);
221
+ }
222
+
223
+ // a dependency's own stylesheet is its business, and it was never scoped to this app
224
+ if (!resolved || resolved.external || resolved.id.includes('node_modules')) {
225
+ return null;
226
+ }
227
+
228
+ throw new Error(
229
+ `${relative(pwd, resolved.id)} is imported as a plain stylesheet by ${relative(pwd, importer)}.\n` +
230
+ 'Please use *.module.css — global styles may leak into other platform elements, and ' +
231
+ 'that surfaces later as weird bugs nobody traces back to this file.\n' +
232
+ 'If you really do want a global style and know what you are doing, keep the .module.css ' +
233
+ 'file and wrap that one selector:\n' +
234
+ ' :global(.my-global-class) { color: red; }',
235
+ );
236
+ },
237
+ };
238
+ };
239
+
240
+ const getCorvaApiEnvVariables = () => {
241
+ const apiEnv = process.env.CORVA_API_ENV || CORVA_API_ENVS.production;
242
+
243
+ if (Object.hasOwn(CORVA_API_ENVS, apiEnv)) {
244
+ return CORVA_API_ENV_VARIABLES[apiEnv];
245
+ }
246
+
247
+ console.error(`CORVA_API_ENV should be one of [${Object.keys(CORVA_API_ENVS).join(', ')}]`);
248
+
249
+ return process.exit(1);
250
+ };
251
+
252
+ // webpack's dev config added this entry point whenever the app depends on
253
+ // mapbox-gl: it reads the access token off @corva/ui and publishes the
254
+ // configured instance as window['mapbox-gl'].
255
+ // A virtual module rather than a direct node_modules URL: mapbox-gl's dist is
256
+ // CJS, so it only imports cleanly once Vite's resolver and dep optimizer see it.
257
+ const MAPBOX_DEV_SETUP_ID = 'virtual:corva-mapbox-dev-setup';
258
+
259
+ const mapboxDevSetupPlugin = () => ({
260
+ name: 'corva-mapbox-dev-setup',
261
+ resolveId(id) {
262
+ return id === MAPBOX_DEV_SETUP_ID ? `\0${MAPBOX_DEV_SETUP_ID}` : null;
263
+ },
264
+ load(id) {
265
+ return id === `\0${MAPBOX_DEV_SETUP_ID}`
266
+ ? "import '@corva/dc-platform-shared/cjs/devcenter/webpack/MapboxGlDevSetupEntryPoint.js';"
267
+ : null;
268
+ },
269
+ transformIndexHtml: () => [
270
+ {
271
+ tag: 'script',
272
+ attrs: { type: 'module', src: `/@id/__x00__${MAPBOX_DEV_SETUP_ID}` },
273
+ injectTo: 'body',
274
+ },
275
+ ],
276
+ });
277
+
278
+ // The DevCenter dev page edits the app's .env through this endpoint.
279
+ const dotEnvEndpointPlugin = appDirectory => ({
280
+ name: 'corva-dotenv-endpoint',
281
+ configureServer(server) {
282
+ server.middlewares.use('/dotenv', (req, res, next) => {
283
+ if (req.method !== 'PATCH') {
284
+ return next();
285
+ }
286
+
287
+ const envPath = resolve(appDirectory, '.env');
288
+ const chunks = [];
289
+
290
+ req.on('data', chunk => chunks.push(chunk));
291
+ req.on('end', () => {
292
+ let update;
293
+
294
+ // webpack's dev server parsed the body with express.json(), which answers
295
+ // 400 on a malformed one; parsing it here would take the server down
296
+ try {
297
+ update = JSON.parse(Buffer.concat(chunks));
298
+ } catch {
299
+ res.statusCode = 400;
300
+ res.end();
301
+
302
+ return;
303
+ }
304
+
305
+ const currentEnv = existsSync(envPath) ? readFileSync(envPath, 'utf-8') : '';
306
+
307
+ writeFileSync(envPath, updateDotEnv(currentEnv, update));
308
+ res.statusCode = 200;
309
+ res.end();
310
+ });
311
+ });
312
+ },
313
+ });
314
+
315
+ export const createViteConfig = ({ pwd, overrides = {} }) => {
316
+ expand(dotenv.config({ path: resolve(pwd, '.env'), quiet: true }));
317
+
318
+ const packageJson = JSON.parse(readFileSync(resolve(pwd, 'package.json'), 'utf-8'));
319
+ const manifestJson = JSON.parse(readFileSync(resolve(pwd, 'manifest.json'), 'utf-8'));
320
+
321
+ const appKey = manifestJson.application.key;
322
+ const version = packageJson.version;
323
+ const appIdentifier = `${appKey.replace(/\./g, '_')}-${version}`;
324
+ const appIifeName = toLegalIdentifier(appIdentifier);
325
+
326
+ const globalDependencies = Object.keys(CORVA_GLOBAL_DEPENDENCIES);
327
+ const externalPatterns = globalDependencies.map(
328
+ libName => new RegExp(`(^${libName}$|^${libName}/)([^]+)?`),
329
+ );
330
+
331
+ const isExternal = source =>
332
+ !SKIP_IN_EXTERNALS.some(skipped => source.startsWith(skipped)) &&
333
+ externalPatterns.some(pattern => pattern.test(source));
334
+
335
+ // Package names are not valid JS identifiers, so every global is read off `this`.
336
+ // The name has to match on a package boundary, as isExternal does: matching on a
337
+ // bare prefix maps react-dom/client onto react and emits `this["react"]-dom.client`.
338
+ const toGlobal = id => {
339
+ const dep = globalDependencies.find(libName => id === libName || id.startsWith(`${libName}/`));
340
+
341
+ if (!dep) {
342
+ throw new Error(`Unknown dependency: ${id}`);
343
+ }
344
+
345
+ if (dep === id) {
346
+ return `this["${dep}"].default || this["${dep}"]`;
347
+ }
348
+
349
+ return `this["${dep}"]${id
350
+ .slice(dep.length)
351
+ .split('/')
352
+ .filter(Boolean)
353
+ .map(segment => `.${segment}`)
354
+ .join('')}`;
355
+ };
356
+
357
+ const getAlias = () => {
358
+ // jest resolved '~' to src for both templates; keep it in the build too
359
+ const defaultAlias = { '~': resolve(pwd, 'src') };
360
+ const tsconfigPath = resolve(pwd, 'tsconfig.json');
361
+
362
+ if (!existsSync(tsconfigPath)) {
363
+ return defaultAlias;
364
+ }
365
+
366
+ // tsconfig.json is JSONC, which JSON.parse rejects; a comment in it must not
367
+ // take the build down
368
+ let compilerOptions;
369
+
370
+ try {
371
+ ({ compilerOptions } = JSON.parse(readFileSync(tsconfigPath, 'utf-8')));
372
+ } catch {
373
+ console.warn(`Could not read compilerOptions.paths from ${tsconfigPath}; only '~' is aliased.`);
374
+
375
+ return defaultAlias;
376
+ }
377
+
378
+ return {
379
+ ...defaultAlias,
380
+ ...Object.fromEntries(
381
+ Object.entries(compilerOptions?.paths || {}).map(([key, [value]]) => [
382
+ key.replace('/*', ''),
383
+ resolve(pwd, value.replace('/*', '')),
384
+ ]),
385
+ ),
386
+ };
387
+ };
388
+
389
+ const resolveConfig = ({ command }) => {
390
+ validateDependencies({ packageJson });
391
+
392
+ const config = {
393
+ root: pwd,
394
+ server: {
395
+ host: process.env.HOST || 'app.local.corva.ai',
396
+ port: Number(process.env.PORT) || 8080,
397
+ },
398
+ css: {
399
+ modules: { localsConvention: 'camelCase' },
400
+ // webpack's postcss-loader ran preset-env with no options, i.e. stage 2; a narrower
401
+ // stage silently stops transpiling CSS features the app already relies on
402
+ postcss: { plugins: [presetEnv()] },
403
+ },
404
+ plugins: [react(), dotEnvEndpointPlugin(pwd), rejectUnscopedCssPlugin(pwd)],
405
+ // webpack ran with `resolve.symlinks: false`; a yarn-linked @corva/ui has to keep
406
+ // resolving through the link, not through its real path
407
+ resolve: { alias: getAlias(), preserveSymlinks: true },
408
+ test: {
409
+ watch: false,
410
+ globals: true,
411
+ // jest ran with resetMocks: true; vitest defaults it to false
412
+ mockReset: true,
413
+ environment: 'jsdom',
414
+ setupFiles: ['./vitest.setup.js'],
415
+ globalSetup: ['./vitest.global-setup.js'],
416
+ coverage: {
417
+ provider: 'v8',
418
+ include: ['src/**/*.{js,jsx,ts,tsx}'],
419
+ thresholds: { branches: 60, functions: 60, lines: 60, statements: 60 },
420
+ },
421
+ },
422
+ };
423
+
424
+ if (command === 'serve') {
425
+ return {
426
+ ...config,
427
+ define: {
428
+ global: {},
429
+ // Webpack's EnvironmentPlugin exposed the whole environment, so apps read
430
+ // their own REACT_APP_* vars straight off process.env.
431
+ 'process.env': {
432
+ ...process.env,
433
+ DEV_CENTER_APP_PATH: '',
434
+ NODE_ENV: 'development',
435
+ REACT_APP_ENVIRONMENT: process.env.REACT_APP_ENVIRONMENT || 'development',
436
+ ...getCorvaApiEnvVariables(),
437
+ },
438
+ },
439
+ plugins: [
440
+ ...config.plugins,
441
+ ...(packageJson.dependencies?.['mapbox-gl'] ? [mapboxDevSetupPlugin()] : []),
442
+ viteStaticCopy({
443
+ watch: { reloadPageOnChange: false },
444
+ targets: [
445
+ { src: `${PLATFORM_SHARED_DIR}/assets/*`, dest: 'static/media' },
446
+ {
447
+ src: `${join(PLATFORM_SHARED_DIR, '../ui/assets')}/*`,
448
+ dest: 'static/media',
449
+ },
450
+ ],
451
+ }),
452
+ ],
453
+ };
454
+ }
455
+
456
+ return {
457
+ ...config,
458
+ define: {
459
+ // webpack's prod config ran `EnvironmentPlugin(process.env)`: one textual replacement
460
+ // per key, so only the keys the source actually reads reach the bundle. Without it an
461
+ // app reading e.g. process.env.CORVA_API_ENV silently compiles to undefined.
462
+ ...Object.fromEntries(
463
+ Object.entries(process.env)
464
+ .filter(([key]) => /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key))
465
+ .map(([key, value]) => [`process.env.${key}`, JSON.stringify(value)]),
466
+ ),
467
+ 'process.env': { NODE_ENV: 'production' },
468
+ 'process.env.NODE_ENV': '"production"',
469
+ },
470
+ plugins: [
471
+ ...config.plugins,
472
+ emitAssetsAsFilesPlugin(),
473
+ viteStaticCopy({ targets: [{ src: 'manifest.json', dest: '.' }] }),
474
+ {
475
+ name: 'corva-single-file-bundle',
476
+ writeBundle(_options, bundle) {
477
+ finalizeJsBundle({
478
+ outDir: resolve(pwd, 'dist'),
479
+ cssFileNames: Object.keys(bundle).filter(fileName => fileName.endsWith('.css')),
480
+ appIdentifier,
481
+ emittedGlobalName: appIifeName,
482
+ });
483
+ },
484
+ },
485
+ ],
486
+ // an asset URL has to be resolved at runtime, from the bundle's own location: the same
487
+ // app.js is served from a different package directory for every version
488
+ experimental: {
489
+ renderBuiltUrl: (filename, { hostType }) =>
490
+ (hostType === 'css'
491
+ ? `${ASSET_MARKER}${filename}`
492
+ : { runtime: `__CORVA_ASSET_BASE__+${JSON.stringify(filename)}` }),
493
+ },
494
+ build: {
495
+ outDir: 'dist',
496
+ emptyOutDir: true,
497
+ copyPublicDir: false,
498
+ sourcemap: false,
499
+ cssCodeSplit: false,
500
+ minify: 'terser',
501
+ terserOptions: {
502
+ format: { comments: false },
503
+ },
504
+ // lib mode plus named exports keeps the IIFE compatible with the platform
505
+ // loader, which reads `this["appKey-version"].default.component`.
506
+ lib: {
507
+ entry: resolve(pwd, 'src/index.js'),
508
+ name: appIifeName,
509
+ fileName: () => 'app.js',
510
+ formats: ['iife'],
511
+ },
512
+ rolldownOptions: {
513
+ external: isExternal,
514
+ moduleTypes: { '.cjs': 'js' },
515
+ output: {
516
+ format: 'iife',
517
+ name: appIifeName,
518
+ entryFileNames: 'app.js',
519
+ // flat next to app.js, the way webpack's default assetModuleFilename emitted them
520
+ assetFileNames: '[hash][extname]',
521
+ globals: toGlobal,
522
+ exports: 'named',
523
+ // Rolldown doesn't rewrite `require()` inside bundled CJS modules (e.g.
524
+ // react-is) to their external global references. This shim resolves them
525
+ // to the globals the IIFE receives as params.
526
+ intro:
527
+ `var require=function(id){var g=globalThis[id];return g&&g.default||g;};` +
528
+ `var __CORVA_ASSET_BASE__=${ASSET_BASE_EXPR};`,
529
+ },
530
+ plugins: [
531
+ {
532
+ // Rolldown rejects `typeof require` as a define key (not an identifier),
533
+ // so the CJS-detection branches are stripped by transform instead.
534
+ name: 'replace-typeof-require',
535
+ transform(code) {
536
+ if (!code.includes('typeof require')) return null;
537
+
538
+ return { code: code.replaceAll('typeof require', '"undefined"'), map: null };
539
+ },
540
+ },
541
+ license({
542
+ thirdParty: { output: resolve(pwd, 'dist/app.js.LICENSE.txt') },
543
+ }),
544
+ ],
545
+ },
546
+ },
547
+ };
548
+ };
549
+
550
+ return defineConfig(env => mergeConfig(resolveConfig(env), overrides));
551
+ };
@@ -0,0 +1 @@
1
+ import '@corva/dc-platform-shared/cjs/devcenter/webpack/DevServerEntryPoint.js';
@@ -0,0 +1,17 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <link rel="icon" href="/favicon.ico" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
7
+ <meta name="theme-color" content="#000000" />
8
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500" />
9
+ <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons" />
10
+ <title>Corva React App</title>
11
+ </head>
12
+ <body data-corva-theme-dark>
13
+ <noscript>You need to enable JavaScript to run this app.</noscript>
14
+ <div id="root"></div>
15
+ <script type="module" src="/devApp.js"></script>
16
+ </body>
17
+ </html>
@@ -0,0 +1 @@
1
+ import '@corva/dc-platform-shared/cjs/devcenter/webpack/DevServerIsolatedAppEntryPoint.js';
@@ -0,0 +1,26 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <link rel="icon" href="/favicon.ico" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
7
+ <meta name="theme-color" content="#000000" />
8
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500" />
9
+ <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons" />
10
+ <title>Corva React App</title>
11
+ </head>
12
+ <body data-corva-theme-dark>
13
+ <noscript>You need to enable JavaScript to run this app.</noscript>
14
+ <div id="root"></div>
15
+ <!-- Vite only injects the react-refresh preamble into index.html, so this page installs it itself. -->
16
+ <script type="module">
17
+ import RefreshRuntime from '/@react-refresh';
18
+
19
+ RefreshRuntime.injectIntoGlobalHook(window);
20
+ window.$RefreshReg$ = () => {};
21
+ window.$RefreshSig$ = () => type => type;
22
+ window.__vite_plugin_react_preamble_installed__ = true;
23
+ </script>
24
+ <script type="module" src="/isolatedApp.js"></script>
25
+ </body>
26
+ </html>
Binary file
@@ -0,0 +1,4 @@
1
+ import { createViteConfig } from './config/vite/corva-vite-config.mjs';
2
+
3
+ // NOTE: app-specific Vite plugins and options can be provided as `overrides`
4
+ export default createViteConfig({ pwd: import.meta.dirname, overrides: {} });
@@ -0,0 +1,4 @@
1
+ export const setup = () => {
2
+ // UTC keeps tests independent of the machine's timezone
3
+ process.env.TZ = 'UTC';
4
+ };
@@ -0,0 +1,26 @@
1
+ import { vi } from 'vitest';
2
+
3
+ import '@testing-library/jest-dom/vitest';
4
+
5
+ class FakeObserver {
6
+ observe() {}
7
+
8
+ unobserve() {}
9
+
10
+ disconnect() {}
11
+ }
12
+
13
+ vi.stubGlobal('ResizeObserver', FakeObserver);
14
+ vi.stubGlobal('MutationObserver', FakeObserver);
15
+
16
+ // jsdom 22 cannot parse the container queries @corva/ui's components inject, and
17
+ // reports that straight to stderr on every import. Drop those lines only.
18
+ const writeToStderr = process.stderr.write.bind(process.stderr);
19
+
20
+ process.stderr.write = (chunk, ...rest) => {
21
+ if (String(chunk).includes('Could not parse CSS stylesheet')) {
22
+ return true;
23
+ }
24
+
25
+ return writeToStderr(chunk, ...rest);
26
+ };
@@ -205,6 +205,10 @@ async function addTemplate(root, manifest, runtime, opts) {
205
205
 
206
206
  copyFolderRecursiveSync(templateFolder, root, excludeFiles);
207
207
 
208
+ if (manifest.isUi()) {
209
+ copyFolderRecursiveSync(join(cliRoot, 'common', 'ui'), root);
210
+ }
211
+
208
212
  if (manifest.isNode()) {
209
213
  copyFolderRecursiveSync(join(cliRoot, 'common', 'node'), root);
210
214
  }