react-cheminfo 0.26.0 → 0.28.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/bin/check-deploy.mjs +14 -4
- package/lib/clipboard/ui/CopyableValue.d.ts +12 -9
- package/lib/clipboard/ui/CopyableValue.d.ts.map +1 -1
- package/lib/clipboard/ui/CopyableValue.js +11 -15
- package/lib/clipboard/ui/CopyableValue.js.map +1 -1
- package/lib/deploy/core/checkDeploy.d.ts +3 -1
- package/lib/deploy/core/checkDeploy.d.ts.map +1 -1
- package/lib/deploy/core/checkDeploy.js +7 -1
- package/lib/deploy/core/checkDeploy.js.map +1 -1
- package/lib/deploy/core/index.d.ts +1 -0
- package/lib/deploy/core/index.d.ts.map +1 -1
- package/lib/deploy/core/index.js +1 -0
- package/lib/deploy/core/index.js.map +1 -1
- package/lib/deploy/core/pageCache.d.ts +24 -0
- package/lib/deploy/core/pageCache.d.ts.map +1 -0
- package/lib/deploy/core/pageCache.js +91 -0
- package/lib/deploy/core/pageCache.js.map +1 -0
- package/lib/deploy/core/types.d.ts +1 -1
- package/lib/deploy/core/types.d.ts.map +1 -1
- package/lib/deploy/core/types.js.map +1 -1
- package/package.json +1 -1
- package/src/clipboard/ui/CopyableValue.tsx +22 -35
- package/src/deploy/core/checkDeploy.ts +7 -1
- package/src/deploy/core/index.ts +1 -0
- package/src/deploy/core/pageCache.ts +120 -0
- package/src/deploy/core/types.ts +2 -1
package/bin/check-deploy.mjs
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
/*
|
|
3
3
|
* Report every way a repository would break, or silently mis-deploy, under the
|
|
4
4
|
* server's global deploy script: a compose file it would not find, an image with
|
|
5
|
-
* no selectable tag,
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* no selectable tag, a page the browser keeps across a deploy and then cannot
|
|
6
|
+
* run, an `IMAGE_NAME` naming a package this repository never publishes, an
|
|
7
|
+
* `env_file` that fails on a checkout with no `.env` yet, a service with no
|
|
8
|
+
* health probe to read, an `.env.example` that hides a mode.
|
|
8
9
|
*
|
|
9
10
|
* Wire it as an npm script, next to check-tokens:
|
|
10
11
|
* "check-deploy": "cheminfo-check-deploy"
|
|
@@ -25,6 +26,8 @@ import process from 'node:process';
|
|
|
25
26
|
|
|
26
27
|
import {
|
|
27
28
|
DEPLOY_COMPOSE_FILES,
|
|
29
|
+
NGINX_CONFIG,
|
|
30
|
+
PAGE_CACHE_CONFIG,
|
|
28
31
|
findDeployProblems,
|
|
29
32
|
} from '../lib/deploy/core/index.js';
|
|
30
33
|
|
|
@@ -67,7 +70,14 @@ if (imageName === undefined) {
|
|
|
67
70
|
// Every mode is read, present or not: a missing one is itself a problem.
|
|
68
71
|
const expected = composeFiles ?? DEPLOY_COMPOSE_FILES;
|
|
69
72
|
const files = [];
|
|
70
|
-
for (const path of [
|
|
73
|
+
for (const path of [
|
|
74
|
+
...expected,
|
|
75
|
+
'.env.example',
|
|
76
|
+
'.gitignore',
|
|
77
|
+
'Dockerfile',
|
|
78
|
+
PAGE_CACHE_CONFIG,
|
|
79
|
+
NGINX_CONFIG,
|
|
80
|
+
]) {
|
|
71
81
|
const text = read(path);
|
|
72
82
|
if (text !== undefined) files.push({ path, text });
|
|
73
83
|
}
|
|
@@ -3,7 +3,7 @@ import type { ReactElement, ReactNode } from 'react';
|
|
|
3
3
|
export interface CopyableValueProps {
|
|
4
4
|
/** What the value is, written above it, e.g. `InChIKey`. */
|
|
5
5
|
label: string;
|
|
6
|
-
/** The value itself, which is
|
|
6
|
+
/** The value itself, which is what a click on it copies. */
|
|
7
7
|
value: string;
|
|
8
8
|
/**
|
|
9
9
|
* A longer explanation of the label, shown on hover.
|
|
@@ -22,8 +22,9 @@ export interface CopyableValueProps {
|
|
|
22
22
|
*/
|
|
23
23
|
maxHeight?: number | string;
|
|
24
24
|
/**
|
|
25
|
-
* What the pointer and a screen reader are told the
|
|
26
|
-
*
|
|
25
|
+
* What the pointer and a screen reader are told, replacing the title built
|
|
26
|
+
* from the label and the value.
|
|
27
|
+
* @default undefined — `Copy the ${label} (${value})`
|
|
27
28
|
*/
|
|
28
29
|
copyTitle?: string;
|
|
29
30
|
/**
|
|
@@ -38,14 +39,16 @@ export interface CopyableValueProps {
|
|
|
38
39
|
className?: string;
|
|
39
40
|
}
|
|
40
41
|
/**
|
|
41
|
-
* One read-only value, named,
|
|
42
|
+
* One read-only value, named, copied by clicking it.
|
|
42
43
|
*
|
|
43
|
-
* A derived notation — a SMILES, an InChIKey, an accession
|
|
44
|
-
* somewhere else, so
|
|
45
|
-
*
|
|
46
|
-
*
|
|
44
|
+
* A derived notation — a SMILES, an InChIKey, an accession, a computed energy
|
|
45
|
+
* — is only useful somewhere else, so the value is the button that copies it:
|
|
46
|
+
* the cursor carries a clipboard over it, a click or `Enter` puts it on the
|
|
47
|
+
* clipboard, and a tick confirms. Nothing is drawn beside the label, which
|
|
48
|
+
* names the value and no more. An empty value reads as the missing marker and
|
|
49
|
+
* offers nothing to copy.
|
|
47
50
|
* @param props - See {@link CopyableValueProps}.
|
|
48
|
-
* @returns The labelled value
|
|
51
|
+
* @returns The labelled value.
|
|
49
52
|
*/
|
|
50
53
|
export declare function CopyableValue(props: CopyableValueProps): ReactElement;
|
|
51
54
|
//# sourceMappingURL=CopyableValue.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CopyableValue.d.ts","sourceRoot":"","sources":["../../../src/clipboard/ui/CopyableValue.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAiB,YAAY,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"CopyableValue.d.ts","sourceRoot":"","sources":["../../../src/clipboard/ui/CopyableValue.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAiB,YAAY,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AASpE,0DAA0D;AAC1D,MAAM,WAAW,kBAAkB;IACjC,4DAA4D;IAC5D,KAAK,EAAE,MAAM,CAAC;IACd,4DAA4D;IAC5D,KAAK,EAAE,MAAM,CAAC;IACd;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;OAIG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC5B;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,kBAAkB,GAAG,YAAY,CA0CrE"}
|
|
@@ -2,32 +2,28 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { MISSING_VALUE } from "../../format/core/missing.js";
|
|
3
3
|
import { TOKEN } from "../../tokens/core/familyTokens.js";
|
|
4
4
|
import { ClickToCopy } from "./ClickToCopy.js";
|
|
5
|
-
import { CopyButton } from "./CopyButton.js";
|
|
6
5
|
const DEFAULT_BLOCK_HEIGHT = 160;
|
|
7
6
|
/**
|
|
8
|
-
* One read-only value, named,
|
|
7
|
+
* One read-only value, named, copied by clicking it.
|
|
9
8
|
*
|
|
10
|
-
* A derived notation — a SMILES, an InChIKey, an accession
|
|
11
|
-
* somewhere else, so
|
|
12
|
-
*
|
|
13
|
-
*
|
|
9
|
+
* A derived notation — a SMILES, an InChIKey, an accession, a computed energy
|
|
10
|
+
* — is only useful somewhere else, so the value is the button that copies it:
|
|
11
|
+
* the cursor carries a clipboard over it, a click or `Enter` puts it on the
|
|
12
|
+
* clipboard, and a tick confirms. Nothing is drawn beside the label, which
|
|
13
|
+
* names the value and no more. An empty value reads as the missing marker and
|
|
14
|
+
* offers nothing to copy.
|
|
14
15
|
* @param props - See {@link CopyableValueProps}.
|
|
15
|
-
* @returns The labelled value
|
|
16
|
+
* @returns The labelled value.
|
|
16
17
|
*/
|
|
17
18
|
export function CopyableValue(props) {
|
|
18
|
-
const { label, value, hint, block = false, maxHeight = DEFAULT_BLOCK_HEIGHT, copyTitle
|
|
19
|
+
const { label, value, hint, block = false, maxHeight = DEFAULT_BLOCK_HEIGHT, copyTitle, children, className, } = props;
|
|
19
20
|
const isEmpty = value === '';
|
|
20
21
|
return (_jsxs("div", { className: className === undefined
|
|
21
22
|
? 'copyable-value'
|
|
22
|
-
: `copyable-value ${className}`, children: [
|
|
23
|
+
: `copyable-value ${className}`, children: [_jsx("span", { className: "copyable-value__label", style: hint === undefined ? LABEL_STYLE : HINTED_LABEL_STYLE, title: hint, children: label }), _jsx(ClickToCopy, { as: "code", value: value, label: label, title: copyTitle, disabled: isEmpty, className: "copyable-value__value", style: block ? { ...BLOCK_VALUE_STYLE, maxHeight } : VALUE_STYLE, children: isEmpty ? MISSING_VALUE : value }), children] }));
|
|
23
24
|
}
|
|
24
|
-
const HEADER_STYLE = {
|
|
25
|
-
display: 'flex',
|
|
26
|
-
alignItems: 'center',
|
|
27
|
-
justifyContent: 'space-between',
|
|
28
|
-
gap: 8,
|
|
29
|
-
};
|
|
30
25
|
const LABEL_STYLE = {
|
|
26
|
+
display: 'block',
|
|
31
27
|
color: TOKEN.textMuted,
|
|
32
28
|
fontSize: 12,
|
|
33
29
|
fontWeight: 600,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CopyableValue.js","sourceRoot":"","sources":["../../../src/clipboard/ui/CopyableValue.tsx"],"names":[],"mappings":";AAEA,OAAO,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAC7D,OAAO,EAAE,KAAK,EAAE,MAAM,mCAAmC,CAAC;AAE1D,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"CopyableValue.js","sourceRoot":"","sources":["../../../src/clipboard/ui/CopyableValue.tsx"],"names":[],"mappings":";AAEA,OAAO,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAC7D,OAAO,EAAE,KAAK,EAAE,MAAM,mCAAmC,CAAC;AAE1D,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAmB,CAAC;AAEhD,MAAM,oBAAoB,GAAG,GAAG,CAAC;AA0CjC;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,aAAa,CAAC,KAAyB;IACrD,MAAM,EACJ,KAAK,EACL,KAAK,EACL,IAAI,EACJ,KAAK,GAAG,KAAK,EACb,SAAS,GAAG,oBAAoB,EAChC,SAAS,EACT,QAAQ,EACR,SAAS,GACV,GAAG,KAAK,CAAC;IACV,MAAM,OAAO,GAAG,KAAK,KAAK,EAAE,CAAC;IAE7B,OAAO,CACL,eACE,SAAS,EACP,SAAS,KAAK,SAAS;YACrB,CAAC,CAAC,gBAAgB;YAClB,CAAC,CAAC,kBAAkB,SAAS,EAAE,aAGnC,eACE,SAAS,EAAC,uBAAuB,EACjC,KAAK,EAAE,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,kBAAkB,EAC5D,KAAK,EAAE,IAAI,YAEV,KAAK,GACD,EACP,KAAC,WAAW,IACV,EAAE,EAAC,MAAM,EACT,KAAK,EAAE,KAAK,EACZ,KAAK,EAAE,KAAK,EACZ,KAAK,EAAE,SAAS,EAChB,QAAQ,EAAE,OAAO,EACjB,SAAS,EAAC,uBAAuB,EACjC,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,GAAG,iBAAiB,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,WAAW,YAE/D,OAAO,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,KAAK,GACpB,EACb,QAAQ,IACL,CACP,CAAC;AACJ,CAAC;AAED,MAAM,WAAW,GAAG;IAClB,OAAO,EAAE,OAAO;IAChB,KAAK,EAAE,KAAK,CAAC,SAAS;IACtB,QAAQ,EAAE,EAAE;IACZ,UAAU,EAAE,GAAG;CACiB,CAAC;AAEnC,MAAM,kBAAkB,GAAG;IACzB,GAAG,WAAW;IACd,MAAM,EAAE,MAAM;CACkB,CAAC;AAEnC,MAAM,WAAW,GAAG;IAClB,OAAO,EAAE,OAAO;IAChB,UAAU,EAAE,gDAAgD;IAC5D,QAAQ,EAAE,EAAE;IACZ,YAAY,EAAE,UAAU;CACQ,CAAC;AAEnC,MAAM,iBAAiB,GAAG;IACxB,GAAG,WAAW;IACd,UAAU,EAAE,KAAK;IACjB,YAAY,EAAE,QAAQ;IACtB,QAAQ,EAAE,MAAM;CACgB,CAAC"}
|
|
@@ -8,7 +8,9 @@ import type { CheckDeployOptions, DeployFile, DeployProblem } from './types.ts';
|
|
|
8
8
|
* reports it healthy. So a repository has to hand it a selectable tag on every
|
|
9
9
|
* image we build, an `IMAGE_NAME` that names the package our own workflow
|
|
10
10
|
* publishes, a compose file that parses on a checkout with no `.env` yet, and a
|
|
11
|
-
* health probe to read.
|
|
11
|
+
* health probe to read. It also has to hand the browser a page the browser will
|
|
12
|
+
* not keep, or the next deploy leaves returning visitors on a blank one. Each
|
|
13
|
+
* of those is invisible until a deploy, which is the
|
|
12
14
|
* worst moment to discover it — an `env_file` without `required: false` fails
|
|
13
15
|
* before a container starts, and an `IMAGE_NAME` off by a suffix pulls a
|
|
14
16
|
* package that was never published.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"checkDeploy.d.ts","sourceRoot":"","sources":["../../../src/deploy/core/checkDeploy.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"checkDeploy.d.ts","sourceRoot":"","sources":["../../../src/deploy/core/checkDeploy.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,kBAAkB,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAGhF;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,SAAS,UAAU,EAAE,EAC5B,OAAO,GAAE,kBAAuB,GAC/B,aAAa,EAAE,CA6DjB"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readComposeFile } from "./composeFile.js";
|
|
2
2
|
import { readEnvExample } from "./envExample.js";
|
|
3
|
+
import { readPageCache } from "./pageCache.js";
|
|
3
4
|
import { DEPLOY_COMPOSE_FILES } from "./types.js";
|
|
4
5
|
/**
|
|
5
6
|
* Every way a repository would break, or silently mis-deploy, under the
|
|
@@ -10,7 +11,9 @@ import { DEPLOY_COMPOSE_FILES } from "./types.js";
|
|
|
10
11
|
* reports it healthy. So a repository has to hand it a selectable tag on every
|
|
11
12
|
* image we build, an `IMAGE_NAME` that names the package our own workflow
|
|
12
13
|
* publishes, a compose file that parses on a checkout with no `.env` yet, and a
|
|
13
|
-
* health probe to read.
|
|
14
|
+
* health probe to read. It also has to hand the browser a page the browser will
|
|
15
|
+
* not keep, or the next deploy leaves returning visitors on a blank one. Each
|
|
16
|
+
* of those is invisible until a deploy, which is the
|
|
14
17
|
* worst moment to discover it — an `env_file` without `required: false` fails
|
|
15
18
|
* before a container starts, and an `IMAGE_NAME` off by a suffix pulls a
|
|
16
19
|
* package that was never published.
|
|
@@ -59,6 +62,9 @@ export function findDeployProblems(files, options = {}) {
|
|
|
59
62
|
else {
|
|
60
63
|
readEnvExample(envExample, expected, problems);
|
|
61
64
|
}
|
|
65
|
+
const dockerfile = byPath.get('Dockerfile');
|
|
66
|
+
if (dockerfile !== undefined)
|
|
67
|
+
readPageCache(dockerfile, byPath, problems);
|
|
62
68
|
const gitignore = byPath.get('.gitignore');
|
|
63
69
|
if (gitignore === undefined || !/^\s*\/?\.deploy\/?\s*$/m.test(gitignore)) {
|
|
64
70
|
problems.push({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"checkDeploy.js","sourceRoot":"","sources":["../../../src/deploy/core/checkDeploy.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"checkDeploy.js","sourceRoot":"","sources":["../../../src/deploy/core/checkDeploy.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAE/C,OAAO,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAElD;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,kBAAkB,CAChC,KAA4B,EAC5B,UAA8B,EAAE;IAEhC,MAAM,QAAQ,GAAG,OAAO,CAAC,YAAY,IAAI,oBAAoB,CAAC;IAC9D,MAAM,KAAK,GAAG;QACZ,SAAS,EAAE,OAAO,CAAC,SAAS;QAC5B,aAAa,EAAE,OAAO,CAAC,aAAa,IAAI,EAAE;QAC1C,kBAAkB,EAAE,OAAO,CAAC,kBAAkB,IAAI,IAAI;QACtD,iBAAiB,EAAE,CACjB,OAAO,CAAC,iBAAiB,IAAI,gBAAgB,CAC9C,CAAC,WAAW,EAAE;QACf,SAAS,EAAE,CAAC,OAAO,CAAC,SAAS,IAAI,QAAQ,CAAC,CAAC,WAAW,EAAE;KACzD,CAAC;IAEF,MAAM,MAAM,GAAG,IAAI,GAAG,EAAkB,CAAC;IACzC,KAAK,MAAM,IAAI,IAAI,KAAK;QAAE,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;IAE3D,MAAM,QAAQ,GAAoB,EAAE,CAAC;IAErC,KAAK,MAAM,IAAI,IAAI,QAAQ,EAAE,CAAC;QAC5B,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAC9B,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;YACvB,QAAQ,CAAC,IAAI,CAAC;gBACZ,IAAI,EAAE,IAAI;gBACV,IAAI,EAAE,CAAC;gBACP,IAAI,EAAE,iBAAiB;gBACvB,OAAO,EAAE,8CAA8C;gBACvD,IAAI,EAAE,UAAU,IAAI,yBAAyB;aAC9C,CAAC,CAAC;QACL,CAAC;aAAM,CAAC;YACN,eAAe,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,CAAC,CAAC;QAC/C,CAAC;IACH,CAAC;IAED,MAAM,UAAU,GAAG,MAAM,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAC9C,IAAI,UAAU,KAAK,SAAS,EAAE,CAAC;QAC7B,QAAQ,CAAC,IAAI,CAAC;YACZ,IAAI,EAAE,cAAc;YACpB,IAAI,EAAE,CAAC;YACP,IAAI,EAAE,qBAAqB;YAC3B,OAAO,EAAE,8CAA8C;YACvD,IAAI,EAAE,kFAAkF;SACzF,CAAC,CAAC;IACL,CAAC;SAAM,CAAC;QACN,cAAc,CAAC,UAAU,EAAE,QAAQ,EAAE,QAAQ,CAAC,CAAC;IACjD,CAAC;IAED,MAAM,UAAU,GAAG,MAAM,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;IAC5C,IAAI,UAAU,KAAK,SAAS;QAAE,aAAa,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC;IAE1E,MAAM,SAAS,GAAG,MAAM,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;IAC3C,IAAI,SAAS,KAAK,SAAS,IAAI,CAAC,yBAAyB,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC;QAC1E,QAAQ,CAAC,IAAI,CAAC;YACZ,IAAI,EAAE,YAAY;YAClB,IAAI,EAAE,CAAC;YACP,IAAI,EAAE,uBAAuB;YAC7B,OAAO,EACL,8EAA8E;YAChF,IAAI,EAAE,kCAAkC;SACzC,CAAC,CAAC;IACL,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { findDeployProblems } from './checkDeploy.ts';
|
|
2
2
|
export type { CheckDeployOptions, DeployFile, DeployProblem, DeployProblemKind, } from './types.ts';
|
|
3
|
+
export { NGINX_CONFIG, PAGE_CACHE_CONFIG } from './pageCache.ts';
|
|
3
4
|
export { DEPLOY_COMPOSE_FILES } from './types.ts';
|
|
4
5
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/deploy/core/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AACtD,YAAY,EACV,kBAAkB,EAClB,UAAU,EACV,aAAa,EACb,iBAAiB,GAClB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/deploy/core/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AACtD,YAAY,EACV,kBAAkB,EAClB,UAAU,EACV,aAAa,EACb,iBAAiB,GAClB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACjE,OAAO,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC"}
|
package/lib/deploy/core/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/deploy/core/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAOtD,OAAO,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/deploy/core/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAOtD,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACjE,OAAO,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { DeployProblem } from './types.ts';
|
|
2
|
+
/** The page cache policy of a static-web-server site, at the repository root. */
|
|
3
|
+
export declare const PAGE_CACHE_CONFIG = "sws.toml";
|
|
4
|
+
/** The nginx configuration of a site served by nginx. */
|
|
5
|
+
export declare const NGINX_CONFIG = "nginx.conf";
|
|
6
|
+
/**
|
|
7
|
+
* Report a server that lets a browser keep the page itself.
|
|
8
|
+
*
|
|
9
|
+
* A page names the hashed bundles of the build that wrote it, so a browser
|
|
10
|
+
* still holding a copy from an earlier deploy asks the new image for files it
|
|
11
|
+
* does not have; the request falls through to the page, the browser refuses to
|
|
12
|
+
* run HTML as a module, and the site comes up blank until the copy expires.
|
|
13
|
+
* Nothing on the server can see it and no test can either, because a test
|
|
14
|
+
* always loads a page and its bundles from the same build. So it is read off
|
|
15
|
+
* the server's configuration instead: static-web-server hands pages out with
|
|
16
|
+
* `max-age=86400` of its own, and nginx says nothing and lets the browser
|
|
17
|
+
* guess, so both have to be told. The bundles, named by their content, are the
|
|
18
|
+
* one thing worth keeping.
|
|
19
|
+
* @param dockerfile - The Dockerfile, which says what serves the build.
|
|
20
|
+
* @param byPath - Every file that was read, by repository path.
|
|
21
|
+
* @param problems - Where each problem found is appended.
|
|
22
|
+
*/
|
|
23
|
+
export declare function readPageCache(dockerfile: string, byPath: ReadonlyMap<string, string>, problems: DeployProblem[]): void;
|
|
24
|
+
//# sourceMappingURL=pageCache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pageCache.d.ts","sourceRoot":"","sources":["../../../src/deploy/core/pageCache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD,iFAAiF;AACjF,eAAO,MAAM,iBAAiB,aAAa,CAAC;AAE5C,yDAAyD;AACzD,eAAO,MAAM,YAAY,eAAe,CAAC;AAKzC;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,aAAa,CAC3B,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,EACnC,QAAQ,EAAE,aAAa,EAAE,GACxB,IAAI,CAMN"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/** The page cache policy of a static-web-server site, at the repository root. */
|
|
2
|
+
export const PAGE_CACHE_CONFIG = 'sws.toml';
|
|
3
|
+
/** The nginx configuration of a site served by nginx. */
|
|
4
|
+
export const NGINX_CONFIG = 'nginx.conf';
|
|
5
|
+
const NO_CACHE = /cache-control[\s:=]+["']?[^"'\n]*(?:no-cache|no-store|max-age=0)/i;
|
|
6
|
+
/**
|
|
7
|
+
* Report a server that lets a browser keep the page itself.
|
|
8
|
+
*
|
|
9
|
+
* A page names the hashed bundles of the build that wrote it, so a browser
|
|
10
|
+
* still holding a copy from an earlier deploy asks the new image for files it
|
|
11
|
+
* does not have; the request falls through to the page, the browser refuses to
|
|
12
|
+
* run HTML as a module, and the site comes up blank until the copy expires.
|
|
13
|
+
* Nothing on the server can see it and no test can either, because a test
|
|
14
|
+
* always loads a page and its bundles from the same build. So it is read off
|
|
15
|
+
* the server's configuration instead: static-web-server hands pages out with
|
|
16
|
+
* `max-age=86400` of its own, and nginx says nothing and lets the browser
|
|
17
|
+
* guess, so both have to be told. The bundles, named by their content, are the
|
|
18
|
+
* one thing worth keeping.
|
|
19
|
+
* @param dockerfile - The Dockerfile, which says what serves the build.
|
|
20
|
+
* @param byPath - Every file that was read, by repository path.
|
|
21
|
+
* @param problems - Where each problem found is appended.
|
|
22
|
+
*/
|
|
23
|
+
export function readPageCache(dockerfile, byPath, problems) {
|
|
24
|
+
if (dockerfile.includes('static-web-server')) {
|
|
25
|
+
readStaticWebServer(dockerfile, byPath, problems);
|
|
26
|
+
}
|
|
27
|
+
else if (/^FROM\s+\S*nginx/im.test(dockerfile)) {
|
|
28
|
+
readNginx(byPath, problems);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function readStaticWebServer(dockerfile, byPath, problems) {
|
|
32
|
+
const named = /^\s*ENV\s+SERVER_CONFIG_FILE=(?<path>\S+)/m.exec(dockerfile);
|
|
33
|
+
const destination = named?.groups?.path;
|
|
34
|
+
if (destination === undefined) {
|
|
35
|
+
problems.push({
|
|
36
|
+
file: 'Dockerfile',
|
|
37
|
+
line: lineOf(dockerfile, /static-web-server/),
|
|
38
|
+
kind: 'cacheable-page',
|
|
39
|
+
message: 'static-web-server keeps every page in the browser for a day, so each deploy blanks the site for anyone who came by before it',
|
|
40
|
+
hint: `COPY ${PAGE_CACHE_CONFIG} /etc/sws.toml and set SERVER_CONFIG_FILE to it`,
|
|
41
|
+
});
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const copied = new RegExp(String.raw `^\s*COPY\s+(?:--\S+\s+)*${escapeRegExp(PAGE_CACHE_CONFIG)}\s+${escapeRegExp(destination)}\s*$`, 'm');
|
|
45
|
+
if (!copied.test(dockerfile)) {
|
|
46
|
+
problems.push({
|
|
47
|
+
file: 'Dockerfile',
|
|
48
|
+
line: lineOf(dockerfile, /^\s*ENV\s+SERVER_CONFIG_FILE=/m),
|
|
49
|
+
kind: 'cacheable-page',
|
|
50
|
+
message: `SERVER_CONFIG_FILE names ${destination}, which nothing puts there`,
|
|
51
|
+
hint: `COPY ${PAGE_CACHE_CONFIG} ${destination}`,
|
|
52
|
+
});
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
readNoCacheRule(PAGE_CACHE_CONFIG, byPath, problems);
|
|
56
|
+
}
|
|
57
|
+
function readNginx(byPath, problems) {
|
|
58
|
+
readNoCacheRule(NGINX_CONFIG, byPath, problems);
|
|
59
|
+
}
|
|
60
|
+
function readNoCacheRule(path, byPath, problems) {
|
|
61
|
+
const text = byPath.get(path);
|
|
62
|
+
if (text === undefined) {
|
|
63
|
+
problems.push({
|
|
64
|
+
file: path,
|
|
65
|
+
line: 0,
|
|
66
|
+
kind: 'cacheable-page',
|
|
67
|
+
message: 'the server has no page cache policy to read',
|
|
68
|
+
hint: `commit ${path} at the repository root, telling the browser to check the page on every visit`,
|
|
69
|
+
});
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (!NO_CACHE.test(text)) {
|
|
73
|
+
problems.push({
|
|
74
|
+
file: path,
|
|
75
|
+
line: 0,
|
|
76
|
+
kind: 'cacheable-page',
|
|
77
|
+
message: 'no rule stops a browser from keeping the page between deploys',
|
|
78
|
+
hint: 'give the page Cache-Control: no-cache, and keep the long life for the hashed bundles alone',
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function lineOf(text, pattern) {
|
|
83
|
+
const match = pattern.exec(text);
|
|
84
|
+
if (match === null)
|
|
85
|
+
return 0;
|
|
86
|
+
return text.slice(0, match.index).split('\n').length;
|
|
87
|
+
}
|
|
88
|
+
function escapeRegExp(value) {
|
|
89
|
+
return value.replaceAll(/[$()*+.?[\\\]^{|}]/g, String.raw `\$&`);
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=pageCache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pageCache.js","sourceRoot":"","sources":["../../../src/deploy/core/pageCache.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** What a repository got wrong against the deployment contract. */
|
|
2
|
-
export type DeployProblemKind = 'missing-compose' | 'invalid-compose' | 'unpinned-image' | 'missing-build' | 'wrong-image-name' | 'required-env-file' | 'floating-sidecar' | 'missing-healthcheck' | 'missing-env-example' | 'missing-deploy-ignore';
|
|
2
|
+
export type DeployProblemKind = 'missing-compose' | 'invalid-compose' | 'unpinned-image' | 'missing-build' | 'wrong-image-name' | 'required-env-file' | 'floating-sidecar' | 'missing-healthcheck' | 'missing-env-example' | 'missing-deploy-ignore' | 'cacheable-page';
|
|
3
3
|
/** One way a repository would fail, or silently mis-deploy, on the server. */
|
|
4
4
|
export interface DeployProblem {
|
|
5
5
|
/** The file it was found in, named as the caller named it. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/deploy/core/types.ts"],"names":[],"mappings":"AAAA,mEAAmE;AACnE,MAAM,MAAM,iBAAiB,GACzB,iBAAiB,GACjB,iBAAiB,GACjB,gBAAgB,GAChB,eAAe,GACf,kBAAkB,GAClB,mBAAmB,GACnB,kBAAkB,GAClB,qBAAqB,GACrB,qBAAqB,GACrB,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/deploy/core/types.ts"],"names":[],"mappings":"AAAA,mEAAmE;AACnE,MAAM,MAAM,iBAAiB,GACzB,iBAAiB,GACjB,iBAAiB,GACjB,gBAAgB,GAChB,eAAe,GACf,kBAAkB,GAClB,mBAAmB,GACnB,kBAAkB,GAClB,qBAAqB,GACrB,qBAAqB,GACrB,uBAAuB,GACvB,gBAAgB,CAAC;AAErB,8EAA8E;AAC9E,MAAM,WAAW,aAAa;IAC5B,8DAA8D;IAC9D,IAAI,EAAE,MAAM,CAAC;IACb,6EAA6E;IAC7E,IAAI,EAAE,MAAM,CAAC;IACb,kCAAkC;IAClC,IAAI,EAAE,iBAAiB,CAAC;IACxB,kCAAkC;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,0CAA0C;IAC1C,IAAI,EAAE,MAAM,CAAC;CACd;AAED,sEAAsE;AACtE,MAAM,WAAW,UAAU;IACzB,iDAAiD;IACjD,IAAI,EAAE,MAAM,CAAC;IACb,mBAAmB;IACnB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,4CAA4C;AAC5C,MAAM,WAAW,kBAAkB;IACjC;;;;;OAKG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,aAAa,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC;;;;OAIG;IACH,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B;;;;;OAKG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;GAGG;AACH,eAAO,MAAM,oBAAoB,EAAE,SAAS,MAAM,EAIjD,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/deploy/core/types.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/deploy/core/types.ts"],"names":[],"mappings":"AA+EA;;;GAGG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAsB;IACrD,cAAc;IACd,sBAAsB;IACtB,0BAA0B;CAC3B,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-cheminfo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.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",
|
|
@@ -4,7 +4,6 @@ import { MISSING_VALUE } from '../../format/core/missing.ts';
|
|
|
4
4
|
import { TOKEN } from '../../tokens/core/familyTokens.ts';
|
|
5
5
|
|
|
6
6
|
import { ClickToCopy } from './ClickToCopy.tsx';
|
|
7
|
-
import { CopyButton } from './CopyButton.tsx';
|
|
8
7
|
|
|
9
8
|
const DEFAULT_BLOCK_HEIGHT = 160;
|
|
10
9
|
|
|
@@ -12,7 +11,7 @@ const DEFAULT_BLOCK_HEIGHT = 160;
|
|
|
12
11
|
export interface CopyableValueProps {
|
|
13
12
|
/** What the value is, written above it, e.g. `InChIKey`. */
|
|
14
13
|
label: string;
|
|
15
|
-
/** The value itself, which is
|
|
14
|
+
/** The value itself, which is what a click on it copies. */
|
|
16
15
|
value: string;
|
|
17
16
|
/**
|
|
18
17
|
* A longer explanation of the label, shown on hover.
|
|
@@ -31,8 +30,9 @@ export interface CopyableValueProps {
|
|
|
31
30
|
*/
|
|
32
31
|
maxHeight?: number | string;
|
|
33
32
|
/**
|
|
34
|
-
* What the pointer and a screen reader are told the
|
|
35
|
-
*
|
|
33
|
+
* What the pointer and a screen reader are told, replacing the title built
|
|
34
|
+
* from the label and the value.
|
|
35
|
+
* @default undefined — `Copy the ${label} (${value})`
|
|
36
36
|
*/
|
|
37
37
|
copyTitle?: string;
|
|
38
38
|
/**
|
|
@@ -48,14 +48,16 @@ export interface CopyableValueProps {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
/**
|
|
51
|
-
* One read-only value, named,
|
|
51
|
+
* One read-only value, named, copied by clicking it.
|
|
52
52
|
*
|
|
53
|
-
* A derived notation — a SMILES, an InChIKey, an accession
|
|
54
|
-
* somewhere else, so
|
|
55
|
-
*
|
|
56
|
-
*
|
|
53
|
+
* A derived notation — a SMILES, an InChIKey, an accession, a computed energy
|
|
54
|
+
* — is only useful somewhere else, so the value is the button that copies it:
|
|
55
|
+
* the cursor carries a clipboard over it, a click or `Enter` puts it on the
|
|
56
|
+
* clipboard, and a tick confirms. Nothing is drawn beside the label, which
|
|
57
|
+
* names the value and no more. An empty value reads as the missing marker and
|
|
58
|
+
* offers nothing to copy.
|
|
57
59
|
* @param props - See {@link CopyableValueProps}.
|
|
58
|
-
* @returns The labelled value
|
|
60
|
+
* @returns The labelled value.
|
|
59
61
|
*/
|
|
60
62
|
export function CopyableValue(props: CopyableValueProps): ReactElement {
|
|
61
63
|
const {
|
|
@@ -64,7 +66,7 @@ export function CopyableValue(props: CopyableValueProps): ReactElement {
|
|
|
64
66
|
hint,
|
|
65
67
|
block = false,
|
|
66
68
|
maxHeight = DEFAULT_BLOCK_HEIGHT,
|
|
67
|
-
copyTitle
|
|
69
|
+
copyTitle,
|
|
68
70
|
children,
|
|
69
71
|
className,
|
|
70
72
|
} = props;
|
|
@@ -78,27 +80,18 @@ export function CopyableValue(props: CopyableValueProps): ReactElement {
|
|
|
78
80
|
: `copyable-value ${className}`
|
|
79
81
|
}
|
|
80
82
|
>
|
|
81
|
-
<
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
</span>
|
|
89
|
-
<CopyButton
|
|
90
|
-
content={value}
|
|
91
|
-
title={copyTitle}
|
|
92
|
-
disabled={isEmpty}
|
|
93
|
-
minimal
|
|
94
|
-
small
|
|
95
|
-
/>
|
|
96
|
-
</div>
|
|
83
|
+
<span
|
|
84
|
+
className="copyable-value__label"
|
|
85
|
+
style={hint === undefined ? LABEL_STYLE : HINTED_LABEL_STYLE}
|
|
86
|
+
title={hint}
|
|
87
|
+
>
|
|
88
|
+
{label}
|
|
89
|
+
</span>
|
|
97
90
|
<ClickToCopy
|
|
98
91
|
as="code"
|
|
99
92
|
value={value}
|
|
100
93
|
label={label}
|
|
101
|
-
|
|
94
|
+
title={copyTitle}
|
|
102
95
|
disabled={isEmpty}
|
|
103
96
|
className="copyable-value__value"
|
|
104
97
|
style={block ? { ...BLOCK_VALUE_STYLE, maxHeight } : VALUE_STYLE}
|
|
@@ -110,14 +103,8 @@ export function CopyableValue(props: CopyableValueProps): ReactElement {
|
|
|
110
103
|
);
|
|
111
104
|
}
|
|
112
105
|
|
|
113
|
-
const HEADER_STYLE = {
|
|
114
|
-
display: 'flex',
|
|
115
|
-
alignItems: 'center',
|
|
116
|
-
justifyContent: 'space-between',
|
|
117
|
-
gap: 8,
|
|
118
|
-
} as const satisfies CSSProperties;
|
|
119
|
-
|
|
120
106
|
const LABEL_STYLE = {
|
|
107
|
+
display: 'block',
|
|
121
108
|
color: TOKEN.textMuted,
|
|
122
109
|
fontSize: 12,
|
|
123
110
|
fontWeight: 600,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readComposeFile } from './composeFile.ts';
|
|
2
2
|
import { readEnvExample } from './envExample.ts';
|
|
3
|
+
import { readPageCache } from './pageCache.ts';
|
|
3
4
|
import type { CheckDeployOptions, DeployFile, DeployProblem } from './types.ts';
|
|
4
5
|
import { DEPLOY_COMPOSE_FILES } from './types.ts';
|
|
5
6
|
|
|
@@ -12,7 +13,9 @@ import { DEPLOY_COMPOSE_FILES } from './types.ts';
|
|
|
12
13
|
* reports it healthy. So a repository has to hand it a selectable tag on every
|
|
13
14
|
* image we build, an `IMAGE_NAME` that names the package our own workflow
|
|
14
15
|
* publishes, a compose file that parses on a checkout with no `.env` yet, and a
|
|
15
|
-
* health probe to read.
|
|
16
|
+
* health probe to read. It also has to hand the browser a page the browser will
|
|
17
|
+
* not keep, or the next deploy leaves returning visitors on a blank one. Each
|
|
18
|
+
* of those is invisible until a deploy, which is the
|
|
16
19
|
* worst moment to discover it — an `env_file` without `required: false` fails
|
|
17
20
|
* before a container starts, and an `IMAGE_NAME` off by a suffix pulls a
|
|
18
21
|
* package that was never published.
|
|
@@ -68,6 +71,9 @@ export function findDeployProblems(
|
|
|
68
71
|
readEnvExample(envExample, expected, problems);
|
|
69
72
|
}
|
|
70
73
|
|
|
74
|
+
const dockerfile = byPath.get('Dockerfile');
|
|
75
|
+
if (dockerfile !== undefined) readPageCache(dockerfile, byPath, problems);
|
|
76
|
+
|
|
71
77
|
const gitignore = byPath.get('.gitignore');
|
|
72
78
|
if (gitignore === undefined || !/^\s*\/?\.deploy\/?\s*$/m.test(gitignore)) {
|
|
73
79
|
problems.push({
|
package/src/deploy/core/index.ts
CHANGED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import type { DeployProblem } from './types.ts';
|
|
2
|
+
|
|
3
|
+
/** The page cache policy of a static-web-server site, at the repository root. */
|
|
4
|
+
export const PAGE_CACHE_CONFIG = 'sws.toml';
|
|
5
|
+
|
|
6
|
+
/** The nginx configuration of a site served by nginx. */
|
|
7
|
+
export const NGINX_CONFIG = 'nginx.conf';
|
|
8
|
+
|
|
9
|
+
const NO_CACHE =
|
|
10
|
+
/cache-control[\s:=]+["']?[^"'\n]*(?:no-cache|no-store|max-age=0)/i;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Report a server that lets a browser keep the page itself.
|
|
14
|
+
*
|
|
15
|
+
* A page names the hashed bundles of the build that wrote it, so a browser
|
|
16
|
+
* still holding a copy from an earlier deploy asks the new image for files it
|
|
17
|
+
* does not have; the request falls through to the page, the browser refuses to
|
|
18
|
+
* run HTML as a module, and the site comes up blank until the copy expires.
|
|
19
|
+
* Nothing on the server can see it and no test can either, because a test
|
|
20
|
+
* always loads a page and its bundles from the same build. So it is read off
|
|
21
|
+
* the server's configuration instead: static-web-server hands pages out with
|
|
22
|
+
* `max-age=86400` of its own, and nginx says nothing and lets the browser
|
|
23
|
+
* guess, so both have to be told. The bundles, named by their content, are the
|
|
24
|
+
* one thing worth keeping.
|
|
25
|
+
* @param dockerfile - The Dockerfile, which says what serves the build.
|
|
26
|
+
* @param byPath - Every file that was read, by repository path.
|
|
27
|
+
* @param problems - Where each problem found is appended.
|
|
28
|
+
*/
|
|
29
|
+
export function readPageCache(
|
|
30
|
+
dockerfile: string,
|
|
31
|
+
byPath: ReadonlyMap<string, string>,
|
|
32
|
+
problems: DeployProblem[],
|
|
33
|
+
): void {
|
|
34
|
+
if (dockerfile.includes('static-web-server')) {
|
|
35
|
+
readStaticWebServer(dockerfile, byPath, problems);
|
|
36
|
+
} else if (/^FROM\s+\S*nginx/im.test(dockerfile)) {
|
|
37
|
+
readNginx(byPath, problems);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function readStaticWebServer(
|
|
42
|
+
dockerfile: string,
|
|
43
|
+
byPath: ReadonlyMap<string, string>,
|
|
44
|
+
problems: DeployProblem[],
|
|
45
|
+
): void {
|
|
46
|
+
const named = /^\s*ENV\s+SERVER_CONFIG_FILE=(?<path>\S+)/m.exec(dockerfile);
|
|
47
|
+
const destination = named?.groups?.path;
|
|
48
|
+
if (destination === undefined) {
|
|
49
|
+
problems.push({
|
|
50
|
+
file: 'Dockerfile',
|
|
51
|
+
line: lineOf(dockerfile, /static-web-server/),
|
|
52
|
+
kind: 'cacheable-page',
|
|
53
|
+
message:
|
|
54
|
+
'static-web-server keeps every page in the browser for a day, so each deploy blanks the site for anyone who came by before it',
|
|
55
|
+
hint: `COPY ${PAGE_CACHE_CONFIG} /etc/sws.toml and set SERVER_CONFIG_FILE to it`,
|
|
56
|
+
});
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const copied = new RegExp(
|
|
61
|
+
String.raw`^\s*COPY\s+(?:--\S+\s+)*${escapeRegExp(PAGE_CACHE_CONFIG)}\s+${escapeRegExp(destination)}\s*$`,
|
|
62
|
+
'm',
|
|
63
|
+
);
|
|
64
|
+
if (!copied.test(dockerfile)) {
|
|
65
|
+
problems.push({
|
|
66
|
+
file: 'Dockerfile',
|
|
67
|
+
line: lineOf(dockerfile, /^\s*ENV\s+SERVER_CONFIG_FILE=/m),
|
|
68
|
+
kind: 'cacheable-page',
|
|
69
|
+
message: `SERVER_CONFIG_FILE names ${destination}, which nothing puts there`,
|
|
70
|
+
hint: `COPY ${PAGE_CACHE_CONFIG} ${destination}`,
|
|
71
|
+
});
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
readNoCacheRule(PAGE_CACHE_CONFIG, byPath, problems);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function readNginx(
|
|
79
|
+
byPath: ReadonlyMap<string, string>,
|
|
80
|
+
problems: DeployProblem[],
|
|
81
|
+
): void {
|
|
82
|
+
readNoCacheRule(NGINX_CONFIG, byPath, problems);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function readNoCacheRule(
|
|
86
|
+
path: string,
|
|
87
|
+
byPath: ReadonlyMap<string, string>,
|
|
88
|
+
problems: DeployProblem[],
|
|
89
|
+
): void {
|
|
90
|
+
const text = byPath.get(path);
|
|
91
|
+
if (text === undefined) {
|
|
92
|
+
problems.push({
|
|
93
|
+
file: path,
|
|
94
|
+
line: 0,
|
|
95
|
+
kind: 'cacheable-page',
|
|
96
|
+
message: 'the server has no page cache policy to read',
|
|
97
|
+
hint: `commit ${path} at the repository root, telling the browser to check the page on every visit`,
|
|
98
|
+
});
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (!NO_CACHE.test(text)) {
|
|
102
|
+
problems.push({
|
|
103
|
+
file: path,
|
|
104
|
+
line: 0,
|
|
105
|
+
kind: 'cacheable-page',
|
|
106
|
+
message: 'no rule stops a browser from keeping the page between deploys',
|
|
107
|
+
hint: 'give the page Cache-Control: no-cache, and keep the long life for the hashed bundles alone',
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function lineOf(text: string, pattern: RegExp): number {
|
|
113
|
+
const match = pattern.exec(text);
|
|
114
|
+
if (match === null) return 0;
|
|
115
|
+
return text.slice(0, match.index).split('\n').length;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function escapeRegExp(value: string): string {
|
|
119
|
+
return value.replaceAll(/[$()*+.?[\\\]^{|}]/g, String.raw`\$&`);
|
|
120
|
+
}
|
package/src/deploy/core/types.ts
CHANGED
|
@@ -9,7 +9,8 @@ export type DeployProblemKind =
|
|
|
9
9
|
| 'floating-sidecar'
|
|
10
10
|
| 'missing-healthcheck'
|
|
11
11
|
| 'missing-env-example'
|
|
12
|
-
| 'missing-deploy-ignore'
|
|
12
|
+
| 'missing-deploy-ignore'
|
|
13
|
+
| 'cacheable-page';
|
|
13
14
|
|
|
14
15
|
/** One way a repository would fail, or silently mis-deploy, on the server. */
|
|
15
16
|
export interface DeployProblem {
|