@corva/create-app 0.124.0 → 0.126.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.
@@ -1,4 +1,4 @@
1
- import { createViteConfig } from './config/vite/corva-vite-config.mjs';
1
+ import { createViteConfig } from '@corva/dc-platform-shared/vite/corva-vite-config.mjs';
2
2
 
3
3
  // NOTE: app-specific Vite plugins and options can be provided as `overrides`
4
4
  export default createViteConfig({ pwd: import.meta.dirname, overrides: {} });
@@ -28,19 +28,9 @@ const jsUiDevDependencies = {
28
28
  '@testing-library/jest-dom': '7.0.1',
29
29
  '@testing-library/react': '16.3.2',
30
30
  '@testing-library/user-event': '14.6.6',
31
- '@vitejs/plugin-react': '6.1.1',
32
- '@vitest/coverage-v8': '4.1.11',
33
- 'dotenv': '16.4.7',
34
- 'dotenv-expand': '11.0.7',
35
31
  'eslint': '7.32.0',
36
- 'jsdom': '22.1.0',
37
- 'postcss-preset-env': '11.3.0',
38
- 'rollup-plugin-license': '3.6.0',
39
32
  'sass-embedded': '1.100.0',
40
33
  'terser': '5.47.1',
41
- 'vite': '8.3.0',
42
- 'vite-plugin-static-copy': '4.1.0',
43
- 'vitest': '4.1.11',
44
34
  };
45
35
 
46
36
  const tsUiDevDependencies = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@corva/create-app",
3
- "version": "0.124.0",
3
+ "version": "0.126.0",
4
4
  "private": false,
5
5
  "description": "Create an app to use it in CORVA.AI",
6
6
  "keywords": [
@@ -20,4 +20,5 @@ jobs:
20
20
  prod-api-key: ${{ secrets.API_KEY }}
21
21
  npm-token: ${{ secrets.CORVA_NPM_TOKEN }}
22
22
  jira-user-email: ${{ secrets.JIRA_AUTOTEST_USERNAME }}
23
+ linear-api-key: ${{ secrets.LINEAR_API_KEY }}
23
24
  jira-api-token: ${{ secrets.JIRA_AUTOTEST_API_TOKEN }}
@@ -1,410 +0,0 @@
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, unlinkSync, writeFileSync } from 'node:fs';
6
- import { join, 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
- const createCssInjection = cssContent =>
104
- `(function(){var s=document.createElement('style');s.textContent=${JSON.stringify(
105
- cssContent,
106
- )};document.head.appendChild(s)})();`;
107
-
108
- // The platform loads a single file, so the emitted stylesheets are folded back into it.
109
- const finalizeJsBundle = ({ outDir, cssFileNames, appIdentifier, emittedGlobalName }) => {
110
- const jsPath = join(outDir, 'app.js');
111
- const css = cssFileNames
112
- .map(fileName => readFileSync(join(outDir, fileName), 'utf-8'))
113
- .join('');
114
- // normalise first: the fallbacks read the head of the bundle, so a stylesheet
115
- // sitting in front of it would take the global assignment for itself
116
- const jsContent = normalizeIifeGlobalAssignment(
117
- readFileSync(jsPath, 'utf-8'),
118
- appIdentifier,
119
- emittedGlobalName,
120
- );
121
-
122
- writeFileSync(jsPath, css ? createCssInjection(css) + jsContent : jsContent);
123
-
124
- cssFileNames.forEach(fileName => unlinkSync(join(outDir, fileName)));
125
- };
126
-
127
- const getCorvaApiEnvVariables = () => {
128
- const apiEnv = process.env.CORVA_API_ENV || CORVA_API_ENVS.production;
129
-
130
- if (Object.hasOwn(CORVA_API_ENVS, apiEnv)) {
131
- return CORVA_API_ENV_VARIABLES[apiEnv];
132
- }
133
-
134
- console.error(`CORVA_API_ENV should be one of [${Object.keys(CORVA_API_ENVS).join(', ')}]`);
135
-
136
- return process.exit(1);
137
- };
138
-
139
- // webpack's dev config added this entry point whenever the app depends on
140
- // mapbox-gl: it reads the access token off @corva/ui and publishes the
141
- // configured instance as window['mapbox-gl'].
142
- // A virtual module rather than a direct node_modules URL: mapbox-gl's dist is
143
- // CJS, so it only imports cleanly once Vite's resolver and dep optimizer see it.
144
- const MAPBOX_DEV_SETUP_ID = 'virtual:corva-mapbox-dev-setup';
145
-
146
- const mapboxDevSetupPlugin = () => ({
147
- name: 'corva-mapbox-dev-setup',
148
- resolveId(id) {
149
- return id === MAPBOX_DEV_SETUP_ID ? `\0${MAPBOX_DEV_SETUP_ID}` : null;
150
- },
151
- load(id) {
152
- return id === `\0${MAPBOX_DEV_SETUP_ID}`
153
- ? "import '@corva/dc-platform-shared/cjs/devcenter/webpack/MapboxGlDevSetupEntryPoint.js';"
154
- : null;
155
- },
156
- transformIndexHtml: () => [
157
- {
158
- tag: 'script',
159
- attrs: { type: 'module', src: `/@id/__x00__${MAPBOX_DEV_SETUP_ID}` },
160
- injectTo: 'body',
161
- },
162
- ],
163
- });
164
-
165
- // The DevCenter dev page edits the app's .env through this endpoint.
166
- const dotEnvEndpointPlugin = appDirectory => ({
167
- name: 'corva-dotenv-endpoint',
168
- configureServer(server) {
169
- server.middlewares.use('/dotenv', (req, res, next) => {
170
- if (req.method !== 'PATCH') {
171
- return next();
172
- }
173
-
174
- const envPath = resolve(appDirectory, '.env');
175
- const chunks = [];
176
-
177
- req.on('data', chunk => chunks.push(chunk));
178
- req.on('end', () => {
179
- let update;
180
-
181
- // webpack's dev server parsed the body with express.json(), which answers
182
- // 400 on a malformed one; parsing it here would take the server down
183
- try {
184
- update = JSON.parse(Buffer.concat(chunks));
185
- } catch {
186
- res.statusCode = 400;
187
- res.end();
188
-
189
- return;
190
- }
191
-
192
- const currentEnv = existsSync(envPath) ? readFileSync(envPath, 'utf-8') : '';
193
-
194
- writeFileSync(envPath, updateDotEnv(currentEnv, update));
195
- res.statusCode = 200;
196
- res.end();
197
- });
198
- });
199
- },
200
- });
201
-
202
- export const createViteConfig = ({ pwd, overrides = {} }) => {
203
- expand(dotenv.config({ path: resolve(pwd, '.env'), quiet: true }));
204
-
205
- const packageJson = JSON.parse(readFileSync(resolve(pwd, 'package.json'), 'utf-8'));
206
- const manifestJson = JSON.parse(readFileSync(resolve(pwd, 'manifest.json'), 'utf-8'));
207
-
208
- const appKey = manifestJson.application.key;
209
- const version = packageJson.version;
210
- const appIdentifier = `${appKey.replace(/\./g, '_')}-${version}`;
211
- const appIifeName = toLegalIdentifier(appIdentifier);
212
-
213
- const globalDependencies = Object.keys(CORVA_GLOBAL_DEPENDENCIES);
214
- const externalPatterns = globalDependencies.map(
215
- libName => new RegExp(`(^${libName}$|^${libName}/)([^]+)?`),
216
- );
217
-
218
- const isExternal = source =>
219
- !SKIP_IN_EXTERNALS.some(skipped => source.startsWith(skipped)) &&
220
- externalPatterns.some(pattern => pattern.test(source));
221
-
222
- // Package names are not valid JS identifiers, so every global is read off `this`.
223
- // The name has to match on a package boundary, as isExternal does: matching on a
224
- // bare prefix maps react-dom/client onto react and emits `this["react"]-dom.client`.
225
- const toGlobal = id => {
226
- const dep = globalDependencies.find(libName => id === libName || id.startsWith(`${libName}/`));
227
-
228
- if (!dep) {
229
- throw new Error(`Unknown dependency: ${id}`);
230
- }
231
-
232
- if (dep === id) {
233
- return `this["${dep}"].default || this["${dep}"]`;
234
- }
235
-
236
- return `this["${dep}"]${id.slice(dep.length).replace(/\//g, '.')}`;
237
- };
238
-
239
- const getAlias = () => {
240
- // jest resolved '~' to src for both templates; keep it in the build too
241
- const defaultAlias = { '~': resolve(pwd, 'src') };
242
- const tsconfigPath = resolve(pwd, 'tsconfig.json');
243
-
244
- if (!existsSync(tsconfigPath)) {
245
- return defaultAlias;
246
- }
247
-
248
- // tsconfig.json is JSONC, which JSON.parse rejects; a comment in it must not
249
- // take the build down
250
- let compilerOptions;
251
-
252
- try {
253
- ({ compilerOptions } = JSON.parse(readFileSync(tsconfigPath, 'utf-8')));
254
- } catch {
255
- console.warn(`Could not read compilerOptions.paths from ${tsconfigPath}; only '~' is aliased.`);
256
-
257
- return defaultAlias;
258
- }
259
-
260
- return {
261
- ...defaultAlias,
262
- ...Object.fromEntries(
263
- Object.entries(compilerOptions?.paths || {}).map(([key, [value]]) => [
264
- key.replace('/*', ''),
265
- resolve(pwd, value.replace('/*', '')),
266
- ]),
267
- ),
268
- };
269
- };
270
-
271
- const resolveConfig = ({ command }) => {
272
- validateDependencies({ packageJson });
273
-
274
- const config = {
275
- root: pwd,
276
- server: {
277
- host: process.env.HOST || 'app.local.corva.ai',
278
- port: Number(process.env.PORT) || 8080,
279
- },
280
- css: {
281
- modules: { localsConvention: 'camelCase' },
282
- postcss: { plugins: [presetEnv({ stage: 3 })] },
283
- },
284
- plugins: [react(), dotEnvEndpointPlugin(pwd)],
285
- resolve: { alias: getAlias() },
286
- test: {
287
- watch: false,
288
- globals: true,
289
- // jest ran with resetMocks: true; vitest defaults it to false
290
- mockReset: true,
291
- environment: 'jsdom',
292
- setupFiles: ['./vitest.setup.js'],
293
- globalSetup: ['./vitest.global-setup.js'],
294
- coverage: {
295
- provider: 'v8',
296
- include: ['src/**/*.{js,jsx,ts,tsx}'],
297
- thresholds: { branches: 60, functions: 60, lines: 60, statements: 60 },
298
- },
299
- },
300
- };
301
-
302
- if (command === 'serve') {
303
- return {
304
- ...config,
305
- define: {
306
- global: {},
307
- // Webpack's EnvironmentPlugin exposed the whole environment, so apps read
308
- // their own REACT_APP_* vars straight off process.env.
309
- 'process.env': {
310
- ...process.env,
311
- DEV_CENTER_APP_PATH: '',
312
- NODE_ENV: 'development',
313
- REACT_APP_ENVIRONMENT: process.env.REACT_APP_ENVIRONMENT || 'development',
314
- ...getCorvaApiEnvVariables(),
315
- },
316
- },
317
- plugins: [
318
- ...config.plugins,
319
- ...(packageJson.dependencies?.['mapbox-gl'] ? [mapboxDevSetupPlugin()] : []),
320
- viteStaticCopy({
321
- watch: { reloadPageOnChange: false },
322
- targets: [
323
- { src: `${PLATFORM_SHARED_DIR}/assets/*`, dest: 'static/media' },
324
- {
325
- src: `${join(PLATFORM_SHARED_DIR, '../ui/assets')}/*`,
326
- dest: 'static/media',
327
- },
328
- ],
329
- }),
330
- ],
331
- };
332
- }
333
-
334
- return {
335
- ...config,
336
- define: {
337
- 'process.env': { NODE_ENV: 'production' },
338
- 'process.env.NODE_ENV': '"production"',
339
- },
340
- plugins: [
341
- ...config.plugins,
342
- viteStaticCopy({ targets: [{ src: 'manifest.json', dest: '.' }] }),
343
- {
344
- name: 'corva-single-file-bundle',
345
- writeBundle(_options, bundle) {
346
- finalizeJsBundle({
347
- outDir: resolve(pwd, 'dist'),
348
- cssFileNames: Object.keys(bundle).filter(fileName => fileName.endsWith('.css')),
349
- appIdentifier,
350
- emittedGlobalName: appIifeName,
351
- });
352
- },
353
- },
354
- ],
355
- build: {
356
- outDir: 'dist',
357
- emptyOutDir: true,
358
- copyPublicDir: false,
359
- sourcemap: false,
360
- cssCodeSplit: false,
361
- minify: 'terser',
362
- terserOptions: {
363
- compress: { drop_console: true, drop_debugger: true },
364
- format: { comments: false },
365
- },
366
- // lib mode plus named exports keeps the IIFE compatible with the platform
367
- // loader, which reads `this["appKey-version"].default.component`.
368
- lib: {
369
- entry: resolve(pwd, 'src/index.js'),
370
- name: appIifeName,
371
- fileName: () => 'app.js',
372
- formats: ['iife'],
373
- },
374
- rolldownOptions: {
375
- external: isExternal,
376
- moduleTypes: { '.cjs': 'js' },
377
- output: {
378
- format: 'iife',
379
- name: appIifeName,
380
- entryFileNames: 'app.js',
381
- globals: toGlobal,
382
- exports: 'named',
383
- // Rolldown doesn't rewrite `require()` inside bundled CJS modules (e.g.
384
- // react-is) to their external global references. This shim resolves them
385
- // to the globals the IIFE receives as params.
386
- intro:
387
- 'var require=function(id){var g=globalThis[id];return g&&g.default||g;};',
388
- },
389
- plugins: [
390
- {
391
- // Rolldown rejects `typeof require` as a define key (not an identifier),
392
- // so the CJS-detection branches are stripped by transform instead.
393
- name: 'replace-typeof-require',
394
- transform(code) {
395
- if (!code.includes('typeof require')) return null;
396
-
397
- return { code: code.replaceAll('typeof require', '"undefined"'), map: null };
398
- },
399
- },
400
- license({
401
- thirdParty: { output: resolve(pwd, 'dist/app.js.LICENSE.txt') },
402
- }),
403
- ],
404
- },
405
- },
406
- };
407
- };
408
-
409
- return defineConfig(env => mergeConfig(resolveConfig(env), overrides));
410
- };