@vscode/component-explorer-cli 0.1.1-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/dist/browserPage.d.ts +15 -0
- package/dist/browserPage.d.ts.map +1 -0
- package/dist/browserPage.js +39 -0
- package/dist/browserPage.js.map +1 -0
- package/dist/commands/acceptCommand.d.ts +9 -0
- package/dist/commands/acceptCommand.d.ts.map +1 -0
- package/dist/commands/acceptCommand.js +34 -0
- package/dist/commands/acceptCommand.js.map +1 -0
- package/dist/commands/compareCommand.d.ts +8 -0
- package/dist/commands/compareCommand.d.ts.map +1 -0
- package/dist/commands/compareCommand.js +84 -0
- package/dist/commands/compareCommand.js.map +1 -0
- package/dist/commands/screenshotCommand.d.ts +10 -0
- package/dist/commands/screenshotCommand.d.ts.map +1 -0
- package/dist/commands/screenshotCommand.js +56 -0
- package/dist/commands/screenshotCommand.js.map +1 -0
- package/dist/commands/watchCommand.d.ts +12 -0
- package/dist/commands/watchCommand.d.ts.map +1 -0
- package/dist/commands/watchCommand.js +307 -0
- package/dist/commands/watchCommand.js.map +1 -0
- package/dist/componentExplorer.d.ts +46 -0
- package/dist/componentExplorer.d.ts.map +1 -0
- package/dist/componentExplorer.js +99 -0
- package/dist/componentExplorer.js.map +1 -0
- package/dist/dependencyInstaller.d.ts +4 -0
- package/dist/dependencyInstaller.d.ts.map +1 -0
- package/dist/dependencyInstaller.js +45 -0
- package/dist/dependencyInstaller.js.map +1 -0
- package/dist/explorerSession.d.ts +15 -0
- package/dist/explorerSession.d.ts.map +1 -0
- package/dist/explorerSession.js +24 -0
- package/dist/explorerSession.js.map +1 -0
- package/dist/git/gitCommitId.d.ts +8 -0
- package/dist/git/gitCommitId.d.ts.map +1 -0
- package/dist/git/gitCommitId.js +16 -0
- package/dist/git/gitCommitId.js.map +1 -0
- package/dist/git/gitCommitResolver.d.ts +23 -0
- package/dist/git/gitCommitResolver.d.ts.map +1 -0
- package/dist/git/gitCommitResolver.js +85 -0
- package/dist/git/gitCommitResolver.js.map +1 -0
- package/dist/git/gitService.d.ts +18 -0
- package/dist/git/gitService.d.ts.map +1 -0
- package/dist/git/gitService.js +38 -0
- package/dist/git/gitService.js.map +1 -0
- package/dist/git/gitUtils.d.ts +6 -0
- package/dist/git/gitUtils.d.ts.map +1 -0
- package/dist/git/gitUtils.js +30 -0
- package/dist/git/gitUtils.js.map +1 -0
- package/dist/git/gitWorktreeManager.d.ts +23 -0
- package/dist/git/gitWorktreeManager.d.ts.map +1 -0
- package/dist/git/gitWorktreeManager.js +91 -0
- package/dist/git/gitWorktreeManager.js.map +1 -0
- package/dist/httpServer.d.ts +23 -0
- package/dist/httpServer.d.ts.map +1 -0
- package/dist/httpServer.js +63 -0
- package/dist/httpServer.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +11 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +13 -0
- package/dist/logger.js.map +1 -0
- package/dist/screenshotCache.d.ts +16 -0
- package/dist/screenshotCache.d.ts.map +1 -0
- package/dist/screenshotCache.js +59 -0
- package/dist/screenshotCache.js.map +1 -0
- package/dist/sourceTreeId.d.ts +5 -0
- package/dist/sourceTreeId.d.ts.map +1 -0
- package/dist/sourceTreeId.js +7 -0
- package/dist/sourceTreeId.js.map +1 -0
- package/dist/storage.d.ts +18 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +55 -0
- package/dist/storage.js.map +1 -0
- package/dist/viteProjectRef.d.ts +22 -0
- package/dist/viteProjectRef.d.ts.map +1 -0
- package/dist/viteProjectRef.js +33 -0
- package/dist/viteProjectRef.js.map +1 -0
- package/dist/watchConfig.d.ts +49 -0
- package/dist/watchConfig.d.ts.map +1 -0
- package/dist/watchConfig.js +148 -0
- package/dist/watchConfig.js.map +1 -0
- package/package.json +45 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface BrowserPageFactory {
|
|
2
|
+
openPage(url: string): Promise<BrowserPage>;
|
|
3
|
+
dispose(): Promise<void>;
|
|
4
|
+
}
|
|
5
|
+
export interface BrowserPage {
|
|
6
|
+
evaluateJs<T>(expression: string): Promise<T>;
|
|
7
|
+
screenshot(domNodeExpression: string): Promise<Uint8Array>;
|
|
8
|
+
dispose(): Promise<void>;
|
|
9
|
+
}
|
|
10
|
+
export declare class PlaywrightBrowserPageFactory implements BrowserPageFactory {
|
|
11
|
+
private _browser;
|
|
12
|
+
openPage(url: string): Promise<BrowserPage>;
|
|
13
|
+
dispose(): Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=browserPage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browserPage.d.ts","sourceRoot":"","sources":["../src/browserPage.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,kBAAkB;IAClC,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;IAC5C,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACzB;AAED,MAAM,WAAW,WAAW;IAC3B,UAAU,CAAC,CAAC,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IAC9C,UAAU,CAAC,iBAAiB,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IAC3D,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACzB;AAED,qBAAa,4BAA6B,YAAW,kBAAkB;IACtE,OAAO,CAAC,QAAQ,CAA2C;IAErD,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC;IAU3C,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;CAI9B"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export class PlaywrightBrowserPageFactory {
|
|
2
|
+
_browser;
|
|
3
|
+
async openPage(url) {
|
|
4
|
+
if (!this._browser) {
|
|
5
|
+
const { chromium } = await import('playwright');
|
|
6
|
+
this._browser = await chromium.launch();
|
|
7
|
+
}
|
|
8
|
+
const page = await this._browser.newPage();
|
|
9
|
+
await page.goto(url, { waitUntil: 'networkidle' });
|
|
10
|
+
return new PlaywrightBrowserPage(page);
|
|
11
|
+
}
|
|
12
|
+
async dispose() {
|
|
13
|
+
await this._browser?.close();
|
|
14
|
+
this._browser = undefined;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
class PlaywrightBrowserPage {
|
|
18
|
+
_page;
|
|
19
|
+
constructor(_page) {
|
|
20
|
+
this._page = _page;
|
|
21
|
+
}
|
|
22
|
+
async evaluateJs(expression) {
|
|
23
|
+
return this._page.evaluate(expression);
|
|
24
|
+
}
|
|
25
|
+
async screenshot(domNodeExpression) {
|
|
26
|
+
const handle = await this._page.evaluateHandle(domNodeExpression);
|
|
27
|
+
const element = handle.asElement();
|
|
28
|
+
if (!element) {
|
|
29
|
+
throw new Error(`Expression did not return a DOM element: ${domNodeExpression}`);
|
|
30
|
+
}
|
|
31
|
+
// ElementHandle.screenshot exists at runtime; cast through unknown for type compatibility
|
|
32
|
+
const buffer = await element.screenshot({ type: 'png' });
|
|
33
|
+
return new Uint8Array(buffer);
|
|
34
|
+
}
|
|
35
|
+
async dispose() {
|
|
36
|
+
await this._page.close();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=browserPage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browserPage.js","sourceRoot":"","sources":["../src/browserPage.ts"],"names":[],"mappings":"AAWA,MAAM,OAAO,4BAA4B;IAChC,QAAQ,CAA2C;IAE3D,KAAK,CAAC,QAAQ,CAAC,GAAW;QACzB,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC;YACpB,MAAM,EAAE,QAAQ,EAAE,GAAG,MAAM,MAAM,CAAC,YAAY,CAAC,CAAC;YAChD,IAAI,CAAC,QAAQ,GAAG,MAAM,QAAQ,CAAC,MAAM,EAAE,CAAC;QACzC,CAAC;QACD,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,OAAO,EAAE,CAAC;QAC3C,MAAM,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,aAAa,EAAE,CAAC,CAAC;QACnD,OAAO,IAAI,qBAAqB,CAAC,IAAI,CAAC,CAAC;IACxC,CAAC;IAED,KAAK,CAAC,OAAO;QACZ,MAAM,IAAI,CAAC,QAAQ,EAAE,KAAK,EAAE,CAAC;QAC7B,IAAI,CAAC,QAAQ,GAAG,SAAS,CAAC;IAC3B,CAAC;CACD;AAED,MAAM,qBAAqB;IACG;IAA7B,YAA6B,KAAgC;QAAhC,UAAK,GAAL,KAAK,CAA2B;IAAG,CAAC;IAEjE,KAAK,CAAC,UAAU,CAAI,UAAkB;QACrC,OAAO,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,UAAU,CAAe,CAAC;IACtD,CAAC;IAED,KAAK,CAAC,UAAU,CAAC,iBAAyB;QACzC,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,iBAAiB,CAAC,CAAC;QAClE,MAAM,OAAO,GAAG,MAAM,CAAC,SAAS,EAAE,CAAC;QACnC,IAAI,CAAC,OAAO,EAAE,CAAC;YACd,MAAM,IAAI,KAAK,CAAC,4CAA4C,iBAAiB,EAAE,CAAC,CAAC;QAClF,CAAC;QACD,0FAA0F;QAC1F,MAAM,MAAM,GAAG,MAAO,OAA8E,CAAC,UAAU,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;QACjI,OAAO,IAAI,UAAU,CAAC,MAAM,CAAC,CAAC;IAC/B,CAAC;IAED,KAAK,CAAC,OAAO;QACZ,MAAM,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;IAC1B,CAAC;CACD"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Command } from 'clipanion';
|
|
2
|
+
export declare class AcceptCommand extends Command {
|
|
3
|
+
static paths: string[][];
|
|
4
|
+
static usage: import("clipanion").Usage;
|
|
5
|
+
readonly filter: string | undefined;
|
|
6
|
+
readonly root: string;
|
|
7
|
+
execute(): Promise<void>;
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=acceptCommand.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"acceptCommand.d.ts","sourceRoot":"","sources":["../../src/commands/acceptCommand.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAU,MAAM,WAAW,CAAC;AAG5C,qBAAa,aAAc,SAAQ,OAAO;IACzC,OAAgB,KAAK,aAAgB;IAErC,OAAgB,KAAK,4BAMlB;IAEH,QAAQ,CAAC,MAAM,qBAAkG;IACjH,QAAQ,CAAC,IAAI,SAAqF;IAE5F,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;CA0B9B"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Command, Option } from 'clipanion';
|
|
2
|
+
import { FileSystemStorage } from '../storage.js';
|
|
3
|
+
export class AcceptCommand extends Command {
|
|
4
|
+
static paths = [['accept']];
|
|
5
|
+
static usage = Command.Usage({
|
|
6
|
+
description: 'Promote current screenshots as baseline',
|
|
7
|
+
examples: [
|
|
8
|
+
['Accept all', '$0 accept'],
|
|
9
|
+
['Accept specific fixtures', '$0 accept --filter "Button/*"'],
|
|
10
|
+
],
|
|
11
|
+
});
|
|
12
|
+
filter = Option.String('--filter', { required: false, description: 'Filter fixtures by glob pattern' });
|
|
13
|
+
root = Option.String('--root', process.cwd(), { description: 'Project root directory' });
|
|
14
|
+
async execute() {
|
|
15
|
+
const storage = new FileSystemStorage(`${this.root}/.screenshots`);
|
|
16
|
+
let currentFiles = await storage.list('current');
|
|
17
|
+
if (this.filter) {
|
|
18
|
+
const regex = new RegExp('^current/' + this.filter.replace(/\*/g, '.*').replace(/\?/g, '.') + '$');
|
|
19
|
+
currentFiles = currentFiles.filter(f => regex.test(f));
|
|
20
|
+
}
|
|
21
|
+
if (currentFiles.length === 0) {
|
|
22
|
+
this.context.stdout.write('No current screenshots to accept.\n');
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
for (const file of currentFiles) {
|
|
26
|
+
const relativePath = file.replace('current/', '');
|
|
27
|
+
const data = await storage.read(file);
|
|
28
|
+
await storage.write(`baseline/${relativePath}`, data);
|
|
29
|
+
this.context.stdout.write(` ✓ ${relativePath}\n`);
|
|
30
|
+
}
|
|
31
|
+
this.context.stdout.write(`\nAccepted ${currentFiles.length} screenshot(s) as baseline.\n`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=acceptCommand.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"acceptCommand.js","sourceRoot":"","sources":["../../src/commands/acceptCommand.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAC5C,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAElD,MAAM,OAAO,aAAc,SAAQ,OAAO;IACzC,MAAM,CAAU,KAAK,GAAG,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC;IAErC,MAAM,CAAU,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC;QACrC,WAAW,EAAE,yCAAyC;QACtD,QAAQ,EAAE;YACT,CAAC,YAAY,EAAE,WAAW,CAAC;YAC3B,CAAC,0BAA0B,EAAE,+BAA+B,CAAC;SAC7D;KACD,CAAC,CAAC;IAEM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,UAAU,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,WAAW,EAAE,iCAAiC,EAAE,CAAC,CAAC;IACxG,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,QAAQ,EAAE,OAAO,CAAC,GAAG,EAAE,EAAE,EAAE,WAAW,EAAE,wBAAwB,EAAE,CAAC,CAAC;IAElG,KAAK,CAAC,OAAO;QACZ,MAAM,OAAO,GAAG,IAAI,iBAAiB,CAAC,GAAG,IAAI,CAAC,IAAI,eAAe,CAAC,CAAC;QAEnE,IAAI,YAAY,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QAEjD,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YACjB,MAAM,KAAK,GAAG,IAAI,MAAM,CACvB,WAAW,GAAG,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,GAAG,GAAG,CACxE,CAAC;YACF,YAAY,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;QACxD,CAAC;QAED,IAAI,YAAY,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC/B,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,qCAAqC,CAAC,CAAC;YACjE,OAAO;QACR,CAAC;QAED,KAAK,MAAM,IAAI,IAAI,YAAY,EAAE,CAAC;YACjC,MAAM,YAAY,GAAG,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;YAClD,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACtC,MAAM,OAAO,CAAC,KAAK,CAAC,YAAY,YAAY,EAAE,EAAE,IAAI,CAAC,CAAC;YACtD,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,OAAO,YAAY,IAAI,CAAC,CAAC;QACpD,CAAC;QAED,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,cAAc,YAAY,CAAC,MAAM,+BAA+B,CAAC,CAAC;IAC7F,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Command } from 'clipanion';
|
|
2
|
+
export declare class CompareCommand extends Command {
|
|
3
|
+
static paths: string[][];
|
|
4
|
+
static usage: import("clipanion").Usage;
|
|
5
|
+
readonly root: string;
|
|
6
|
+
execute(): Promise<number>;
|
|
7
|
+
}
|
|
8
|
+
//# sourceMappingURL=compareCommand.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compareCommand.d.ts","sourceRoot":"","sources":["../../src/commands/compareCommand.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAU,MAAM,WAAW,CAAC;AAG5C,qBAAa,cAAe,SAAQ,OAAO;IAC1C,OAAgB,KAAK,aAAiB;IAEtC,OAAgB,KAAK,4BAKlB;IAEH,QAAQ,CAAC,IAAI,SAAqF;IAE5F,OAAO,IAAI,OAAO,CAAC,MAAM,CAAC;CAgEhC"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { Command, Option } from 'clipanion';
|
|
2
|
+
import { FileSystemStorage } from '../storage.js';
|
|
3
|
+
export class CompareCommand extends Command {
|
|
4
|
+
static paths = [['compare']];
|
|
5
|
+
static usage = Command.Usage({
|
|
6
|
+
description: 'Compare current screenshots against baseline',
|
|
7
|
+
examples: [
|
|
8
|
+
['Compare default directories', '$0 compare'],
|
|
9
|
+
],
|
|
10
|
+
});
|
|
11
|
+
root = Option.String('--root', process.cwd(), { description: 'Project root directory' });
|
|
12
|
+
async execute() {
|
|
13
|
+
const storage = new FileSystemStorage(`${this.root}/.screenshots`);
|
|
14
|
+
const baselineFiles = await storage.list('baseline');
|
|
15
|
+
const currentFiles = await storage.list('current');
|
|
16
|
+
const baselineSet = new Set(baselineFiles.map(f => f.replace('baseline/', '')));
|
|
17
|
+
const currentSet = new Set(currentFiles.map(f => f.replace('current/', '')));
|
|
18
|
+
const added = [];
|
|
19
|
+
const removed = [];
|
|
20
|
+
const changed = [];
|
|
21
|
+
const unchanged = [];
|
|
22
|
+
for (const file of currentSet) {
|
|
23
|
+
if (!baselineSet.has(file)) {
|
|
24
|
+
added.push(file);
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
const baselineData = await storage.read(`baseline/${file}`);
|
|
28
|
+
const currentData = await storage.read(`current/${file}`);
|
|
29
|
+
if (buffersEqual(baselineData, currentData)) {
|
|
30
|
+
unchanged.push(file);
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
changed.push(file);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
for (const file of baselineSet) {
|
|
38
|
+
if (!currentSet.has(file)) {
|
|
39
|
+
removed.push(file);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (added.length > 0) {
|
|
43
|
+
this.context.stdout.write(`Added (${added.length}):\n`);
|
|
44
|
+
for (const f of added) {
|
|
45
|
+
this.context.stdout.write(` + ${f}\n`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (removed.length > 0) {
|
|
49
|
+
this.context.stdout.write(`Removed (${removed.length}):\n`);
|
|
50
|
+
for (const f of removed) {
|
|
51
|
+
this.context.stdout.write(` - ${f}\n`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (changed.length > 0) {
|
|
55
|
+
this.context.stdout.write(`Changed (${changed.length}):\n`);
|
|
56
|
+
for (const f of changed) {
|
|
57
|
+
this.context.stdout.write(` ~ ${f}\n`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (unchanged.length > 0) {
|
|
61
|
+
this.context.stdout.write(`Unchanged (${unchanged.length}):\n`);
|
|
62
|
+
for (const f of unchanged) {
|
|
63
|
+
this.context.stdout.write(` = ${f}\n`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const hasDiffs = added.length > 0 || removed.length > 0 || changed.length > 0;
|
|
67
|
+
if (hasDiffs) {
|
|
68
|
+
this.context.stdout.write('\nDifferences found.\n');
|
|
69
|
+
return 1;
|
|
70
|
+
}
|
|
71
|
+
this.context.stdout.write('\nNo differences.\n');
|
|
72
|
+
return 0;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function buffersEqual(a, b) {
|
|
76
|
+
if (a.length !== b.length)
|
|
77
|
+
return false;
|
|
78
|
+
for (let i = 0; i < a.length; i++) {
|
|
79
|
+
if (a[i] !== b[i])
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=compareCommand.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compareCommand.js","sourceRoot":"","sources":["../../src/commands/compareCommand.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Command } from 'clipanion';
|
|
2
|
+
export declare class ScreenshotCommand extends Command {
|
|
3
|
+
static paths: string[][];
|
|
4
|
+
static usage: import("clipanion").Usage;
|
|
5
|
+
readonly filter: string | undefined;
|
|
6
|
+
readonly accept: boolean;
|
|
7
|
+
readonly root: string;
|
|
8
|
+
execute(): Promise<void>;
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=screenshotCommand.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"screenshotCommand.d.ts","sourceRoot":"","sources":["../../src/commands/screenshotCommand.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAU,MAAM,WAAW,CAAC;AAO5C,qBAAa,iBAAkB,SAAQ,OAAO;IAC7C,OAAgB,KAAK,aAAoB;IAEzC,OAAgB,KAAK,4BAOlB;IAEH,QAAQ,CAAC,MAAM,qBAAkG;IACjH,QAAQ,CAAC,MAAM,UAAuG;IACtH,QAAQ,CAAC,IAAI,SAAqF;IAE5F,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;CAqC9B"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { Command, Option } from 'clipanion';
|
|
2
|
+
import { PlaywrightBrowserPageFactory } from '../browserPage.js';
|
|
3
|
+
import { BrowserComponentExplorer } from '../componentExplorer.js';
|
|
4
|
+
import { DefaultComponentExplorerHttpServerFactory } from '../httpServer.js';
|
|
5
|
+
import { FileSystemStorage } from '../storage.js';
|
|
6
|
+
import { ViteProjectRef } from '../viteProjectRef.js';
|
|
7
|
+
export class ScreenshotCommand extends Command {
|
|
8
|
+
static paths = [['screenshot']];
|
|
9
|
+
static usage = Command.Usage({
|
|
10
|
+
description: 'Capture screenshots of all fixtures',
|
|
11
|
+
examples: [
|
|
12
|
+
['Capture all fixtures', '$0 screenshot'],
|
|
13
|
+
['Capture and accept as baseline', '$0 screenshot --accept'],
|
|
14
|
+
['Filter by glob', '$0 screenshot --filter "Button/*"'],
|
|
15
|
+
],
|
|
16
|
+
});
|
|
17
|
+
filter = Option.String('--filter', { required: false, description: 'Filter fixtures by glob pattern' });
|
|
18
|
+
accept = Option.Boolean('--accept', false, { description: 'Write directly to baseline instead of current' });
|
|
19
|
+
root = Option.String('--root', process.cwd(), { description: 'Project root directory' });
|
|
20
|
+
async execute() {
|
|
21
|
+
const outputDir = this.accept ? 'baseline' : 'current';
|
|
22
|
+
const storage = new FileSystemStorage(`${this.root}/.screenshots`);
|
|
23
|
+
const serverFactory = new DefaultComponentExplorerHttpServerFactory();
|
|
24
|
+
const browserFactory = new PlaywrightBrowserPageFactory();
|
|
25
|
+
let server;
|
|
26
|
+
let explorer;
|
|
27
|
+
try {
|
|
28
|
+
this.context.stdout.write('Starting Vite dev server...\n');
|
|
29
|
+
server = await serverFactory.createViteServer(ViteProjectRef.fromSourceRootDir(this.root));
|
|
30
|
+
this.context.stdout.write(`Server running at ${server.url}\n`);
|
|
31
|
+
explorer = new BrowserComponentExplorer(browserFactory, server);
|
|
32
|
+
const fixtures = await explorer.listFixtures();
|
|
33
|
+
const filtered = this.filter
|
|
34
|
+
? fixtures.filter(f => matchGlob(f.fixtureId, this.filter))
|
|
35
|
+
: fixtures;
|
|
36
|
+
this.context.stdout.write(`Found ${filtered.length} fixture(s)\n`);
|
|
37
|
+
for (const fixture of filtered) {
|
|
38
|
+
const png = await explorer.screenshotFixture(fixture.fixtureId);
|
|
39
|
+
const filePath = `${outputDir}/${fixture.fixtureId}.png`;
|
|
40
|
+
await storage.write(filePath, png);
|
|
41
|
+
this.context.stdout.write(` ✓ ${fixture.fixtureId}\n`);
|
|
42
|
+
}
|
|
43
|
+
this.context.stdout.write(`\nScreenshots saved to .screenshots/${outputDir}/\n`);
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
await explorer?.dispose();
|
|
47
|
+
await browserFactory.dispose();
|
|
48
|
+
await server?.dispose();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function matchGlob(fixtureId, pattern) {
|
|
53
|
+
const regex = new RegExp('^' + pattern.replace(/\*/g, '.*').replace(/\?/g, '.') + '$');
|
|
54
|
+
return regex.test(fixtureId);
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=screenshotCommand.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"screenshotCommand.js","sourceRoot":"","sources":["../../src/commands/screenshotCommand.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Command } from 'clipanion';
|
|
2
|
+
export declare class WatchCommand extends Command {
|
|
3
|
+
static paths: string[][];
|
|
4
|
+
static usage: import("clipanion").Usage;
|
|
5
|
+
readonly root: string;
|
|
6
|
+
readonly config: string | undefined;
|
|
7
|
+
execute(): Promise<void>;
|
|
8
|
+
private _executeLegacy;
|
|
9
|
+
private _executeWithConfig;
|
|
10
|
+
private _setupSession;
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=watchCommand.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"watchCommand.d.ts","sourceRoot":"","sources":["../../src/commands/watchCommand.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAU,MAAM,WAAW,CAAC;AAiB5C,qBAAa,YAAa,SAAQ,OAAO;IACxC,OAAgB,KAAK,aAAe;IAEpC,OAAgB,KAAK,4BAMlB;IAEH,QAAQ,CAAC,IAAI,SAAqF;IAClG,QAAQ,CAAC,MAAM,qBAA+F;IAExG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;YAOhB,cAAc;YAoCd,kBAAkB;YAkHlB,aAAa;CA6C3B"}
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import { Command, Option } from 'clipanion';
|
|
2
|
+
import { autorun } from '@vscode/observables';
|
|
3
|
+
import { PlaywrightBrowserPageFactory } from '../browserPage.js';
|
|
4
|
+
import { BrowserComponentExplorer } from '../componentExplorer.js';
|
|
5
|
+
import { DefaultComponentExplorerHttpServerFactory } from '../httpServer.js';
|
|
6
|
+
import { ConsoleLogger } from '../logger.js';
|
|
7
|
+
import { FileSystemStorage } from '../storage.js';
|
|
8
|
+
import { ViteProjectRef } from '../viteProjectRef.js';
|
|
9
|
+
import { ResolvedWatchConfig } from '../watchConfig.js';
|
|
10
|
+
import { ExplorerSession } from '../explorerSession.js';
|
|
11
|
+
import { installDependencies } from '../dependencyInstaller.js';
|
|
12
|
+
import { ScreenshotCache, compareScreenshots, contentHash } from '../screenshotCache.js';
|
|
13
|
+
export class WatchCommand extends Command {
|
|
14
|
+
static paths = [['watch']];
|
|
15
|
+
static usage = Command.Usage({
|
|
16
|
+
description: 'Watch for changes and compare screenshots against baseline',
|
|
17
|
+
examples: [
|
|
18
|
+
['Watch all fixtures', '$0 watch'],
|
|
19
|
+
['Watch with config', '$0 watch -c component-explorer-watch-config.json'],
|
|
20
|
+
],
|
|
21
|
+
});
|
|
22
|
+
root = Option.String('--root', process.cwd(), { description: 'Project root directory' });
|
|
23
|
+
config = Option.String('-c,--config', { required: false, description: 'Path to watch config JSON' });
|
|
24
|
+
async execute() {
|
|
25
|
+
if (this.config) {
|
|
26
|
+
return this._executeWithConfig();
|
|
27
|
+
}
|
|
28
|
+
return this._executeLegacy();
|
|
29
|
+
}
|
|
30
|
+
async _executeLegacy() {
|
|
31
|
+
const logger = new ConsoleLogger('watch', this.context.stdout);
|
|
32
|
+
const storage = new FileSystemStorage(`${this.root}/.screenshots`);
|
|
33
|
+
const serverFactory = new DefaultComponentExplorerHttpServerFactory();
|
|
34
|
+
const browserFactory = new PlaywrightBrowserPageFactory();
|
|
35
|
+
logger.log('Starting Vite dev server...');
|
|
36
|
+
const server = await serverFactory.createViteServer(ViteProjectRef.fromSourceRootDir(this.root));
|
|
37
|
+
logger.log(`Server running at ${server.url}`);
|
|
38
|
+
const explorer = new BrowserComponentExplorer(browserFactory, server, logger);
|
|
39
|
+
logger.log('Taking initial screenshots...');
|
|
40
|
+
await captureAndCompare(explorer, storage, this.context.stdout);
|
|
41
|
+
this.context.stdout.write('\nWatching for changes... (Ctrl+C to stop)\n\n');
|
|
42
|
+
let stopped = false;
|
|
43
|
+
process.on('SIGINT', () => { stopped = true; });
|
|
44
|
+
while (!stopped) {
|
|
45
|
+
try {
|
|
46
|
+
await explorer.waitForUpdate();
|
|
47
|
+
await captureAndCompare(explorer, storage, this.context.stdout);
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
if (stopped)
|
|
51
|
+
break;
|
|
52
|
+
this.context.stderr.write(`[watch] Error: ${err}\n`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
this.context.stdout.write('\nShutting down...\n');
|
|
56
|
+
await explorer.dispose();
|
|
57
|
+
await browserFactory.dispose();
|
|
58
|
+
await server.dispose();
|
|
59
|
+
}
|
|
60
|
+
async _executeWithConfig() {
|
|
61
|
+
const logger = new ConsoleLogger('watch', this.context.stdout);
|
|
62
|
+
const serverFactory = new DefaultComponentExplorerHttpServerFactory();
|
|
63
|
+
const browserFactory = new PlaywrightBrowserPageFactory();
|
|
64
|
+
logger.log(`Reading config from ${this.config}`);
|
|
65
|
+
const resolvedConfig = await ResolvedWatchConfig.fromFile(this.config);
|
|
66
|
+
const git = resolvedConfig.repo;
|
|
67
|
+
const storage = new FileSystemStorage(resolvedConfig.screenshotDir);
|
|
68
|
+
const sessions = new Map();
|
|
69
|
+
const resolvers = new Map();
|
|
70
|
+
const caches = new Map();
|
|
71
|
+
try {
|
|
72
|
+
// The "current" session's project dir is used to resolve vite for worktree sessions.
|
|
73
|
+
// This ensures worktree sessions use the same vite version as the main worktree.
|
|
74
|
+
const currentSession = resolvedConfig.sessions.find(s => s.source.kind === 'current');
|
|
75
|
+
const resolveViteFrom = currentSession?.viteProject.cwd;
|
|
76
|
+
// Set up sessions
|
|
77
|
+
for (const sessionConfig of resolvedConfig.sessions) {
|
|
78
|
+
await this._setupSession(sessionConfig, git, serverFactory, browserFactory, logger, sessions, resolvers, caches, resolveViteFrom);
|
|
79
|
+
}
|
|
80
|
+
// Initial capture from all sessions
|
|
81
|
+
for (const [name, session] of sessions) {
|
|
82
|
+
logger.log(`Capturing initial screenshots: ${name}`);
|
|
83
|
+
const screenshots = await captureAll(session.explorer);
|
|
84
|
+
caches.get(name).replaceAll(screenshots);
|
|
85
|
+
await writeScreenshotsToStorage(storage, name, screenshots);
|
|
86
|
+
}
|
|
87
|
+
// Print initial comparison
|
|
88
|
+
if (resolvedConfig.compare) {
|
|
89
|
+
const bl = caches.get(resolvedConfig.compare.baseline);
|
|
90
|
+
const cr = caches.get(resolvedConfig.compare.current);
|
|
91
|
+
printCompareResult(compareScreenshots(bl, cr), this.context.stdout, bl, cr);
|
|
92
|
+
}
|
|
93
|
+
this.context.stdout.write('\nWatching for changes... (Ctrl+C to stop)\n\n');
|
|
94
|
+
let stopped = false;
|
|
95
|
+
process.on('SIGINT', () => { stopped = true; });
|
|
96
|
+
while (!stopped) {
|
|
97
|
+
try {
|
|
98
|
+
const event = await raceUpdates(sessions, resolvers);
|
|
99
|
+
if (event.kind === 'hmr') {
|
|
100
|
+
logger.log(`HMR update in session: ${event.sessionName}`);
|
|
101
|
+
const cache = caches.get(event.sessionName);
|
|
102
|
+
const session = sessions.get(event.sessionName);
|
|
103
|
+
const screenshots = await captureAll(session.explorer);
|
|
104
|
+
cache.replaceAll(screenshots);
|
|
105
|
+
await writeScreenshotsToStorage(storage, event.sessionName, screenshots);
|
|
106
|
+
}
|
|
107
|
+
else if (event.kind === 'ref-change') {
|
|
108
|
+
logger.log(`Ref ${event.ref} moved to ${event.newCommit.toShort()}`);
|
|
109
|
+
const sessionConfig = resolvedConfig.sessions.find(s => s.source.kind === 'worktree' && s.source.worktree.ref === event.ref);
|
|
110
|
+
if (sessionConfig && sessionConfig.source.kind === 'worktree') {
|
|
111
|
+
const wt = sessionConfig.source.worktree;
|
|
112
|
+
const wtInfo = await git.worktrees.info(wt.worktreePath);
|
|
113
|
+
if (wtInfo && wtInfo.isDirty) {
|
|
114
|
+
logger.log(`Worktree is dirty, skipping update to ${event.newCommit.toShort()}`);
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
// Dispose old session
|
|
118
|
+
const oldSession = sessions.get(sessionConfig.name);
|
|
119
|
+
await oldSession?.dispose();
|
|
120
|
+
// Checkout + reinstall
|
|
121
|
+
if (wtInfo) {
|
|
122
|
+
await git.worktrees.checkout(wt.worktreePath, event.newCommit);
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
await git.worktrees.create(wt.worktreePath, event.newCommit);
|
|
126
|
+
}
|
|
127
|
+
await installDependencies(wt.worktreePath, wt.install, logger);
|
|
128
|
+
// Recreate session
|
|
129
|
+
const newSession = await ExplorerSession.create(sessionConfig.name, sessionConfig.viteProject, serverFactory, browserFactory, logger, resolveViteFrom);
|
|
130
|
+
sessions.set(sessionConfig.name, newSession);
|
|
131
|
+
const cache = caches.get(sessionConfig.name);
|
|
132
|
+
const screenshots = await captureAll(newSession.explorer);
|
|
133
|
+
cache.replaceAll(screenshots);
|
|
134
|
+
await writeScreenshotsToStorage(storage, sessionConfig.name, screenshots);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// Print comparison
|
|
139
|
+
if (resolvedConfig.compare) {
|
|
140
|
+
const bl = caches.get(resolvedConfig.compare.baseline);
|
|
141
|
+
const cr = caches.get(resolvedConfig.compare.current);
|
|
142
|
+
printCompareResult(compareScreenshots(bl, cr), this.context.stdout, bl, cr);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
catch (err) {
|
|
146
|
+
if (stopped)
|
|
147
|
+
break;
|
|
148
|
+
this.context.stderr.write(`[watch] Error: ${err}\n`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
finally {
|
|
153
|
+
this.context.stdout.write('\nShutting down...\n');
|
|
154
|
+
for (const session of sessions.values()) {
|
|
155
|
+
await session.dispose();
|
|
156
|
+
}
|
|
157
|
+
for (const resolver of resolvers.values()) {
|
|
158
|
+
resolver.dispose();
|
|
159
|
+
}
|
|
160
|
+
git.dispose();
|
|
161
|
+
await browserFactory.dispose();
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
async _setupSession(sessionConfig, git, serverFactory, browserFactory, logger, sessions, resolvers, caches, resolveViteFrom) {
|
|
165
|
+
if (sessionConfig.source.kind === 'worktree') {
|
|
166
|
+
const wt = sessionConfig.source.worktree;
|
|
167
|
+
const resolver = await git.createCommitResolver(wt.ref);
|
|
168
|
+
resolvers.set(sessionConfig.name, resolver);
|
|
169
|
+
const resolvedCommit = resolver.resolvedCommit.get();
|
|
170
|
+
const wtInfo = await git.worktrees.info(wt.worktreePath);
|
|
171
|
+
if (!wtInfo) {
|
|
172
|
+
logger.log(`Creating worktree at ${wt.worktreePath} (${wt.ref} @ ${resolvedCommit.toShort()})`);
|
|
173
|
+
await git.worktrees.create(wt.worktreePath, resolvedCommit);
|
|
174
|
+
await installDependencies(wt.worktreePath, wt.install, logger);
|
|
175
|
+
}
|
|
176
|
+
else if (!wtInfo.checkedOutCommit.equals(resolvedCommit)) {
|
|
177
|
+
if (wtInfo.isDirty) {
|
|
178
|
+
logger.log(`Worktree is dirty, using existing checkout at ${wtInfo.checkedOutCommit.toShort()}`);
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
logger.log(`Updating worktree to ${resolvedCommit.toShort()}`);
|
|
182
|
+
await git.worktrees.checkout(wt.worktreePath, resolvedCommit);
|
|
183
|
+
await installDependencies(wt.worktreePath, wt.install, logger);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
logger.log(`Worktree already at ${resolvedCommit.toShort()}`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
logger.log(`Starting server: ${sessionConfig.name}`);
|
|
191
|
+
const session = await ExplorerSession.create(sessionConfig.name, sessionConfig.viteProject, serverFactory, browserFactory, logger, sessionConfig.source.kind === 'worktree' ? resolveViteFrom : undefined);
|
|
192
|
+
sessions.set(sessionConfig.name, session);
|
|
193
|
+
caches.set(sessionConfig.name, new ScreenshotCache());
|
|
194
|
+
logger.log(`Server ready: ${sessionConfig.name} (${session.serverUrl})`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
async function raceUpdates(sessions, resolvers) {
|
|
198
|
+
const hmrPromises = [...sessions.entries()].map(([name, session]) => session.explorer.waitForUpdate().then(() => ({ kind: 'hmr', sessionName: name })));
|
|
199
|
+
const refPromise = new Promise((resolve) => {
|
|
200
|
+
if (resolvers.size === 0) {
|
|
201
|
+
// Never resolves — no resolvers to watch
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
const previousCommits = new Map();
|
|
205
|
+
for (const [name, resolver] of resolvers) {
|
|
206
|
+
previousCommits.set(name, resolver.resolvedCommit.get());
|
|
207
|
+
}
|
|
208
|
+
const disposable = autorun(reader => {
|
|
209
|
+
for (const [name, resolver] of resolvers) {
|
|
210
|
+
const commit = resolver.resolvedCommit.read(reader);
|
|
211
|
+
const prev = previousCommits.get(name);
|
|
212
|
+
if (prev && !prev.equals(commit)) {
|
|
213
|
+
disposable.dispose();
|
|
214
|
+
resolve({ kind: 'ref-change', ref: resolver.ref, newCommit: commit });
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
return Promise.race([...hmrPromises, refPromise]);
|
|
221
|
+
}
|
|
222
|
+
// --- Capture helpers ---
|
|
223
|
+
async function captureAll(explorer) {
|
|
224
|
+
const fixtures = await explorer.listFixtures();
|
|
225
|
+
const screenshots = new Map();
|
|
226
|
+
for (const fixture of fixtures) {
|
|
227
|
+
const png = await explorer.screenshotFixture(fixture.fixtureId);
|
|
228
|
+
screenshots.set(fixture.fixtureId, png);
|
|
229
|
+
}
|
|
230
|
+
return screenshots;
|
|
231
|
+
}
|
|
232
|
+
async function writeScreenshotsToStorage(storage, sessionName, screenshots) {
|
|
233
|
+
for (const [fixtureId, png] of screenshots) {
|
|
234
|
+
await storage.write(`${sessionName}/${fixtureId}.png`, png);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
function printCompareResult(result, stdout, baselineCache, currentCache) {
|
|
238
|
+
const total = result.added.length + result.removed.length + result.changed.length + result.unchanged.length;
|
|
239
|
+
const timestamp = new Date().toLocaleTimeString();
|
|
240
|
+
stdout.write(`[${timestamp}] ${total} fixtures`);
|
|
241
|
+
if (result.changed.length === 0 && result.added.length === 0 && result.removed.length === 0) {
|
|
242
|
+
stdout.write(` — no changes\n`);
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
stdout.write(`\n`);
|
|
246
|
+
for (const f of result.changed) {
|
|
247
|
+
const bHash = baselineCache?.get(f) ? contentHash(baselineCache.get(f)) : '?';
|
|
248
|
+
const cHash = currentCache?.get(f) ? contentHash(currentCache.get(f)) : '?';
|
|
249
|
+
stdout.write(` ~ ${f} (baseline: ${bHash}, current: ${cHash})\n`);
|
|
250
|
+
}
|
|
251
|
+
for (const f of result.added) {
|
|
252
|
+
stdout.write(` + ${f}\n`);
|
|
253
|
+
}
|
|
254
|
+
for (const f of result.removed) {
|
|
255
|
+
stdout.write(` - ${f}\n`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
// --- Legacy helpers ---
|
|
260
|
+
async function captureAndCompare(explorer, storage, stdout) {
|
|
261
|
+
const fixtures = await explorer.listFixtures();
|
|
262
|
+
const added = [];
|
|
263
|
+
const changed = [];
|
|
264
|
+
const unchanged = [];
|
|
265
|
+
for (const fixture of fixtures) {
|
|
266
|
+
const png = await explorer.screenshotFixture(fixture.fixtureId);
|
|
267
|
+
const currentPath = `current/${fixture.fixtureId}.png`;
|
|
268
|
+
await storage.write(currentPath, png);
|
|
269
|
+
const baselinePath = `baseline/${fixture.fixtureId}.png`;
|
|
270
|
+
if (await storage.exists(baselinePath)) {
|
|
271
|
+
const baseline = await storage.read(baselinePath);
|
|
272
|
+
if (buffersEqual(png, baseline)) {
|
|
273
|
+
unchanged.push(fixture.fixtureId);
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
changed.push(fixture.fixtureId);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
added.push(fixture.fixtureId);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
const timestamp = new Date().toLocaleTimeString();
|
|
284
|
+
stdout.write(`[${timestamp}] ${fixtures.length} fixtures`);
|
|
285
|
+
if (changed.length === 0 && added.length === 0) {
|
|
286
|
+
stdout.write(` — no changes\n`);
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
stdout.write(`\n`);
|
|
290
|
+
for (const f of changed) {
|
|
291
|
+
stdout.write(` ~ ${f}\n`);
|
|
292
|
+
}
|
|
293
|
+
for (const f of added) {
|
|
294
|
+
stdout.write(` + ${f}\n`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
function buffersEqual(a, b) {
|
|
299
|
+
if (a.length !== b.length)
|
|
300
|
+
return false;
|
|
301
|
+
for (let i = 0; i < a.length; i++) {
|
|
302
|
+
if (a[i] !== b[i])
|
|
303
|
+
return false;
|
|
304
|
+
}
|
|
305
|
+
return true;
|
|
306
|
+
}
|
|
307
|
+
//# sourceMappingURL=watchCommand.js.map
|