@data-fair/dev-server 2.6.4 → 2.7.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/package.json +1 -1
- package/src/app.js +73 -11
- package/src/dev-env.js +3 -2
- package/src/enrich.d.ts +8 -2
- package/src/enrich.js +109 -37
- package/src/env-file.d.ts +1 -0
- package/src/env-file.js +9 -0
- package/ui/dist/assets/{index-D8biKNGI.js → index-BvfgViF2.js} +25 -25
- package/ui/dist/index.html +4 -1
package/package.json
CHANGED
package/src/app.js
CHANGED
|
@@ -10,7 +10,7 @@ import { WebSocket, WebSocketServer } from 'ws';
|
|
|
10
10
|
import { createServer } from 'node:http';
|
|
11
11
|
import express from 'express';
|
|
12
12
|
import cors from 'cors';
|
|
13
|
-
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
13
|
+
import { existsSync, readFileSync, writeFileSync, watchFile } from 'node:fs';
|
|
14
14
|
import debugModule from 'debug';
|
|
15
15
|
import { createProxyMiddleware, fixRequestBody } from 'http-proxy-middleware';
|
|
16
16
|
import * as parse5 from 'parse5';
|
|
@@ -111,22 +111,46 @@ app.get('/config', (req, res, next) => {
|
|
|
111
111
|
debug('read dev config', devConfig);
|
|
112
112
|
res.send(devConfig);
|
|
113
113
|
});
|
|
114
|
-
// the config with datasets enriched from the remote data-fair,
|
|
115
|
-
//
|
|
116
|
-
//
|
|
114
|
+
// the config with datasets enriched from the remote data-fair, as the application receives it in
|
|
115
|
+
// window.APPLICATION ; ?select= adds dataset properties for a dev-server tool that needs them
|
|
116
|
+
// whatever the application selects (the filter tester and its schema fields)
|
|
117
117
|
app.get('/config/enriched', async (req, res) => {
|
|
118
118
|
try {
|
|
119
|
-
|
|
119
|
+
const extraSelect = typeof req.query.select === 'string' ? req.query.select.split(',') : undefined;
|
|
120
|
+
res.send(await prepareConfig(readDevConfig(), extraSelect));
|
|
120
121
|
}
|
|
121
122
|
catch (err) {
|
|
122
123
|
res.status(500).send({ error: err.message });
|
|
123
124
|
}
|
|
124
125
|
});
|
|
126
|
+
// what the UI last wrote, to tell its own saves from a hand edit of the file
|
|
127
|
+
let lastWrittenConfig;
|
|
125
128
|
app.put('/config', (req, res, next) => {
|
|
126
129
|
debug('save dev config', req.body);
|
|
127
|
-
|
|
130
|
+
lastWrittenConfig = JSON.stringify(req.body, null, 2);
|
|
131
|
+
writeFileSync('.dev-config.json', lastWrittenConfig);
|
|
128
132
|
res.send(req.body);
|
|
129
133
|
});
|
|
134
|
+
// A hand edit of .dev-config.json (an editor, an agent) reloads the UI, otherwise the form state
|
|
135
|
+
// would overwrite it on its next save. Polled rather than fs.watch'ed: editors that save through
|
|
136
|
+
// a rename would detach a watcher. A file that does not parse yet is a save in progress, skipped.
|
|
137
|
+
watchFile('.dev-config.json', { interval: 500 }, () => {
|
|
138
|
+
if (!existsSync('.dev-config.json'))
|
|
139
|
+
return;
|
|
140
|
+
const content = readFileSync('.dev-config.json', 'utf8');
|
|
141
|
+
if (content === lastWrittenConfig)
|
|
142
|
+
return;
|
|
143
|
+
try {
|
|
144
|
+
JSON.parse(content);
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
lastWrittenConfig = content;
|
|
150
|
+
debug('.dev-config.json edited outside of the UI');
|
|
151
|
+
for (const ws of devServerWS)
|
|
152
|
+
ws.send(JSON.stringify({ type: 'config-file-changed' }));
|
|
153
|
+
});
|
|
130
154
|
app.post('/config/error', (req, res) => {
|
|
131
155
|
console.log('Application sent an error', req.body);
|
|
132
156
|
for (const ws of devServerWS) {
|
|
@@ -186,8 +210,16 @@ const localAppInfo = async () => {
|
|
|
186
210
|
};
|
|
187
211
|
// Enrich the datasets from the remote data-fair and rewrite every remote origin to ours
|
|
188
212
|
// (pure helpers in enrich.ts, wired here to our remote api and local origins).
|
|
189
|
-
const prepareConfig = (configuration) => prepareRemoteConfig(configuration, {
|
|
213
|
+
const prepareConfig = (configuration, extraSelect) => prepareRemoteConfig(configuration, {
|
|
190
214
|
fetchJson: remoteFetch,
|
|
215
|
+
// the local file, as data-fair reads the one of the base application to deduce its selects
|
|
216
|
+
fetchConfigSchema: async () => {
|
|
217
|
+
const res = await fetch(appUrl.origin + appPrefix + '/config-schema.json');
|
|
218
|
+
if (!res.ok)
|
|
219
|
+
throw new Error('config-schema.json ' + res.status);
|
|
220
|
+
return res.json();
|
|
221
|
+
},
|
|
222
|
+
extraSelect,
|
|
191
223
|
localize: localizeConfig,
|
|
192
224
|
remoteOrigin: new URL(config.dataFair.url).origin,
|
|
193
225
|
localOrigin: `http://localhost:${config.port}`
|
|
@@ -318,6 +350,22 @@ const captureSimulationScript = (opts) => {
|
|
|
318
350
|
})()
|
|
319
351
|
`;
|
|
320
352
|
};
|
|
353
|
+
// Count what the previewed app writes to the console as error / warning and report it to the
|
|
354
|
+
// dev-server UI, which shows the counts as chips nudging toward the DevTools console.
|
|
355
|
+
// Failed requests are counted from their HTTP status (what Chrome logs as "Failed to load
|
|
356
|
+
// resource", caught by the app or not), not from the resource's error event, which also fires
|
|
357
|
+
// on a decoding failure Chrome never logs (an image answered by Vite's SPA fallback). Kept on a
|
|
358
|
+
// single line so that the inline source map (one "AAAA" segment) covers all of it and its
|
|
359
|
+
// x_google_ignoreList hides the wrapper: DevTools then locates console.error / console.warn at
|
|
360
|
+
// the app's call site instead of here.
|
|
361
|
+
const CONSOLE_COUNTER_SOURCE_MAP = Buffer.from(JSON.stringify({
|
|
362
|
+
version: 3, sources: ['df-dev-server-console-counter.js'], mappings: 'AAAA', x_google_ignoreList: [0]
|
|
363
|
+
})).toString('base64');
|
|
364
|
+
const CONSOLE_COUNTER_SCRIPT = [
|
|
365
|
+
";(function () { if (window.parent === window) return; var post = function (level) { window.parent.postMessage({ type: 'df-dev-server:console', level: level }, '*') }; post('reset'); ['error', 'warn'].forEach(function (method) { var original = console[method]; console[method] = function () { original.apply(console, arguments); post(method === 'warn' ? 'warning' : 'error') } }); window.addEventListener('error', function (e) { if (!/^ResizeObserver loop/.test(e.message)) post('error') }); new PerformanceObserver(function (list) { list.getEntries().forEach(function (e) { if (e.responseStatus >= 400) post('error') }) }).observe({ type: 'resource', buffered: true }); window.addEventListener('unhandledrejection', function () { post('error') }) })()",
|
|
366
|
+
'//# sourceURL=df-dev-server-console-counter.js',
|
|
367
|
+
'//# sourceMappingURL=data:application/json;base64,' + CONSOLE_COUNTER_SOURCE_MAP
|
|
368
|
+
].join('\n');
|
|
321
369
|
// extract the major.minor part of a version, versions on remote base apps look like "1.3"
|
|
322
370
|
const minorVersion = (version) => version.split('.').slice(0, 2).join('.');
|
|
323
371
|
// list configurations of the app under development that exist on the remote data-fair
|
|
@@ -505,6 +553,14 @@ app.use('/app', createProxyMiddleware({
|
|
|
505
553
|
// reliable here as in production.
|
|
506
554
|
headNode.childNodes.unshift(script);
|
|
507
555
|
}
|
|
556
|
+
// right after <meta charset>, so that the byte offset of the charset (checked by the
|
|
557
|
+
// UI on this same proxied document) stays unaffected, and before the app's own scripts
|
|
558
|
+
// so that errors raised while it boots are counted too (vite's client script placed
|
|
559
|
+
// above is a module, it runs later anyway)
|
|
560
|
+
const consoleScript = createElement('script', { type: 'text/javascript' });
|
|
561
|
+
appendChild(consoleScript, createTextNode(CONSOLE_COUNTER_SCRIPT));
|
|
562
|
+
const charsetIndex = headNode.childNodes.findIndex(c => isElementNode(c) && c.tagName === 'meta' && c.attrs.some(a => a.name === 'charset'));
|
|
563
|
+
headNode.childNodes.splice(charsetIndex + 1, 0, consoleScript);
|
|
508
564
|
// companion script that lets the embedded app report its height / sync params
|
|
509
565
|
// to the parent <d-frame> ; injected for every mode so the UI mode toggle works
|
|
510
566
|
// without requiring a df:overflow meta (same variant & version as data-fair prod)
|
|
@@ -562,9 +618,12 @@ app.use('/data-fair', createProxyMiddleware({
|
|
|
562
618
|
proxyRes(proxyRes, req, res) {
|
|
563
619
|
// console.log('DF RES', proxyRes)
|
|
564
620
|
if (proxyRes.headers['content-type'] && proxyRes.headers['content-type'].startsWith('application/json')) {
|
|
565
|
-
|
|
566
|
-
|
|
621
|
+
// decode the whole body once: decoding chunk by chunk turns a multi-byte character
|
|
622
|
+
// split across two chunks into U+FFFD (« Repr��sentation »)
|
|
623
|
+
const chunks = [];
|
|
624
|
+
proxyRes.on('data', (data) => { chunks.push(data); });
|
|
567
625
|
proxyRes.on('end', () => {
|
|
626
|
+
const body = Buffer.concat(chunks).toString();
|
|
568
627
|
const output = body.replace(new RegExp(escapeStringRegexp(config.dataFair.url), 'g'), `http://localhost:${config.port}/data-fair`);
|
|
569
628
|
// proxyRes.headers['content-length'] = output.length
|
|
570
629
|
delete proxyRes.headers['content-length'];
|
|
@@ -596,9 +655,12 @@ app.use('/tileserver', createProxyMiddleware({
|
|
|
596
655
|
},
|
|
597
656
|
proxyRes(proxyRes, req, res) {
|
|
598
657
|
if (proxyRes.headers['content-type'] && proxyRes.headers['content-type'].startsWith('application/json')) {
|
|
599
|
-
|
|
600
|
-
|
|
658
|
+
// decode the whole body once: decoding chunk by chunk turns a multi-byte character
|
|
659
|
+
// split across two chunks into U+FFFD (« Repr��sentation »)
|
|
660
|
+
const chunks = [];
|
|
661
|
+
proxyRes.on('data', (data) => { chunks.push(data); });
|
|
601
662
|
proxyRes.on('end', () => {
|
|
663
|
+
const body = Buffer.concat(chunks).toString();
|
|
602
664
|
// make all references to the tileserver url point to the local proxy
|
|
603
665
|
const output = body.replace(new RegExp(escapeStringRegexp(tileserverUrl), 'g'), `http://localhost:${config.port}/tileserver`);
|
|
604
666
|
delete proxyRes.headers['content-length'];
|
package/src/dev-env.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { createServer } from 'node:net';
|
|
4
4
|
import { parseArgs } from 'node:util';
|
|
5
|
-
import { MIN_BASE, MAX_BASE, renderEnv, findBase, readEnvVar } from './env-file.js';
|
|
5
|
+
import { MIN_BASE, MAX_BASE, renderEnv, mergeEnv, findBase, readEnvVar } from './env-file.js';
|
|
6
6
|
const { values } = parseArgs({
|
|
7
7
|
options: {
|
|
8
8
|
force: { type: 'boolean', default: false },
|
|
@@ -35,5 +35,6 @@ const draw = () => MIN_BASE + Math.floor(Math.random() * (MAX_BASE - MIN_BASE +
|
|
|
35
35
|
// application served at the root back under /app/ along the way.
|
|
36
36
|
const appPath = values['app-path'] ?? storedAppPath ?? '/app/';
|
|
37
37
|
const base = await findBase(isFree, draw);
|
|
38
|
-
|
|
38
|
+
const generated = renderEnv(base, appPath);
|
|
39
|
+
writeFileSync('.env', existing === undefined ? generated : mergeEnv(existing, generated));
|
|
39
40
|
console.log(`.env généré — app ${base}, dev-server ${base + 1}, e2e ${base + 2}, chemin ${appPath || '/ (racine)'}`);
|
package/src/enrich.d.ts
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
|
-
export
|
|
2
|
-
|
|
1
|
+
export interface DatasetFilter {
|
|
2
|
+
select?: string[];
|
|
3
|
+
properties?: Record<string, any>;
|
|
4
|
+
}
|
|
5
|
+
export declare const datasetsFiltersFromSchema: (configSchema: any) => DatasetFilter[];
|
|
6
|
+
export declare const enrichDataset: (dataset: any, fetchJson: (path: string) => Promise<any>, filter?: DatasetFilter) => Promise<any>;
|
|
3
7
|
export interface PrepareConfigDeps {
|
|
4
8
|
fetchJson: (path: string) => Promise<any>;
|
|
9
|
+
fetchConfigSchema: () => Promise<any>;
|
|
10
|
+
extraSelect?: string[];
|
|
5
11
|
localize: (configuration: any, remoteOrigin: string, localOrigin: string) => any;
|
|
6
12
|
remoteOrigin: string;
|
|
7
13
|
localOrigin: string;
|
package/src/enrich.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// Rebuild the configuration dataset entries the same way data-fair does in production
|
|
2
2
|
// (refreshConfigDatasetsRefs in api/src/applications/utils.ts): an application stores only
|
|
3
|
-
// minimal dataset references, data-fair
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
3
|
+
// minimal dataset references, data-fair refreshes them at serving time with the keys already
|
|
4
|
+
// stored in the entry, plus `finalizedAt` and `slug`, plus the `select` of the dataset selector
|
|
5
|
+
// declared in the application's config schema (baseApp.datasetsFilters, deduced by initBaseApp in
|
|
6
|
+
// api/src/base-applications/service.ts). Nothing more: an application that reads a property its
|
|
7
|
+
// selector does not ask for works here only if it works in production too.
|
|
7
8
|
//
|
|
8
9
|
// Pure module, no imports: it takes its remote accessor and origin rewrite as dependencies,
|
|
9
10
|
// wired in app.ts — so it can be unit tested without a build (test/enrich.test.ts).
|
|
@@ -12,52 +13,123 @@
|
|
|
12
13
|
// key would otherwise be re-fetched, and re-warned about, on every single reload.
|
|
13
14
|
const datasetsCache = new Map();
|
|
14
15
|
const DATASETS_CACHE_TTL = 30_000;
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
16
|
+
// data-fair's resolveLocalRefs (api/src/misc/utils/json-refs.ts): "#/..." pointers replaced by
|
|
17
|
+
// their target, circular and unresolvable ones left as they are.
|
|
18
|
+
const resolveLocalRefs = (root) => {
|
|
19
|
+
const atPointer = (pointer) => pointer.split('/').slice(1)
|
|
20
|
+
.map(p => p.replace(/~1/g, '/').replace(/~0/g, '~'))
|
|
21
|
+
.reduce((o, k) => (o !== null && typeof o === 'object') ? o[k] : undefined, root);
|
|
22
|
+
const walk = (node, stack) => {
|
|
23
|
+
if (Array.isArray(node))
|
|
24
|
+
return node.map(n => walk(n, stack));
|
|
25
|
+
if (node === null || typeof node !== 'object')
|
|
26
|
+
return node;
|
|
27
|
+
if (typeof node.$ref === 'string' && node.$ref.startsWith('#')) {
|
|
28
|
+
if (stack.includes(node.$ref))
|
|
29
|
+
return node;
|
|
30
|
+
const target = atPointer(node.$ref);
|
|
31
|
+
if (target === undefined)
|
|
32
|
+
return node;
|
|
33
|
+
return walk(target, [...stack, node.$ref]);
|
|
34
|
+
}
|
|
35
|
+
const res = {};
|
|
36
|
+
for (const [k, v] of Object.entries(node))
|
|
37
|
+
res[k] = walk(v, stack);
|
|
38
|
+
return res;
|
|
39
|
+
};
|
|
40
|
+
return walk(root, []);
|
|
41
|
+
};
|
|
42
|
+
// data-fair's getFragmentFetchUrl (api/src/base-applications/operations.ts)
|
|
43
|
+
const fragmentFetchUrl = (fragment) => {
|
|
44
|
+
if (!fragment)
|
|
45
|
+
return null;
|
|
46
|
+
if (fragment['x-fromUrl'])
|
|
47
|
+
return fragment['x-fromUrl'];
|
|
48
|
+
const url = fragment.layout?.getItems?.url;
|
|
49
|
+
if (url)
|
|
50
|
+
return typeof url === 'string' ? url : url.expr;
|
|
51
|
+
return null;
|
|
52
|
+
};
|
|
53
|
+
// The datasetsFilters data-fair stores on the base application, one per dataset selector of the
|
|
54
|
+
// config schema, matched by position with configuration.datasets. Like data-fair, a schema that
|
|
55
|
+
// cannot be read gives no filter at all.
|
|
56
|
+
export const datasetsFiltersFromSchema = (configSchema) => {
|
|
57
|
+
try {
|
|
58
|
+
const schema = resolveLocalRefs(configSchema);
|
|
59
|
+
const definition = schema?.properties?.datasets ?? schema?.allOf?.[0]?.properties?.datasets;
|
|
60
|
+
if (!definition)
|
|
61
|
+
return [];
|
|
62
|
+
let fetches = [];
|
|
63
|
+
if (definition.items && fragmentFetchUrl(definition))
|
|
64
|
+
fetches = [{ fromUrl: fragmentFetchUrl(definition), properties: definition.items.properties }];
|
|
65
|
+
if (fragmentFetchUrl(definition.items))
|
|
66
|
+
fetches = [{ fromUrl: fragmentFetchUrl(definition.items), properties: definition.items.properties }];
|
|
67
|
+
if (Array.isArray(definition.items))
|
|
68
|
+
fetches = definition.items.filter((item) => fragmentFetchUrl(item)).map((item) => ({ fromUrl: fragmentFetchUrl(item), properties: item.properties }));
|
|
69
|
+
return fetches.map(({ fromUrl, properties }) => {
|
|
70
|
+
const select = new URL(fromUrl, 'http://localhost').searchParams.get('select');
|
|
71
|
+
return { ...(select ? { select: select.split(',') } : {}), ...(properties ? { properties } : {}) };
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return [];
|
|
34
76
|
}
|
|
77
|
+
};
|
|
78
|
+
const fetchFreshDataset = async (id, fetchJson) => {
|
|
79
|
+
const cached = datasetsCache.get(id);
|
|
80
|
+
if (cached && Date.now() - cached.fetchedAt < DATASETS_CACHE_TTL)
|
|
81
|
+
return cached.data;
|
|
35
82
|
try {
|
|
36
|
-
const fresh = await fetchJson('/datasets/' + encodeURIComponent(
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if (fresh[prop] !== undefined)
|
|
40
|
-
data[prop] = fresh[prop];
|
|
41
|
-
}
|
|
42
|
-
data.userPermissions = fresh.userPermissions ?? [];
|
|
43
|
-
datasetsCache.set(dataset.id, { data, fetchedAt: Date.now() });
|
|
44
|
-
return { ...dataset, ...data };
|
|
83
|
+
const fresh = await fetchJson('/datasets/' + encodeURIComponent(id));
|
|
84
|
+
datasetsCache.set(id, { data: fresh, fetchedAt: Date.now() });
|
|
85
|
+
return fresh;
|
|
45
86
|
}
|
|
46
87
|
catch (err) {
|
|
47
88
|
// a private dataset without an api key, or a network failure: keep the raw
|
|
48
89
|
// configuration entry so the app still loads, and warn in the dev-server UI
|
|
49
|
-
console.warn('[dev-server] failed to enrich dataset ' +
|
|
50
|
-
datasetsCache.set(
|
|
90
|
+
console.warn('[dev-server] failed to enrich dataset ' + id + ', keeping raw configuration entry', err);
|
|
91
|
+
datasetsCache.set(id, { data: null, fetchedAt: Date.now() });
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
export const enrichDataset = async (dataset, fetchJson, filter = {}) => {
|
|
96
|
+
if (!dataset?.id)
|
|
51
97
|
return dataset;
|
|
98
|
+
const fresh = await fetchFreshDataset(dataset.id, fetchJson);
|
|
99
|
+
const result = { ...dataset };
|
|
100
|
+
if (fresh) {
|
|
101
|
+
for (const key of [...Object.keys(dataset), 'finalizedAt', 'slug', ...(filter.select ?? [])]) {
|
|
102
|
+
if (key === 'userPermissions')
|
|
103
|
+
result.userPermissions = fresh.userPermissions ?? [];
|
|
104
|
+
else if (key in fresh)
|
|
105
|
+
result[key] = fresh[key];
|
|
106
|
+
else if (key === 'partOf')
|
|
107
|
+
delete result.partOf;
|
|
108
|
+
}
|
|
52
109
|
}
|
|
110
|
+
for (const [key, prop] of Object.entries(filter.properties ?? {})) {
|
|
111
|
+
if (prop?.default && !(key in result))
|
|
112
|
+
result[key] = prop.default;
|
|
113
|
+
if (prop?.const)
|
|
114
|
+
result[key] = prop.const;
|
|
115
|
+
}
|
|
116
|
+
return result;
|
|
53
117
|
};
|
|
54
118
|
// Enrich the datasets from the remote data-fair, then rewrite every remote origin to ours.
|
|
55
119
|
// The rewrite is applied even when there is no dataset to enrich: a configuration can carry
|
|
56
120
|
// remote urls anywhere (logos, links, tileserver styles), not only in datasets[].href.
|
|
57
121
|
export const prepareConfig = async (configuration, deps) => {
|
|
58
122
|
const datasets = configuration?.datasets?.filter((d) => !!d);
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
123
|
+
let enriched = configuration;
|
|
124
|
+
if (datasets?.length) {
|
|
125
|
+
const filters = datasetsFiltersFromSchema(await deps.fetchConfigSchema().catch(() => null));
|
|
126
|
+
enriched = {
|
|
127
|
+
...configuration,
|
|
128
|
+
datasets: await Promise.all(datasets.map((d, i) => {
|
|
129
|
+
const filter = filters[i] ?? {};
|
|
130
|
+
return enrichDataset(d, deps.fetchJson, deps.extraSelect ? { ...filter, select: [...(filter.select ?? []), ...deps.extraSelect] } : filter);
|
|
131
|
+
}))
|
|
132
|
+
};
|
|
133
|
+
}
|
|
62
134
|
return deps.localize(enriched, deps.remoteOrigin, deps.localOrigin);
|
|
63
135
|
};
|
package/src/env-file.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export declare const MIN_BASE = 20000;
|
|
2
2
|
export declare const MAX_BASE = 29997;
|
|
3
3
|
export declare const renderEnv: (base: number, appPath: string) => string;
|
|
4
|
+
export declare const mergeEnv: (existing: string, generated: string) => string;
|
|
4
5
|
export declare const readEnvVar: (content: string, name: string) => string | undefined;
|
|
5
6
|
export declare const findBase: (isFree: (port: number) => Promise<boolean>, draw: () => number, attempts?: number) => Promise<number>;
|
package/src/env-file.js
CHANGED
|
@@ -10,6 +10,15 @@ DEV_SERVER_PORT=${base + 1}
|
|
|
10
10
|
E2E_PORT=${base + 2}
|
|
11
11
|
APP_PATH=${appPath}
|
|
12
12
|
`;
|
|
13
|
+
// --force redraws our own lines and keeps every other one of an existing .env: the remote
|
|
14
|
+
// settings a developer added there (DATAFAIR_URL, DATAFAIR_API_KEY...) are not ours to drop.
|
|
15
|
+
const OWN_LINE = /^(# généré par df-dev-env|(APP_PORT|DEV_SERVER_PORT|E2E_PORT|APP_PATH)=)/;
|
|
16
|
+
export const mergeEnv = (existing, generated) => {
|
|
17
|
+
const kept = existing.split('\n').filter(l => !OWN_LINE.test(l));
|
|
18
|
+
while (kept.length && kept[kept.length - 1].trim() === '')
|
|
19
|
+
kept.pop();
|
|
20
|
+
return kept.length ? generated + kept.join('\n') + '\n' : generated;
|
|
21
|
+
};
|
|
13
22
|
// Reads one variable out of an already written .env. Used to tell a .env that carries our ports
|
|
14
23
|
// from one written for another purpose, and to keep the APP_PATH a developer chose when --force
|
|
15
24
|
// redraws the ports. Deliberately naive: the file we read is the one we wrote.
|