@data-fair/dev-server 2.2.0 → 2.3.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@data-fair/dev-server",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "description": "A development server for optimal development experience of data-fair applications.",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -38,6 +38,7 @@
38
38
  },
39
39
  "homepage": "https://github.com/koumoul-dev/df-dev-server#readme",
40
40
  "devDependencies": {
41
+ "@data-fair/lib-common-types": "^1.21.0",
41
42
  "@data-fair/lib-types-builder": "^1.14.0",
42
43
  "@types/config": "^4.4.0",
43
44
  "@types/cors": "^2.8.19",
package/src/app.js CHANGED
@@ -77,6 +77,9 @@ server.on('upgrade', function upgrade(req, socket, head) {
77
77
  });
78
78
  app.use(cors());
79
79
  app.use(express.json());
80
+ // URL of the app under development, used by the proxy and by the remote configuration import
81
+ const appUrl = new URL(config.app.url);
82
+ const appPrefix = appUrl.pathname.endsWith('/') ? appUrl.pathname.substring(0, appUrl.pathname.length - 1) : appUrl.pathname;
80
83
  // very basic CRUD of config
81
84
  app.get('/config', (req, res, next) => {
82
85
  const devConfig = existsSync('.dev-config.json') ? JSON.parse(readFileSync('.dev-config.json', 'utf8')) : {};
@@ -95,9 +98,88 @@ app.post('/config/error', (req, res) => {
95
98
  }
96
99
  res.send();
97
100
  });
101
+ // Identification of the app under development: its name is read from the
102
+ // "application-name" meta tag of the local index.html and its version from the
103
+ // package.json of the current directory.
104
+ const localAppInfo = async () => {
105
+ let name;
106
+ try {
107
+ const res = await fetch(appUrl.origin + appPrefix + '/index.html');
108
+ const html = await res.text();
109
+ const document = parse5.parse(html);
110
+ const htmlNode = document.childNodes?.find((c) => c.tagName === 'html');
111
+ const headNode = htmlNode?.childNodes?.find((c) => c.tagName === 'head');
112
+ const metaNode = (headNode?.childNodes ?? []).find((c) => c.tagName === 'meta' && c.attrs?.find((a) => a.name === 'name')?.value === 'application-name');
113
+ name = metaNode?.attrs?.find((a) => a.name === 'content')?.value;
114
+ }
115
+ catch (err) {
116
+ debug('failed to fetch local app index.html', err);
117
+ }
118
+ let version;
119
+ try {
120
+ version = JSON.parse(readFileSync('package.json', 'utf8')).version;
121
+ }
122
+ catch (err) {
123
+ debug('failed to read local package.json', err);
124
+ }
125
+ return { name, version };
126
+ };
127
+ // Fetch a resource from the remote data-fair api
128
+ const remoteFetch = async (path) => {
129
+ const res = await fetch(config.dataFair.url + '/api/v1' + path, {
130
+ headers: config.dataFair.apiKey ? { 'x-apiKey': config.dataFair.apiKey } : {}
131
+ });
132
+ if (!res.ok)
133
+ throw new Error(`error ${res.status} on remote data-fair: ${await res.text()}`);
134
+ return res.json();
135
+ };
136
+ // extract the major.minor part of a version, versions on remote base apps look like "1.3"
137
+ const minorVersion = (version) => version.split('.').slice(0, 2).join('.');
138
+ // list configurations of the app under development that exist on the remote data-fair
139
+ // with the same minor version, useful to test quickly for regressions
140
+ app.get('/configurations', async (req, res) => {
141
+ const { name, version } = await localAppInfo();
142
+ const response = { appName: name, localVersion: version, remoteUrl: config.dataFair.url };
143
+ if (!name) {
144
+ res.status(400).send({ ...response, error: 'The "application-name" meta tag was not found in the local app index.html' });
145
+ return;
146
+ }
147
+ if (!version) {
148
+ res.status(400).send({ ...response, error: 'The version could not be read from the local package.json' });
149
+ return;
150
+ }
151
+ response.minorVersion = minorVersion(version);
152
+ try {
153
+ const baseApps = await remoteFetch('/base-applications?applicationName=' + encodeURIComponent(name) + '&size=1000&count=false');
154
+ const matchingBaseApps = (baseApps.results ?? []).filter((b) => typeof b.version === 'string' && minorVersion(b.version) === response.minorVersion);
155
+ response.results = [];
156
+ for (const baseApp of matchingBaseApps) {
157
+ const applications = await remoteFetch('/applications?base-application=' + encodeURIComponent(baseApp.url) + '&size=1000&count=false&select=id,title,owner');
158
+ for (const application of applications.results ?? []) {
159
+ response.results.push({ id: application.id, title: application.title, owner: application.owner, baseAppVersion: baseApp.version });
160
+ }
161
+ }
162
+ response.results.sort((a, b) => (a.title || '').localeCompare(b.title || ''));
163
+ res.send(response);
164
+ }
165
+ catch (err) {
166
+ res.status(500).send({ ...response, error: err.message });
167
+ }
168
+ });
169
+ // copy a configuration from the remote data-fair, replacing all references to the
170
+ // remote origin by the local one so that data goes through the local proxies
171
+ app.get('/configurations/:id', async (req, res) => {
172
+ try {
173
+ const configuration = await remoteFetch('/applications/' + encodeURIComponent(req.params.id) + '/configuration');
174
+ const remoteOrigin = new URL(config.dataFair.url).origin;
175
+ const localOrigin = `http://localhost:${config.port}`;
176
+ res.send(JSON.parse(JSON.stringify(configuration).replaceAll(remoteOrigin, localOrigin)));
177
+ }
178
+ catch (err) {
179
+ res.status(500).send({ error: err.message });
180
+ }
181
+ });
98
182
  // re-expose the application performing similar modifications to the body as data-fair
99
- const appUrl = new URL(config.app.url);
100
- const appPrefix = appUrl.pathname.endsWith('/') ? appUrl.pathname.substring(0, appUrl.pathname.length - 1) : appUrl.pathname;
101
183
  app.use('/app', createProxyMiddleware({
102
184
  target: appUrl.origin,
103
185
  secure: false,
@@ -258,6 +340,39 @@ app.use('/data-fair', createProxyMiddleware({
258
340
  }
259
341
  }
260
342
  }));
343
+ // re-expose a tileserver to serve base map styles, tiles, sprites and fonts.
344
+ // derived from the data-fair origin (each site exposes its tileserver on <origin>/tileserver).
345
+ const tileserverUrl = new URL(config.dataFair.url).origin + '/tileserver';
346
+ app.use('/tileserver', createProxyMiddleware({
347
+ target: new URL(config.dataFair.url).origin,
348
+ secure: false,
349
+ changeOrigin: true,
350
+ selfHandleResponse: true, // so that the proxyRes handler takes care of sending the response
351
+ on: {
352
+ proxyReq(proxyReq, req, res) {
353
+ proxyReq.path = '/tileserver' + proxyReq.path;
354
+ // no gzip so that we can process the content
355
+ proxyReq.setHeader('accept-encoding', 'identity');
356
+ },
357
+ proxyRes(proxyRes, req, res) {
358
+ if (proxyRes.headers['content-type'] && proxyRes.headers['content-type'].startsWith('application/json')) {
359
+ let body = '';
360
+ proxyRes.on('data', (data) => { body += data.toString(); });
361
+ proxyRes.on('end', () => {
362
+ // make all references to the tileserver url point to the local proxy
363
+ const output = body.replace(new RegExp(escapeStringRegexp(tileserverUrl), 'g'), `http://localhost:${config.port}/tileserver`);
364
+ delete proxyRes.headers['content-length'];
365
+ res.writeHead(proxyRes.statusCode ?? 200, proxyRes.headers);
366
+ res.end(output);
367
+ });
368
+ }
369
+ else {
370
+ res.writeHead(proxyRes.statusCode ?? 200, proxyRes.headers);
371
+ proxyRes.pipe(res);
372
+ }
373
+ }
374
+ }
375
+ }));
261
376
  // also re-expose the simple-directory instance matching data-fair
262
377
  app.use('/simple-directory', createProxyMiddleware({
263
378
  target: dfUrl.origin,
package/src/index.js CHANGED
File without changes