@juxtapose/adapter-ladle 0.2.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Magic Factory LLC
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,17 @@
1
+ # @juxtapose/adapter-ladle
2
+
3
+ Ladle platform adapter for juxtapose visual testing.
4
+
5
+ Part of [juxtapose](https://github.com/juxtapose-dev/juxtapose), visual
6
+ regression testing for component workbenches.
7
+
8
+ **You probably don't need to install this directly.** Use
9
+ [`@juxtapose/cli`](https://www.npmjs.com/package/@juxtapose/cli) for the
10
+ command line, or
11
+ [`@juxtapose/storybook`](https://www.npmjs.com/package/@juxtapose/storybook)
12
+ to review diffs inside Storybook. This package is a building block those
13
+ depend on.
14
+
15
+ Full documentation: https://github.com/juxtapose-dev/juxtapose
16
+
17
+ MIT © Magic Factory LLC
@@ -0,0 +1,26 @@
1
+ import type { Page } from 'playwright';
2
+ import type { Platform, EnumerateTarget, SettleContext, StoryManifest } from '@juxtapose/core';
3
+ export declare class LadlePlatform implements Platform {
4
+ readonly name = "ladle";
5
+ detect(target: EnumerateTarget): Promise<boolean>;
6
+ enumerate(target: EnumerateTarget): Promise<StoryManifest[]>;
7
+ storyUrl(baseUrl: string, storyId: string): string;
8
+ /** URL-addressable, so showing a story is just a navigation. */
9
+ showStory(page: Page, storyId: string, baseUrl: string): Promise<void>;
10
+ previewUrl(baseUrl: string): string;
11
+ settle(page: Page, ctx: SettleContext): Promise<void>;
12
+ /**
13
+ * Switch stories without a navigation via Ladle's exposed dispatch.
14
+ *
15
+ * `window.ladleDispatch` is set deliberately — Ladle's own public `linkTo()`
16
+ * API is built on it — so this is a supported path, not a private poke.
17
+ *
18
+ * The subtlety: `data-storyloaded` is a plain attribute toggle, so a naive
19
+ * wait after dispatching can match the *outgoing* story's attribute before
20
+ * React tears it down. Clear it ourselves first, then wait for it to come
21
+ * back, which makes the signal unambiguous.
22
+ */
23
+ switchStory(page: Page, storyId: string, ctx: SettleContext): Promise<boolean>;
24
+ readonly configDirs: string[];
25
+ readonly storyFileGlobs: string[];
26
+ }
package/dist/index.js ADDED
@@ -0,0 +1,127 @@
1
+ // Ladle platform adapter.
2
+ //
3
+ // Written second, against the interface the Storybook adapter shaped. It needed
4
+ // no changes to `Platform` — which is the evidence the seam is real rather than
5
+ // a Storybook API wearing a trenchcoat.
6
+ import { readFile } from 'node:fs/promises';
7
+ import path from 'node:path';
8
+ import { navigateToStory } from '@juxtapose/core';
9
+ /** Where a story declares juxtapose config. Ladle's `meta` is an open index
10
+ * signature, so a namespaced key passes through untouched. */
11
+ const META_NAMESPACE = 'juxtapose';
12
+ export class LadlePlatform {
13
+ name = 'ladle';
14
+ async detect(target) {
15
+ return (await loadMeta(target)) !== null;
16
+ }
17
+ async enumerate(target) {
18
+ const meta = await loadMeta(target);
19
+ if (!meta) {
20
+ throw new Error(`[ladle] no meta.json at ${target.origin ?? target.buildDir}. ` +
21
+ `Run \`ladle build\`, or point --origin at a running dev server.`);
22
+ }
23
+ return Object.entries(meta.stories)
24
+ .map(([id, story]) => ({
25
+ storyId: id,
26
+ title: story.levels.join('/'),
27
+ name: story.name,
28
+ // Empty when absent rather than fabricated — affected-story analysis
29
+ // treats a blank path as "can't prove unaffected" and captures it.
30
+ componentPath: story.filePath
31
+ ? path.resolve(target.projectRoot, story.filePath)
32
+ : '',
33
+ params: readParams(story.meta),
34
+ }))
35
+ .sort((a, b) => a.storyId.localeCompare(b.storyId));
36
+ }
37
+ storyUrl(baseUrl, storyId) {
38
+ // No iframe route; `mode=preview` is what strips the sidebar and addon
39
+ // chrome, leaving only the story.
40
+ return `${stripSlash(baseUrl)}/?story=${encodeURIComponent(storyId)}&mode=preview`;
41
+ }
42
+ /** URL-addressable, so showing a story is just a navigation. */
43
+ async showStory(page, storyId, baseUrl) {
44
+ await navigateToStory((b, s) => this.storyUrl(b, s))(page, storyId, baseUrl);
45
+ }
46
+ previewUrl(baseUrl) {
47
+ return `${stripSlash(baseUrl)}/?mode=preview`;
48
+ }
49
+ async settle(page, ctx) {
50
+ await waitForStoryLoaded(page, ctx.timeoutMs);
51
+ }
52
+ /**
53
+ * Switch stories without a navigation via Ladle's exposed dispatch.
54
+ *
55
+ * `window.ladleDispatch` is set deliberately — Ladle's own public `linkTo()`
56
+ * API is built on it — so this is a supported path, not a private poke.
57
+ *
58
+ * The subtlety: `data-storyloaded` is a plain attribute toggle, so a naive
59
+ * wait after dispatching can match the *outgoing* story's attribute before
60
+ * React tears it down. Clear it ourselves first, then wait for it to come
61
+ * back, which makes the signal unambiguous.
62
+ */
63
+ async switchStory(page, storyId, ctx) {
64
+ const dispatched = await page
65
+ .evaluate((id) => {
66
+ const w = window;
67
+ if (typeof w.ladleDispatch !== 'function')
68
+ return false;
69
+ document.documentElement.removeAttribute('data-storyloaded');
70
+ w.ladleDispatch({ type: 'update-story', value: id });
71
+ return true;
72
+ }, storyId)
73
+ .catch(() => false);
74
+ if (!dispatched)
75
+ return false;
76
+ await waitForStoryLoaded(page, ctx.timeoutMs);
77
+ return true;
78
+ }
79
+ configDirs = ['.ladle'];
80
+ storyFileGlobs = ['**/*.stories.@(ts|tsx|js|jsx|mdx)'];
81
+ }
82
+ /**
83
+ * Ladle's authoritative render signal: `data-storyloaded` on <html>.
84
+ *
85
+ * Set by a component rendered inside Suspense after the story commits, and
86
+ * removed by the loading spinner on mount — so its presence means the story
87
+ * chunk loaded and React committed, not merely that the page fired `load`.
88
+ * This is what Ladle's own screenshot suite waits on.
89
+ */
90
+ async function waitForStoryLoaded(page, timeoutMs) {
91
+ await page
92
+ .waitForSelector('[data-storyloaded]', { timeout: timeoutMs })
93
+ // Timing out is not fatal: capture what rendered. A slow story should
94
+ // surface as a reviewable diff, not abort the run.
95
+ .catch(() => { });
96
+ }
97
+ function readParams(meta) {
98
+ const ns = meta?.[META_NAMESPACE];
99
+ return ns && typeof ns === 'object' ? ns : {};
100
+ }
101
+ async function loadMeta(target) {
102
+ if (target.origin) {
103
+ try {
104
+ const res = await fetch(new URL('meta.json', withSlash(target.origin)));
105
+ if (!res.ok)
106
+ return null;
107
+ const parsed = (await res.json());
108
+ return parsed?.stories ? parsed : null;
109
+ }
110
+ catch {
111
+ return null;
112
+ }
113
+ }
114
+ if (!target.buildDir)
115
+ return null;
116
+ try {
117
+ const raw = await readFile(path.join(target.buildDir, 'meta.json'), 'utf8');
118
+ const parsed = JSON.parse(raw);
119
+ return parsed?.stories ? parsed : null;
120
+ }
121
+ catch {
122
+ return null;
123
+ }
124
+ }
125
+ const withSlash = (u) => (u.endsWith('/') ? u : u + '/');
126
+ const stripSlash = (u) => u.replace(/\/+$/, '');
127
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@juxtapose/adapter-ladle",
3
+ "version": "0.2.0",
4
+ "type": "module",
5
+ "description": "Ladle platform adapter for juxtapose visual testing.",
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
9
+ "license": "MIT",
10
+ "main": "./dist/index.js",
11
+ "types": "./dist/index.d.ts",
12
+ "exports": {
13
+ ".": "./dist/index.js",
14
+ "./package.json": "./package.json"
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "dependencies": {
20
+ "@juxtapose/core": "0.2.0"
21
+ },
22
+ "peerDependencies": {
23
+ "playwright": "^1.48.0"
24
+ },
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/juxtapose-dev/juxtapose.git",
28
+ "directory": "packages/adapter-ladle"
29
+ },
30
+ "peerDependenciesMeta": {
31
+ "playwright": {
32
+ "optional": true
33
+ }
34
+ },
35
+ "scripts": {
36
+ "build": "tsc -p tsconfig.json"
37
+ }
38
+ }