@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.
- package/README.md +9 -1
- package/common/ui/config/vite/corva-vite-config.mjs +551 -0
- package/common/ui/devApp.js +1 -0
- package/common/ui/index.html +17 -0
- package/common/ui/isolatedApp.js +1 -0
- package/common/ui/public/DevCenterIsolatedAppPage.html +26 -0
- package/common/ui/public/favicon.ico +0 -0
- package/common/ui/vite.config.mjs +4 -0
- package/common/ui/vitest.global-setup.js +4 -0
- package/common/ui/vitest.setup.js +26 -0
- package/lib/commands/create.js +4 -0
- package/lib/constants/package.js +26 -57
- package/lib/flows/steps/zip-file-list-resolve.js +9 -0
- package/lib/helpers/utils.js +2 -2
- package/package.json +1 -1
- package/template_extensions/corva/.eslintrc +4 -0
- package/template_extensions/corva/.github/workflows/feat-fix.yml +1 -0
- package/templates/ui/javascript/.eslintrc +7 -2
- package/templates/ui/javascript/AGENTS.md +13 -11
- package/templates/ui/javascript/src/{App.completion.js → App.completion.jsx} +3 -3
- package/templates/ui/javascript/src/{App.drilling.js → App.drilling.jsx} +3 -3
- package/templates/ui/javascript/src/{AppSettings.js → AppSettings.jsx} +1 -1
- package/templates/ui/javascript/src/__tests__/{AppSettings.test.js → AppSettings.test.jsx} +1 -1
- package/templates/ui/typescript/.eslintrc +4 -0
- package/templates/ui/typescript/AGENTS.md +11 -8
- package/templates/ui/typescript/src/App.completion.tsx +1 -1
- package/templates/ui/typescript/src/App.drilling.tsx +1 -1
- package/templates/ui/typescript/src/__tests__/AppSettings.test.tsx +1 -1
- package/templates/ui/typescript/tsconfig.json +17 -4
- package/templates/ui/javascript/config/jest/babelTransform.js +0 -16
- package/templates/ui/javascript/config/jest/cssTransform.js +0 -16
- package/templates/ui/javascript/config/jest/fileTransform.js +0 -48
- package/templates/ui/javascript/config/jest/globalSetup.js +0 -5
- package/templates/ui/javascript/config/jest/setupTests.js +0 -33
- package/templates/ui/javascript/config-overrides.js +0 -10
- package/templates/ui/typescript/config/jest/babelTransform.js +0 -16
- package/templates/ui/typescript/config/jest/cssTransform.js +0 -16
- package/templates/ui/typescript/config/jest/fileTransform.js +0 -48
- package/templates/ui/typescript/config/jest/globalSetup.js +0 -5
- package/templates/ui/typescript/config/jest/setupTests.js +0 -33
- package/templates/ui/typescript/config-overrides.js +0 -10
- /package/templates/ui/javascript/src/{App.scss → App.module.scss} +0 -0
- /package/templates/ui/javascript/src/__tests__/{App.test.js → App.test.jsx} +0 -0
- /package/templates/ui/javascript/src/__tests__/{TestsExample.test.js → TestsExample.test.jsx} +0 -0
- /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,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
|
+
};
|
package/lib/commands/create.js
CHANGED
|
@@ -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
|
}
|