react-cheminfo 0.49.0 → 0.50.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 CHANGED
@@ -35,6 +35,7 @@ only when it drags a heavy dependency behind it.
35
35
  | `react-cheminfo/core` | every framework-free helper — 140 value exports | nothing |
36
36
  | `react-cheminfo/ui` | every React component and hook — 60 value exports | React |
37
37
  | `react-cheminfo/vite` | the prerender plugin and the OG card | nothing; React on the card |
38
+ | `react-cheminfo/pages` | the Cloudflare Pages analytics middleware | nothing |
38
39
  | `react-cheminfo/orbital` | the 3D atomic-orbital viewer | React, molstar |
39
40
  | `react-cheminfo/molecule3d` | the 3D molecule viewer and its toolbar | React, molstar |
40
41
  | `react-cheminfo/structure` | the structure editor and renderer | React, react-ocl, OCL |
@@ -1096,6 +1097,28 @@ disk. A server writing more into the same place composes it itself, with
1096
1097
  `pageHeadTags` and `fill(page, PAGE_BODY_MARKER, noscriptIndex(…))`. All of that
1097
1098
  is `react-cheminfo/core`, so a backend loads no React and no Vite.
1098
1099
 
1100
+ ### On Cloudflare Pages
1101
+
1102
+ Pages serves the build as it is, so there is no server and no entrypoint to put
1103
+ the analytics snippet in a page. `onRequest` from `react-cheminfo/pages` adds it
1104
+ per request instead, from the project's `TRACKING_SCRIPT` variable, and returns
1105
+ everything that is not a page untouched:
1106
+
1107
+ ```ts
1108
+ // functions/_middleware.ts
1109
+ export { onRequest } from 'react-cheminfo/pages';
1110
+ ```
1111
+
1112
+ It is a door of its own so the Functions bundler reads two small files and
1113
+ never the rest of `core`, whose JSON imports an older esbuild cannot parse.
1114
+
1115
+ Keep the bundles out of the function with a `public/_routes.json`, so a page
1116
+ view costs one invocation rather than one per asset:
1117
+
1118
+ ```json
1119
+ { "version": 1, "include": ["/*"], "exclude": ["/assets/*"] }
1120
+ ```
1121
+
1099
1122
  ## Saying which build is running
1100
1123
 
1101
1124
  A report of something going wrong is worth answering only when we know what was
package/bin/check-seo.mjs CHANGED
@@ -60,6 +60,7 @@ if (stats === undefined || !stats.isDirectory()) {
60
60
  process.exit(usageError(TOOL, `no such build directory: ${root}`));
61
61
  }
62
62
 
63
+ const named = sitemapAddresses();
63
64
  const pages = [];
64
65
  collect(root, pages);
65
66
  if (pages.length === 0) {
@@ -358,6 +359,11 @@ function exists(path) {
358
359
 
359
360
  /**
360
361
  * Add every built page under a path to the list, with the address it answers.
362
+ *
363
+ * A folder's `index.html` is always a page. Any other `.html` file is one only
364
+ * when the sitemap names its address, as every prerendered `<route>.html` is:
365
+ * a demo the build emits or a document copied from `public` is not a page of
366
+ * the site, and holding it to a page's head would report a correct build.
361
367
  * @param {string} path - A directory of the build.
362
368
  * @param {Array<{ file: string, address: string }>} pages - Collected so far.
363
369
  */
@@ -369,8 +375,27 @@ function collect(path, pages) {
369
375
  collect(next, pages);
370
376
  } else if (entry.name.endsWith('.html')) {
371
377
  const page = relative(root, next).replace(/\.html$/, '');
372
- const address = `/${page.replace(/(?:^|\/)index$/, '')}`;
373
- pages.push({ file: next, address: trimSlash(address) });
378
+ const address = trimSlash(`/${page.replace(/(?:^|\/)index$/, '')}`);
379
+ if (entry.name === 'index.html' || named.has(address)) {
380
+ pages.push({ file: next, address });
381
+ }
374
382
  }
375
383
  }
376
384
  }
385
+
386
+ /**
387
+ * Read which addresses of this build the sitemap names.
388
+ * @returns {Set<string>} Every address of this build the sitemap names.
389
+ */
390
+ function sitemapAddresses() {
391
+ const sitemap = join(root, 'sitemap.xml');
392
+ const addresses = new Set();
393
+ if (!exists(sitemap)) return addresses;
394
+ for (const match of readFileSync(sitemap, 'utf8').matchAll(
395
+ /<loc>(?<loc>[^<]+)<\/loc>/g,
396
+ )) {
397
+ const own = ownPath(match.groups?.loc ?? '');
398
+ if (own !== undefined) addresses.add(own === '' ? '/' : own);
399
+ }
400
+ return addresses;
401
+ }
package/lib/pages.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ export type { TrackingMiddlewareContext } from './seo/core/trackingMiddleware.ts';
2
+ export { trackingScriptMiddleware as onRequest } from './seo/core/trackingMiddleware.ts';
3
+ //# sourceMappingURL=pages.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pages.d.ts","sourceRoot":"","sources":["../src/pages.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,yBAAyB,EAAE,MAAM,kCAAkC,CAAC;AAClF,OAAO,EAAE,wBAAwB,IAAI,SAAS,EAAE,MAAM,kCAAkC,CAAC"}
package/lib/pages.js ADDED
@@ -0,0 +1,2 @@
1
+ export { trackingScriptMiddleware as onRequest } from "./seo/core/trackingMiddleware.js";
2
+ //# sourceMappingURL=pages.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pages.js","sourceRoot":"","sources":["../src/pages.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,wBAAwB,IAAI,SAAS,EAAE,MAAM,kCAAkC,CAAC"}
@@ -0,0 +1,27 @@
1
+ /** What a Cloudflare Pages Function hands its middleware, as far as it is read. */
2
+ export interface TrackingMiddlewareContext {
3
+ /** The visitor's request. */
4
+ request: Request;
5
+ /** The project's variables; `TRACKING_SCRIPT` holds the analytics snippet. */
6
+ env: {
7
+ TRACKING_SCRIPT?: string;
8
+ };
9
+ /** Serve the request from the build, as Pages would without a function. */
10
+ next: () => Promise<Response>;
11
+ }
12
+ /**
13
+ * Put the analytics snippet in every page a Cloudflare Pages site serves.
14
+ *
15
+ * Pages has no server and no entrypoint to write the deployment's snippet into
16
+ * the build, so it is added per request instead, from the project's
17
+ * `TRACKING_SCRIPT` variable — the deployment owns it, never the build. With
18
+ * the variable unset, and for anything that is not a page, the response the
19
+ * build gives is returned untouched. A site wires it in
20
+ * `functions/_middleware.ts`:
21
+ *
22
+ * export { onRequest } from 'react-cheminfo/pages';
23
+ * @param context - The Pages Function context.
24
+ * @returns The response, carrying the snippet when it is an HTML page.
25
+ */
26
+ export declare function trackingScriptMiddleware(context: TrackingMiddlewareContext): Promise<Response>;
27
+ //# sourceMappingURL=trackingMiddleware.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"trackingMiddleware.d.ts","sourceRoot":"","sources":["../../../src/seo/core/trackingMiddleware.ts"],"names":[],"mappings":"AAEA,mFAAmF;AACnF,MAAM,WAAW,yBAAyB;IACxC,6BAA6B;IAC7B,OAAO,EAAE,OAAO,CAAC;IACjB,8EAA8E;IAC9E,GAAG,EAAE;QAAE,eAAe,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAClC,2EAA2E;IAC3E,IAAI,EAAE,MAAM,OAAO,CAAC,QAAQ,CAAC,CAAC;CAC/B;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,wBAAwB,CAC5C,OAAO,EAAE,yBAAyB,GACjC,OAAO,CAAC,QAAQ,CAAC,CAmBnB"}
@@ -0,0 +1,33 @@
1
+ import { injectTrackingScript } from "./trackingScript.js";
2
+ /**
3
+ * Put the analytics snippet in every page a Cloudflare Pages site serves.
4
+ *
5
+ * Pages has no server and no entrypoint to write the deployment's snippet into
6
+ * the build, so it is added per request instead, from the project's
7
+ * `TRACKING_SCRIPT` variable — the deployment owns it, never the build. With
8
+ * the variable unset, and for anything that is not a page, the response the
9
+ * build gives is returned untouched. A site wires it in
10
+ * `functions/_middleware.ts`:
11
+ *
12
+ * export { onRequest } from 'react-cheminfo/pages';
13
+ * @param context - The Pages Function context.
14
+ * @returns The response, carrying the snippet when it is an HTML page.
15
+ */
16
+ export async function trackingScriptMiddleware(context) {
17
+ const response = await context.next();
18
+ const snippet = context.env.TRACKING_SCRIPT?.trim() ?? '';
19
+ if (snippet === '' ||
20
+ context.request.method !== 'GET' ||
21
+ response.status === 304 ||
22
+ !response.headers.get('content-type')?.includes('text/html')) {
23
+ return response;
24
+ }
25
+ const headers = new Headers(response.headers);
26
+ headers.delete('content-length');
27
+ return new Response(injectTrackingScript(await response.text(), snippet), {
28
+ status: response.status,
29
+ statusText: response.statusText,
30
+ headers,
31
+ });
32
+ }
33
+ //# sourceMappingURL=trackingMiddleware.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"trackingMiddleware.js","sourceRoot":"","sources":["../../../src/seo/core/trackingMiddleware.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAY3D;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,KAAK,UAAU,wBAAwB,CAC5C,OAAkC;IAElC,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,IAAI,EAAE,CAAC;IACtC,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,eAAe,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;IAC1D,IACE,OAAO,KAAK,EAAE;QACd,OAAO,CAAC,OAAO,CAAC,MAAM,KAAK,KAAK;QAChC,QAAQ,CAAC,MAAM,KAAK,GAAG;QACvB,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,EAAE,QAAQ,CAAC,WAAW,CAAC,EAC5D,CAAC;QACD,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED,MAAM,OAAO,GAAG,IAAI,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;IAC9C,OAAO,CAAC,MAAM,CAAC,gBAAgB,CAAC,CAAC;IACjC,OAAO,IAAI,QAAQ,CAAC,oBAAoB,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,EAAE,OAAO,CAAC,EAAE;QACxE,MAAM,EAAE,QAAQ,CAAC,MAAM;QACvB,UAAU,EAAE,QAAQ,CAAC,UAAU;QAC/B,OAAO;KACR,CAAC,CAAC;AACL,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-cheminfo",
3
- "version": "0.49.0",
3
+ "version": "0.50.0",
4
4
  "license": "MIT",
5
5
  "description": "React components, hooks and framework-free helpers shared by the cheminfo sites: site chrome, citations, sharing, routing, pedagogy, charts, and the structure, orbital and 3D molecule viewers",
6
6
  "author": "Luc Patiny",
@@ -22,6 +22,7 @@
22
22
  "./molecule3d": "./lib/molecule3d.js",
23
23
  "./molstar/*": "./lib/molstar/*/index.js",
24
24
  "./orbital": "./lib/orbital.js",
25
+ "./pages": "./lib/pages.js",
25
26
  "./slides": "./lib/slides.js",
26
27
  "./spectra": "./lib/spectra.js",
27
28
  "./structure": "./lib/structure.js",
package/src/pages.ts ADDED
@@ -0,0 +1,2 @@
1
+ export type { TrackingMiddlewareContext } from './seo/core/trackingMiddleware.ts';
2
+ export { trackingScriptMiddleware as onRequest } from './seo/core/trackingMiddleware.ts';
@@ -0,0 +1,48 @@
1
+ import { injectTrackingScript } from './trackingScript.ts';
2
+
3
+ /** What a Cloudflare Pages Function hands its middleware, as far as it is read. */
4
+ export interface TrackingMiddlewareContext {
5
+ /** The visitor's request. */
6
+ request: Request;
7
+ /** The project's variables; `TRACKING_SCRIPT` holds the analytics snippet. */
8
+ env: { TRACKING_SCRIPT?: string };
9
+ /** Serve the request from the build, as Pages would without a function. */
10
+ next: () => Promise<Response>;
11
+ }
12
+
13
+ /**
14
+ * Put the analytics snippet in every page a Cloudflare Pages site serves.
15
+ *
16
+ * Pages has no server and no entrypoint to write the deployment's snippet into
17
+ * the build, so it is added per request instead, from the project's
18
+ * `TRACKING_SCRIPT` variable — the deployment owns it, never the build. With
19
+ * the variable unset, and for anything that is not a page, the response the
20
+ * build gives is returned untouched. A site wires it in
21
+ * `functions/_middleware.ts`:
22
+ *
23
+ * export { onRequest } from 'react-cheminfo/pages';
24
+ * @param context - The Pages Function context.
25
+ * @returns The response, carrying the snippet when it is an HTML page.
26
+ */
27
+ export async function trackingScriptMiddleware(
28
+ context: TrackingMiddlewareContext,
29
+ ): Promise<Response> {
30
+ const response = await context.next();
31
+ const snippet = context.env.TRACKING_SCRIPT?.trim() ?? '';
32
+ if (
33
+ snippet === '' ||
34
+ context.request.method !== 'GET' ||
35
+ response.status === 304 ||
36
+ !response.headers.get('content-type')?.includes('text/html')
37
+ ) {
38
+ return response;
39
+ }
40
+
41
+ const headers = new Headers(response.headers);
42
+ headers.delete('content-length');
43
+ return new Response(injectTrackingScript(await response.text(), snippet), {
44
+ status: response.status,
45
+ statusText: response.statusText,
46
+ headers,
47
+ });
48
+ }