pagespeed-quest 0.10.0 → 0.11.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 (49) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.ja.md +42 -0
  3. package/README.md +43 -0
  4. package/build/cli.d.ts +8 -0
  5. package/build/cli.js +264 -0
  6. package/build/command.js +3 -229
  7. package/build/llm.d.ts +20 -0
  8. package/build/llm.js +42 -0
  9. package/llmdocs/00-guide.md +124 -0
  10. package/llmdocs/10-inventory.md +133 -0
  11. package/llmdocs/20-experiments.md +194 -0
  12. package/llmdocs/30-measuring.md +117 -0
  13. package/llmdocs/90-commands.md +181 -0
  14. package/package.json +12 -6
  15. package/scripts/build-llm-docs.js +70 -0
  16. package/scripts/validate-plugin-skills.js +117 -0
  17. package/build/docs/assets/css/main.css +0 -1
  18. package/build/docs/assets/images/icons.png +0 -0
  19. package/build/docs/assets/images/icons@2x.png +0 -0
  20. package/build/docs/assets/images/widgets.png +0 -0
  21. package/build/docs/assets/images/widgets@2x.png +0 -0
  22. package/build/docs/assets/js/main.js +0 -51
  23. package/build/docs/classes/inventoryrepository.html +0 -305
  24. package/build/docs/classes/playbackproxy.html +0 -524
  25. package/build/docs/classes/proxy.html +0 -474
  26. package/build/docs/classes/recordingproxy.html +0 -532
  27. package/build/docs/classes/testproxy.html +0 -497
  28. package/build/docs/classes/throttle.html +0 -425
  29. package/build/docs/classes/throttlinglog.html +0 -174
  30. package/build/docs/classes/throttlingtransform.html +0 -6297
  31. package/build/docs/globals.html +0 -1131
  32. package/build/docs/index.html +0 -348
  33. package/build/docs/interfaces/beautifyresult.html +0 -174
  34. package/build/docs/interfaces/contentencodingpair.html +0 -174
  35. package/build/docs/interfaces/inventory.html +0 -174
  36. package/build/docs/interfaces/lighthouseoptions.html +0 -230
  37. package/build/docs/interfaces/playbacktransaction.html +0 -272
  38. package/build/docs/interfaces/proxyoptions.html +0 -423
  39. package/build/docs/interfaces/recordingsession.html +0 -174
  40. package/build/docs/interfaces/recordingtransaction.html +0 -286
  41. package/build/docs/interfaces/resource.html +0 -314
  42. package/build/docs/interfaces/transaction.html +0 -258
  43. package/build/docs/interfaces/withproxyoptions.html +0 -418
  44. package/build/logger.d.ts +0 -2
  45. package/build/logger.js +0 -15
  46. package/build/proxy.d.ts +0 -35
  47. package/build/proxy.js +0 -96
  48. package/build/throttling.d.ts +0 -33
  49. package/build/throttling.js +0 -89
package/build/command.js CHANGED
@@ -1,230 +1,4 @@
1
1
  #!/usr/bin/env node
2
- import Path from 'path';
3
- import { Command } from 'commander';
4
- import Watch from 'node-watch';
5
- import { Dependency } from './dependency.js';
6
- import { execLighthouse } from './lighthouse.js';
7
- import { execLoadshow } from './loadshow.js';
8
- import { execWebshotCapture, execWebshotCompare } from './webshot.js';
9
- import { InventoryRepository, withPlaybackProxy, withRecordingProxy } from './index.js';
10
- const dependency = new Dependency();
11
- function collect(value, previous) {
12
- return previous.concat([value]);
13
- }
14
- const main = new Command();
15
- main.option('-i, --inventory <dir>', 'Inventory directory', './inventory');
16
- function registerLighthouseCommands(main) {
17
- const lighthouse = main.command('lighthouse');
18
- lighthouse.description('Run Lighthouse (performance category) via a proxy');
19
- lighthouse.option('-a, --artifacts <dir>', 'Artifacts directory', './artifacts');
20
- lighthouse.option('-l, --laud', 'Show browser and open the report after completion', false);
21
- lighthouse.option('-t, --timeout <ms>', 'Timeout milliseconds', '30000');
22
- const recording = lighthouse.command('recording');
23
- recording.description('Record contents by lighthouse');
24
- recording.option('-d, --device <mobile|desktop>', 'Device type', 'mobile');
25
- recording.option('-x, --exclude <pattern>', 'Regex patterns to exclude URLs from recording (repeatable)', collect, []);
26
- recording.argument('<url>', 'Url to measure performance');
27
- recording.action(async (url) => {
28
- const inventoryRepository = new InventoryRepository(main.opts().inventory || './inventory');
29
- const deviceType = recording.opts().device || 'mobile';
30
- const artifactsDir = lighthouse.opts().artifacts || './artifacts';
31
- const laud = !!lighthouse.opts().laud;
32
- const timeout = Number(lighthouse.opts().timeout || '30000');
33
- const excludePatterns = recording.opts().exclude || [];
34
- await withRecordingProxy({
35
- entryUrl: url,
36
- deviceType,
37
- inventoryRepository,
38
- excludePatterns: excludePatterns.length > 0 ? excludePatterns : undefined,
39
- }, dependency, async (proxy) => {
40
- await execLighthouse({
41
- url,
42
- proxyPort: proxy.port,
43
- deviceType,
44
- view: laud,
45
- artifactsDir,
46
- headless: !laud,
47
- timeout,
48
- }, dependency);
49
- dependency.logger?.info('Lighthouse completed. Saving inventory...');
50
- });
51
- });
52
- const playback = lighthouse.command('playback');
53
- playback.description('Playback contents for lighthouse');
54
- playback.action(async () => {
55
- const inventoryDir = main.opts().inventory || './inventory';
56
- const inventoryRepository = new InventoryRepository(inventoryDir);
57
- const artifactsDir = lighthouse.opts().artifacts || './artifacts';
58
- const laud = !!lighthouse.opts().laud;
59
- const timeout = Number(lighthouse.opts().timeout || '30000');
60
- // Load inventory for traffic statistics
61
- const inventory = await inventoryRepository.loadInventory();
62
- await withPlaybackProxy({
63
- inventoryRepository,
64
- }, dependency, async (proxy) => {
65
- await execLighthouse({
66
- url: proxy.entryUrl,
67
- proxyPort: proxy.port,
68
- deviceType: proxy.deviceType,
69
- view: laud,
70
- artifactsDir,
71
- headless: !laud,
72
- timeout,
73
- inventoryDir,
74
- resources: inventory.resources,
75
- }, dependency);
76
- dependency.logger?.info('Lighthouse completed');
77
- });
78
- });
79
- }
80
- function registerLoadshowCommands(main) {
81
- const loadshow = main.command('loadshow');
82
- loadshow.description('Run loadshow via a proxy');
83
- loadshow.option('-a, --artifacts <dir>', 'Artifacts directory', './artifacts');
84
- loadshow.option('-c, --credit <string>', 'Credit string');
85
- loadshow.option('-t, --timeout <ms>', 'Timeout milliseconds', '30000');
86
- const recording = loadshow.command('recording');
87
- recording.description('Record contents by loadshow');
88
- recording.option('-d, --device <mobile|desktop>', 'Device type', 'mobile');
89
- recording.option('-x, --exclude <pattern>', 'Regex patterns to exclude URLs from recording (repeatable)', collect, []);
90
- recording.argument('<url>', 'Url to measure performance');
91
- recording.action(async (url) => {
92
- const inventoryRepository = new InventoryRepository(main.opts().inventory || './inventory');
93
- const deviceType = recording.opts().device || 'mobile';
94
- const artifactsDir = loadshow.opts().artifacts || './artifacts';
95
- const credit = loadshow.opts().credit || '';
96
- const timeout = Number(loadshow.opts().timeout || '30000');
97
- const excludePatterns = recording.opts().exclude || [];
98
- await withRecordingProxy({
99
- entryUrl: url,
100
- deviceType,
101
- inventoryRepository,
102
- excludePatterns: excludePatterns.length > 0 ? excludePatterns : undefined,
103
- }, dependency, async (proxy) => {
104
- await execLoadshow({ url, proxyPort: proxy.port, deviceType, artifactsDir, credit, timeout }, dependency);
105
- dependency.logger?.info('Loadshow completed. Saving inventory...');
106
- });
107
- });
108
- const playback = loadshow.command('playback');
109
- playback.description('Playback contents for loadshow');
110
- playback.action(async () => {
111
- const inventoryRepository = new InventoryRepository(main.opts().inventory || './inventory');
112
- const artifactsDir = loadshow.opts().artifacts || './artifacts';
113
- const credit = loadshow.opts().credit || '';
114
- const timeout = Number(loadshow.opts().timeout || '30000');
115
- await withPlaybackProxy({
116
- inventoryRepository,
117
- }, dependency, async (proxy) => {
118
- await execLoadshow({
119
- url: proxy.entryUrl,
120
- proxyPort: proxy.port,
121
- deviceType: proxy.deviceType,
122
- artifactsDir,
123
- credit,
124
- timeout,
125
- }, dependency);
126
- dependency.logger?.info('Loadshow completed');
127
- });
128
- });
129
- }
130
- function registerProxyCommands(main) {
131
- const proxy = main.command('proxy');
132
- proxy.option('-p, --port <number>', 'Proxy port', '8080');
133
- proxy.option('-r, --record <url>', 'Recording URL to start the proxy as recording mode', '');
134
- proxy.option('-x, --exclude <pattern>', 'Regex patterns to exclude URLs from recording (repeatable)', collect, []);
135
- proxy.action(async () => {
136
- const inventoryRepository = new InventoryRepository(main.opts().inventory || './inventory');
137
- const port = Number(proxy.opts().port || '8080');
138
- if (proxy.opts().record) {
139
- const url = proxy.opts().record;
140
- if (!url) {
141
- throw new Error('Recording URL must be specified with --record option.');
142
- }
143
- const excludePatterns = proxy.opts().exclude || [];
144
- // Recordingモード
145
- await withRecordingProxy({
146
- inventoryRepository,
147
- port,
148
- entryUrl: url,
149
- excludePatterns: excludePatterns.length > 0 ? excludePatterns : undefined,
150
- }, dependency, async () => {
151
- dependency.logger?.info(`Recording proxy started on port ${port}. Press Ctrl+C to stop.`);
152
- // Wait for Ctrl+C signal
153
- return new Promise((resolve) => {
154
- process.on('SIGINT', () => {
155
- dependency.logger?.info('Saving the inventory...');
156
- resolve();
157
- });
158
- });
159
- });
160
- }
161
- else {
162
- // Playbackモード
163
- // eslint-disable-next-line no-constant-condition
164
- while (true) {
165
- await withPlaybackProxy({ inventoryRepository, port }, dependency, async () => {
166
- const watcher = Watch(inventoryRepository.dirPath, { recursive: true });
167
- return new Promise((ok) => {
168
- watcher.on('change', () => {
169
- watcher.close();
170
- dependency.logger?.info('Inventory changed. Restarting proxy...');
171
- ok();
172
- });
173
- });
174
- });
175
- }
176
- }
177
- });
178
- }
179
- function registerCaptureCommands(main) {
180
- const visualTest = main.command('capture');
181
- visualTest.description('Capture a screenshot via playback proxy and optionally compare with a baseline');
182
- visualTest.option('-a, --artifacts <dir>', 'Artifacts directory', './artifacts');
183
- visualTest.option('-t, --timeout <ms>', 'Timeout milliseconds', '30000');
184
- visualTest.option('--compare <file>', 'Baseline PNG file to compare against');
185
- visualTest.option('--baseline-label <label>', 'Label for the baseline image', '前回');
186
- visualTest.option('--current-label <label>', 'Label for the current image', '今回');
187
- visualTest.action(async () => {
188
- const inventoryRepository = new InventoryRepository(main.opts().inventory || './inventory');
189
- const artifactsDir = visualTest.opts().artifacts || './artifacts';
190
- const timeout = Number(visualTest.opts().timeout || '30000');
191
- const compareFile = visualTest.opts().compare;
192
- const baselineLabel = visualTest.opts().baselineLabel || '前回';
193
- const currentLabel = visualTest.opts().currentLabel || '今回';
194
- await withPlaybackProxy({
195
- inventoryRepository,
196
- fullThrottle: true,
197
- }, dependency, async (proxy) => {
198
- const outputPath = Path.join(artifactsDir, 'capture.png');
199
- await dependency.mkdirp(artifactsDir);
200
- await execWebshotCapture({
201
- url: proxy.entryUrl,
202
- proxyPort: proxy.port,
203
- preset: proxy.deviceType === 'desktop' ? 'desktop' : 'mobile',
204
- output: outputPath,
205
- timeout,
206
- }, dependency);
207
- dependency.logger?.info(`Screenshot saved to ${outputPath}`);
208
- if (compareFile) {
209
- const diffOutput = Path.join(artifactsDir, 'capture-diff.png');
210
- const digestTxt = Path.join(artifactsDir, 'capture-diff.txt');
211
- await execWebshotCompare({
212
- baseline: compareFile,
213
- current: outputPath,
214
- output: diffOutput,
215
- digestTxt,
216
- baselineLabel,
217
- currentLabel,
218
- }, dependency);
219
- dependency.logger?.info(`Diff image saved to ${diffOutput}`);
220
- dependency.logger?.info(`Diff digest saved to ${digestTxt}`);
221
- }
222
- });
223
- });
224
- }
225
- registerLighthouseCommands(main);
226
- registerLoadshowCommands(main);
227
- registerProxyCommands(main);
228
- registerCaptureCommands(main);
229
- main.parse(process.argv);
230
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY29tbWFuZC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3NyYy9jb21tYW5kLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7QUFFQSxPQUFPLElBQUksTUFBTSxNQUFNLENBQUE7QUFFdkIsT0FBTyxFQUFFLE9BQU8sRUFBRSxNQUFNLFdBQVcsQ0FBQTtBQUNuQyxPQUFPLEtBQUssTUFBTSxZQUFZLENBQUE7QUFFOUIsT0FBTyxFQUFFLFVBQVUsRUFBRSxNQUFNLGlCQUFpQixDQUFBO0FBQzVDLE9BQU8sRUFBRSxjQUFjLEVBQUUsTUFBTSxpQkFBaUIsQ0FBQTtBQUNoRCxPQUFPLEVBQUUsWUFBWSxFQUFFLE1BQU0sZUFBZSxDQUFBO0FBRTVDLE9BQU8sRUFBRSxrQkFBa0IsRUFBRSxrQkFBa0IsRUFBRSxNQUFNLGNBQWMsQ0FBQTtBQUVyRSxPQUFPLEVBQUUsbUJBQW1CLEVBQUUsaUJBQWlCLEVBQUUsa0JBQWtCLEVBQUUsTUFBTSxZQUFZLENBQUE7QUFFdkYsTUFBTSxVQUFVLEdBQUcsSUFBSSxVQUFVLEVBQUUsQ0FBQTtBQUVuQyxTQUFTLE9BQU8sQ0FBQyxLQUFhLEVBQUUsUUFBa0I7SUFDaEQsT0FBTyxRQUFRLENBQUMsTUFBTSxDQUFDLENBQUMsS0FBSyxDQUFDLENBQUMsQ0FBQTtBQUNqQyxDQUFDO0FBRUQsTUFBTSxJQUFJLEdBQUcsSUFBSSxPQUFPLEVBQUUsQ0FBQTtBQUMxQixJQUFJLENBQUMsTUFBTSxDQUFDLHVCQUF1QixFQUFFLHFCQUFxQixFQUFFLGFBQWEsQ0FBQyxDQUFBO0FBRTFFLFNBQVMsMEJBQTBCLENBQUMsSUFBYTtJQUMvQyxNQUFNLFVBQVUsR0FBRyxJQUFJLENBQUMsT0FBTyxDQUFDLFlBQVksQ0FBQyxDQUFBO0lBQzdDLFVBQVUsQ0FBQyxXQUFXLENBQUMsbURBQW1ELENBQUMsQ0FBQTtJQUMzRSxVQUFVLENBQUMsTUFBTSxDQUFDLHVCQUF1QixFQUFFLHFCQUFxQixFQUFFLGFBQWEsQ0FBQyxDQUFBO0lB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
2
+ import { createProgram } from './cli.js';
3
+ createProgram().parse(process.argv);
4
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY29tbWFuZC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3NyYy9jb21tYW5kLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7QUFFQSxPQUFPLEVBQUUsYUFBYSxFQUFFLE1BQU0sVUFBVSxDQUFBO0FBRXhDLGFBQWEsRUFBRSxDQUFDLEtBQUssQ0FBQyxPQUFPLENBQUMsSUFBSSxDQUFDLENBQUEifQ==
package/build/llm.d.ts ADDED
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Directory holding the single source of knowledge served to AI agents.
3
+ *
4
+ * The Markdown files are shipped with the npm package (see the `files` field in
5
+ * package.json) so that `psq llm` works offline and always matches the version
6
+ * of the package that is installed. They are also what context7 crawls.
7
+ */
8
+ export declare const LlmDocsDir: string;
9
+ /** Version of the installed package, used by `psq --version`. */
10
+ export declare function packageVersion(): string;
11
+ export interface LlmDocChapter {
12
+ /** File name without the numeric prefix and extension, e.g. `guide` */
13
+ name: string;
14
+ /** First level-1 heading of the chapter */
15
+ title: string;
16
+ /** Markdown body, heading included */
17
+ content: string;
18
+ }
19
+ export declare function loadLlmDocs(dir?: string): Promise<LlmDocChapter[]>;
20
+ export declare function renderLlmDocs(format?: 'markdown' | 'json', dir?: string): Promise<string>;
package/build/llm.js ADDED
@@ -0,0 +1,42 @@
1
+ import Fs from 'fs';
2
+ import Fsp from 'fs/promises';
3
+ import Path from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = Path.dirname(__filename);
7
+ /**
8
+ * Directory holding the single source of knowledge served to AI agents.
9
+ *
10
+ * The Markdown files are shipped with the npm package (see the `files` field in
11
+ * package.json) so that `psq llm` works offline and always matches the version
12
+ * of the package that is installed. They are also what context7 crawls.
13
+ */
14
+ export const LlmDocsDir = Path.resolve(__dirname, '..', 'llmdocs');
15
+ /** Version of the installed package, used by `psq --version`. */
16
+ export function packageVersion() {
17
+ const packageJsonPath = Path.resolve(__dirname, '..', 'package.json');
18
+ const packageJson = JSON.parse(Fs.readFileSync(packageJsonPath, 'utf8'));
19
+ return packageJson.version;
20
+ }
21
+ function titleOf(markdown, fallback) {
22
+ const heading = markdown.split(/\r?\n/).find((line) => line.startsWith('# '));
23
+ return heading ? heading.slice(2).trim() : fallback;
24
+ }
25
+ export async function loadLlmDocs(dir = LlmDocsDir) {
26
+ const entries = await Fsp.readdir(dir);
27
+ const files = entries.filter((name) => name.endsWith('.md')).sort();
28
+ const chapters = [];
29
+ for (const file of files) {
30
+ const content = await Fsp.readFile(Path.join(dir, file), 'utf8');
31
+ const name = Path.basename(file, '.md').replace(/^\d+-/, '');
32
+ chapters.push({ name, title: titleOf(content, name), content: content.trimEnd() });
33
+ }
34
+ return chapters;
35
+ }
36
+ export async function renderLlmDocs(format = 'markdown', dir) {
37
+ const chapters = await loadLlmDocs(dir);
38
+ if (format === 'json')
39
+ return `${JSON.stringify(chapters, null, 2)}\n`;
40
+ return `${chapters.map((chapter) => chapter.content).join('\n\n---\n\n')}\n`;
41
+ }
42
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibGxtLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vc3JjL2xsbS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBQUUsTUFBTSxJQUFJLENBQUE7QUFDbkIsT0FBTyxHQUFHLE1BQU0sYUFBYSxDQUFBO0FBQzdCLE9BQU8sSUFBSSxNQUFNLE1BQU0sQ0FBQTtBQUN2QixPQUFPLEVBQUUsYUFBYSxFQUFFLE1BQU0sS0FBSyxDQUFBO0FBRW5DLE1BQU0sVUFBVSxHQUFHLGFBQWEsQ0FBQyxNQUFNLENBQUMsSUFBSSxDQUFDLEdBQUcsQ0FBQyxDQUFBO0FBQ2pELE1BQU0sU0FBUyxHQUFHLElBQUksQ0FBQyxPQUFPLENBQUMsVUFBVSxDQUFDLENBQUE7QUFFMUM7Ozs7OztHQU1HO0FBQ0gsTUFBTSxDQUFDLE1BQU0sVUFBVSxHQUFHLElBQUksQ0FBQyxPQUFPLENBQUMsU0FBUyxFQUFFLElBQUksRUFBRSxTQUFTLENBQUMsQ0FBQTtBQUVsRSxpRUFBaUU7QUFDakUsTUFBTSxVQUFVLGNBQWM7SUFDNUIsTUFBTSxlQUFlLEdBQUcsSUFBSSxDQUFDLE9BQU8sQ0FBQyxTQUFTLEVBQUUsSUFBSSxFQUFFLGNBQWMsQ0FBQyxDQUFBO0lBQ3JFLE1BQU0sV0FBVyxHQUFHLElBQUksQ0FBQyxLQUFLLENBQUMsRUFBRSxDQUFDLFlBQVksQ0FBQyxlQUFlLEVBQUUsTUFBTSxDQUFDLENBQUMsQ0FBQTtJQUN4RSxPQUFPLFdBQVcsQ0FBQyxPQUFpQixDQUFBO0FBQ3RDLENBQUM7QUFXRCxTQUFTLE9BQU8sQ0FBQyxRQUFnQixFQUFFLFFBQWdCO0lBQ2pELE1BQU0sT0FBTyxHQUFHLFFBQVEsQ0FBQyxLQUFLLENBQUMsT0FBTyxDQUFDLENBQUMsSUFBSSxDQUFDLENBQUMsSUFBSSxFQUFFLEVBQUUsQ0FBQyxJQUFJLENBQUMsVUFBVSxDQUFDLElBQUksQ0FBQyxDQUFDLENBQUE7SUFDN0UsT0FBTyxPQUFPLENBQUMsQ0FBQyxDQUFDLE9BQU8sQ0FBQyxLQUFLLENBQUMsQ0FBQyxDQUFDLENBQUMsSUFBSSxFQUFFLENBQUMsQ0FBQyxDQUFDLFFBQVEsQ0FBQTtBQUNyRCxDQUFDO0FBRUQsTUFBTSxDQUFDLEtBQUssVUFBVSxXQUFXLENBQUMsTUFBYyxVQUFVO0lBQ3hELE1BQU0sT0FBTyxHQUFHLE1BQU0sR0FBRyxDQUFDLE9BQU8sQ0FBQyxHQUFHLENBQUMsQ0FBQTtJQUN0QyxNQUFNLEtBQUssR0FBRyxPQUFPLENBQUMsTUFBTSxDQUFDLENBQUMsSUFBSSxFQUFFLEVBQUUsQ0FBQyxJQUFJLENBQUMsUUFBUSxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUMsSUFBSSxFQUFFLENBQUE7SUFFbkUsTUFBTSxRQUFRLEdBQW9CLEVBQUUsQ0FBQTtJQUNwQyxLQUFLLE1BQU0sSUFBSSxJQUFJLEtBQUssRUFBRSxDQUFDO1FBQ3pCLE1BQU0sT0FBTyxHQUFHLE1BQU0sR0FBRyxDQUFDLFFBQVEsQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLEdBQUcsRUFBRSxJQUFJLENBQUMsRUFBRSxNQUFNLENBQUMsQ0FBQTtRQUNoRSxNQUFNLElBQUksR0FBRyxJQUFJLENBQUMsUUFBUSxDQUFDLElBQUksRUFBRSxLQUFLLENBQUMsQ0FBQyxPQUFPLENBQUMsT0FBTyxFQUFFLEVBQUUsQ0FBQyxDQUFBO1FBQzVELFFBQVEsQ0FBQyxJQUFJLENBQUMsRUFBRSxJQUFJLEVBQUUsS0FBSyxFQUFFLE9BQU8sQ0FBQyxPQUFPLEVBQUUsSUFBSSxDQUFDLEVBQUUsT0FBTyxFQUFFLE9BQU8sQ0FBQyxPQUFPLEVBQUUsRUFBRSxDQUFDLENBQUE7SUFDcEYsQ0FBQztJQUVELE9BQU8sUUFBUSxDQUFBO0FBQ2pCLENBQUM7QUFFRCxNQUFNLENBQUMsS0FBSyxVQUFVLGFBQWEsQ0FBQyxTQUE4QixVQUFVLEVBQUUsR0FBWTtJQUN4RixNQUFNLFFBQVEsR0FBRyxNQUFNLFdBQVcsQ0FBQyxHQUFHLENBQUMsQ0FBQTtJQUN2QyxJQUFJLE1BQU0sS0FBSyxNQUFNO1FBQUUsT0FBTyxHQUFHLElBQUksQ0FBQyxTQUFTLENBQUMsUUFBUSxFQUFFLElBQUksRUFBRSxDQUFDLENBQUMsSUFBSSxDQUFBO0lBQ3RFLE9BQU8sR0FBRyxRQUFRLENBQUMsR0FBRyxDQUFDLENBQUMsT0FBTyxFQUFFLEVBQUUsQ0FBQyxPQUFPLENBQUMsT0FBTyxDQUFDLENBQUMsSUFBSSxDQUFDLGFBQWEsQ0FBQyxJQUFJLENBQUE7QUFDOUUsQ0FBQyJ9
@@ -0,0 +1,124 @@
1
+ # pagespeed-quest — reference for AI agents
2
+
3
+ PageSpeed Quest (`psq`) records a real page load through an HTTP proxy, writes
4
+ it to disk as an editable set of files, and replays it with the **original
5
+ timing** reproduced. You then edit those files — the HTML, the CSS, the images,
6
+ even the per-resource latency — replay again, and measure the difference.
7
+
8
+ The point is that you do **not** need the site's development environment, its
9
+ repository, or a deploy. Any public page can be frozen into a local, editable,
10
+ repeatable copy of itself. A hypothesis like "inlining critical CSS would gain
11
+ 0.4s of LCP" becomes something you can answer in minutes, on someone else's
12
+ site, before writing a line of production code.
13
+
14
+ Recording is powered by [rust-http-playback-proxy]. Measurement is delegated to
15
+ [Lighthouse] (scores and metrics), [loadshow] (a video of the load) and
16
+ [static-webshot] (screenshots and visual diffs). `psq` is the layer that makes
17
+ the recording **editable** and the comparison **repeatable**.
18
+
19
+ [rust-http-playback-proxy]: https://github.com/ideamans/rust-http-playback-proxy
20
+ [Lighthouse]: https://developer.chrome.com/docs/lighthouse/overview/
21
+ [loadshow]: https://github.com/ideamans/go-loadshow
22
+ [static-webshot]: https://github.com/ideamans/static-webshot
23
+
24
+ This reference ships inside the package, so `psq llm` always describes the exact
25
+ version installed in the project you are working in.
26
+
27
+ ## Ground rules
28
+
29
+ 1. **Record once. Compare against that recording, never against the live site.**
30
+ The live site changes between runs; the recording does not. That is the whole
31
+ value proposition. A "before" measured against the network and an "after"
32
+ measured against the inventory are not comparable numbers.
33
+
34
+ 2. **Copy the inventory before you edit it.** There is no undo, and the baseline
35
+ is the thing you are comparing to.
36
+
37
+ ```sh
38
+ cp -r inventory baseline
39
+ ```
40
+
41
+ Then measure either side by pointing `-i` at it. Keep `baseline/` untouched
42
+ for the rest of the quest.
43
+
44
+ 3. **`-i/--inventory` is a root option — it goes before the subcommand.**
45
+ `psq -i ./baseline lighthouse playback` works; `psq lighthouse playback -i
46
+ ./baseline` does not. The same applies one level down: `-a`, `-t`, `-l` and
47
+ `-c` belong to the `lighthouse` / `loadshow` group, so they go *before*
48
+ `recording` / `playback`.
49
+
50
+ 4. **`playback` takes no URL.** The entry URL and the device type are stored in
51
+ the inventory at recording time and reused. If you need to change either,
52
+ record again.
53
+
54
+ 5. **Lighthouse throttling is deliberately disabled** (`--throttling.*=0`,
55
+ `cpuSlowdownMultiplier=1`). Timing is reproduced by the proxy, per resource.
56
+ Re-enabling Lighthouse throttling would apply the penalty twice.
57
+
58
+ 6. **Unmatched requests get a 404.** The playback proxy only serves what is in
59
+ the inventory. That is deliberate — falling through to the network would make
60
+ the replay non-deterministic. So a URL you delete from `index.json` becomes a
61
+ failed request, not a request that never happens. To model "this resource is
62
+ gone", also remove the reference to it from the HTML.
63
+
64
+ 7. **Treat small deltas as noise.** Even against a fixed recording, Lighthouse
65
+ varies by a few points and tens of milliseconds run to run. Repeat a
66
+ measurement before believing a change under ~5% .
67
+
68
+ ## The loop
69
+
70
+ ```sh
71
+ # 1. Freeze the page (writes ./inventory)
72
+ npx psq lighthouse recording https://example.com/
73
+
74
+ # 2. Keep the untouched copy
75
+ cp -r inventory baseline
76
+
77
+ # 3. Measure the baseline → artifacts/summary.md, artifacts/lighthouse.report.html
78
+ npx psq -i ./baseline lighthouse -a ./artifacts-baseline playback
79
+
80
+ # 4. Edit ./inventory — see chapter "Experiments"
81
+
82
+ # 5. Measure the edited version
83
+ npx psq lighthouse playback
84
+
85
+ # 6. Compare artifacts-baseline/summary.md with artifacts/summary.md
86
+ ```
87
+
88
+ `summary.md` is a short Markdown digest (metrics, per-metric scores with their
89
+ Lighthouse weights, traffic by resource type). It is written for exactly this
90
+ comparison — diff the two files rather than opening two HTML reports.
91
+
92
+ ## What lives where
93
+
94
+ | Path | What it is |
95
+ | --- | --- |
96
+ | `inventory/index.json` | the resource list and every resource's metadata. This is the file the proxy reads |
97
+ | `inventory/contents/<METHOD>/<protocol>/<host>/<path>` | one file per recorded response body |
98
+ | `artifacts/lighthouse.report.html`, `.report.json` | the full Lighthouse report |
99
+ | `artifacts/summary.md` | the digest to diff |
100
+ | `artifacts/lighthouse.digest.png` | score and metrics as an image |
101
+ | `artifacts/loadshow.mp4` | filmstrip video of the load |
102
+ | `artifacts/capture.png`, `capture-diff.png`, `capture-diff.txt` | screenshot and visual diff |
103
+
104
+ ## Failure modes
105
+
106
+ | Symptom | Cause | Fix |
107
+ | --- | --- | --- |
108
+ | `playback` serves nothing, every request 404s | the inventory is empty, or `-i` points at the wrong directory | record first; check `inventory/index.json` exists and `resources` is non-empty |
109
+ | `unknown option '-i'` / the option is ignored | root and group options were placed after the subcommand | `psq -i ./baseline lighthouse -a ./art playback` |
110
+ | the "after" is faster but the page is visibly broken | a referenced resource was deleted from `index.json` but not from the HTML | run `psq capture --compare` and look at the diff before trusting any number |
111
+ | edits to a `.js` / `.css` file seem to have no effect on transfer size | the resource has `"minify": true`, so playback re-minifies before serving | expected — the wire size reflects the minified bytes, which is what the original site shipped |
112
+ | the page looks fine but LCP got worse for no reason | an edit changed the byte size of a resource, and transfer time scales with size at the recorded `mbps` | that *is* the simulation working; check `summary.md` traffic figures |
113
+ | recording captured analytics and ad beacons | third parties were not excluded | re-record with `-x 'google-analytics|doubleclick|facebook'` (repeatable) |
114
+ | `loadshow` / `static-webshot` not found | the postinstall step that downloads them did not run | re-run `npm rebuild pagespeed-quest`, or set `LOADSHOW_PATH` / `WEBSHOT_PATH` |
115
+ | Lighthouse cannot be found | it resolves to `./node_modules/.bin/lighthouse` | run inside the quest project, or set `LIGHTHOUSE_PATH` |
116
+ | numbers drift between identical runs | normal measurement noise | repeat; do not act on deltas under ~5% |
117
+
118
+ ## What this tool will not do
119
+
120
+ - It does not crawl. One entry URL plus whatever that page load requests.
121
+ - It does not modify the live site, and nothing you edit locally reaches it.
122
+ - It does not rewrite your resources for you. Editing the inventory is the
123
+ work; this reference tells you which edit models which optimization.
124
+ - It does not replay user interaction. What is recorded is one page load.
@@ -0,0 +1,133 @@
1
+ # The inventory — what you edit
2
+
3
+ The inventory is the reverse-engineered page. Everything the browser will
4
+ receive during playback comes from here, and nothing else does.
5
+
6
+ ```
7
+ inventory/
8
+ ├── index.json ← the resource list. THIS is what the proxy reads
9
+ └── contents/
10
+ └── GET/
11
+ └── https/
12
+ └── example.com/
13
+ ├── index.html
14
+ ├── assets/app.css
15
+ ├── assets/app.js
16
+ └── img/hero.jpg
17
+ ```
18
+
19
+ - **`index.json` is authoritative.** The playback proxy reads exactly that file
20
+ name. If a file called `inventory.json` also exists (older recordings wrote
21
+ both names), `psq` will read *that* one for the entry URL, device type and
22
+ traffic figures while the proxy serves from `index.json` — the two silently
23
+ disagree. Delete `inventory.json`, or keep it byte-identical.
24
+ - **`contents/` holds one file per response body.** Text bodies are stored
25
+ decoded, converted to UTF-8 and pretty-printed, so they are diffable and
26
+ editable by hand.
27
+
28
+ ## How a URL becomes a path
29
+
30
+ `contents/<METHOD>/<protocol>/<host>/<pathname>`
31
+
32
+ | URL | File |
33
+ | --- | --- |
34
+ | `https://example.com/` | `contents/GET/https/example.com/index.html` |
35
+ | `https://example.com/assets/app.css` | `contents/GET/https/example.com/assets/app.css` |
36
+ | `https://example.com/api?id=1` | `contents/GET/https/example.com/api~id=1` |
37
+ | `https://cdn.example.com:8443/a.js` | `contents/GET/https/cdn.example.com~8443/a.js` |
38
+
39
+ Rules: a trailing `/` (or a path with no extension) gets `index.html`; the query
40
+ string is appended after `~`; a colon in the host becomes `~`; an over-long
41
+ basename is truncated and suffixed with a short SHA-1. Do not rename these files
42
+ by hand — the mapping comes from `contentFilePath` in `index.json`, and the file
43
+ name is only a convenience.
44
+
45
+ ## `index.json`
46
+
47
+ ```json
48
+ {
49
+ "entryUrl": "https://example.com/",
50
+ "deviceType": "mobile",
51
+ "resources": [ ... ]
52
+ }
53
+ ```
54
+
55
+ `entryUrl` and `deviceType` are what `playback` uses instead of asking you for a
56
+ URL. Changing `deviceType` here changes the Lighthouse form factor and the
57
+ loadshow preset on the next playback — it does **not** re-record.
58
+
59
+ ### A resource
60
+
61
+ | Field | Effect at playback time |
62
+ | --- | --- |
63
+ | `method`, `url` | how the request is matched. A request that matches nothing gets a 404 |
64
+ | `ttfbMs` | **the proxy sleeps this long after receiving the request, before the first byte.** Per request, not an offset from page start |
65
+ | `durationMs` | recorded transfer duration. Informational; playback derives its own from size and `mbps` |
66
+ | `mbps` | transfer speed for the body. Missing → 1.0 Mbps |
67
+ | `statusCode` | the status served |
68
+ | `rawHeaders` | headers served, minus `content-length` and `content-encoding`, which are recomputed |
69
+ | `contentEncoding` | `gzip` / `br` / `deflate` / `zstd` / `compress` / `identity`. The body is **re-compressed with this before it goes on the wire**, so it decides the transferred size |
70
+ | `contentTypeMime`, `contentCharset` | the `Content-Type` served. `contentCharset` re-encodes the body from UTF-8 on the way out |
71
+ | `contentFilePath` | the file under `contents/` to serve |
72
+ | `contentUtf8` / `contentBase64` | inline body, used instead of the file when present |
73
+ | `minify` | `true` → the body is **re-minified before serving**. Set during recording when the original was minified |
74
+ | `httpVersion` | protocol version reported for the response |
75
+
76
+ ### The timing model, precisely
77
+
78
+ ```
79
+ response time ≈ ttfbMs + (bytes on the wire × 8) / (mbps × 1,000,000)
80
+ ```
81
+
82
+ - `ttfbMs` is a fixed per-request delay. It models server think time plus
83
+ connection latency. Lower it to model a faster origin or a nearer CDN edge.
84
+ - The second term is recomputed **from the bytes you leave in the file**, after
85
+ minification and after `contentEncoding` compression. So shrinking a file
86
+ makes it arrive sooner, automatically, with no other edit.
87
+ - `--full-throttle` (used internally by `psq capture`) skips both. Never compare
88
+ a full-throttle run with a normal one.
89
+
90
+ ### Two traps worth stating plainly
91
+
92
+ **`minify: true` re-minifies your edit.** The pretty-printed file in `contents/`
93
+ is for you; the wire sees the minified form. This keeps the byte count honest —
94
+ the original site shipped minified bytes — but it means whitespace edits have no
95
+ size effect, and it means a syntax error introduced during editing can surface
96
+ as a minifier failure rather than a browser error.
97
+
98
+ **`contentEncoding` decides the transferred size, not the file size.** A 300 KB
99
+ JavaScript file with `"contentEncoding": "br"` is perhaps 60 KB on the wire, and
100
+ that 60 KB is what the timing is computed from. To model "we turned compression
101
+ off", set it to `identity`; to model "we finally enabled Brotli", set it to
102
+ `br`. Do not edit `content-length` — it is recomputed and your value is
103
+ discarded.
104
+
105
+ ## Editing safely
106
+
107
+ 1. Copy `inventory/` to `baseline/` before the first edit.
108
+ 2. Change **one thing at a time**, then measure. Two edits in one run means you
109
+ cannot attribute the delta.
110
+ 3. After a structural edit (removing a script, changing a URL), run
111
+ `psq capture --compare` and look at the picture. A faster broken page is easy
112
+ to produce and easy to mistake for a win.
113
+ 4. Keep the inventory in version control if the quest lasts more than a day.
114
+ `git diff` over `contents/` is a readable record of the hypothesis.
115
+
116
+ ## Live editing with a browser
117
+
118
+ ```sh
119
+ psq proxy -p 8080
120
+ ```
121
+
122
+ serves the inventory and **watches it**: any change under `inventory/` restarts
123
+ the proxy, so a browser pointed at `http://localhost:8080` picks up your edit on
124
+ the next reload. The proxy uses a dummy certificate, so the browser has to be
125
+ told to accept it:
126
+
127
+ ```sh
128
+ /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome \
129
+ --ignore-certificate-errors --proxy-server=http://localhost:8080
130
+ ```
131
+
132
+ This is the fastest way to iterate on markup edits before spending a Lighthouse
133
+ run on them.