describe-me 0.5.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -3
- package/dist-cli/file-names-in.d.ts +2 -0
- package/dist-cli/file-names-in.js +11 -0
- package/dist-cli/remote-stylesheets.d.ts +7 -0
- package/dist-cli/remote-stylesheets.js +178 -0
- package/dist-cli/replace-asset-names.d.ts +2 -0
- package/dist-cli/replace-asset-names.js +13 -0
- package/dist-cli/replace-remote-urls.d.ts +6 -0
- package/dist-cli/replace-remote-urls.js +18 -0
- package/dist-cli/rewrite-stored-files.d.ts +27 -0
- package/dist-cli/rewrite-stored-files.js +174 -0
- package/dist-cli/store-vendored-file.d.ts +5 -2
- package/dist-cli/store-vendored-file.js +7 -4
- package/dist-cli/vendor-fonts.d.ts +5 -2
- package/dist-cli/vendor-fonts.js +77 -49
- package/dist-cli/vendored-name.d.ts +1 -1
- package/dist-cli/vendored-name.js +1 -1
- package/package.json +2 -2
- package/src/apply-viewport.ts +5 -1
- package/src/data.ts +32 -13
- package/src/diagnostics-menu.ts +17 -4
- package/src/fonts-settled.ts +62 -13
- package/src/keyboard.ts +27 -7
- package/src/load-style.ts +38 -9
- package/src/min-frame-height.ts +2 -0
- package/src/natural-height.ts +53 -13
- package/src/parse-viewport-size.ts +4 -3
- package/src/regions.ts +5 -2
- package/src/render-all.ts +7 -1
- package/src/render-tree.ts +5 -0
- package/src/restore-styles.ts +10 -4
- package/src/sidebar-order.ts +31 -0
- package/src/stage-note.ts +32 -0
- package/src/stage.ts +6 -2
- package/src/state.ts +8 -4
- package/src/style.css +31 -2
- package/src/test-view.ts +7 -4
- package/src/version-banner.ts +35 -0
- package/src/viewport-controls.ts +23 -6
package/README.md
CHANGED
|
@@ -17,6 +17,9 @@ pnpm add -D describe-me @describe-me/vitest @describe-me/react
|
|
|
17
17
|
`vite` is a peer dependency (Vitest requires it anyway): pnpm and npm install
|
|
18
18
|
it automatically; with yarn, add `vite` to your `devDependencies`.
|
|
19
19
|
|
|
20
|
+
All describe-me packages must be on the same version
|
|
21
|
+
([Add it to your project](https://github.com/grzehub/describe-me#add-it-to-your-project)).
|
|
22
|
+
|
|
20
23
|
## Usage
|
|
21
24
|
|
|
22
25
|
```sh
|
|
@@ -39,9 +42,16 @@ viewport sizes and shareable links
|
|
|
39
42
|
from Google Fonts, Bunny Fonts and Fontsource on jsDelivr into the site, so
|
|
40
43
|
the docs show them offline. Adobe Fonts stay remote, because their license
|
|
41
44
|
does not allow self-hosting, and so do fonts on any other host. The build
|
|
42
|
-
lists both.
|
|
43
|
-
|
|
44
|
-
|
|
45
|
+
lists both.
|
|
46
|
+
|
|
47
|
+
This needs the network. By default `build` connects to Google Fonts, Bunny
|
|
48
|
+
Fonts and jsDelivr when the data loads fonts from them and the cache has no
|
|
49
|
+
fresh copy. Downloads are cached in `node_modules/.cache/describe-me/fonts`,
|
|
50
|
+
so after one online build later builds run offline. An unreachable host costs
|
|
51
|
+
up to about 10 s once, then it is skipped. A font that does not download keeps
|
|
52
|
+
loading from the network, and the build never fails because of it.
|
|
53
|
+
`--no-vendor-fonts` keeps the build off the network, for CI without network
|
|
54
|
+
access or with privacy rules.
|
|
45
55
|
|
|
46
56
|
The static site uses relative URLs, so it works from a sub-path such as
|
|
47
57
|
GitHub Pages.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { existsSync, readdirSync } from 'node:fs';
|
|
2
|
+
/** The sorted names of the regular files in `dir` that end in `extension`, none when it is missing. */
|
|
3
|
+
export function fileNamesIn(dir, extension) {
|
|
4
|
+
if (!existsSync(dir)) {
|
|
5
|
+
return [];
|
|
6
|
+
}
|
|
7
|
+
return readdirSync(dir, { withFileTypes: true })
|
|
8
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith(extension))
|
|
9
|
+
.map((entry) => entry.name)
|
|
10
|
+
.sort();
|
|
11
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Manifest, RemoteStylesheet } from '@describe-me/core/types';
|
|
2
|
+
/**
|
|
3
|
+
* The hosts that frames load stylesheets from, most frames first, counted
|
|
4
|
+
* from the files on disk by the rules of the reporter's `FontAudit`. It is a
|
|
5
|
+
* copy, because the viewer cannot depend on the reporter's package.
|
|
6
|
+
*/
|
|
7
|
+
export declare function remoteStylesheets(dataDir: string, manifest: Manifest): RemoteStylesheet[];
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { cssReferences } from '@describe-me/core/css-references';
|
|
4
|
+
import { ASSET_URL_PREFIX, STYLE_URL_PREFIX } from '@describe-me/core/types';
|
|
5
|
+
/** The test server, left out as the reporter leaves it out. */
|
|
6
|
+
const TEST_SERVER_HOSTS = new Set(['localhost', '127.0.0.1', '[::1]', '0.0.0.0']);
|
|
7
|
+
const REMOTE_URL = /^https?:/i;
|
|
8
|
+
/** A stored asset's name: a content hash and, when the file has one, its extension. */
|
|
9
|
+
const ASSET_NAME = /^[0-9a-f]{16}(\.[a-z0-9]+)?$/;
|
|
10
|
+
function readText(path) {
|
|
11
|
+
try {
|
|
12
|
+
return readFileSync(path, 'utf8');
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function parsedJson(json) {
|
|
19
|
+
if (json === null) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
return JSON.parse(json);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function isRemoteUrl(url) {
|
|
30
|
+
return REMOTE_URL.test(url) || url.startsWith('//');
|
|
31
|
+
}
|
|
32
|
+
function withoutQuery(url) {
|
|
33
|
+
return url.split(/[?#]/)[0];
|
|
34
|
+
}
|
|
35
|
+
function importsOf(css) {
|
|
36
|
+
return cssReferences(css)
|
|
37
|
+
.filter((reference) => reference.kind === 'import')
|
|
38
|
+
.map((reference) => reference.url);
|
|
39
|
+
}
|
|
40
|
+
function chunkImports(reader, hash) {
|
|
41
|
+
const known = reader.chunks.get(hash);
|
|
42
|
+
if (known !== undefined) {
|
|
43
|
+
return known;
|
|
44
|
+
}
|
|
45
|
+
const css = readText(join(reader.dataDir, 'styles', `${hash}.css`));
|
|
46
|
+
const imports = css === null ? [] : importsOf(css);
|
|
47
|
+
reader.chunks.set(hash, imports);
|
|
48
|
+
return imports;
|
|
49
|
+
}
|
|
50
|
+
function cssAsset(reader, name) {
|
|
51
|
+
const known = reader.cssAssets.get(name);
|
|
52
|
+
if (known !== undefined) {
|
|
53
|
+
return known;
|
|
54
|
+
}
|
|
55
|
+
const css = readText(join(reader.dataDir, 'assets', name)) ?? '';
|
|
56
|
+
const asset = {
|
|
57
|
+
remoteImports: importsOf(css).filter(isRemoteUrl),
|
|
58
|
+
siblings: cssReferences(css)
|
|
59
|
+
.map((reference) => withoutQuery(reference.url))
|
|
60
|
+
.filter((sibling) => ASSET_NAME.test(sibling) && sibling.endsWith('.css')),
|
|
61
|
+
};
|
|
62
|
+
reader.cssAssets.set(name, asset);
|
|
63
|
+
return asset;
|
|
64
|
+
}
|
|
65
|
+
function isNode(value) {
|
|
66
|
+
return typeof value === 'object' && value !== null;
|
|
67
|
+
}
|
|
68
|
+
function childrenOf(node) {
|
|
69
|
+
return Array.isArray(node.childNodes) ? node.childNodes.filter(isNode) : [];
|
|
70
|
+
}
|
|
71
|
+
function isStylesheetLink(node, attributes) {
|
|
72
|
+
const rel = attributes.rel;
|
|
73
|
+
return (node.tagName === 'link' &&
|
|
74
|
+
typeof rel === 'string' &&
|
|
75
|
+
rel.toLowerCase().split(/\s+/).includes('stylesheet'));
|
|
76
|
+
}
|
|
77
|
+
function cssTextImports(reader, cssText) {
|
|
78
|
+
if (!cssText.startsWith(STYLE_URL_PREFIX)) {
|
|
79
|
+
return importsOf(cssText);
|
|
80
|
+
}
|
|
81
|
+
const hashes = cssText.slice(STYLE_URL_PREFIX.length).split('+');
|
|
82
|
+
return hashes.flatMap((hash) => chunkImports(reader, hash));
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Add the `@import` targets and stylesheet links of a node and everything
|
|
86
|
+
* below it. rrweb keeps shadow roots and iframe documents among the child
|
|
87
|
+
* nodes, so they are read too.
|
|
88
|
+
*/
|
|
89
|
+
function addTargets(reader, node, targets) {
|
|
90
|
+
const attributes = isNode(node.attributes) ? node.attributes : {};
|
|
91
|
+
const { _cssText: cssText, href } = attributes;
|
|
92
|
+
if (typeof cssText === 'string') {
|
|
93
|
+
targets.push(...cssTextImports(reader, cssText));
|
|
94
|
+
}
|
|
95
|
+
else if (isStylesheetLink(node, attributes) && typeof href === 'string') {
|
|
96
|
+
targets.push(href);
|
|
97
|
+
}
|
|
98
|
+
const children = childrenOf(node);
|
|
99
|
+
// rrweb writes the text of a `<style>` only when it could not read the sheet.
|
|
100
|
+
if (node.tagName === 'style') {
|
|
101
|
+
for (const child of children) {
|
|
102
|
+
if (typeof child.textContent === 'string' && child.textContent.trim() !== '') {
|
|
103
|
+
targets.push(...importsOf(child.textContent));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
for (const child of children) {
|
|
108
|
+
addTargets(reader, child, targets);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function hostOf(url) {
|
|
112
|
+
const absolute = url.startsWith('//') ? `https:${url}` : url;
|
|
113
|
+
return URL.canParse(absolute) ? new URL(absolute).hostname : null;
|
|
114
|
+
}
|
|
115
|
+
/** The hosts of the remote stylesheets that targets reach, through CSS assets too. */
|
|
116
|
+
function reachedHosts(reader, targets) {
|
|
117
|
+
const remoteUrls = [];
|
|
118
|
+
const pending = [];
|
|
119
|
+
const seen = new Set();
|
|
120
|
+
const follow = (url) => {
|
|
121
|
+
if (isRemoteUrl(url)) {
|
|
122
|
+
remoteUrls.push(url);
|
|
123
|
+
}
|
|
124
|
+
else if (url.startsWith(ASSET_URL_PREFIX)) {
|
|
125
|
+
pending.push(withoutQuery(url.slice(ASSET_URL_PREFIX.length)));
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
targets.forEach(follow);
|
|
129
|
+
for (let name = pending.pop(); name !== undefined; name = pending.pop()) {
|
|
130
|
+
if (seen.has(name) || !name.endsWith('.css')) {
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
seen.add(name);
|
|
134
|
+
const asset = cssAsset(reader, name);
|
|
135
|
+
remoteUrls.push(...asset.remoteImports);
|
|
136
|
+
pending.push(...asset.siblings);
|
|
137
|
+
}
|
|
138
|
+
const hosts = new Set();
|
|
139
|
+
for (const url of remoteUrls) {
|
|
140
|
+
const host = hostOf(url);
|
|
141
|
+
if (host && !TEST_SERVER_HOSTS.has(host)) {
|
|
142
|
+
hosts.add(host);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return [...hosts];
|
|
146
|
+
}
|
|
147
|
+
function snapshotHosts(reader, file) {
|
|
148
|
+
const known = reader.snapshots.get(file);
|
|
149
|
+
if (known !== undefined) {
|
|
150
|
+
return known;
|
|
151
|
+
}
|
|
152
|
+
const node = parsedJson(readText(join(reader.dataDir, file)));
|
|
153
|
+
const targets = [];
|
|
154
|
+
if (isNode(node)) {
|
|
155
|
+
addTargets(reader, node, targets);
|
|
156
|
+
}
|
|
157
|
+
const hosts = reachedHosts(reader, targets);
|
|
158
|
+
reader.snapshots.set(file, hosts);
|
|
159
|
+
return hosts;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* The hosts that frames load stylesheets from, most frames first, counted
|
|
163
|
+
* from the files on disk by the rules of the reporter's `FontAudit`. It is a
|
|
164
|
+
* copy, because the viewer cannot depend on the reporter's package.
|
|
165
|
+
*/
|
|
166
|
+
export function remoteStylesheets(dataDir, manifest) {
|
|
167
|
+
const reader = { dataDir, chunks: new Map(), cssAssets: new Map(), snapshots: new Map() };
|
|
168
|
+
const frames = new Map();
|
|
169
|
+
const snapshots = manifest.modules.flatMap((mod) => {
|
|
170
|
+
return mod.tests.flatMap((test) => test.frames.map((frame) => frame.snapshot));
|
|
171
|
+
});
|
|
172
|
+
for (const snapshot of snapshots) {
|
|
173
|
+
for (const host of snapshotHosts(reader, snapshot)) {
|
|
174
|
+
frames.set(host, (frames.get(host) ?? 0) + 1);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return Array.from(frames, ([host, count]) => ({ host, frames: count })).sort((left, right) => right.frames - left.frames || left.host.localeCompare(right.host));
|
|
178
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { ASSET_URL_PREFIX } from '@describe-me/core/types';
|
|
2
|
+
/** A stored asset's name: a content hash and, when the file has one, its extension. */
|
|
3
|
+
const ASSET_REFERENCE = new RegExp(`${ASSET_URL_PREFIX}([0-9a-f]{16}(?:\\.[a-z0-9]+)?)`, 'g');
|
|
4
|
+
/** The text with each `describe-me-asset:` name that `renames` maps replaced by its new name. */
|
|
5
|
+
export function replaceAssetNames(text, renames) {
|
|
6
|
+
if (renames.size === 0) {
|
|
7
|
+
return text;
|
|
8
|
+
}
|
|
9
|
+
return text.replace(ASSET_REFERENCE, (reference, name) => {
|
|
10
|
+
const renamed = renames.get(name);
|
|
11
|
+
return renamed === undefined ? reference : `${ASSET_URL_PREFIX}${renamed}`;
|
|
12
|
+
});
|
|
13
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { TextSyntax } from './find-remote-urls.js';
|
|
2
|
+
/**
|
|
3
|
+
* The text with `prefix` and the stored name written in place of every remote
|
|
4
|
+
* URL that `names` maps, left to right. Other URLs stay as written.
|
|
5
|
+
*/
|
|
6
|
+
export declare function replaceRemoteUrls(text: string, syntax: TextSyntax, prefix: string, names: Map<string, string>): string;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { findRemoteUrls } from './find-remote-urls.js';
|
|
2
|
+
/**
|
|
3
|
+
* The text with `prefix` and the stored name written in place of every remote
|
|
4
|
+
* URL that `names` maps, left to right. Other URLs stay as written.
|
|
5
|
+
*/
|
|
6
|
+
export function replaceRemoteUrls(text, syntax, prefix, names) {
|
|
7
|
+
const parts = [];
|
|
8
|
+
let last = 0;
|
|
9
|
+
for (const match of findRemoteUrls(text, syntax)) {
|
|
10
|
+
const name = names.get(match.url);
|
|
11
|
+
if (name !== undefined) {
|
|
12
|
+
parts.push(text.slice(last, match.start), `${prefix}${name}`);
|
|
13
|
+
last = match.end;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
parts.push(text.slice(last));
|
|
17
|
+
return parts.join('');
|
|
18
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** What a data directory held before the downloads, and what they stored. */
|
|
2
|
+
export interface StoredFilesInput {
|
|
3
|
+
/** The stored name for each vendored URL. */
|
|
4
|
+
names: Map<string, string>;
|
|
5
|
+
/** Names of the `assets/*.css` files before the downloads. Only these may change. */
|
|
6
|
+
cssAssets: string[];
|
|
7
|
+
/** Paths of the texts in which the scan found a URL to vendor. */
|
|
8
|
+
kept: Set<string>;
|
|
9
|
+
/** Names of the files stored in this run. Their names are final. */
|
|
10
|
+
vendored: Set<string>;
|
|
11
|
+
}
|
|
12
|
+
/** What the rewrite changed. */
|
|
13
|
+
export interface RewrittenFiles {
|
|
14
|
+
/** Old → new name of each CSS asset stored under a new name. */
|
|
15
|
+
assetRenames: Map<string, string>;
|
|
16
|
+
/** Whether a snapshot, style chunk or CSS asset changed. */
|
|
17
|
+
changed: boolean;
|
|
18
|
+
/** Paths of the files that renamed ones replace, to remove once nothing names them. */
|
|
19
|
+
replaced: string[];
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Point the CSS assets, style chunks and snapshots of a data directory at the
|
|
23
|
+
* vendored files, in that order. A changed CSS asset or chunk moves to the
|
|
24
|
+
* hash of its new text before anything that names it changes. The files it
|
|
25
|
+
* replaces stay until the caller removes them.
|
|
26
|
+
*/
|
|
27
|
+
export declare function rewriteStoredFiles(dataDir: string, input: StoredFilesInput): RewrittenFiles;
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { cssReferences } from '@describe-me/core/css-references';
|
|
4
|
+
import { ASSET_URL_PREFIX, STYLE_URL_PREFIX } from '@describe-me/core/types';
|
|
5
|
+
import { fileNamesIn } from './file-names-in.js';
|
|
6
|
+
import { replaceAssetNames } from './replace-asset-names.js';
|
|
7
|
+
import { replaceRemoteUrls } from './replace-remote-urls.js';
|
|
8
|
+
import { storeVendoredFile } from './store-vendored-file.js';
|
|
9
|
+
/** A name that the style and asset stores gave: the sha1-16 of the text. */
|
|
10
|
+
const CONTENT_NAME = /^[0-9a-f]{16}\.css$/;
|
|
11
|
+
const STYLE_REFERENCE = new RegExp(`${STYLE_URL_PREFIX}([0-9a-f]{16}(?:\\+[0-9a-f]{16})*)`, 'g');
|
|
12
|
+
/**
|
|
13
|
+
* Write a changed text. A content-named file moves to the hash of its new
|
|
14
|
+
* bytes, written only when missing, so identical texts share one file. Any
|
|
15
|
+
* other file keeps its name.
|
|
16
|
+
*/
|
|
17
|
+
function storeText(dir, name, text) {
|
|
18
|
+
if (!CONTENT_NAME.test(name)) {
|
|
19
|
+
writeFileSync(join(dir, name), text);
|
|
20
|
+
return name;
|
|
21
|
+
}
|
|
22
|
+
return storeVendoredFile(dir, new TextEncoder().encode(text), '.css');
|
|
23
|
+
}
|
|
24
|
+
/** The CSS with each target replaced from the end, so earlier offsets stay valid. */
|
|
25
|
+
function replaceReferences(css, replacements) {
|
|
26
|
+
let rewritten = css;
|
|
27
|
+
for (const [reference, replacement] of [...replacements].reverse()) {
|
|
28
|
+
rewritten = rewritten.slice(0, reference.start) + replacement + rewritten.slice(reference.end);
|
|
29
|
+
}
|
|
30
|
+
return rewritten;
|
|
31
|
+
}
|
|
32
|
+
/** Each bare `.css` sibling that moved, with its query and fragment kept. */
|
|
33
|
+
function siblingReplacements(run, css) {
|
|
34
|
+
return cssReferences(css).flatMap((reference) => {
|
|
35
|
+
const name = reference.url.split(/[?#]/)[0];
|
|
36
|
+
if (!CONTENT_NAME.test(name)) {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
const finalName = resolveCssAsset(run, name);
|
|
40
|
+
if (finalName === name) {
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
return [[reference, `${finalName}${reference.url.slice(name.length)}`]];
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
function rewriteCssAsset(run, name) {
|
|
47
|
+
const css = readFileSync(join(run.dir, name), 'utf8');
|
|
48
|
+
const vendored = replaceRemoteUrls(css, 'css', '', run.names);
|
|
49
|
+
const rewritten = replaceReferences(vendored, siblingReplacements(run, vendored));
|
|
50
|
+
if (rewritten === css) {
|
|
51
|
+
return name;
|
|
52
|
+
}
|
|
53
|
+
run.changed = true;
|
|
54
|
+
return storeText(run.dir, name, rewritten);
|
|
55
|
+
}
|
|
56
|
+
/** The name a CSS asset ends up with, its siblings resolved first. */
|
|
57
|
+
function resolveCssAsset(run, name) {
|
|
58
|
+
const known = run.finalNames.get(name);
|
|
59
|
+
if (known !== undefined) {
|
|
60
|
+
return known;
|
|
61
|
+
}
|
|
62
|
+
if (!run.listed.has(name)) {
|
|
63
|
+
return name;
|
|
64
|
+
}
|
|
65
|
+
// Content hashes cannot form a cycle, so this only stops a hand-made one.
|
|
66
|
+
if (run.resolving.has(name)) {
|
|
67
|
+
return name;
|
|
68
|
+
}
|
|
69
|
+
run.resolving.add(name);
|
|
70
|
+
const finalName = rewriteCssAsset(run, name);
|
|
71
|
+
run.resolving.delete(name);
|
|
72
|
+
run.finalNames.set(name, finalName);
|
|
73
|
+
return finalName;
|
|
74
|
+
}
|
|
75
|
+
function rewriteCssAssets(dataDir, input) {
|
|
76
|
+
const run = {
|
|
77
|
+
dir: join(dataDir, 'assets'),
|
|
78
|
+
names: input.names,
|
|
79
|
+
listed: new Set(input.cssAssets.filter((name) => !input.vendored.has(name))),
|
|
80
|
+
finalNames: new Map(),
|
|
81
|
+
resolving: new Set(),
|
|
82
|
+
changed: false,
|
|
83
|
+
};
|
|
84
|
+
for (const name of run.listed) {
|
|
85
|
+
resolveCssAsset(run, name);
|
|
86
|
+
}
|
|
87
|
+
const renames = new Map([...run.finalNames].filter(([name, finalName]) => finalName !== name));
|
|
88
|
+
return { renames, changed: run.changed };
|
|
89
|
+
}
|
|
90
|
+
function rewriteChunks(dataDir, input, assetRenames) {
|
|
91
|
+
const dir = join(dataDir, 'styles');
|
|
92
|
+
const result = { renames: new Map(), changed: false };
|
|
93
|
+
for (const name of fileNamesIn(dir, '.css')) {
|
|
94
|
+
const path = join(dir, name);
|
|
95
|
+
if (assetRenames.size === 0 && !input.kept.has(path)) {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
const css = readFileSync(path, 'utf8');
|
|
99
|
+
const vendored = replaceRemoteUrls(css, 'css', ASSET_URL_PREFIX, input.names);
|
|
100
|
+
const rewritten = replaceAssetNames(vendored, assetRenames);
|
|
101
|
+
if (rewritten !== css) {
|
|
102
|
+
result.changed = true;
|
|
103
|
+
const stored = storeText(dir, name, rewritten);
|
|
104
|
+
if (stored !== name) {
|
|
105
|
+
result.renames.set(name, stored);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
111
|
+
/** Each renamed hash inside a `describe-me-style:` reference, in the order written. */
|
|
112
|
+
function replaceChunkHashes(json, renames) {
|
|
113
|
+
if (renames.size === 0) {
|
|
114
|
+
return json;
|
|
115
|
+
}
|
|
116
|
+
return json.replace(STYLE_REFERENCE, (_reference, hashes) => {
|
|
117
|
+
const renamed = hashes.split('+').map((hash) => renames.get(hash) ?? hash);
|
|
118
|
+
return `${STYLE_URL_PREFIX}${renamed.join('+')}`;
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
function hashRenames(chunkRenames) {
|
|
122
|
+
const hashOf = (name) => name.slice(0, -'.css'.length);
|
|
123
|
+
return new Map([...chunkRenames].map(([name, renamed]) => [hashOf(name), hashOf(renamed)]));
|
|
124
|
+
}
|
|
125
|
+
/** Snapshots keep their names, because the viewer always fetches them fresh. */
|
|
126
|
+
function rewriteSnapshots(dataDir, input, assetRenames, chunkRenames) {
|
|
127
|
+
const dir = join(dataDir, 'snapshots');
|
|
128
|
+
const readAll = assetRenames.size > 0 || chunkRenames.size > 0;
|
|
129
|
+
const chunkHashes = hashRenames(chunkRenames);
|
|
130
|
+
let changed = false;
|
|
131
|
+
for (const name of fileNamesIn(dir, '.json')) {
|
|
132
|
+
const path = join(dir, name);
|
|
133
|
+
if (!readAll && !input.kept.has(path)) {
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
const json = readFileSync(path, 'utf8');
|
|
137
|
+
const vendored = replaceRemoteUrls(json, 'json', ASSET_URL_PREFIX, input.names);
|
|
138
|
+
const rewritten = replaceChunkHashes(replaceAssetNames(vendored, assetRenames), chunkHashes);
|
|
139
|
+
if (rewritten !== json) {
|
|
140
|
+
writeFileSync(path, rewritten);
|
|
141
|
+
changed = true;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return changed;
|
|
145
|
+
}
|
|
146
|
+
/** The old files that no new name reuses. */
|
|
147
|
+
function replacedPaths(dir, renames, vendored) {
|
|
148
|
+
const finalNames = new Set(renames.values());
|
|
149
|
+
return [...renames.keys()]
|
|
150
|
+
.filter((name) => !finalNames.has(name) && !vendored.has(name))
|
|
151
|
+
.map((name) => join(dir, name));
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Point the CSS assets, style chunks and snapshots of a data directory at the
|
|
155
|
+
* vendored files, in that order. A changed CSS asset or chunk moves to the
|
|
156
|
+
* hash of its new text before anything that names it changes. The files it
|
|
157
|
+
* replaces stay until the caller removes them.
|
|
158
|
+
*/
|
|
159
|
+
export function rewriteStoredFiles(dataDir, input) {
|
|
160
|
+
if (input.names.size === 0) {
|
|
161
|
+
return { assetRenames: new Map(), changed: false, replaced: [] };
|
|
162
|
+
}
|
|
163
|
+
const assets = rewriteCssAssets(dataDir, input);
|
|
164
|
+
const chunks = rewriteChunks(dataDir, input, assets.renames);
|
|
165
|
+
const snapshotsChanged = rewriteSnapshots(dataDir, input, assets.renames, chunks.renames);
|
|
166
|
+
return {
|
|
167
|
+
assetRenames: assets.renames,
|
|
168
|
+
changed: assets.changed || chunks.changed || snapshotsChanged,
|
|
169
|
+
replaced: [
|
|
170
|
+
...replacedPaths(join(dataDir, 'assets'), assets.renames, input.vendored),
|
|
171
|
+
...replacedPaths(join(dataDir, 'styles'), chunks.renames, new Set()),
|
|
172
|
+
],
|
|
173
|
+
};
|
|
174
|
+
}
|
|
@@ -1,2 +1,5 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Write a file into `dir` under its content hash and return the name. Downloads
|
|
3
|
+
* go into `assets/`, rewritten style chunks and CSS assets into their own directory.
|
|
4
|
+
*/
|
|
5
|
+
export declare function storeVendoredFile(dir: string, bytes: Uint8Array, extension: string): string;
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { vendoredName } from './vendored-name.js';
|
|
4
|
-
/**
|
|
5
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Write a file into `dir` under its content hash and return the name. Downloads
|
|
6
|
+
* go into `assets/`, rewritten style chunks and CSS assets into their own directory.
|
|
7
|
+
*/
|
|
8
|
+
export function storeVendoredFile(dir, bytes, extension) {
|
|
6
9
|
const name = vendoredName(bytes, extension);
|
|
7
|
-
const target = join(
|
|
10
|
+
const target = join(dir, name);
|
|
8
11
|
if (!existsSync(target)) {
|
|
9
|
-
mkdirSync(
|
|
12
|
+
mkdirSync(dir, { recursive: true });
|
|
10
13
|
writeFileSync(target, bytes);
|
|
11
14
|
}
|
|
12
15
|
return name;
|
|
@@ -33,7 +33,10 @@ export interface VendorReport {
|
|
|
33
33
|
/**
|
|
34
34
|
* Download the web fonts that a data directory loads from known font hosts
|
|
35
35
|
* into its `assets/`, and point the manifest head, snapshots, style chunks and
|
|
36
|
-
* CSS assets at the copies. Meant for the copy inside a built site.
|
|
37
|
-
*
|
|
36
|
+
* CSS assets at the copies. Meant for the copy inside a built site. A style
|
|
37
|
+
* chunk or CSS asset whose text changes moves to the hash of its new text, and
|
|
38
|
+
* the manifest's `remoteStylesheets` is counted again. Every file is stored
|
|
39
|
+
* before any text that names it changes, and replaced files are removed last,
|
|
40
|
+
* so a crash leaves only valid references.
|
|
38
41
|
*/
|
|
39
42
|
export declare function vendorFonts(dataDir: string, options: VendorFontsOptions): Promise<VendorReport>;
|