react-cheminfo 0.14.0 → 0.15.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.
Files changed (77) hide show
  1. package/README.md +79 -21
  2. package/lib/about/core/about.d.ts +13 -7
  3. package/lib/about/core/about.d.ts.map +1 -1
  4. package/lib/about/core/about.js +4 -2
  5. package/lib/about/core/about.js.map +1 -1
  6. package/lib/about/ui/AboutBuild.d.ts +20 -0
  7. package/lib/about/ui/AboutBuild.d.ts.map +1 -0
  8. package/lib/about/ui/AboutBuild.js +38 -0
  9. package/lib/about/ui/AboutBuild.js.map +1 -0
  10. package/lib/about/ui/AboutPage.d.ts +7 -0
  11. package/lib/about/ui/AboutPage.d.ts.map +1 -1
  12. package/lib/about/ui/AboutPage.js +5 -2
  13. package/lib/about/ui/AboutPage.js.map +1 -1
  14. package/lib/about/ui/index.d.ts +2 -0
  15. package/lib/about/ui/index.d.ts.map +1 -1
  16. package/lib/about/ui/index.js +1 -0
  17. package/lib/about/ui/index.js.map +1 -1
  18. package/lib/build/core/buildInfo.d.ts +39 -0
  19. package/lib/build/core/buildInfo.d.ts.map +1 -0
  20. package/lib/build/core/buildInfo.js +20 -0
  21. package/lib/build/core/buildInfo.js.map +1 -0
  22. package/lib/build/core/current.d.ts +12 -0
  23. package/lib/build/core/current.d.ts.map +1 -0
  24. package/lib/build/core/current.js +11 -0
  25. package/lib/build/core/current.js.map +1 -0
  26. package/lib/build/core/index.d.ts +3 -0
  27. package/lib/build/core/index.d.ts.map +1 -0
  28. package/lib/build/core/index.js +2 -0
  29. package/lib/build/core/index.js.map +1 -0
  30. package/lib/build/vite/buildInfo.d.ts +57 -0
  31. package/lib/build/vite/buildInfo.d.ts.map +1 -0
  32. package/lib/build/vite/buildInfo.js +151 -0
  33. package/lib/build/vite/buildInfo.js.map +1 -0
  34. package/lib/build/vite/gitHead.d.ts +29 -0
  35. package/lib/build/vite/gitHead.d.ts.map +1 -0
  36. package/lib/build/vite/gitHead.js +102 -0
  37. package/lib/build/vite/gitHead.js.map +1 -0
  38. package/lib/build/vite/index.d.ts +4 -0
  39. package/lib/build/vite/index.d.ts.map +1 -0
  40. package/lib/build/vite/index.js +3 -0
  41. package/lib/build/vite/index.js.map +1 -0
  42. package/lib/build-info.d.ts +3 -0
  43. package/lib/build-info.d.ts.map +1 -0
  44. package/lib/build-info.js +2 -0
  45. package/lib/build-info.js.map +1 -0
  46. package/lib/core.d.ts +1 -0
  47. package/lib/core.d.ts.map +1 -1
  48. package/lib/core.js +1 -0
  49. package/lib/core.js.map +1 -1
  50. package/lib/credits/core/credits.d.ts +18 -0
  51. package/lib/credits/core/credits.d.ts.map +1 -1
  52. package/lib/credits/core/credits.js +21 -0
  53. package/lib/credits/core/credits.js.map +1 -1
  54. package/lib/ecosystem/ui/Wordmark.d.ts +14 -3
  55. package/lib/ecosystem/ui/Wordmark.d.ts.map +1 -1
  56. package/lib/ecosystem/ui/Wordmark.js +8 -4
  57. package/lib/ecosystem/ui/Wordmark.js.map +1 -1
  58. package/lib/vite.d.ts +1 -0
  59. package/lib/vite.d.ts.map +1 -1
  60. package/lib/vite.js +1 -0
  61. package/lib/vite.js.map +1 -1
  62. package/package.json +2 -1
  63. package/src/about/core/about.ts +18 -9
  64. package/src/about/ui/AboutBuild.tsx +83 -0
  65. package/src/about/ui/AboutPage.tsx +15 -7
  66. package/src/about/ui/index.ts +2 -0
  67. package/src/build/core/buildInfo.ts +48 -0
  68. package/src/build/core/current.ts +12 -0
  69. package/src/build/core/index.ts +2 -0
  70. package/src/build/vite/buildInfo.ts +178 -0
  71. package/src/build/vite/gitHead.ts +114 -0
  72. package/src/build/vite/index.ts +8 -0
  73. package/src/build-info.ts +2 -0
  74. package/src/core.ts +1 -0
  75. package/src/credits/core/credits.ts +21 -0
  76. package/src/ecosystem/ui/Wordmark.tsx +22 -7
  77. package/src/vite.ts +1 -0
@@ -0,0 +1,83 @@
1
+ import type { CSSProperties, ReactElement, ReactNode } from 'react';
2
+
3
+ import type { BuildInfo } from '../../build/core/buildInfo.ts';
4
+ import { formatBuiltAt, shortCommit } from '../../build/core/buildInfo.ts';
5
+
6
+ export interface AboutBuildProps {
7
+ /** What the build published about itself. */
8
+ build: BuildInfo;
9
+ /** Where the sources live, which is what the version and commit link into. */
10
+ repository: string;
11
+ }
12
+
13
+ /**
14
+ * The one line that says which build of the site a visitor has open.
15
+ *
16
+ * A report of something going wrong is worth answering only when we know what
17
+ * was running, and a visitor cannot read that off the page otherwise. The
18
+ * version and the commit link into the repository, so the change that caused a
19
+ * problem is one click from the page that shows it.
20
+ * @param props - See {@link AboutBuildProps}.
21
+ * @returns The line.
22
+ */
23
+ export function AboutBuild(props: AboutBuildProps): ReactElement {
24
+ const { build, repository } = props;
25
+ const sources = repository.endsWith('/')
26
+ ? repository.slice(0, -1)
27
+ : repository;
28
+ const onGitHub = sources.startsWith(GITHUB);
29
+
30
+ return (
31
+ <p style={PARAGRAPH_STYLE}>
32
+ Running version{' '}
33
+ <Reference
34
+ href={
35
+ onGitHub ? `${sources}/releases/tag/v${build.version}` : undefined
36
+ }
37
+ >
38
+ {build.version}
39
+ </Reference>
40
+ , built {formatBuiltAt(build.builtAt)}
41
+ {build.commit === undefined ? null : (
42
+ <>
43
+ {' from commit '}
44
+ <Reference
45
+ href={onGitHub ? `${sources}/commit/${build.commit}` : undefined}
46
+ >
47
+ {shortCommit(build.commit)}
48
+ </Reference>
49
+ </>
50
+ )}
51
+ .
52
+ </p>
53
+ );
54
+ }
55
+
56
+ function Reference(props: {
57
+ href: string | undefined;
58
+ children: ReactNode;
59
+ }): ReactElement {
60
+ const { href, children } = props;
61
+ if (href === undefined) return <code style={CODE_STYLE}>{children}</code>;
62
+
63
+ return (
64
+ <a href={href} target="_blank" rel="noopener noreferrer" style={LINK_STYLE}>
65
+ <code style={CODE_STYLE}>{children}</code>
66
+ </a>
67
+ );
68
+ }
69
+
70
+ const GITHUB = 'https://github.com/';
71
+
72
+ const PARAGRAPH_STYLE = {
73
+ margin: '8px 0 0',
74
+ color: 'var(--text-muted)',
75
+ fontSize: 13,
76
+ } as const satisfies CSSProperties;
77
+
78
+ const CODE_STYLE = {
79
+ fontFamily: 'var(--font-mono, ui-monospace, monospace)',
80
+ fontSize: 13,
81
+ } as const satisfies CSSProperties;
82
+
83
+ const LINK_STYLE = { color: 'var(--accent)' } as const satisfies CSSProperties;
@@ -6,6 +6,7 @@ import { SiteMark } from '../../ecosystem/ui/marks.tsx';
6
6
  import type { AboutContent } from '../core/about.ts';
7
7
  import { resolveAbout } from '../core/about.ts';
8
8
 
9
+ import { AboutBuild } from './AboutBuild.tsx';
9
10
  import { AboutCitations } from './AboutCitations.tsx';
10
11
  import { AboutProvidedBy } from './AboutProvidedBy.tsx';
11
12
  import { AboutSection } from './AboutSection.tsx';
@@ -25,6 +26,13 @@ export interface AboutPageProps {
25
26
  * @default undefined
26
27
  */
27
28
  children?: ReactNode;
29
+ /**
30
+ * The site's own mark, for a site the shared glyph set does not hold —
31
+ * one that is deliberately not listed in `ECOSYSTEM_SITES`. It stands where
32
+ * `SiteMark` would, and the hero is otherwise unchanged.
33
+ * @default undefined — the family's mark for that site
34
+ */
35
+ mark?: ReactNode;
28
36
  /**
29
37
  * The site's own drawn lockup, for a site that has one. It stands in the
30
38
  * hero in place of the mark and the written name, above the tagline, and
@@ -48,7 +56,7 @@ export interface AboutPageProps {
48
56
  * @throws {Error} When the record names a site or a credit that does not exist.
49
57
  */
50
58
  export function AboutPage(props: AboutPageProps): ReactElement {
51
- const { content, className, children, logo } = props;
59
+ const { content, className, children, logo, mark } = props;
52
60
  const about = resolveAbout(content);
53
61
  const site = about.site;
54
62
 
@@ -63,10 +71,12 @@ export function AboutPage(props: AboutPageProps): ReactElement {
63
71
  className="about-hero"
64
72
  style={logo === undefined ? HERO_STYLE : LOGO_HERO_STYLE}
65
73
  >
66
- {logo === undefined ? <SiteMark siteId={site.id} size={56} /> : null}
74
+ {logo === undefined
75
+ ? (mark ?? <SiteMark site={site} size={56} />)
76
+ : null}
67
77
  <div>
68
78
  <h1 style={logo === undefined ? NAME_STYLE : LOGO_NAME_STYLE}>
69
- {logo ?? <Wordmark siteId={site.id} size={26} />}
79
+ {logo ?? <Wordmark site={site} size={26} />}
70
80
  </h1>
71
81
  <p style={TAGLINE_STYLE}>{site.tagline}</p>
72
82
  <p style={WHAT_STYLE}>{about.what}</p>
@@ -118,10 +128,8 @@ export function AboutPage(props: AboutPageProps): ReactElement {
118
128
  piece of it into something else. The sources are at{' '}
119
129
  <ExternalLink href={about.repository} />.
120
130
  </p>
121
- {about.version === undefined ? null : (
122
- <p style={PARAGRAPH_STYLE}>
123
- This page is running version {about.version}.
124
- </p>
131
+ {about.build === undefined ? null : (
132
+ <AboutBuild build={about.build} repository={about.repository} />
125
133
  )}
126
134
  </AboutSection>
127
135
 
@@ -1,3 +1,5 @@
1
+ export type { AboutBuildProps } from './AboutBuild.tsx';
2
+ export { AboutBuild } from './AboutBuild.tsx';
1
3
  export type { AboutCitationsProps } from './AboutCitations.tsx';
2
4
  export { AboutCitations } from './AboutCitations.tsx';
3
5
  export type { AboutPageProps } from './AboutPage.tsx';
@@ -0,0 +1,48 @@
1
+ /**
2
+ * What a deployed page can say about itself: which release it is, when it was
3
+ * built, and from which commit.
4
+ *
5
+ * A site of ours is a link somebody handed out in a course, so the first
6
+ * question a report raises is *which one of these is running*. The record is
7
+ * filled by the build rather than written by hand — a version a human types
8
+ * into a source file is a version that is wrong by the next release.
9
+ */
10
+ export interface BuildInfo {
11
+ /** The released version, from the package.json at the repository root. */
12
+ version: string;
13
+ /** When the build ran, as an ISO instant to the second: `2026-09-16T09:41:07Z`. */
14
+ builtAt: string;
15
+ /**
16
+ * The full commit the build was made from.
17
+ * @default undefined — the build could not see the repository
18
+ */
19
+ commit?: string;
20
+ /**
21
+ * The version of `react-cheminfo` the site is built against, which is what
22
+ * says whether a site is running the current chrome.
23
+ * @default undefined — the build could not read it
24
+ */
25
+ reactCheminfo?: string;
26
+ }
27
+
28
+ /**
29
+ * The commit as it is written for a reader.
30
+ * @param commit - The full hash.
31
+ * @returns Its first seven characters, which is what git itself shows.
32
+ */
33
+ export function shortCommit(commit: string): string {
34
+ return commit.slice(0, SHORT_COMMIT_LENGTH);
35
+ }
36
+
37
+ /**
38
+ * The build instant as it is read on the page.
39
+ * @param builtAt - The ISO instant the build wrote.
40
+ * @returns The day and the time, in UTC, e.g. `2026-09-16 09:41:07 UTC`.
41
+ */
42
+ export function formatBuiltAt(builtAt: string): string {
43
+ const seconds = builtAt.slice(0, SECOND_LENGTH).replace('T', ' ');
44
+ return `${seconds} UTC`;
45
+ }
46
+
47
+ const SHORT_COMMIT_LENGTH = 7;
48
+ const SECOND_LENGTH = 19;
@@ -0,0 +1,12 @@
1
+ import type { BuildInfo } from './buildInfo.ts';
2
+
3
+ /**
4
+ * Which build of the site is running, or `undefined` when nothing filled it in.
5
+ *
6
+ * The module is replaced at build time by `cheminfoBuildInfo`, which is what
7
+ * puts the real version, instant and commit here. It stays a real module rather
8
+ * than a virtual one so that a test, a Playwright spec or a backend can import
9
+ * the About record in plain Node: outside a build there is simply no record,
10
+ * and the About page leaves the line out.
11
+ */
12
+ export const BUILD_INFO: BuildInfo | undefined = undefined;
@@ -0,0 +1,2 @@
1
+ export type { BuildInfo } from './buildInfo.ts';
2
+ export { formatBuiltAt, shortCommit } from './buildInfo.ts';
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Tell a site which build of itself it is running.
3
+ *
4
+ * Every site of the family carries an About page, and the one thing that page
5
+ * cannot state from a hand-written record is the release it is serving. So the
6
+ * build states it: the plugin resolves the version, the day and the commit
7
+ * once, hands them to the page through a virtual module, and writes the same
8
+ * record to `build-info.json` so the whole family can be asked what it is
9
+ * running with one request each.
10
+ */
11
+
12
+ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
13
+ import { dirname, join, resolve } from 'node:path';
14
+
15
+ import type { Plugin } from 'vite';
16
+
17
+ import type { BuildInfo } from '../core/buildInfo.ts';
18
+
19
+ import { commitFromEnvironment, readGitHead } from './gitHead.ts';
20
+
21
+ /** The module a site imports its build record from. */
22
+ export const BUILD_INFO_MODULE = 'react-cheminfo/build-info';
23
+
24
+ /** What the build writes about itself. */
25
+ export interface BuildInfoOptions {
26
+ /**
27
+ * Where the record is written in the build, so a deployment can be asked
28
+ * what it is running. `false` writes none.
29
+ * @default 'build-info.json'
30
+ */
31
+ fileName?: string | false;
32
+ }
33
+
34
+ /**
35
+ * Resolve the build's own version, instant and commit, and publish them.
36
+ *
37
+ * The site reads them from the module the plugin fills in:
38
+ *
39
+ * ```ts
40
+ * import { BUILD_INFO } from 'react-cheminfo/build-info';
41
+ * ```
42
+ *
43
+ * That module exists on disk and exports `undefined`, so a Playwright spec or
44
+ * a unit test importing the same About record in plain Node still resolves it —
45
+ * a virtual specifier does not, and the whole suite dies on the import.
46
+ * @param options - See {@link BuildInfoOptions}.
47
+ * @returns The Vite plugin.
48
+ */
49
+ export function cheminfoBuildInfo(options: BuildInfoOptions = {}): Plugin {
50
+ const { fileName = 'build-info.json' } = options;
51
+ let root = process.cwd();
52
+ let outDir = 'dist';
53
+ let info: BuildInfo = resolveBuildInfo(root);
54
+
55
+ return {
56
+ name: 'cheminfo-build-info',
57
+ // Vite's own resolver runs first and would hand back the module on disk,
58
+ // which exports `undefined`; and the dependency optimizer pre-bundles a
59
+ // bare specifier without asking a plugin at all.
60
+ enforce: 'pre',
61
+ config() {
62
+ return { optimizeDeps: { exclude: [BUILD_INFO_MODULE] } };
63
+ },
64
+ configResolved(config) {
65
+ root = config.root;
66
+ outDir = config.build.outDir;
67
+ info = resolveBuildInfo(root);
68
+ },
69
+ resolveId(id) {
70
+ return id === BUILD_INFO_MODULE ? RESOLVED_MODULE : undefined;
71
+ },
72
+ load(id) {
73
+ if (id !== RESOLVED_MODULE) return undefined;
74
+ return `export const BUILD_INFO = ${JSON.stringify(info)};\n`;
75
+ },
76
+ closeBundle() {
77
+ if (fileName === false) return;
78
+ const target = resolve(root, outDir, fileName);
79
+ mkdirSync(dirname(target), { recursive: true });
80
+ writeFileSync(target, `${JSON.stringify(info, null, 2)}\n`);
81
+ },
82
+ };
83
+ }
84
+
85
+ /**
86
+ * What the build knows about itself, read off the checkout it runs in.
87
+ * @param root - The project Vite is building, which may sit under the
88
+ * repository root as `frontend` does.
89
+ * @returns The record, with whatever it could not resolve left out.
90
+ */
91
+ export function resolveBuildInfo(root: string): BuildInfo {
92
+ const repository = findRepositoryRoot(root);
93
+
94
+ return {
95
+ version: readVersion(repository) ?? readVersion(root) ?? UNRELEASED,
96
+ builtAt: buildInstant(),
97
+ commit: readGitHead(repository) ?? commitFromEnvironment(),
98
+ reactCheminfo: readOwnVersion(),
99
+ };
100
+ }
101
+
102
+ /**
103
+ * The topmost directory of the run of ancestors that all hold a `package.json`.
104
+ *
105
+ * It is the repository root, and it is where the released version lives:
106
+ * release-please bumps the root `package.json`, while the `frontend` workspace
107
+ * of a site that has one keeps the `0.0.0` it was scaffolded with.
108
+ * @param start - Where to start climbing.
109
+ * @returns The highest such directory, or `start` when it holds none itself.
110
+ */
111
+ export function findRepositoryRoot(start: string): string {
112
+ let directory = resolve(start);
113
+ let highest = directory;
114
+
115
+ for (;;) {
116
+ if (readPackage(directory) !== undefined) highest = directory;
117
+ const parent = dirname(directory);
118
+ // The run has ended: a directory with no package.json is above the project,
119
+ // and so is everything above it.
120
+ if (parent === directory || readPackage(parent) === undefined) {
121
+ return highest;
122
+ }
123
+ directory = parent;
124
+ }
125
+ }
126
+
127
+ function readVersion(directory: string): string | undefined {
128
+ const version = readPackage(directory)?.version;
129
+ return typeof version === 'string' && version.length > 0
130
+ ? version
131
+ : undefined;
132
+ }
133
+
134
+ /**
135
+ * The version of this package, so a site can be asked which chrome it runs.
136
+ * @returns The version, or `undefined` when the manifest is out of reach.
137
+ */
138
+ function readOwnVersion(): string | undefined {
139
+ let directory = import.meta.dirname;
140
+
141
+ for (;;) {
142
+ const manifest = readPackage(directory);
143
+ if (manifest?.name === OWN_NAME && typeof manifest.version === 'string') {
144
+ return manifest.version;
145
+ }
146
+ const parent = dirname(directory);
147
+ if (parent === directory) return undefined;
148
+ directory = parent;
149
+ }
150
+ }
151
+
152
+ function readPackage(
153
+ directory: string,
154
+ ): { name?: unknown; version?: unknown } | undefined {
155
+ try {
156
+ return JSON.parse(
157
+ readFileSync(join(directory, 'package.json'), 'utf8'),
158
+ ) as Record<string, unknown>;
159
+ } catch {
160
+ return undefined;
161
+ }
162
+ }
163
+
164
+ /**
165
+ * When the build ran, honouring `SOURCE_DATE_EPOCH` so a reproducible build
166
+ * stays reproducible.
167
+ * @returns The instant, to the second, as `2026-09-16T09:41:07Z`.
168
+ */
169
+ function buildInstant(): string {
170
+ const epoch = Number(process.env.SOURCE_DATE_EPOCH);
171
+ const when = Number.isFinite(epoch) && epoch > 0 ? epoch * 1000 : Date.now();
172
+ return `${new Date(when).toISOString().slice(0, SECOND_LENGTH)}Z`;
173
+ }
174
+
175
+ const RESOLVED_MODULE = `\0${BUILD_INFO_MODULE}`;
176
+ const OWN_NAME = 'react-cheminfo';
177
+ const UNRELEASED = '0.0.0';
178
+ const SECOND_LENGTH = 19;
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Read the commit a build is made from, without running git.
3
+ *
4
+ * The image build cannot run git: `.dockerignore` keeps `.git` out of the
5
+ * context and `node:alpine` ships no git binary, while the shared
6
+ * `docker-image` workflow passes no build argument a commit could arrive in.
7
+ * What a site does instead is let three paths back into the context —
8
+ * `.git/HEAD`, `.git/refs` and `.git/packed-refs`, a few kilobytes — and those
9
+ * three are enough to resolve the hash by reading files.
10
+ */
11
+
12
+ import { readFileSync } from 'node:fs';
13
+ import { dirname, isAbsolute, join, resolve } from 'node:path';
14
+
15
+ /**
16
+ * The commit `HEAD` names, read from the repository above a directory.
17
+ * @param start - Where to start looking; every ancestor is tried in turn.
18
+ * @returns The full hash, or `undefined` when the repository is out of reach —
19
+ * which is what a build sees when the three paths were not let back in.
20
+ */
21
+ export function readGitHead(start: string): string | undefined {
22
+ const gitDir = findGitDir(start);
23
+ if (gitDir === undefined) return undefined;
24
+
25
+ const head = readText(join(gitDir, 'HEAD'));
26
+ if (head === undefined) return undefined;
27
+
28
+ // A checkout made by `actions/checkout` is detached, so `HEAD` is the hash
29
+ // itself and neither `refs` nor `packed-refs` is read at all.
30
+ if (!head.startsWith('ref:')) return asCommit(head);
31
+
32
+ const ref = head.slice('ref:'.length).trim();
33
+ const loose = readText(join(gitDir, ref));
34
+ return (
35
+ asCommit(loose) ??
36
+ commitInPackedRefs(readText(join(gitDir, 'packed-refs')), ref)
37
+ );
38
+ }
39
+
40
+ /**
41
+ * The commit named by the environment, for a build that cannot see `.git`.
42
+ * @returns The hash, or `undefined` when no variable names one.
43
+ */
44
+ export function commitFromEnvironment(): string | undefined {
45
+ return (
46
+ asCommit(process.env.GITHUB_SHA) ??
47
+ asCommit(process.env.SOURCE_COMMIT) ??
48
+ asCommit(process.env.GIT_COMMIT)
49
+ );
50
+ }
51
+
52
+ /**
53
+ * The `.git` directory governing a path.
54
+ * @param start - Where to start looking.
55
+ * @returns Its absolute path, or `undefined` when there is none above `start`.
56
+ */
57
+ export function findGitDir(start: string): string | undefined {
58
+ let directory = resolve(start);
59
+
60
+ for (;;) {
61
+ const candidate = join(directory, '.git');
62
+ const text = readText(candidate);
63
+ // A worktree and a submodule write a file naming the real directory; a
64
+ // plain repository has the directory itself, which reads as `undefined`.
65
+ if (text?.startsWith('gitdir:')) {
66
+ const target = text.slice('gitdir:'.length).trim();
67
+ return isAbsolute(target) ? target : resolve(directory, target);
68
+ }
69
+ if (readText(join(candidate, 'HEAD')) !== undefined) return candidate;
70
+
71
+ const parent = dirname(directory);
72
+ if (parent === directory) return undefined;
73
+ directory = parent;
74
+ }
75
+ }
76
+
77
+ /**
78
+ * The hash a packed `refs` file gives a ref.
79
+ * @param packed - The contents of `packed-refs`, when there is one.
80
+ * @param ref - The ref to look up, e.g. `refs/heads/main`.
81
+ * @returns The hash, or `undefined` when the file does not carry the ref.
82
+ */
83
+ function commitInPackedRefs(
84
+ packed: string | undefined,
85
+ ref: string,
86
+ ): string | undefined {
87
+ if (packed === undefined) return undefined;
88
+
89
+ for (const line of packed.split('\n')) {
90
+ // `#` is the header and `^` the object a tag points at: neither is a ref.
91
+ if (line.startsWith('#') || line.startsWith('^')) continue;
92
+ const space = line.indexOf(' ');
93
+ if (space === -1 || line.slice(space + 1).trim() !== ref) continue;
94
+ return asCommit(line.slice(0, space));
95
+ }
96
+
97
+ return undefined;
98
+ }
99
+
100
+ function asCommit(value: string | undefined): string | undefined {
101
+ const text = value?.trim();
102
+ return text !== undefined && COMMIT.test(text) ? text : undefined;
103
+ }
104
+
105
+ function readText(path: string): string | undefined {
106
+ try {
107
+ return readFileSync(path, 'utf8').trim();
108
+ } catch {
109
+ return undefined;
110
+ }
111
+ }
112
+
113
+ // Forty hexadecimal characters, or sixty-four in a repository on sha256.
114
+ const COMMIT = /^[\da-f]{40}(?:[\da-f]{24})?$/;
@@ -0,0 +1,8 @@
1
+ export type { BuildInfoOptions } from './buildInfo.ts';
2
+ export {
3
+ BUILD_INFO_MODULE,
4
+ cheminfoBuildInfo,
5
+ findRepositoryRoot,
6
+ resolveBuildInfo,
7
+ } from './buildInfo.ts';
8
+ export { commitFromEnvironment, findGitDir, readGitHead } from './gitHead.ts';
@@ -0,0 +1,2 @@
1
+ export type { BuildInfo } from './build/core/buildInfo.ts';
2
+ export { BUILD_INFO } from './build/core/current.ts';
package/src/core.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './about/core/index.ts';
2
+ export * from './build/core/index.ts';
2
3
  export * from './chart/core/index.ts';
3
4
  export * from './citation/core/index.ts';
4
5
  export * from './clipboard/core/index.ts';
@@ -194,6 +194,27 @@ export const CREDITS = [
194
194
  description: 'the dragging that arranges things into an order.',
195
195
  license: 'MIT',
196
196
  },
197
+ {
198
+ id: 'react-roi',
199
+ name: 'react-roi',
200
+ href: 'https://github.com/zakodium-oss/react-roi',
201
+ description: 'the box a region of an image is drawn with.',
202
+ license: 'MIT',
203
+ },
204
+ {
205
+ id: 'file-collection',
206
+ name: 'file-collection',
207
+ href: 'https://github.com/cheminfo/file-collection',
208
+ description: 'the reader that takes dropped files, folders and archives.',
209
+ license: 'MIT',
210
+ },
211
+ {
212
+ id: 'zip-js',
213
+ name: 'zip.js',
214
+ href: 'https://github.com/gildas-lormeau/zip.js',
215
+ description: 'the ZIP archives that are read and written in the browser.',
216
+ license: 'BSD-3-Clause',
217
+ },
197
218
  {
198
219
  id: 'preact-signals',
199
220
  name: 'Preact Signals',
@@ -3,12 +3,22 @@ import type { CSSProperties, ReactElement } from 'react';
3
3
  import { joinClassNames } from '../../shared/ui/joinClassNames.ts';
4
4
  import { siteById } from '../core/lookup.ts';
5
5
  import { siteNameColors } from '../core/nameColors.ts';
6
- import type { SiteId } from '../core/sites.ts';
6
+ import type { EcosystemSite, SiteId } from '../core/sites.ts';
7
7
 
8
8
  /** What a site's written name needs. */
9
9
  export interface WordmarkProps {
10
- /** The site whose name is written. */
11
- siteId: SiteId;
10
+ /**
11
+ * The site whose name is written, passed rather than named — for a site that
12
+ * is deliberately not one of `ECOSYSTEM_SITES`. One of `site` and `siteId` is
13
+ * required.
14
+ * @default undefined
15
+ */
16
+ site?: EcosystemSite;
17
+ /**
18
+ * The same site, named rather than passed, which is what a header knows.
19
+ * @default undefined
20
+ */
21
+ siteId?: SiteId;
12
22
  /**
13
23
  * Size of the name, in pixels. The weight comes from the surrounding
14
24
  * context, so the same wordmark suits a header bar and a heading.
@@ -32,12 +42,17 @@ export interface WordmarkProps {
32
42
  * its address.
33
43
  * @param props - The site, the size of the name, and extra class names.
34
44
  * @returns The name, as one inline element that never wraps mid-address.
45
+ * @throws {Error} When neither `site` nor `siteId` is given.
35
46
  */
36
47
  export function Wordmark(props: WordmarkProps): ReactElement {
37
- const { siteId, size = 17, className } = props;
38
- const site = siteById(siteId);
39
- const { lead, alt, dot } = site.name;
40
- const colors = siteNameColors(site);
48
+ const { site, siteId, size = 17, className } = props;
49
+ const written = site ?? (siteId === undefined ? undefined : siteById(siteId));
50
+ if (written === undefined) {
51
+ throw new Error('Wordmark needs one of its `site` and `siteId` props');
52
+ }
53
+
54
+ const { lead, alt, dot } = written.name;
55
+ const colors = siteNameColors(written);
41
56
 
42
57
  return (
43
58
  <span
package/src/vite.ts CHANGED
@@ -1,2 +1,3 @@
1
+ export * from './build/vite/index.ts';
1
2
  export * from './seo/vite/index.ts';
2
3
  export * from './slides/vite/index.ts';