react-cheminfo 0.41.0 → 0.43.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 +3 -1
- package/lib/chrome/ui/AboutButton.d.ts +59 -0
- package/lib/chrome/ui/AboutButton.d.ts.map +1 -0
- package/lib/chrome/ui/AboutButton.js +29 -0
- package/lib/chrome/ui/AboutButton.js.map +1 -0
- package/lib/chrome/ui/index.d.ts +2 -0
- package/lib/chrome/ui/index.d.ts.map +1 -1
- package/lib/chrome/ui/index.js +1 -0
- package/lib/chrome/ui/index.js.map +1 -1
- package/lib/deploy/core/buildGate.d.ts.map +1 -1
- package/lib/deploy/core/buildGate.js +4 -34
- package/lib/deploy/core/buildGate.js.map +1 -1
- package/lib/deploy/core/checkDeploy.d.ts.map +1 -1
- package/lib/deploy/core/checkDeploy.js +2 -0
- 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/json.d.ts +27 -0
- package/lib/deploy/core/json.d.ts.map +1 -0
- package/lib/deploy/core/json.js +56 -0
- package/lib/deploy/core/json.js.map +1 -0
- package/lib/deploy/core/reactCopies.d.ts +32 -0
- package/lib/deploy/core/reactCopies.d.ts.map +1 -0
- package/lib/deploy/core/reactCopies.js +212 -0
- package/lib/deploy/core/reactCopies.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/lib/ecosystem/core/sites.d.ts.map +1 -1
- package/lib/ecosystem/core/sites.js +12 -0
- package/lib/ecosystem/core/sites.js.map +1 -1
- package/lib/ecosystem/core/types.d.ts +1 -1
- package/lib/ecosystem/core/types.d.ts.map +1 -1
- package/lib/ecosystem/ui/glyphs.d.ts.map +1 -1
- package/lib/ecosystem/ui/glyphs.js +4 -0
- package/lib/ecosystem/ui/glyphs.js.map +1 -1
- package/lib/i18n/core/chromeCatalog.d.ts +1 -1
- package/lib/i18n/core/chromeCatalog.d.ts.map +1 -1
- package/lib/locales/de.json +2 -0
- package/lib/locales/en.json +2 -0
- package/lib/locales/es.json +2 -0
- package/lib/locales/fr.json +2 -0
- package/lib/locales/it.json +2 -0
- package/lib/periodic/ui/ElementCell.d.ts +22 -2
- package/lib/periodic/ui/ElementCell.d.ts.map +1 -1
- package/lib/periodic/ui/ElementCell.js +20 -145
- package/lib/periodic/ui/ElementCell.js.map +1 -1
- package/lib/periodic/ui/PeriodicTable.d.ts +7 -0
- package/lib/periodic/ui/PeriodicTable.d.ts.map +1 -1
- package/lib/periodic/ui/PeriodicTable.js +21 -3
- package/lib/periodic/ui/PeriodicTable.js.map +1 -1
- package/lib/periodic/ui/cellStyles.d.ts +79 -0
- package/lib/periodic/ui/cellStyles.d.ts.map +1 -0
- package/lib/periodic/ui/cellStyles.js +90 -0
- package/lib/periodic/ui/cellStyles.js.map +1 -0
- package/lib/periodic/ui/cellType.d.ts +106 -0
- package/lib/periodic/ui/cellType.d.ts.map +1 -0
- package/lib/periodic/ui/cellType.js +181 -0
- package/lib/periodic/ui/cellType.js.map +1 -0
- package/lib/periodic/ui/glyphWidths.d.ts +11 -0
- package/lib/periodic/ui/glyphWidths.d.ts.map +1 -0
- package/lib/periodic/ui/glyphWidths.js +69 -0
- package/lib/periodic/ui/glyphWidths.js.map +1 -0
- package/package.json +9 -1
- package/src/chrome/ui/AboutButton.tsx +90 -0
- package/src/chrome/ui/index.ts +2 -0
- package/src/deploy/core/buildGate.ts +4 -35
- package/src/deploy/core/checkDeploy.ts +7 -0
- package/src/deploy/core/index.ts +1 -0
- package/src/deploy/core/json.ts +53 -0
- package/src/deploy/core/reactCopies.ts +243 -0
- package/src/deploy/core/types.ts +5 -1
- package/src/ecosystem/core/sites.ts +13 -0
- package/src/ecosystem/core/types.ts +2 -1
- package/src/ecosystem/ui/glyphs.tsx +16 -0
- package/src/locales/de.json +2 -0
- package/src/locales/en.json +2 -0
- package/src/locales/es.json +2 -0
- package/src/locales/fr.json +2 -0
- package/src/locales/it.json +2 -0
- package/src/periodic/ui/ElementCell.tsx +69 -160
- package/src/periodic/ui/PeriodicTable.tsx +37 -2
- package/src/periodic/ui/cellStyles.ts +100 -0
- package/src/periodic/ui/cellType.ts +223 -0
- package/src/periodic/ui/glyphWidths.ts +68 -0
- package/styles/chrome.css +10 -2
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What each letter advances, in ems, in the family's type.
|
|
3
|
+
*
|
|
4
|
+
* Read off the rendered page at 100px, rounded up, because sorting a glyph into
|
|
5
|
+
* one of four width classes puts `Tm` an eighth wider than it is — which is a
|
|
6
|
+
* symbol written an eighth smaller than its cell can take. A platform
|
|
7
|
+
* substituting another face moves these by a few per cent, which the safety
|
|
8
|
+
* margin of `cellType` covers.
|
|
9
|
+
*/
|
|
10
|
+
export const ADVANCE = {
|
|
11
|
+
a: 0.5,
|
|
12
|
+
b: 0.57,
|
|
13
|
+
c: 0.52,
|
|
14
|
+
d: 0.57,
|
|
15
|
+
e: 0.52,
|
|
16
|
+
f: 0.33,
|
|
17
|
+
g: 0.56,
|
|
18
|
+
h: 0.54,
|
|
19
|
+
i: 0.22,
|
|
20
|
+
j: 0.22,
|
|
21
|
+
k: 0.5,
|
|
22
|
+
l: 0.23,
|
|
23
|
+
m: 0.8,
|
|
24
|
+
n: 0.54,
|
|
25
|
+
o: 0.54,
|
|
26
|
+
p: 0.56,
|
|
27
|
+
q: 0.56,
|
|
28
|
+
r: 0.35,
|
|
29
|
+
s: 0.47,
|
|
30
|
+
t: 0.33,
|
|
31
|
+
u: 0.54,
|
|
32
|
+
v: 0.5,
|
|
33
|
+
w: 0.72,
|
|
34
|
+
x: 0.48,
|
|
35
|
+
y: 0.5,
|
|
36
|
+
z: 0.5,
|
|
37
|
+
A: 0.62,
|
|
38
|
+
B: 0.6,
|
|
39
|
+
C: 0.66,
|
|
40
|
+
D: 0.67,
|
|
41
|
+
E: 0.55,
|
|
42
|
+
F: 0.53,
|
|
43
|
+
G: 0.69,
|
|
44
|
+
H: 0.69,
|
|
45
|
+
I: 0.24,
|
|
46
|
+
J: 0.49,
|
|
47
|
+
K: 0.6,
|
|
48
|
+
L: 0.52,
|
|
49
|
+
M: 0.81,
|
|
50
|
+
N: 0.69,
|
|
51
|
+
O: 0.71,
|
|
52
|
+
P: 0.59,
|
|
53
|
+
Q: 0.71,
|
|
54
|
+
R: 0.6,
|
|
55
|
+
S: 0.59,
|
|
56
|
+
T: 0.59,
|
|
57
|
+
U: 0.68,
|
|
58
|
+
V: 0.62,
|
|
59
|
+
W: 0.9,
|
|
60
|
+
X: 0.62,
|
|
61
|
+
Y: 0.6,
|
|
62
|
+
Z: 0.61,
|
|
63
|
+
' ': 0.27,
|
|
64
|
+
'-': 0.33,
|
|
65
|
+
'–': 0.54,
|
|
66
|
+
'.': 0.25,
|
|
67
|
+
',': 0.25,
|
|
68
|
+
};
|
|
69
|
+
//# sourceMappingURL=glyphWidths.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"glyphWidths.js","sourceRoot":"","sources":["../../../src/periodic/ui/glyphWidths.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,OAAO,GAA2B;IAC7C,CAAC,EAAE,GAAG;IACN,CAAC,EAAE,IAAI;IACP,CAAC,EAAE,IAAI;IACP,CAAC,EAAE,IAAI;IACP,CAAC,EAAE,IAAI;IACP,CAAC,EAAE,IAAI;IACP,CAAC,EAAE,IAAI;IACP,CAAC,EAAE,IAAI;IACP,CAAC,EAAE,IAAI;IACP,CAAC,EAAE,IAAI;IACP,CAAC,EAAE,GAAG;IACN,CAAC,EAAE,IAAI;IACP,CAAC,EAAE,GAAG;IACN,CAAC,EAAE,IAAI;IACP,CAAC,EAAE,IAAI;IACP,CAAC,EAAE,IAAI;IACP,CAAC,EAAE,IAAI;IACP,CAAC,EAAE,IAAI;IACP,CAAC,EAAE,IAAI;IACP,CAAC,EAAE,IAAI;IACP,CAAC,EAAE,IAAI;IACP,CAAC,EAAE,GAAG;IACN,CAAC,EAAE,IAAI;IACP,CAAC,EAAE,IAAI;IACP,CAAC,EAAE,GAAG;IACN,CAAC,EAAE,GAAG;IACN,CAAC,EAAE,IAAI;IACP,CAAC,EAAE,GAAG;IACN,CAAC,EAAE,IAAI;IACP,CAAC,EAAE,IAAI;IACP,CAAC,EAAE,IAAI;IACP,CAAC,EAAE,IAAI;IACP,CAAC,EAAE,IAAI;IACP,CAAC,EAAE,IAAI;IACP,CAAC,EAAE,IAAI;IACP,CAAC,EAAE,IAAI;IACP,CAAC,EAAE,GAAG;IACN,CAAC,EAAE,IAAI;IACP,CAAC,EAAE,IAAI;IACP,CAAC,EAAE,IAAI;IACP,CAAC,EAAE,IAAI;IACP,CAAC,EAAE,IAAI;IACP,CAAC,EAAE,IAAI;IACP,CAAC,EAAE,GAAG;IACN,CAAC,EAAE,IAAI;IACP,CAAC,EAAE,IAAI;IACP,CAAC,EAAE,IAAI;IACP,CAAC,EAAE,IAAI;IACP,CAAC,EAAE,GAAG;IACN,CAAC,EAAE,IAAI;IACP,CAAC,EAAE,GAAG;IACN,CAAC,EAAE,IAAI;IACP,GAAG,EAAE,IAAI;IACT,GAAG,EAAE,IAAI;IACT,GAAG,EAAE,IAAI;IACT,GAAG,EAAE,IAAI;IACT,GAAG,EAAE,IAAI;CACV,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-cheminfo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.43.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",
|
|
@@ -76,6 +76,7 @@
|
|
|
76
76
|
"react-markdown": "^10.1.0",
|
|
77
77
|
"rehype-raw": "^7.0.0",
|
|
78
78
|
"rehype-sanitize": "^6.0.0",
|
|
79
|
+
"semver": "^7.8.5",
|
|
79
80
|
"yaml": "^2.9.1"
|
|
80
81
|
},
|
|
81
82
|
"devDependencies": {
|
|
@@ -86,6 +87,7 @@
|
|
|
86
87
|
"@types/node": "^26.2.0",
|
|
87
88
|
"@types/react": "^19.2.18",
|
|
88
89
|
"@types/react-dom": "^19.2.4",
|
|
90
|
+
"@types/semver": "^7.8.0",
|
|
89
91
|
"@vitest/coverage-v8": "^4.1.10",
|
|
90
92
|
"@zakodium/tsconfig": "^1.0.5",
|
|
91
93
|
"benchmark": "^2.1.4",
|
|
@@ -154,5 +156,11 @@
|
|
|
154
156
|
"xtb-wasm": {
|
|
155
157
|
"optional": true
|
|
156
158
|
}
|
|
159
|
+
},
|
|
160
|
+
"overrides": {
|
|
161
|
+
"react": "^19.2.8",
|
|
162
|
+
"react-dom": "^19.2.8",
|
|
163
|
+
"@types/react": "^19.2.18",
|
|
164
|
+
"@types/react-dom": "^19.2.4"
|
|
157
165
|
}
|
|
158
166
|
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { ReactElement } from 'react';
|
|
2
|
+
|
|
3
|
+
import { useChromeT } from '../../i18n/ui/useT.ts';
|
|
4
|
+
|
|
5
|
+
import { NavLink } from './NavLink.tsx';
|
|
6
|
+
|
|
7
|
+
/** What the About entry of a header needs. */
|
|
8
|
+
export interface AboutButtonProps {
|
|
9
|
+
/**
|
|
10
|
+
* The site's About address, which the entry opens. It is written rather than
|
|
11
|
+
* assumed, because a deployment may sit under a mount path and a translated
|
|
12
|
+
* site under a language prefix.
|
|
13
|
+
*/
|
|
14
|
+
href: string;
|
|
15
|
+
/**
|
|
16
|
+
* Whether the About is the page on show, which is what the brand tint is
|
|
17
|
+
* spent on.
|
|
18
|
+
* @default false
|
|
19
|
+
*/
|
|
20
|
+
active?: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* What the site does when the entry is picked, for a page that routes in
|
|
23
|
+
* place. A modified click is left to the browser.
|
|
24
|
+
* @default undefined
|
|
25
|
+
*/
|
|
26
|
+
onSelect?: () => void;
|
|
27
|
+
/**
|
|
28
|
+
* What the entry reads. In a compact bar it is not written, but it stays what
|
|
29
|
+
* the pointer and a screen reader are told.
|
|
30
|
+
* @default the chrome's own word for it, in the language of the page
|
|
31
|
+
*/
|
|
32
|
+
label?: string;
|
|
33
|
+
/**
|
|
34
|
+
* What the pointer is told.
|
|
35
|
+
* @default the chrome's own sentence, in the language of the page
|
|
36
|
+
*/
|
|
37
|
+
title?: string;
|
|
38
|
+
/**
|
|
39
|
+
* What the active entry is named by, for a site whose router spells its
|
|
40
|
+
* addresses rather than its tabs.
|
|
41
|
+
* @default 'about'
|
|
42
|
+
*/
|
|
43
|
+
id?: string;
|
|
44
|
+
/**
|
|
45
|
+
* Class names added after the component's own.
|
|
46
|
+
* @default undefined
|
|
47
|
+
*/
|
|
48
|
+
className?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The About entry, which leads the utilities on every site of the family: a
|
|
53
|
+
* real address rather than a dialog, so the page is indexed, linkable and
|
|
54
|
+
* printable.
|
|
55
|
+
*
|
|
56
|
+
* The glyph is `info-sign` on every site, and is not a site's to choose. It is
|
|
57
|
+
* what the entry keeps once a narrow bar drops the labels, and the site's own
|
|
58
|
+
* mark already stands at the other end of the same bar — drawn twice, nothing
|
|
59
|
+
* in the row says which one is the site and which one is about it.
|
|
60
|
+
* @param props - Where the About is, whether it is on show, and how the site
|
|
61
|
+
* opens it.
|
|
62
|
+
* @returns The entry.
|
|
63
|
+
*/
|
|
64
|
+
export function AboutButton(props: AboutButtonProps): ReactElement {
|
|
65
|
+
const {
|
|
66
|
+
href,
|
|
67
|
+
active = false,
|
|
68
|
+
onSelect,
|
|
69
|
+
label,
|
|
70
|
+
title,
|
|
71
|
+
id = 'about',
|
|
72
|
+
className,
|
|
73
|
+
} = props;
|
|
74
|
+
const t = useChromeT();
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<NavLink
|
|
78
|
+
item={{
|
|
79
|
+
id,
|
|
80
|
+
label: label ?? t('chrome.about'),
|
|
81
|
+
icon: 'info-sign',
|
|
82
|
+
href,
|
|
83
|
+
title: title ?? t('chrome.aboutTitle'),
|
|
84
|
+
onSelect,
|
|
85
|
+
}}
|
|
86
|
+
active={active}
|
|
87
|
+
className={className}
|
|
88
|
+
/>
|
|
89
|
+
);
|
|
90
|
+
}
|
package/src/chrome/ui/index.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
export type { AboutButtonProps } from './AboutButton.tsx';
|
|
2
|
+
export { AboutButton } from './AboutButton.tsx';
|
|
1
3
|
export type { AccountButtonProps, AccountIdentity } from './AccountButton.tsx';
|
|
2
4
|
export { AccountButton } from './AccountButton.tsx';
|
|
3
5
|
export type { HeaderToggleOption, HeaderToggleProps } from './HeaderToggle.tsx';
|
|
@@ -1,8 +1,6 @@
|
|
|
1
|
+
import { lineOf, parseJsonObject, stringEntries } from './json.ts';
|
|
1
2
|
import type { DeployProblem } from './types.ts';
|
|
2
3
|
|
|
3
|
-
/** The scripts a repository's own `package.json` declares, as npm would run them. */
|
|
4
|
-
type Scripts = Record<string, string>;
|
|
5
|
-
|
|
6
4
|
/**
|
|
7
5
|
* Report a repository that can bundle, and so publish, code nothing tested.
|
|
8
6
|
*
|
|
@@ -24,8 +22,9 @@ export function readBuildGate(
|
|
|
24
22
|
problems: DeployProblem[],
|
|
25
23
|
): void {
|
|
26
24
|
if (packageJson === undefined) return;
|
|
27
|
-
const
|
|
28
|
-
if (
|
|
25
|
+
const parsed = parseJsonObject(packageJson);
|
|
26
|
+
if (parsed === undefined) return;
|
|
27
|
+
const scripts = stringEntries(parsed.scripts);
|
|
29
28
|
|
|
30
29
|
const build = scripts.build;
|
|
31
30
|
if (build !== undefined && !/\bnpm run test\b/.test(build)) {
|
|
@@ -90,33 +89,3 @@ function gatedBuildLine(dockerfile: string): number | undefined {
|
|
|
90
89
|
}
|
|
91
90
|
return undefined;
|
|
92
91
|
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* The scripts block of a `package.json`.
|
|
96
|
-
* @param text - The file, unparsed.
|
|
97
|
-
* @returns Its scripts, or undefined when the file is not readable as one.
|
|
98
|
-
*/
|
|
99
|
-
function parseScripts(text: string): Scripts | undefined {
|
|
100
|
-
let parsed: unknown;
|
|
101
|
-
try {
|
|
102
|
-
parsed = JSON.parse(text);
|
|
103
|
-
} catch {
|
|
104
|
-
return undefined;
|
|
105
|
-
}
|
|
106
|
-
if (typeof parsed !== 'object' || parsed === null) return undefined;
|
|
107
|
-
const scripts = (parsed as { scripts?: unknown }).scripts;
|
|
108
|
-
if (typeof scripts !== 'object' || scripts === null) return {};
|
|
109
|
-
return scripts as Scripts;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Where a report should point, so the reader lands on the script itself.
|
|
114
|
-
* @param text - The file to search.
|
|
115
|
-
* @param needle - The key to find.
|
|
116
|
-
* @returns Its line, counting from 1, or 0 when it is not there.
|
|
117
|
-
*/
|
|
118
|
-
function lineOf(text: string, needle: string): number {
|
|
119
|
-
const at = text.indexOf(needle);
|
|
120
|
-
if (at === -1) return 0;
|
|
121
|
-
return text.slice(0, at).split('\n').length;
|
|
122
|
-
}
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
readInstallRetries,
|
|
8
8
|
readPublishedImage,
|
|
9
9
|
} from './publishedImage.ts';
|
|
10
|
+
import { readReactCopies } from './reactCopies.ts';
|
|
10
11
|
import type { CheckDeployOptions, DeployFile, DeployProblem } from './types.ts';
|
|
11
12
|
import { DEPLOY_COMPOSE_FILES } from './types.ts';
|
|
12
13
|
|
|
@@ -88,6 +89,12 @@ export function findDeployProblems(
|
|
|
88
89
|
|
|
89
90
|
readBuildGate(byPath.get('package.json'), dockerfile, problems);
|
|
90
91
|
|
|
92
|
+
readReactCopies(
|
|
93
|
+
byPath.get('package.json'),
|
|
94
|
+
byPath.get('package-lock.json'),
|
|
95
|
+
problems,
|
|
96
|
+
);
|
|
97
|
+
|
|
91
98
|
readPublishedImage(byPath.get(DOCKER_IMAGE_WORKFLOW), problems);
|
|
92
99
|
|
|
93
100
|
const gitignore = byPath.get('.gitignore');
|
package/src/deploy/core/index.ts
CHANGED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading the repository's own JSON files, which a checker must do without ever
|
|
3
|
+
* throwing: the absence of a file, and a file somebody has broken, are both
|
|
4
|
+
* things to report rather than to crash on.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A JSON file the repository may not have, or may have broken.
|
|
9
|
+
* @param text - The file, unparsed.
|
|
10
|
+
* @returns Its object form, or undefined when it is neither.
|
|
11
|
+
*/
|
|
12
|
+
export function parseJsonObject(
|
|
13
|
+
text: string | undefined,
|
|
14
|
+
): Record<string, unknown> | undefined {
|
|
15
|
+
if (text === undefined) return undefined;
|
|
16
|
+
let parsed: unknown;
|
|
17
|
+
try {
|
|
18
|
+
parsed = JSON.parse(text);
|
|
19
|
+
} catch {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
if (typeof parsed !== 'object' || parsed === null) return undefined;
|
|
23
|
+
return parsed as Record<string, unknown>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* A block of a `package.json` as a map of strings, dropping any entry that is
|
|
28
|
+
* not one — the nested form of `overrides` pins something other than the
|
|
29
|
+
* package it is written under.
|
|
30
|
+
* @param value - The block, as parsed.
|
|
31
|
+
* @returns Its string entries.
|
|
32
|
+
*/
|
|
33
|
+
export function stringEntries(value: unknown): Record<string, string> {
|
|
34
|
+
if (typeof value !== 'object' || value === null) return {};
|
|
35
|
+
const found: Record<string, string> = {};
|
|
36
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
37
|
+
if (typeof entry === 'string') found[key] = entry;
|
|
38
|
+
}
|
|
39
|
+
return found;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Where a report should point, so the reader lands on the line itself.
|
|
44
|
+
* @param text - The file to search, when there is one.
|
|
45
|
+
* @param needle - The literal to find.
|
|
46
|
+
* @returns Its line, counting from 1, or 0 when it is not there.
|
|
47
|
+
*/
|
|
48
|
+
export function lineOf(text: string | undefined, needle: string): number {
|
|
49
|
+
if (text === undefined) return 0;
|
|
50
|
+
const at = text.indexOf(needle);
|
|
51
|
+
if (at === -1) return 0;
|
|
52
|
+
return text.slice(0, at).split('\n').length;
|
|
53
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import semver from 'semver';
|
|
2
|
+
|
|
3
|
+
import { lineOf, parseJsonObject, stringEntries } from './json.ts';
|
|
4
|
+
import type { DeployProblem } from './types.ts';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The React major the whole family runs. One number, in one place: a site that
|
|
8
|
+
* resolves anything else is reporting a resolution nobody asked for.
|
|
9
|
+
*/
|
|
10
|
+
export const REACT_MAJOR = 19;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The packages that must resolve to exactly one copy. The two runtime ones
|
|
14
|
+
* because two copies are a blank page; the two type packages because two copies
|
|
15
|
+
* are a type error in a file nobody edited.
|
|
16
|
+
*/
|
|
17
|
+
const SINGLE_COPY = [
|
|
18
|
+
'react',
|
|
19
|
+
'react-dom',
|
|
20
|
+
'@types/react',
|
|
21
|
+
'@types/react-dom',
|
|
22
|
+
] as const;
|
|
23
|
+
|
|
24
|
+
/** One package as the lock file resolved it. */
|
|
25
|
+
interface Resolved {
|
|
26
|
+
/** Where npm put it, as the lock keys it. */
|
|
27
|
+
path: string;
|
|
28
|
+
/** The exact version it resolved to. */
|
|
29
|
+
version: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** What the root `package.json` says about a package. */
|
|
33
|
+
interface Declared {
|
|
34
|
+
/** The range a direct dependency asks for, if it is one. */
|
|
35
|
+
dependency?: string;
|
|
36
|
+
/** The range the `overrides` block pins, if it pins one. */
|
|
37
|
+
override?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Report every way a repository would resolve React twice, or resolve a React
|
|
42
|
+
* that is not the family's.
|
|
43
|
+
*
|
|
44
|
+
* Two physical copies of `react` make every component from the offending
|
|
45
|
+
* subtree call hooks against a dispatcher the active renderer never populated,
|
|
46
|
+
* and the app dies at first render on `Cannot read properties of null (reading
|
|
47
|
+
* 'useEffect')` — a blank page with the correct title and the correct CSS,
|
|
48
|
+
* which reads like a routing or a build bug and is neither. Two copies of
|
|
49
|
+
* `@types/react` are the same mistake one layer up: `tsc` rejects a component
|
|
50
|
+
* crossing between them, in a file nobody touched.
|
|
51
|
+
*
|
|
52
|
+
* The cause is almost never a direct dependency. It is a *transitive* peer
|
|
53
|
+
* range that stopped before the current major — `@blueprintjs/core` depends on
|
|
54
|
+
* `react-popper@2`, whose peers stop at `^18` — so npm honours the stale range,
|
|
55
|
+
* installs an older React for that subtree and hoists it. Nothing fails at
|
|
56
|
+
* install, nothing fails at build, and the bundle silently grows by a second
|
|
57
|
+
* copy of React. The fix is a pin in the root `overrides`, which is why a lock
|
|
58
|
+
* holding such a peer without one is reported even while the tree happens to be
|
|
59
|
+
* clean today: it is one fresh resolve away from not being.
|
|
60
|
+
* @param packageJson - The repository's root `package.json`, unparsed.
|
|
61
|
+
* @param packageLock - Its `package-lock.json`, unparsed.
|
|
62
|
+
* @param problems - Where each problem found is appended.
|
|
63
|
+
*/
|
|
64
|
+
export function readReactCopies(
|
|
65
|
+
packageJson: string | undefined,
|
|
66
|
+
packageLock: string | undefined,
|
|
67
|
+
problems: DeployProblem[],
|
|
68
|
+
): void {
|
|
69
|
+
const lock = parseJsonObject(packageLock);
|
|
70
|
+
if (lock === undefined) return;
|
|
71
|
+
const packages = lock.packages;
|
|
72
|
+
if (typeof packages !== 'object' || packages === null) return;
|
|
73
|
+
const entries = Object.entries(packages as Record<string, unknown>);
|
|
74
|
+
|
|
75
|
+
const resolved = resolvedCopies(entries);
|
|
76
|
+
// A repository with no React at all is not a React repository.
|
|
77
|
+
if (resolved.get('react')?.length === undefined) return;
|
|
78
|
+
|
|
79
|
+
const declared = declarations(packageJson);
|
|
80
|
+
|
|
81
|
+
for (const name of SINGLE_COPY) {
|
|
82
|
+
const copies = resolved.get(name);
|
|
83
|
+
if (copies === undefined || copies.length === 0) continue;
|
|
84
|
+
if (copies.length > 1) {
|
|
85
|
+
problems.push({
|
|
86
|
+
file: 'package-lock.json',
|
|
87
|
+
line: lineOf(packageLock, `"${copies[1]?.path ?? name}"`),
|
|
88
|
+
kind: 'duplicate-react',
|
|
89
|
+
message: `${name} resolves to ${copies.length} copies: ${copies
|
|
90
|
+
.map((copy) => `${copy.version} at ${copy.path}`)
|
|
91
|
+
.join(', ')}`,
|
|
92
|
+
hint: `pin ${name} in the root overrides, then reinstall from scratch — npm install alone keeps the stale tree on disk`,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
for (const copy of copies) {
|
|
96
|
+
const major = semver.major(semver.coerce(copy.version) ?? '0.0.0');
|
|
97
|
+
if (major !== REACT_MAJOR) {
|
|
98
|
+
problems.push({
|
|
99
|
+
file: 'package-lock.json',
|
|
100
|
+
line: lineOf(packageLock, `"${copy.path}"`),
|
|
101
|
+
kind: 'stray-react-major',
|
|
102
|
+
message: `${name} resolves to ${copy.version} at ${copy.path}, and the family runs React ${REACT_MAJOR}`,
|
|
103
|
+
hint: `declare ^${REACT_MAJOR} and pin it in the root overrides`,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const stale = stalePeer(entries);
|
|
110
|
+
if (stale !== undefined) {
|
|
111
|
+
for (const name of ['react', 'react-dom'] as const) {
|
|
112
|
+
if (declared.get(name)?.override !== undefined) continue;
|
|
113
|
+
problems.push({
|
|
114
|
+
file: 'package.json',
|
|
115
|
+
line: lineOf(packageJson, '"dependencies"'),
|
|
116
|
+
kind: 'unpinned-react',
|
|
117
|
+
message: `${stale.name} asks for ${stale.key} ${stale.range}, which excludes React ${REACT_MAJOR}, and nothing pins ${name}`,
|
|
118
|
+
hint: `add "overrides": { "react": "^${REACT_MAJOR}…", "react-dom": "^${REACT_MAJOR}…" } to the root package.json`,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
for (const name of SINGLE_COPY) {
|
|
124
|
+
const entry = declared.get(name);
|
|
125
|
+
if (entry?.override === undefined || entry.dependency === undefined) {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (entry.override !== entry.dependency) {
|
|
129
|
+
problems.push({
|
|
130
|
+
file: 'package.json',
|
|
131
|
+
line: lineOf(packageJson, `"${name}"`),
|
|
132
|
+
kind: 'conflicting-react-pin',
|
|
133
|
+
message: `${name} is pinned to ${entry.override} while it is depended on as ${entry.dependency}, so npm install fails with EOVERRIDE`,
|
|
134
|
+
hint: `write the same range in both: the override and the dependency are one decision`,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Every real copy of each tracked package, in lock order. A workspace link is
|
|
142
|
+
* not a copy — it is the repository pointing at itself.
|
|
143
|
+
* @param entries - The lock's `packages`, as entries.
|
|
144
|
+
* @returns The copies found, keyed by package name.
|
|
145
|
+
*/
|
|
146
|
+
function resolvedCopies(
|
|
147
|
+
entries: ReadonlyArray<[string, unknown]>,
|
|
148
|
+
): Map<string, Resolved[]> {
|
|
149
|
+
const found = new Map<string, Resolved[]>();
|
|
150
|
+
for (const [path, value] of entries) {
|
|
151
|
+
if (typeof value !== 'object' || value === null) continue;
|
|
152
|
+
const entry = value as { version?: unknown; link?: unknown };
|
|
153
|
+
if (entry.link === true) continue;
|
|
154
|
+
if (typeof entry.version !== 'string') continue;
|
|
155
|
+
for (const name of SINGLE_COPY) {
|
|
156
|
+
if (
|
|
157
|
+
path !== `node_modules/${name}` &&
|
|
158
|
+
!path.endsWith(`/node_modules/${name}`)
|
|
159
|
+
) {
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
const copies = found.get(name) ?? [];
|
|
163
|
+
copies.push({ path, version: entry.version });
|
|
164
|
+
found.set(name, copies);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return found;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* The first package in the lock whose React peer stopped before the family's
|
|
172
|
+
* major — the thing that makes npm resolve a second copy.
|
|
173
|
+
* @param entries - The lock's `packages`, as entries.
|
|
174
|
+
* @returns What asks for what, or undefined when every peer admits the major.
|
|
175
|
+
*/
|
|
176
|
+
function stalePeer(
|
|
177
|
+
entries: ReadonlyArray<[string, unknown]>,
|
|
178
|
+
): { name: string; key: string; range: string } | undefined {
|
|
179
|
+
for (const [path, value] of entries) {
|
|
180
|
+
if (typeof value !== 'object' || value === null) continue;
|
|
181
|
+
const entry = value as {
|
|
182
|
+
peerDependencies?: unknown;
|
|
183
|
+
peerDependenciesMeta?: unknown;
|
|
184
|
+
};
|
|
185
|
+
const peers = entry.peerDependencies;
|
|
186
|
+
if (typeof peers !== 'object' || peers === null) continue;
|
|
187
|
+
const meta = (entry.peerDependenciesMeta ?? {}) as Record<
|
|
188
|
+
string,
|
|
189
|
+
{ optional?: unknown } | undefined
|
|
190
|
+
>;
|
|
191
|
+
for (const key of ['react', 'react-dom']) {
|
|
192
|
+
const range = (peers as Record<string, unknown>)[key];
|
|
193
|
+
if (typeof range !== 'string') continue;
|
|
194
|
+
if (meta[key]?.optional === true) continue;
|
|
195
|
+
if (admitsMajor(range)) continue;
|
|
196
|
+
return { name: path.replace(/^.*node_modules\//, ''), key, range };
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return undefined;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Whether a range admits any release of the family's React major. A range that
|
|
204
|
+
* cannot be parsed is given the benefit of the doubt: a checker that accuses on
|
|
205
|
+
* a spelling it does not know teaches people to ignore it.
|
|
206
|
+
* @param range - The peer range, as written.
|
|
207
|
+
* @returns True when some version of the major satisfies it.
|
|
208
|
+
*/
|
|
209
|
+
function admitsMajor(range: string): boolean {
|
|
210
|
+
try {
|
|
211
|
+
return (
|
|
212
|
+
semver.intersects(range, `>=${REACT_MAJOR}.0.0 <${REACT_MAJOR + 1}.0.0`, {
|
|
213
|
+
includePrerelease: true,
|
|
214
|
+
}) ||
|
|
215
|
+
semver.satisfies(`${REACT_MAJOR}.0.0`, range, { includePrerelease: true })
|
|
216
|
+
);
|
|
217
|
+
} catch {
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* What the root `package.json` asks for and what it pins, per tracked package.
|
|
224
|
+
* @param packageJson - The file, unparsed.
|
|
225
|
+
* @returns The declaration of each tracked package that has one.
|
|
226
|
+
*/
|
|
227
|
+
function declarations(packageJson: string | undefined): Map<string, Declared> {
|
|
228
|
+
const found = new Map<string, Declared>();
|
|
229
|
+
const parsed = parseJsonObject(packageJson);
|
|
230
|
+
if (parsed === undefined) return found;
|
|
231
|
+
const dependencies = {
|
|
232
|
+
...stringEntries(parsed.dependencies),
|
|
233
|
+
...stringEntries(parsed.devDependencies),
|
|
234
|
+
};
|
|
235
|
+
const overrides = stringEntries(parsed.overrides);
|
|
236
|
+
for (const name of SINGLE_COPY) {
|
|
237
|
+
const dependency = dependencies[name];
|
|
238
|
+
const override = overrides[name];
|
|
239
|
+
if (dependency === undefined && override === undefined) continue;
|
|
240
|
+
found.set(name, { dependency, override });
|
|
241
|
+
}
|
|
242
|
+
return found;
|
|
243
|
+
}
|
package/src/deploy/core/types.ts
CHANGED
|
@@ -16,7 +16,11 @@ export type DeployProblemKind =
|
|
|
16
16
|
| 'ungated-build'
|
|
17
17
|
| 'missing-build-only'
|
|
18
18
|
| 'test-runs-e2e'
|
|
19
|
-
| 'gated-image-build'
|
|
19
|
+
| 'gated-image-build'
|
|
20
|
+
| 'duplicate-react'
|
|
21
|
+
| 'stray-react-major'
|
|
22
|
+
| 'unpinned-react'
|
|
23
|
+
| 'conflicting-react-pin';
|
|
20
24
|
|
|
21
25
|
/** One way a repository would fail, or silently mis-deploy, on the server. */
|
|
22
26
|
export interface DeployProblem {
|
|
@@ -311,6 +311,19 @@ export const ECOSYSTEM_SITES: readonly EcosystemSite[] = [
|
|
|
311
311
|
brandAlt: '#0e7490',
|
|
312
312
|
mark: { plate: '#a3620a', accent: '#22d3ee' },
|
|
313
313
|
},
|
|
314
|
+
{
|
|
315
|
+
id: 'powerpoint',
|
|
316
|
+
name: { lead: 'powerpoint', alt: 'cheminfo', dot: true },
|
|
317
|
+
host: 'powerpoint.cheminfo.org',
|
|
318
|
+
tagline:
|
|
319
|
+
'Put a live tool on a slide, from the link its Share button gives.',
|
|
320
|
+
group: 'computing',
|
|
321
|
+
brand: '#c8460d',
|
|
322
|
+
// The blue sits on the white slide of the mark, where it reads at 6.7:1,
|
|
323
|
+
// and is dark enough to set the second half of the wordmark in as well.
|
|
324
|
+
brandAlt: '#1d4ed8',
|
|
325
|
+
mark: { plate: '#c8460d', accent: '#1d4ed8' },
|
|
326
|
+
},
|
|
314
327
|
];
|
|
315
328
|
|
|
316
329
|
/** How a link to a site is written, beyond the site itself. */
|
|
@@ -470,4 +470,20 @@ export const GLYPHS: Record<SiteId, (alt: string) => ReactNode> = {
|
|
|
470
470
|
<circle cx="22.5" cy="22.5" r="4.3" fill={alt} />
|
|
471
471
|
</>
|
|
472
472
|
),
|
|
473
|
+
// A slide on its stand, with a ring standing on it: the slide is the
|
|
474
|
+
// presentation and the ring is the live tool put on it, so the ring carries
|
|
475
|
+
// the answering colour.
|
|
476
|
+
powerpoint: (alt) => (
|
|
477
|
+
<>
|
|
478
|
+
<rect x="4.5" y="6.5" width="23" height="14.5" rx="1.6" fill="#ffffff" />
|
|
479
|
+
<path
|
|
480
|
+
d="M16 21v5M11.5 26.5h9"
|
|
481
|
+
fill="none"
|
|
482
|
+
stroke="#ffffff"
|
|
483
|
+
strokeWidth="2.2"
|
|
484
|
+
strokeLinecap="round"
|
|
485
|
+
/>
|
|
486
|
+
<path d="M16 8.45 20.59 11.1V16.4L16 19.05 11.41 16.4V11.1Z" fill={alt} />
|
|
487
|
+
</>
|
|
488
|
+
),
|
|
473
489
|
};
|
package/src/locales/de.json
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"language.label": "Sprache",
|
|
3
3
|
"chrome.pages": "Seiten",
|
|
4
|
+
"chrome.about": "Über",
|
|
5
|
+
"chrome.aboutTitle": "Was diese Seite ist, worauf sie aufbaut und wie man sie zitiert",
|
|
4
6
|
"account.signIn": "Anmelden",
|
|
5
7
|
"account.signOut": "Abmelden",
|
|
6
8
|
"account.signedInAs": "Angemeldet als {name}",
|
package/src/locales/en.json
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"language.label": "Language",
|
|
3
3
|
"chrome.pages": "Pages",
|
|
4
|
+
"chrome.about": "About",
|
|
5
|
+
"chrome.aboutTitle": "What this site is, what it is built on, and how to cite it",
|
|
4
6
|
"account.signIn": "Sign in",
|
|
5
7
|
"account.signOut": "Sign out",
|
|
6
8
|
"account.signedInAs": "Signed in as {name}",
|
package/src/locales/es.json
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"language.label": "Idioma",
|
|
3
3
|
"chrome.pages": "Páginas",
|
|
4
|
+
"chrome.about": "Acerca de",
|
|
5
|
+
"chrome.aboutTitle": "Qué es este sitio, en qué se basa y cómo citarlo",
|
|
4
6
|
"account.signIn": "Iniciar sesión",
|
|
5
7
|
"account.signOut": "Cerrar sesión",
|
|
6
8
|
"account.signedInAs": "Sesión iniciada como {name}",
|