@data-fair/dev-server 2.4.1 → 2.5.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 +35 -0
- package/config/custom-environment-variables.js +6 -0
- package/config/default.js +6 -1
- package/package.json +8 -5
- package/src/app.js +16 -16
- package/src/dev-env.d.ts +2 -0
- package/src/dev-env.js +39 -0
- package/src/env-file.d.ts +5 -0
- package/src/env-file.js +28 -0
- package/src/index.js +0 -0
- package/src/localize.d.ts +1 -0
- package/src/localize.js +16 -0
- package/ui/dist/assets/{index-BgRDnJ6S.js → index-BtPwUKGa.js} +1 -1
- package/ui/dist/index.html +1 -1
package/README.md
CHANGED
|
@@ -7,6 +7,41 @@ A development server for optimal development experience of data-fair application
|
|
|
7
7
|
See [data-fair-charts](https://github.com/koumoul-dev/data-fair-charts/blob/master/package.json) for an example with nuxt.
|
|
8
8
|
See [data-fair-minimal](https://github.com/koumoul-dev/data-fair-minimal/blob/master/package.json) for an example with a simple http server.
|
|
9
9
|
|
|
10
|
+
## Ports de développement
|
|
11
|
+
|
|
12
|
+
`df-dev-env` génère un `.env` git-ignoré portant trois ports libres consécutifs,
|
|
13
|
+
tirés dans 20000–29999, pour que plusieurs applications tournent en parallèle :
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
APP_PORT=24730 # le serveur de dev de l'application (Vite)
|
|
17
|
+
DEV_SERVER_PORT=24731 # df-dev-server
|
|
18
|
+
E2E_PORT=24732 # le webServer de Playwright
|
|
19
|
+
APP_PATH=/app/ # chemin sous lequel l'application est servie
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Le fichier est généré une fois, au premier `npm run dev`, puis laissé tel quel.
|
|
23
|
+
`df-dev-env --force` retire de nouveaux ports en cas de collision, en
|
|
24
|
+
conservant l'`APP_PATH` déjà choisi : le remède à une collision de ports ne
|
|
25
|
+
doit pas replacer en silence sous `/app/` une application servie à la racine.
|
|
26
|
+
`--app-path` explicite prime sur la valeur conservée.
|
|
27
|
+
|
|
28
|
+
Un `.env` déjà présent est laissé intact — mais `df-dev-env` prévient quand ce
|
|
29
|
+
silence n'est pas ce qu'on attendait : quand le fichier ne porte pas `APP_PORT`
|
|
30
|
+
(un `.env` écrit pour une autre raison, qui ne recevrait donc aucun port), et
|
|
31
|
+
quand un `--app-path` explicite diffère de celui déjà stocké.
|
|
32
|
+
|
|
33
|
+
`df-dev-server` lit ce `.env` (`dotenv`) : `DEV_SERVER_PORT` fixe son port, et
|
|
34
|
+
`app.url` est dérivée de `APP_PORT` + `APP_PATH`. `APP_URL` reste prioritaire
|
|
35
|
+
pour une application qui n'est pas servie sur `localhost`.
|
|
36
|
+
|
|
37
|
+
`npm run dev-test-app-minimal` relit `.env` lui-même (via `dotenv-cli`) pour
|
|
38
|
+
faire écouter `http-server` sur le bon `APP_PORT`, mais ne peut pas agir sur
|
|
39
|
+
`APP_PATH` : ce script et le processus `df-dev-server` déjà démarré ne
|
|
40
|
+
partagent rien d'autre que le fichier `.env`, écrit une seule fois par
|
|
41
|
+
`df-dev-env` avec `/app/` par défaut. Pour tester `test-apps/minimal` (servie
|
|
42
|
+
à la racine) il faut régénérer `.env` avant de lancer le dev-server :
|
|
43
|
+
`node src/dev-env.js --force --app-path=`.
|
|
44
|
+
|
|
10
45
|
## Development
|
|
11
46
|
|
|
12
47
|
Run development server :
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
export default {
|
|
2
|
+
port: {
|
|
3
|
+
__name: 'DEV_SERVER_PORT',
|
|
4
|
+
// __format, otherwise the config module hands over the raw string: the declared type is a
|
|
5
|
+
// number, and ajv cannot coerce it back since the config object is frozen before assertValid.
|
|
6
|
+
__format: 'json'
|
|
7
|
+
},
|
|
2
8
|
dataFair: {
|
|
3
9
|
url: 'DATAFAIR_URL',
|
|
4
10
|
owner: {
|
package/config/default.js
CHANGED
|
@@ -10,7 +10,12 @@ export default {
|
|
|
10
10
|
apiKey: null
|
|
11
11
|
},
|
|
12
12
|
app: {
|
|
13
|
-
|
|
13
|
+
// Derived from the app port so that a shifted dev setup cannot desynchronize the two.
|
|
14
|
+
// Path defaults to /app/: 27 of the 37 applications are Vite apps served under it, and
|
|
15
|
+
// this is the only value that works in a fresh clone that never ran df-dev-env. An
|
|
16
|
+
// application served at the root opts out with APP_PATH= (empty string is not nullish,
|
|
17
|
+
// so ?? keeps it). APP_URL still overrides it entirely, for apps not served on localhost.
|
|
18
|
+
url: `http://localhost:${process.env.APP_PORT ?? 3000}${process.env.APP_PATH ?? '/app/'}`,
|
|
14
19
|
proxyPaths: ['/_nuxt/']
|
|
15
20
|
},
|
|
16
21
|
site: {
|
package/package.json
CHANGED
|
@@ -1,21 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@data-fair/dev-server",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.0",
|
|
4
4
|
"description": "A development server for optimal development experience of data-fair applications.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
7
|
-
"df-dev-server": "src/index.js"
|
|
7
|
+
"df-dev-server": "src/index.js",
|
|
8
|
+
"df-dev-env": "src/dev-env.js"
|
|
8
9
|
},
|
|
9
10
|
"type": "module",
|
|
10
11
|
"scripts": {
|
|
11
12
|
"build": "tsc",
|
|
12
13
|
"build-watch": "tsc --watch",
|
|
13
14
|
"dev": "NODE_ENV=development node --watch src/index.js",
|
|
14
|
-
"dev-test-app-minimal": "http-server test-apps/minimal --port
|
|
15
|
+
"dev-test-app-minimal": "dotenv -- sh -c 'http-server test-apps/minimal --port=${APP_PORT:-3000} --cors --silent'",
|
|
15
16
|
"dev-test-app-vue3": "cd test-apps/vue3 && npm run dev",
|
|
16
17
|
"dev-test-app-modern": "cd test-apps/modern && npm run dev",
|
|
17
|
-
"dev-zellij": "zellij --layout .zellij.kdl",
|
|
18
|
-
"
|
|
18
|
+
"dev-zellij": "npm run build && node src/dev-env.js && dotenv -- zellij --layout .zellij.kdl",
|
|
19
|
+
"test": "node --test test/*.test.ts",
|
|
20
|
+
"prepublishOnly": "npm run lint && npm test && npm run build && npm -w ui run build",
|
|
19
21
|
"lint": "eslint . && npm -w ui run lint",
|
|
20
22
|
"lint-fix": "eslint --fix . && npm -w ui run lint-fix",
|
|
21
23
|
"build-types": "df-build-types ."
|
|
@@ -46,6 +48,7 @@
|
|
|
46
48
|
"@types/express": "^5.0.6",
|
|
47
49
|
"@types/node": "^26.3.0",
|
|
48
50
|
"@types/ws": "^8.18.1",
|
|
51
|
+
"dotenv-cli": "^11.0.0",
|
|
49
52
|
"eslint": "^9.39.5",
|
|
50
53
|
"eslint-plugin-vue": "^10.10.0",
|
|
51
54
|
"eslint-plugin-vuetify": "^2.7.2",
|
package/src/app.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Express app for TaxMan own API and UI
|
|
2
2
|
import config from './config.js';
|
|
3
3
|
import uiConfig from './ui-config.js';
|
|
4
|
+
import { localizeConfig } from './localize.js';
|
|
4
5
|
import { WebSocket, WebSocketServer } from 'ws';
|
|
5
6
|
import { createServer } from 'node:http';
|
|
6
7
|
import express from 'express';
|
|
@@ -91,7 +92,7 @@ app.get('/config', (req, res, next) => {
|
|
|
91
92
|
// exact same data as the application receives in window.APPLICATION
|
|
92
93
|
app.get('/config/enriched', async (req, res) => {
|
|
93
94
|
try {
|
|
94
|
-
res.send(await
|
|
95
|
+
res.send(await prepareConfig(readDevConfig()));
|
|
95
96
|
}
|
|
96
97
|
catch (err) {
|
|
97
98
|
res.status(500).send({ error: err.message });
|
|
@@ -168,14 +169,11 @@ const enrichDataset = async (dataset) => {
|
|
|
168
169
|
}
|
|
169
170
|
try {
|
|
170
171
|
const fresh = await remoteFetch('/datasets/' + encodeURIComponent(dataset.id));
|
|
171
|
-
const localBase = `http://localhost:${config.port}/data-fair`;
|
|
172
172
|
const data = {};
|
|
173
173
|
for (const prop of INJECTED_DATASET_PROPS) {
|
|
174
174
|
if (fresh[prop] !== undefined)
|
|
175
175
|
data[prop] = fresh[prop];
|
|
176
176
|
}
|
|
177
|
-
if (typeof data.href === 'string')
|
|
178
|
-
data.href = data.href.replace(config.dataFair.url, localBase);
|
|
179
177
|
data.userPermissions = fresh.userPermissions ?? [];
|
|
180
178
|
datasetsCache.set(dataset.id, { data, fetchedAt: Date.now() });
|
|
181
179
|
return { ...dataset, ...data };
|
|
@@ -188,12 +186,15 @@ const enrichDataset = async (dataset) => {
|
|
|
188
186
|
return dataset;
|
|
189
187
|
}
|
|
190
188
|
};
|
|
191
|
-
|
|
189
|
+
// Enrich the datasets from the remote data-fair, then rewrite every remote origin to ours.
|
|
190
|
+
// The rewrite is applied even when there is no dataset to enrich: a configuration can carry
|
|
191
|
+
// remote urls anywhere (logos, links, tileserver styles), not only in datasets[].href.
|
|
192
|
+
const prepareConfig = async (configuration) => {
|
|
192
193
|
const datasets = configuration?.datasets?.filter((d) => !!d);
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
return
|
|
194
|
+
const enriched = datasets?.length
|
|
195
|
+
? { ...configuration, datasets: await Promise.all(datasets.map(enrichDataset)) }
|
|
196
|
+
: configuration;
|
|
197
|
+
return localizeConfig(enriched, new URL(config.dataFair.url).origin, `http://localhost:${config.port}`);
|
|
197
198
|
};
|
|
198
199
|
// read the .dev-config.json file of the app under development
|
|
199
200
|
const readDevConfig = () => existsSync('.dev-config.json') ? JSON.parse(readFileSync('.dev-config.json', 'utf8')) : {};
|
|
@@ -354,14 +355,13 @@ app.get('/configurations', async (req, res) => {
|
|
|
354
355
|
res.status(500).send({ ...response, error: err.message });
|
|
355
356
|
}
|
|
356
357
|
});
|
|
357
|
-
// copy a configuration from the remote data-fair,
|
|
358
|
-
//
|
|
358
|
+
// copy a configuration from the remote data-fair, keeping its remote origins so that
|
|
359
|
+
// .dev-config.json stays portable — see localize.ts
|
|
359
360
|
app.get('/configurations/:id', async (req, res) => {
|
|
360
361
|
try {
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
res.send(JSON.parse(JSON.stringify(configuration).replaceAll(remoteOrigin, localOrigin)));
|
|
362
|
+
// Sent as-is, with its remote origins: the UI stores this straight into .dev-config.json,
|
|
363
|
+
// which must stay portable. Origins are rewritten on the read path, in prepareConfig.
|
|
364
|
+
res.send(await remoteFetch('/applications/' + encodeURIComponent(req.params.id) + '/configuration'));
|
|
365
365
|
}
|
|
366
366
|
catch (err) {
|
|
367
367
|
res.status(500).send({ error: err.message });
|
|
@@ -407,7 +407,7 @@ app.use('/app', createProxyMiddleware({
|
|
|
407
407
|
let output = rawBody.toString();
|
|
408
408
|
if (output.includes('%APPLICATION%')) {
|
|
409
409
|
try {
|
|
410
|
-
configuration = await
|
|
410
|
+
configuration = await prepareConfig(configuration);
|
|
411
411
|
}
|
|
412
412
|
catch (err) {
|
|
413
413
|
console.warn('[dev-server] failed to enrich configuration datasets', err);
|
package/src/dev-env.d.ts
ADDED
package/src/dev-env.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { createServer } from 'node:net';
|
|
4
|
+
import { parseArgs } from 'node:util';
|
|
5
|
+
import { MIN_BASE, MAX_BASE, renderEnv, findBase, readEnvVar } from './env-file.js';
|
|
6
|
+
const { values } = parseArgs({
|
|
7
|
+
options: {
|
|
8
|
+
force: { type: 'boolean', default: false },
|
|
9
|
+
// No default value: an absent --app-path must stay distinguishable from an explicit one, so
|
|
10
|
+
// that --force can redraw the ports without resetting a path the developer chose.
|
|
11
|
+
'app-path': { type: 'string' }
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
const existing = existsSync('.env') ? readFileSync('.env', 'utf8') : undefined;
|
|
15
|
+
const storedAppPath = existing === undefined ? undefined : readEnvVar(existing, 'APP_PATH');
|
|
16
|
+
if (existing !== undefined && !values.force) {
|
|
17
|
+
// An existing .env is left untouched — but say so when leaving it untouched is not what the
|
|
18
|
+
// caller expected, rather than letting the application start on the wrong port or path.
|
|
19
|
+
if (readEnvVar(existing, 'APP_PORT') === undefined) {
|
|
20
|
+
console.warn('.env existe déjà mais ne porte pas APP_PORT : y ajouter les lignes de port à la main, ou df-dev-env --force');
|
|
21
|
+
}
|
|
22
|
+
else if (values['app-path'] !== undefined && values['app-path'] !== storedAppPath) {
|
|
23
|
+
console.warn(`.env existe déjà avec APP_PATH=${storedAppPath} : --app-path=${values['app-path']} est ignoré, df-dev-env --force pour l'appliquer`);
|
|
24
|
+
}
|
|
25
|
+
process.exit(0);
|
|
26
|
+
}
|
|
27
|
+
const isFree = (port) => new Promise(resolve => {
|
|
28
|
+
const server = createServer();
|
|
29
|
+
server.once('error', () => { resolve(false); });
|
|
30
|
+
server.once('listening', () => { server.close(() => { resolve(true); }); });
|
|
31
|
+
server.listen(port, '127.0.0.1');
|
|
32
|
+
});
|
|
33
|
+
const draw = () => MIN_BASE + Math.floor(Math.random() * (MAX_BASE - MIN_BASE + 1));
|
|
34
|
+
// --force is the documented remedy for a port collision: it must not silently move an
|
|
35
|
+
// application served at the root back under /app/ along the way.
|
|
36
|
+
const appPath = values['app-path'] ?? storedAppPath ?? '/app/';
|
|
37
|
+
const base = await findBase(isFree, draw);
|
|
38
|
+
writeFileSync('.env', renderEnv(base, appPath));
|
|
39
|
+
console.log(`.env généré — app ${base}, dev-server ${base + 1}, e2e ${base + 2}, chemin ${appPath || '/ (racine)'}`);
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare const MIN_BASE = 20000;
|
|
2
|
+
export declare const MAX_BASE = 29997;
|
|
3
|
+
export declare const renderEnv: (base: number, appPath: string) => string;
|
|
4
|
+
export declare const readEnvVar: (content: string, name: string) => string | undefined;
|
|
5
|
+
export declare const findBase: (isFree: (port: number) => Promise<boolean>, draw: () => number, attempts?: number) => Promise<number>;
|
package/src/env-file.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Development ports live below the kernel ephemeral range (32768-60999 on Linux), where a port
|
|
2
|
+
// can already be held by an outgoing connection, and above the 191xx block used by the
|
|
3
|
+
// data-fair docker compose.
|
|
4
|
+
export const MIN_BASE = 20000;
|
|
5
|
+
// The base takes three consecutive ports, so it may not go past 29999.
|
|
6
|
+
export const MAX_BASE = 29997;
|
|
7
|
+
export const renderEnv = (base, appPath) => `# généré par df-dev-env — ne pas commiter
|
|
8
|
+
APP_PORT=${base}
|
|
9
|
+
DEV_SERVER_PORT=${base + 1}
|
|
10
|
+
E2E_PORT=${base + 2}
|
|
11
|
+
APP_PATH=${appPath}
|
|
12
|
+
`;
|
|
13
|
+
// Reads one variable out of an already written .env. Used to tell a .env that carries our ports
|
|
14
|
+
// from one written for another purpose, and to keep the APP_PATH a developer chose when --force
|
|
15
|
+
// redraws the ports. Deliberately naive: the file we read is the one we wrote.
|
|
16
|
+
export const readEnvVar = (content, name) => {
|
|
17
|
+
const line = content.split('\n').find(l => l.startsWith(name + '='));
|
|
18
|
+
return line === undefined ? undefined : line.slice(name.length + 1).trim();
|
|
19
|
+
};
|
|
20
|
+
export const findBase = async (isFree, draw, attempts = 10) => {
|
|
21
|
+
for (let i = 0; i < attempts; i++) {
|
|
22
|
+
const base = draw();
|
|
23
|
+
const free = await Promise.all([base, base + 1, base + 2].map(isFree));
|
|
24
|
+
if (free.every(Boolean))
|
|
25
|
+
return base;
|
|
26
|
+
}
|
|
27
|
+
throw new Error(`no free port range found after ${attempts} attempts`);
|
|
28
|
+
};
|
package/src/index.js
CHANGED
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const localizeConfig: <T>(configuration: T, remoteOrigin: string, localOrigin: string) => T;
|
package/src/localize.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Configurations are stored with their remote (koumoul.com) origins so that .dev-config.json
|
|
2
|
+
// stays portable across developers, machines and regenerated ports. Origins are rewritten here,
|
|
3
|
+
// on the read path, on the way to the application — never on the write path.
|
|
4
|
+
// Paths the dev-server re-exposes under its own origin.
|
|
5
|
+
const PROXIED_PATHS = ['/data-fair', '/simple-directory', '/tileserver'];
|
|
6
|
+
// Transition rewrite: .dev-config.json files written before this change carry a hardcoded
|
|
7
|
+
// http://localhost:5888 origin. Only rewrite one when it stands on a path we actually proxy,
|
|
8
|
+
// and only when that path is complete — a deliberate http://localhost:8080/data-fair-lookalike
|
|
9
|
+
// typed by a developer must survive untouched.
|
|
10
|
+
const legacyLocalOrigin = new RegExp(`http://localhost:\\d+(?=(?:${PROXIED_PATHS.join('|')})(?:/|"))`, 'g');
|
|
11
|
+
export const localizeConfig = (configuration, remoteOrigin, localOrigin) => {
|
|
12
|
+
const json = JSON.stringify(configuration);
|
|
13
|
+
if (json === undefined)
|
|
14
|
+
return configuration;
|
|
15
|
+
return JSON.parse(json.replaceAll(remoteOrigin, localOrigin).replace(legacyLocalOrigin, localOrigin));
|
|
16
|
+
};
|