@web-my-money/studio-consumer 2.2.0 → 2.4.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/README.md +111 -88
- package/bin/studio-consumer.mjs +29 -0
- package/cli/claude-step.mjs +48 -0
- package/cli/init.mjs +489 -0
- package/cli/preflight.mjs +103 -0
- package/cli/provisioner.mjs +103 -0
- package/cli/run.mjs +89 -0
- package/cli/scaffold.mjs +155 -0
- package/cli/state.mjs +49 -0
- package/cli/studio-api.mjs +127 -0
- package/cli/templates.mjs +134 -0
- package/cli/ui.mjs +126 -0
- package/cli/writers.mjs +148 -0
- package/package.json +33 -29
- package/skills/onboard-site/SKILL.md +125 -0
- package/src/analytics/index.ts +37 -37
- package/src/attribution/index.ts +14 -14
- package/src/brand/index.ts +66 -66
- package/src/content/index.ts +27 -27
- package/src/content/manifest-handler.ts +11 -9
- package/src/image/index.ts +14 -14
- package/src/next/index.d.mts +13 -0
- package/src/next/index.mjs +103 -0
package/README.md
CHANGED
|
@@ -1,88 +1,111 @@
|
|
|
1
|
-
# @web-my-money/studio-consumer
|
|
2
|
-
|
|
3
|
-
Consumer-side integration for WMM Studio: content, analytics, attribution.
|
|
4
|
-
|
|
5
|
-
This is a source-only workspace package — no build step, no `dist`. Consumers
|
|
6
|
-
are Next.js apps that already transpile workspace and scoped packages, so
|
|
7
|
-
publishing compiled output here would only add a compile-and-publish cycle
|
|
8
|
-
for no gain.
|
|
9
|
-
|
|
10
|
-
##
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
`
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
1
|
+
# @web-my-money/studio-consumer
|
|
2
|
+
|
|
3
|
+
Consumer-side integration for WMM Studio: content, analytics, attribution.
|
|
4
|
+
|
|
5
|
+
This is a source-only workspace package — no build step, no `dist`. Consumers
|
|
6
|
+
are Next.js apps that already transpile workspace and scoped packages, so
|
|
7
|
+
publishing compiled output here would only add a compile-and-publish cycle
|
|
8
|
+
for no gain.
|
|
9
|
+
|
|
10
|
+
## Connect a site: one command (2.3.0; new sites 2.4.0)
|
|
11
|
+
|
|
12
|
+
In Studio, open the site, Site settings, **Get the setup command**, then in the
|
|
13
|
+
app's folder (or, for a brand-new site, in an empty folder):
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npx @web-my-money/studio-consumer init <code>
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
It checks the machine, installs this package, writes the routes, wraps
|
|
20
|
+
`next.config` with `withStudio`, links Vercel and sets the env vars, opens Claude
|
|
21
|
+
Code (the `onboard-site` skill shipped here) to wire the layout and choose the
|
|
22
|
+
editable text, verifies, opens a pull request, waits for the deploy and asks
|
|
23
|
+
Studio to sync. Rerun it after any stop; it resumes. `init --explain` shows the
|
|
24
|
+
plan without changing anything.
|
|
25
|
+
|
|
26
|
+
In an empty folder (2.4.0) it first creates the client's private repository from
|
|
27
|
+
`Web-My-Money/wmm-site-template`, pulls it in and puts the site's key and name in
|
|
28
|
+
place of the template's markers; the Claude step then sets the client's colours
|
|
29
|
+
and first copy instead of wiring a layout.
|
|
30
|
+
|
|
31
|
+
## Entry points
|
|
32
|
+
|
|
33
|
+
- `@web-my-money/studio-consumer/analytics`
|
|
34
|
+
- `@web-my-money/studio-consumer/content`
|
|
35
|
+
- `@web-my-money/studio-consumer/attribution`
|
|
36
|
+
- `@web-my-money/studio-consumer/headers`
|
|
37
|
+
- `@web-my-money/studio-consumer/brand` (2.0.0)
|
|
38
|
+
- `@web-my-money/studio-consumer/image` (2.1.0)
|
|
39
|
+
- `@web-my-money/studio-consumer/next` (2.3.0): `withStudio(nextConfig)`
|
|
40
|
+
|
|
41
|
+
Each entry point exports `PACKAGE_VERSION`, to prove at runtime that a consumer
|
|
42
|
+
is resolving this package rather than a stale copy.
|
|
43
|
+
|
|
44
|
+
## Brand (2.0.0, breaking)
|
|
45
|
+
|
|
46
|
+
A site declares its brand theme in code. Every invalid input throws, and this
|
|
47
|
+
runs while the root layout renders, so a site with no brand, a malformed theme
|
|
48
|
+
id, or WMM's own theme fails its build instead of silently inheriting WMM's
|
|
49
|
+
palette.
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
// lib/brand.ts — its own module: Next refuses unknown named exports from a layout file
|
|
53
|
+
import { defineSiteBrand } from "@web-my-money/studio-consumer/brand";
|
|
54
|
+
|
|
55
|
+
export const brand = defineSiteBrand({ theme: "acme", style: "flat" });
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
```tsx
|
|
59
|
+
// app/layout.tsx
|
|
60
|
+
import { brandRootAttributes } from "@web-my-money/studio-consumer/brand";
|
|
61
|
+
import { brand } from "@/lib/brand";
|
|
62
|
+
|
|
63
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
64
|
+
return <html lang="en" {...brandRootAttributes(brand)}><body>{children}</body></html>;
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
// app/api/content/manifest/route.ts
|
|
70
|
+
import { brand } from "@/lib/brand";
|
|
71
|
+
|
|
72
|
+
export const GET = createManifestHandler(getManifest, brand);
|
|
73
|
+
// No `export const dynamic` (2.3.0+): the handler waits for a real request, so the
|
|
74
|
+
// manifest is always live, and cacheComponents apps refuse that export anyway.
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
`createManifestHandler` now REQUIRES the brand (the breaking change in 2.0.0), so
|
|
78
|
+
Studio records the theme the site ships. Only a WMM site may use a WMM theme
|
|
79
|
+
(`wmm`, `site`, `light`, `dark`), by passing `wmmSite: true`.
|
|
80
|
+
|
|
81
|
+
## Image focal point (2.1.0)
|
|
82
|
+
|
|
83
|
+
An image value edited in Studio can carry `focal: { x, y }` (0–1 from the
|
|
84
|
+
top-left): where the subject is. Apply it so responsive crops keep the subject
|
|
85
|
+
in frame. No focal point, or a malformed one, reads as the centre, which is the
|
|
86
|
+
browser default, so existing images render exactly as before.
|
|
87
|
+
|
|
88
|
+
```tsx
|
|
89
|
+
import { focalObjectPosition } from "@web-my-money/studio-consumer/image";
|
|
90
|
+
|
|
91
|
+
<img src={value.url} alt={value.alt.en} style={{ objectFit: "cover", objectPosition: focalObjectPosition(value) }} />
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Analytics collector (2.2.0)
|
|
95
|
+
|
|
96
|
+
`createCollectHandler` only accepts posts from the site's own pages (`Sec-Fetch-Site`,
|
|
97
|
+
falling back to `Origin`), and it decides each event's A/B arm on the server from the
|
|
98
|
+
`wmm_vid` cookie. The browser's own `variant` is always discarded. Pass the content
|
|
99
|
+
client's `getRunningExperiments` so the arm can be resolved:
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
export const POST = createCollectHandler({
|
|
103
|
+
siteKey: SITE_KEY,
|
|
104
|
+
ingestUrl: process.env.FUNNEL_INGEST_URL ?? "",
|
|
105
|
+
ingestKey: process.env.FUNNEL_INGEST_TOKEN ?? "",
|
|
106
|
+
getRunningExperiments: () => content.getRunningExperiments(),
|
|
107
|
+
});
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Without it, events carry no arm and the site stays out of any A/B comparison.
|
|
111
|
+
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @ts-check
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* `npx @web-my-money/studio-consumer <command>`. Plain ESM: Node will not strip
|
|
6
|
+
* TypeScript inside node_modules, so nothing on this path may be `.ts`.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const USAGE = `Usage:
|
|
10
|
+
npx @web-my-money/studio-consumer init <code> Connect this app to WMM Studio, step by step.
|
|
11
|
+
npx @web-my-money/studio-consumer init Continue where the last run stopped.
|
|
12
|
+
npx @web-my-money/studio-consumer init --explain Show every step without changing anything.
|
|
13
|
+
|
|
14
|
+
Get <code> in Studio: open the site, Site settings, Get the setup command.`;
|
|
15
|
+
|
|
16
|
+
const [command, ...rest] = process.argv.slice(2);
|
|
17
|
+
|
|
18
|
+
if (!command || command === "--help" || command === "-h" || command === "help") {
|
|
19
|
+
process.stdout.write(`${USAGE}\n`);
|
|
20
|
+
process.exit(0);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (command !== "init") {
|
|
24
|
+
process.stderr.write(`Unknown command "${command}".\n\n${USAGE}\n`);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const { runInit } = await import("../cli/init.mjs");
|
|
29
|
+
process.exitCode = await runInit(rest);
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Step 8 of `init`: the edits that need judgement on someone else's code (root
|
|
8
|
+
* layout, proxy/middleware, which text is editable) are done by Claude Code, on
|
|
9
|
+
* the developer's own subscription, through the `onboard-site` skill this
|
|
10
|
+
* package ships. Each edit is shown and approved before it is written.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** @typedef {import("./run.mjs").Runner} Runner */
|
|
14
|
+
/** @typedef {import("./ui.mjs").Ui} Ui */
|
|
15
|
+
|
|
16
|
+
const SKILL_SOURCE = fileURLToPath(new URL("../skills/onboard-site/SKILL.md", import.meta.url));
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {{ cwd: string, run: Runner, ui: Ui }} ctx
|
|
20
|
+
*/
|
|
21
|
+
export async function claudeStep({ cwd, run, ui }) {
|
|
22
|
+
const target = path.join(cwd, ".claude", "skills", "onboard-site", "SKILL.md");
|
|
23
|
+
const source = readFileSync(SKILL_SOURCE, "utf8");
|
|
24
|
+
if (!existsSync(target) || readFileSync(target, "utf8") !== source) {
|
|
25
|
+
mkdirSync(path.dirname(target), { recursive: true });
|
|
26
|
+
copyFileSync(SKILL_SOURCE, target);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const version = await run("claude", ["--version"], { cwd });
|
|
30
|
+
if (version.code !== 0) {
|
|
31
|
+
ui.stop(
|
|
32
|
+
"This step needs Claude Code, to wire your layout and choose the editable text with you. Install it, then run this command again; it continues from here.",
|
|
33
|
+
"Install Claude Code from https://claude.com/claude-code",
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
ui.info("Opening Claude Code. It will show each change and wait for your yes.");
|
|
38
|
+
ui.info("When it says it is done, type /exit and this command carries on.");
|
|
39
|
+
if (ui.explain) return;
|
|
40
|
+
// Claude Code owns the terminal while it runs: the setup stops reading input,
|
|
41
|
+
// or keystrokes would be split between the two and replayed later as answers.
|
|
42
|
+
ui.pause();
|
|
43
|
+
try {
|
|
44
|
+
await run("claude", ["/onboard-site"], { cwd, inherit: true });
|
|
45
|
+
} finally {
|
|
46
|
+
ui.resume();
|
|
47
|
+
}
|
|
48
|
+
}
|