@aeratic/pagetailor 0.0.0-stage → 0.0.1
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 +10 -2
- package/bin/cli.js +16 -0
- package/package.json +40 -4
- package/src/index.js +2 -0
- package/src/init.js +22 -0
- package/src/validate.js +43 -0
- package/templates/components/LineChart.jsx +3 -0
- package/templates/components/Stack.jsx +3 -0
- package/templates/components/Table.jsx +3 -0
- package/templates/components/Text.jsx +3 -0
- package/templates/components/TodoList.jsx +3 -0
- package/templates/components/registry.yaml +14 -0
- package/templates/data-catalog.yaml +10 -0
- package/templates/manifest.yaml +6 -0
- package/templates/overrides/.gitkeep +0 -0
- package/templates/overrides/README.md +6 -0
- package/templates/pages/home.yaml +10 -0
- package/templates/tokens.yaml +6 -0
package/README.md
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @aeratic/pagetailor
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Pages as data. Define your app's pages in YAML, bind them to a typed catalog of components and data sources, tailor them per role and per person with small patches, and let an AI agent compose them through a validation gate.
|
|
4
|
+
|
|
5
|
+
Status: 0.0.1, name reservation. The engine (built on json-render) lands in 0.1.
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
npm init -y
|
|
9
|
+
npx @aeratic/pagetailor init
|
|
10
|
+
npx @aeratic/pagetailor validate
|
|
11
|
+
```
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { init } from "../src/init.js";
|
|
3
|
+
import { validate } from "../src/validate.js";
|
|
4
|
+
|
|
5
|
+
const [cmd, ...args] = process.argv.slice(2);
|
|
6
|
+
const help = `pagetailor <command>
|
|
7
|
+
|
|
8
|
+
init create the pagetailor/ folder with manifest, tokens, data catalog, a sample page and base components
|
|
9
|
+
validate check every page and override against the catalog
|
|
10
|
+
`;
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
if (cmd === "init") await init(process.cwd(), args);
|
|
14
|
+
else if (cmd === "validate") process.exitCode = (await validate(process.cwd())) ? 0 : 1;
|
|
15
|
+
else { console.log(help); if (cmd && cmd !== "help") process.exitCode = 1; }
|
|
16
|
+
} catch (e) { console.error("pagetailor:", e.message); process.exitCode = 1; }
|
package/package.json
CHANGED
|
@@ -1,6 +1,42 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aeratic/pagetailor",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Pages as data: declarative, per-user tailored UI that an AI agent can safely compose.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"pagetailor": "bin/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"main": "./src/index.js",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": "./src/index.js"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"bin",
|
|
16
|
+
"src",
|
|
17
|
+
"templates",
|
|
18
|
+
"README.md"
|
|
19
|
+
],
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=18"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"server-driven-ui",
|
|
25
|
+
"generative-ui",
|
|
26
|
+
"declarative-ui",
|
|
27
|
+
"json-render",
|
|
28
|
+
"yaml",
|
|
29
|
+
"personalization",
|
|
30
|
+
"ai-agent"
|
|
31
|
+
],
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/aeratic/pagetailor.git"
|
|
35
|
+
},
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"yaml": "^2.6.0"
|
|
41
|
+
}
|
|
42
|
+
}
|
package/src/index.js
ADDED
package/src/init.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { cp, mkdir, access } from "node:fs/promises";
|
|
2
|
+
import { join, dirname } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
|
|
7
|
+
export async function init(cwd, args = []) {
|
|
8
|
+
const target = join(cwd, "pagetailor");
|
|
9
|
+
const force = args.includes("--force");
|
|
10
|
+
try { await access(target); if (!force) throw new Error("pagetailor/ already exists (use --force to overwrite)"); } catch (e) { if (e.code !== "ENOENT") throw e; }
|
|
11
|
+
await mkdir(target, { recursive: true });
|
|
12
|
+
await cp(join(here, "..", "templates"), target, { recursive: true, force });
|
|
13
|
+
console.log(`created ${target}/
|
|
14
|
+
manifest.yaml app shape: pages, home, navigation, roles
|
|
15
|
+
tokens.yaml design tokens
|
|
16
|
+
data-catalog.yaml typed data sources with roles
|
|
17
|
+
pages/home.yaml a sample page
|
|
18
|
+
overrides/ per-user and per-role patches
|
|
19
|
+
components/ base components, yours to edit
|
|
20
|
+
|
|
21
|
+
next: npx pagetailor validate`);
|
|
22
|
+
}
|
package/src/validate.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { readFile, readdir } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { parse } from "yaml";
|
|
4
|
+
|
|
5
|
+
const load = async (p) => parse(await readFile(p, "utf8"));
|
|
6
|
+
|
|
7
|
+
export async function validate(cwd) {
|
|
8
|
+
const root = join(cwd, "pagetailor");
|
|
9
|
+
const manifest = await load(join(root, "manifest.yaml"));
|
|
10
|
+
const data = await load(join(root, "data-catalog.yaml"));
|
|
11
|
+
const registry = await load(join(root, "components", "registry.yaml"));
|
|
12
|
+
const errors = [];
|
|
13
|
+
|
|
14
|
+
const walk = (node, page, path = "root") => {
|
|
15
|
+
if (!node || typeof node !== "object") return;
|
|
16
|
+
if (node.ref) {
|
|
17
|
+
const [name, version] = String(node.ref).split("@");
|
|
18
|
+
const comp = registry[name];
|
|
19
|
+
if (!comp) errors.push(`${page}: ${path}: unknown component "${name}"`);
|
|
20
|
+
else if (version && !comp.versions?.[version]) errors.push(`${page}: ${path}: "${name}" has no version ${version}`);
|
|
21
|
+
if (node.data?.source) {
|
|
22
|
+
const src = data[node.data.source];
|
|
23
|
+
if (!src) errors.push(`${page}: ${path}: unknown data source "${node.data.source}"`);
|
|
24
|
+
else if (comp?.accepts && src.returns && !compatible(comp.accepts, src.returns))
|
|
25
|
+
errors.push(`${page}: ${path}: "${name}" accepts ${comp.accepts} but "${node.data.source}" returns ${src.returns}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
(node.children ?? []).forEach((c, i) => walk(c, page, `${path}/${i}`));
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
for (const file of await readdir(join(root, "pages"))) {
|
|
32
|
+
if (!file.endsWith(".yaml")) continue;
|
|
33
|
+
const page = await load(join(root, "pages", file));
|
|
34
|
+
if (!manifest.pages?.includes(page.page)) errors.push(`${file}: page "${page.page}" is not listed in manifest.yaml`);
|
|
35
|
+
walk(page.layout, file);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (errors.length) { console.error(errors.join("\n")); console.error(`\n${errors.length} problem(s)`); return false; }
|
|
39
|
+
console.log("all pages valid"); return true;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// v0: exact match or "any"; a real type lattice comes later.
|
|
43
|
+
const compatible = (accepts, returns) => accepts === "any" || accepts === returns;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# name -> versions, and the data type each component accepts
|
|
2
|
+
layout/stack:
|
|
3
|
+
versions: { "1.0.0": { impl: ./Stack.jsx } }
|
|
4
|
+
charts/line:
|
|
5
|
+
accepts: timeseries<money>
|
|
6
|
+
versions: { "1.0.0": { impl: ./LineChart.jsx } }
|
|
7
|
+
tables/simple:
|
|
8
|
+
accepts: any
|
|
9
|
+
versions: { "1.0.0": { impl: ./Table.jsx } }
|
|
10
|
+
todo/list:
|
|
11
|
+
accepts: list<task>
|
|
12
|
+
versions: { "1.0.0": { impl: ./TodoList.jsx } }
|
|
13
|
+
text/block:
|
|
14
|
+
versions: { "1.0.0": { impl: ./Text.jsx } }
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Typed data sources. Components can only bind to sources whose `returns` they accept.
|
|
2
|
+
analytics.revenue:
|
|
3
|
+
description: "Revenue over time"
|
|
4
|
+
params: { period: [7d, 30d, 90d] }
|
|
5
|
+
returns: timeseries<money>
|
|
6
|
+
roles: [admin]
|
|
7
|
+
tasks.mine:
|
|
8
|
+
description: "Tasks assigned to the current user"
|
|
9
|
+
returns: list<task>
|
|
10
|
+
roles: [admin, user]
|
|
File without changes
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
page: home
|
|
2
|
+
layout:
|
|
3
|
+
ref: layout/stack@1.0.0
|
|
4
|
+
children:
|
|
5
|
+
- ref: charts/line@1.0.0
|
|
6
|
+
props: { title: "Revenue, last 30 days" }
|
|
7
|
+
data: { source: analytics.revenue, params: { period: 30d } }
|
|
8
|
+
- ref: todo/list@1.0.0
|
|
9
|
+
props: { filter: mine }
|
|
10
|
+
data: { source: tasks.mine }
|