Apply pipeline setup
Wire the zdtp-server bin into a non-Astro host's dev script — CORS, --write-root sandboxing, and the routing JSON.
The Apply button POSTs a flat CSS-var diff to the host's apply endpoint, which routes the diff to the bin server (zdtp-server), which atomically rewrites your source CSS files. This recipe wires the bin into a non-Astro host (e.g. a Vite SPA, Next.js, or any Node-based dev server) and sets up the three guards: CORS, --write-root sandboxing, and the single-source routing JSON.
For the full apply contract — request / response envelopes, native- implementation guidance, error shapes — see Apply pipeline reference.
1. Install the package as a dev dep
pnpm add -D @takazudo/zdtpThe package ships an executable named zdtp-server. Verify the install with:
pnpm exec zdtp-server --help2. Author the routing JSON
The routing JSON is a top-level object mapping CSS-var prefix family (without leading -- and trailing -) → repo-relative source-CSS file path. Both the panel UI (PanelConfig.applyRouting) and the bin (--routing flag) read the same file — keeping them in lockstep is the whole point.
// panel-routing.json
{
"myapp": "src/styles/tokens.css",
"myapp-extra": "src/styles/extra-tokens.css"
}Note
Prefix family, not the full var name. myapp matches every var that starts with --myapp- (e.g. --myapp-spacing-md,--myapp-color-primary). A diff token whose prefix is not in the routing map is rejected with a 400.
The routing JSON only says WHICH FILE a token family lands in — it says nothing about where inside that file the value gets rewritten. The rewriter itself scans the first top-level :root { ... } block AND the first top-level @theme { ... } block of the routed file (Tailwind v4's token scope), so a token declared inside either one is reachable; see Which CSS blocks get rewritten for the full scanning rules and a Tailwind v4 worked example.
3. Wire it into your dev script
Use concurrently (or npm-run-all) to run the bin alongside your dev server. Replace vite with whichever long-running process your project uses.
// package.json
{
"scripts": {
"dev": "concurrently --kill-others-on-fail --names dev,panel \"vite\" \"zdtp-server --routing ./panel-routing.json --write-root ./src/styles --allow-origin http://localhost:5173\""
},
"devDependencies": {
"@takazudo/zdtp": "^0.1.0",
"concurrently": "^9.0.0"
}
}The flags above are the recommended minimum:
-— required. The single source of truth for prefix → file routing.- routing . / panel- routing. json --write-root ./src/styles— required. This is the narrow directory tree the bin is allowed to write into; keep it limited to the files the panel edits so a typo or path-escape attempt cannot clobber other files in your repo.--root <dir>— optional. This is the CWD/resolution base for relative--routingand--write-rootpaths, and defaults toprocess.cwd().-— required for any browser to apply. Pass each dev origin you actually serve from. Repeatable.- allow- origin http: / / localhost: 5173
Warning
--allow-origin is matched verbatim on the full scheme + host + port string. http: and http: are different origins as far as the bin is concerned. Pass each one you serve from, or your browser will see a 403 with no Access-Control-Allow-Origin header.
4. Point the panel at the bin
Import the same routing JSON into your panel config so the UI and the bin agree without two declarations to keep in sync.
// src/lib/my-panel-config.ts
import type { PanelConfig } from '@takazudo/zdtp';
import routing from '../../panel-routing.json' assert { type: 'json' };
import { myTabs } from './my-tabs';
export const myPanelConfig: PanelConfig = {
storagePrefix: 'myapp-design-token-panel',
consoleNamespace: 'myapp',
modalClassPrefix: 'myapp-design-token-panel-modal',
schemaId: 'myapp-design-tokens/v1',
exportFilenameBase: 'myapp-design-tokens',
tabs: myTabs,
applyEndpoint: 'http://127.0.0.1:24681/apply',
applyRouting: routing,
};applyEndpoint is the URL the browser POSTs to. The bin defaults to 127.0.0.1:24681, so the default http: "just works" — change --port or --host and you must update applyEndpoint to match.
5. Vite-only host wiring (no Astro)
A Vite host (or any non-Astro host) does not get the <DesignTokenPanelHost> SSR component. Call configurePanel directly at app boot, then import the panel module so its side-effects register:
// src/main.ts
import { configurePanel } from '@takazudo/zdtp';
import '@takazudo/zdtp/styles';
import { myPanelConfig } from './lib/my-panel-config';
configurePanel(myPanelConfig);
// Lazy-load the panel module on demand (e.g. when a dev keybinding fires).
window.addEventListener('keydown', (event) => {
if (event.altKey && event.shiftKey && event.code === 'KeyP') {
void import('@takazudo/zdtp').then((mod) => {
mod.toggleDesignPanel();
});
}
});Tip
Validate before configure. If your config crosses any boundary that might mangle it (an <input>-hosted JSON blob, a server response, etc.), call assertValidPanelConfig(value) first — it throws with a message naming the offending field when something is wrong, instead of failing later with a cryptic runtime error.
6. Shut down cleanly
concurrently --kill-others-on-fail (or concurrently -k) ensures SIGINT from your terminal propagates to the bin. The bin handles SIGINT / SIGTERM by stopping accept, draining in-flight requests, and exiting with code 0. A 5 s timeout force-exits if close hangs on a keep-alive socket.
If you spawn the bin from a custom Node wrapper instead, forward signals yourself:
// scripts/dev-with-panel.ts
import { spawn } from 'node:child_process';
const bin = spawn(
'zdtp-server',
[
'--routing', './panel-routing.json',
'--write-root', './src/styles',
'--allow-origin', 'http://localhost:5173',
],
{ stdio: 'inherit', shell: false },
);
const forward = (signal: NodeJS.Signals): void => {
bin.kill(signal);
};
process.on('SIGINT', forward);
process.on('SIGTERM', forward);
bin.on('exit', (code) => process.exit(code ?? 0));7. Smoke-test the wiring
Once both processes are up, hit the bin's healthz endpoint to confirm:
curl http://127.0.0.1:24681/healthz
# {"ok":true,"writeRoot":"/abs/path/to/src/styles","routing":"/abs/path/to/panel-routing.json","port":24681}Then click Apply in the panel. A successful apply returns { "ok": true, "updated": [...] }. See the apply pipeline reference for the full response shape and every error code.
Danger
The bin is dev-only. It binds to loopback by default and writes to your source tree. Never run it in production, never expose it on the public internet, and never widen --allow-origin to *.
Related
Apply pipeline reference — request / response envelopes, error codes, native-implementation guidance.
configurePanelreference —applyEndpointandapplyRoutingfields.Token manifest reference — which tokens get included in an Apply diff.