@decocms/eitri 7.20.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 ADDED
@@ -0,0 +1,123 @@
1
+ # `@decocms/eitri`
2
+
3
+ Deco binding for the **Eitri** mobile stack. Unlike `@decocms/nextjs` /
4
+ `@decocms/tanstack`, this package does **not** render anything — Eitri renders
5
+ sections natively on its own mobile runtime. `@decocms/eitri` only *produces a
6
+ well-filled `.deco`* so Studio can author content against your Eitri sections:
7
+
8
+ - a **self-contained** `.deco/meta.gen.json` (section schemas **plus** the
9
+ framework block types — Page, matchers, `Resolvable`, the section picker —
10
+ baked in, so an FS-based Studio reads it verbatim with no runtime), and
11
+ - the bundled `.deco/blocks.gen.json` decofile snapshot the Eitri runtime
12
+ consumes.
13
+
14
+ It is a thin, Eitri-flavored wrapper over `@decocms/blocks-cli`'s `generate`
15
+ orchestrator (`--platform eitri`). See
16
+ [`docs/eitri-stack-design.md`](../../docs/eitri-stack-design.md) for the full
17
+ design.
18
+
19
+ ## What Eitri owns vs. what deco owns
20
+
21
+ | | Owner |
22
+ |---|---|
23
+ | Section `Props` → JSON Schema, decofile snapshot (`.deco`) | **`@decocms/eitri`** (this package) |
24
+ | Authoring content (forms, page composition) | Studio (reads `.deco`) |
25
+ | Fetching the decofile on-device + rendering sections | **Eitri platform / your app** |
26
+
27
+ There is intentionally **no runtime SDK** here.
28
+
29
+ ## Sections
30
+
31
+ An Eitri section is exactly a deco section: a default-exported component in
32
+ `src/sections/**` with an exported `Props` interface documented with JSDoc.
33
+
34
+ ```tsx
35
+ // src/sections/Banners/Hero.tsx
36
+ import { Image, View } from "eitri-luminus";
37
+
38
+ export interface Props {
39
+ /** @title Hero image. */
40
+ image: string;
41
+ /** @title Alt text. */
42
+ alt?: string;
43
+ /** @title Publish date. @format datetime */
44
+ datetime?: string;
45
+ }
46
+
47
+ export default function HeroBanner({ image, alt }: Props) {
48
+ return <View><Image src={image} alt={alt} /></View>;
49
+ }
50
+ ```
51
+
52
+ `@title`, `@format` (e.g. `textarea`, `datetime` → normalized to `date-time`),
53
+ and the other JSDoc widget tags drive the Studio form. Sections may be `.tsx`,
54
+ `.ts`, `.jsx`, or `.js`.
55
+
56
+ ## Install
57
+
58
+ ```bash
59
+ # in your Eitri app (as a dev tool)
60
+ npm i -D @decocms/eitri
61
+ ```
62
+
63
+ ## Setup
64
+
65
+ ```bash
66
+ npx deco-eitri init
67
+ ```
68
+
69
+ This scaffolds (never overwriting existing files):
70
+
71
+ - `tsconfig.json` extending `@decocms/eitri/tsconfig` — **required**;
72
+ `generate-schema` needs a tsconfig to parse your `Props` types.
73
+ - `src/eitri-env.d.ts` — ambient shims for `eitri-luminus` / `eitri-bifrost` so
74
+ your editor and `tsc` don't flag those imports. Purely ergonomic; generation
75
+ works without it. Delete it if you install the real `eitri-luminus` types.
76
+
77
+ If you prefer to wire it by hand, your `tsconfig.json` need only be:
78
+
79
+ ```json
80
+ { "extends": "@decocms/eitri/tsconfig", "include": ["src"] }
81
+ ```
82
+
83
+ ## Generate
84
+
85
+ ```bash
86
+ # from the app root
87
+ npx deco-eitri generate
88
+
89
+ # or, targeting a sub-app in a monorepo without cd:
90
+ npx deco-eitri generate --root eitri-shopping-monte-carlo-shared
91
+ ```
92
+
93
+ Add it as a script:
94
+
95
+ ```json
96
+ { "scripts": { "deco:generate": "deco-eitri generate" } }
97
+ ```
98
+
99
+ Output under `.deco/`:
100
+
101
+ - `meta.gen.json` — self-contained schema (`framework: "eitri"`).
102
+ - `blocks.gen.json` — decofile snapshot (starts as `{}` for a fresh app; Studio
103
+ fills it as content is authored).
104
+ - `generate.digests.json` — the incremental-generation cache (commit it).
105
+
106
+ The React-runtime artifacts (`sections.gen.ts`, `blocksManifest.gen.ts`,
107
+ `loaders.gen.ts`, `invoke.gen.ts`) are intentionally **not** produced — Eitri
108
+ doesn't use them.
109
+
110
+ ## Programmatic API
111
+
112
+ ```ts
113
+ import { generateEitri } from "@decocms/eitri";
114
+
115
+ const code = await generateEitri({ root: "path/to/app", force: true });
116
+ ```
117
+
118
+ ## Flags
119
+
120
+ `deco-eitri generate` forwards every `@decocms/blocks-cli` `generate` flag
121
+ (`--root`, `--force`, `--dry-run`, `--sections-dir`, `--namespace`, `--site`,
122
+ …). Run `npx deco-eitri generate --help` for the full list. `--platform eitri`
123
+ is always applied.
@@ -0,0 +1,17 @@
1
+ {
2
+ "//": "Base tsconfig for Eitri apps that generate a .deco with @decocms/eitri. Extend it from your app root: { \"extends\": \"@decocms/eitri/tsconfig\", \"include\": [\"src\"] }. generate-schema (ts-morph) reads this to parse section Props interfaces. allowJs + checkJs:false because Eitri apps are mostly plain JS with .tsx sections; skipLibCheck because eitri-luminus/eitri-bifrost types are shimmed, not installed.",
3
+ "compilerOptions": {
4
+ "target": "ESNext",
5
+ "module": "ESNext",
6
+ "moduleResolution": "Bundler",
7
+ "jsx": "react-jsx",
8
+ "allowJs": true,
9
+ "checkJs": false,
10
+ "strict": false,
11
+ "skipLibCheck": true,
12
+ "noEmit": true,
13
+ "isolatedModules": true,
14
+ "esModuleInterop": true,
15
+ "resolveJsonModule": true
16
+ }
17
+ }
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@decocms/eitri",
3
+ "version": "7.20.0",
4
+ "type": "module",
5
+ "description": "Deco binding for the Eitri mobile stack: produces a well-filled .deco (self-contained schema + decofile snapshot) so Studio can author content. No rendering — Eitri renders natively.",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/decocms/blocks.git",
9
+ "directory": "packages/eitri"
10
+ },
11
+ "main": "./src/index.ts",
12
+ "exports": {
13
+ ".": "./src/index.ts",
14
+ "./tsconfig": "./configs/tsconfig.json",
15
+ "./types": "./types/eitri-luminus.d.ts"
16
+ },
17
+ "bin": {
18
+ "deco-eitri": "./src/cli.ts"
19
+ },
20
+ "scripts": {
21
+ "build": "tsc",
22
+ "test": "vitest run --root ../.. packages/eitri/",
23
+ "typecheck": "tsc --noEmit",
24
+ "lint:unused": "knip"
25
+ },
26
+ "dependencies": {
27
+ "@decocms/blocks-cli": "7.20.0",
28
+ "tsx": "^4.22.5"
29
+ },
30
+ "devDependencies": {
31
+ "typescript": "^5.9.0",
32
+ "knip": "^5.86.0"
33
+ },
34
+ "publishConfig": {
35
+ "registry": "https://registry.npmjs.org",
36
+ "access": "public"
37
+ }
38
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * `deco-eitri` — CLI for producing a Studio-ready `.deco` from an Eitri app.
4
+ *
5
+ * deco-eitri init [--root <dir>]
6
+ * Scaffold tsconfig.json (+ src/eitri-env.d.ts shim). Idempotent.
7
+ *
8
+ * deco-eitri generate [--root <dir>] [--force] [any generate flags]
9
+ * Run the blocks-cli orchestrator with --platform eitri: emits a
10
+ * self-contained .deco/meta.gen.json + the .deco/blocks.gen.json
11
+ * snapshot, skipping the React-runtime generators.
12
+ *
13
+ * `generate` forwards every flag through to @decocms/blocks-cli's `generate`
14
+ * (see `deco-eitri generate --help`), only ensuring --platform eitri is set.
15
+ */
16
+ import { runGenerate } from "@decocms/blocks-cli/generate";
17
+ import { runEitriInit } from "./init";
18
+
19
+ const USAGE = `\
20
+ deco-eitri — produce a Studio-ready .deco for an Eitri app
21
+
22
+ Usage:
23
+ deco-eitri init [--root <dir>]
24
+ Scaffold tsconfig.json (extends @decocms/eitri/tsconfig) and a
25
+ src/eitri-env.d.ts shim. Never overwrites existing files.
26
+
27
+ deco-eitri generate [--root <dir>] [--force] [...generate flags]
28
+ Generate .deco (self-contained meta.gen.json + blocks.gen.json snapshot)
29
+ with --platform eitri. All @decocms/blocks-cli generate flags are
30
+ forwarded; run \`deco-eitri generate --help\` for the full list.
31
+ `;
32
+
33
+ /** Pull `--root <dir>` out of an argv slice (used by init). */
34
+ function valueFlag(argv: string[], flag: string): string | undefined {
35
+ const i = argv.indexOf(flag);
36
+ return i !== -1 ? argv[i + 1] : undefined;
37
+ }
38
+
39
+ async function main(): Promise<number> {
40
+ const [command, ...rest] = process.argv.slice(2);
41
+
42
+ if (!command || command === "--help" || command === "-h") {
43
+ console.log(USAGE);
44
+ return command ? 0 : 1;
45
+ }
46
+
47
+ if (command === "init") {
48
+ const { created, skipped } = runEitriInit({ root: valueFlag(rest, "--root") });
49
+ for (const f of created) console.log(`[deco-eitri] created ${f}`);
50
+ for (const f of skipped) console.log(`[deco-eitri] skipped ${f} (already exists)`);
51
+ console.log(
52
+ created.length
53
+ ? "[deco-eitri] init done — run `deco-eitri generate` to produce .deco"
54
+ : "[deco-eitri] init: nothing to do (already set up)",
55
+ );
56
+ return 0;
57
+ }
58
+
59
+ if (command === "generate") {
60
+ // Forward everything to the orchestrator, guaranteeing --platform eitri.
61
+ const args = rest.includes("--platform") ? rest : ["--platform", "eitri", ...rest];
62
+ return runGenerate(args);
63
+ }
64
+
65
+ console.error(`[deco-eitri] unknown command "${command}"\n`);
66
+ console.error(USAGE);
67
+ return 1;
68
+ }
69
+
70
+ main()
71
+ .then((code) => {
72
+ process.exitCode = code;
73
+ })
74
+ .catch((err) => {
75
+ console.error(err);
76
+ process.exit(1);
77
+ });
package/src/index.ts ADDED
@@ -0,0 +1,49 @@
1
+ /**
2
+ * @decocms/eitri — programmatic entry.
3
+ *
4
+ * Deco does not render Eitri apps (Eitri renders natively on its mobile
5
+ * runtime); this package only *produces* a well-filled `.deco` so Studio can
6
+ * author content. It is a thin, Eitri-flavored wrapper over
7
+ * `@decocms/blocks-cli`'s `generate` orchestrator: `--platform eitri` selects
8
+ * the schema + blocks generators only and emits a SELF-CONTAINED
9
+ * `meta.gen.json` (Page, matchers, Resolvable baked in via composeMeta), plus
10
+ * the bundled `blocks.gen.json` snapshot the Eitri runtime consumes.
11
+ *
12
+ * The CLI (`deco-eitri`) is the usual entry; this function is for scripting.
13
+ */
14
+ import { runGenerate } from "@decocms/blocks-cli/generate";
15
+
16
+ export interface GenerateEitriOptions {
17
+ /** App folder to generate for (input dirs + `.deco/` resolve against it). */
18
+ root?: string;
19
+ /** Site name written into the schema (default: blocks-cli's default). */
20
+ site?: string;
21
+ /** Section namespace (default: "site"). */
22
+ namespace?: string;
23
+ /** Ignore the incremental cache and regenerate. */
24
+ force?: boolean;
25
+ /** Extra raw flags forwarded verbatim to the orchestrator. */
26
+ extraArgs?: string[];
27
+ }
28
+
29
+ /** Build the orchestrator argv for an Eitri generation run. */
30
+ export function eitriGenerateArgs(opts: GenerateEitriOptions = {}): string[] {
31
+ const args = ["--platform", "eitri"];
32
+ if (opts.root) args.push("--root", opts.root);
33
+ if (opts.site) args.push("--site", opts.site);
34
+ if (opts.namespace) args.push("--namespace", opts.namespace);
35
+ if (opts.force) args.push("--force");
36
+ if (opts.extraArgs?.length) args.push(...opts.extraArgs);
37
+ return args;
38
+ }
39
+
40
+ /**
41
+ * Generate the `.deco` for an Eitri app. Resolves to the process exit code
42
+ * (0 = success), mirroring the CLI.
43
+ */
44
+ export function generateEitri(opts: GenerateEitriOptions = {}): Promise<number> {
45
+ return runGenerate(eitriGenerateArgs(opts));
46
+ }
47
+
48
+ export { runEitriInit } from "./init";
49
+ export type { EitriInitOptions, EitriInitResult } from "./init";
@@ -0,0 +1,71 @@
1
+ import * as fs from "node:fs";
2
+ import * as os from "node:os";
3
+ import * as path from "node:path";
4
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
5
+ import { eitriGenerateArgs } from "./index";
6
+ import { runEitriInit } from "./init";
7
+
8
+ describe("eitriGenerateArgs", () => {
9
+ it("always applies --platform eitri", () => {
10
+ expect(eitriGenerateArgs()).toEqual(["--platform", "eitri"]);
11
+ });
12
+
13
+ it("maps options onto orchestrator flags", () => {
14
+ expect(
15
+ eitriGenerateArgs({ root: "apps/foo", site: "monte", namespace: "site", force: true }),
16
+ ).toEqual([
17
+ "--platform",
18
+ "eitri",
19
+ "--root",
20
+ "apps/foo",
21
+ "--site",
22
+ "monte",
23
+ "--namespace",
24
+ "site",
25
+ "--force",
26
+ ]);
27
+ });
28
+
29
+ it("appends extra raw flags verbatim", () => {
30
+ expect(eitriGenerateArgs({ extraArgs: ["--dry-run"] })).toEqual([
31
+ "--platform",
32
+ "eitri",
33
+ "--dry-run",
34
+ ]);
35
+ });
36
+ });
37
+
38
+ describe("runEitriInit", () => {
39
+ let dir: string;
40
+ beforeEach(() => {
41
+ dir = fs.mkdtempSync(path.join(os.tmpdir(), "eitri-init-"));
42
+ });
43
+ afterEach(() => {
44
+ fs.rmSync(dir, { recursive: true, force: true });
45
+ });
46
+
47
+ it("scaffolds tsconfig.json + src/eitri-env.d.ts on a fresh app", () => {
48
+ const res = runEitriInit({ root: dir });
49
+ expect(res.created).toEqual(["tsconfig.json", path.join("src", "eitri-env.d.ts")]);
50
+ expect(res.skipped).toEqual([]);
51
+
52
+ const tsconfig = JSON.parse(fs.readFileSync(path.join(dir, "tsconfig.json"), "utf-8"));
53
+ expect(tsconfig.extends).toBe("@decocms/eitri/tsconfig");
54
+ expect(tsconfig.include).toEqual(["src"]);
55
+
56
+ const shim = fs.readFileSync(path.join(dir, "src", "eitri-env.d.ts"), "utf-8");
57
+ expect(shim).toContain('declare module "eitri-luminus"');
58
+ expect(shim).toContain('declare module "eitri-bifrost"');
59
+ });
60
+
61
+ it("is idempotent — never overwrites existing files", () => {
62
+ fs.writeFileSync(path.join(dir, "tsconfig.json"), '{"custom":true}\n');
63
+ const res = runEitriInit({ root: dir });
64
+ expect(res.skipped).toContain("tsconfig.json");
65
+ expect(res.created).toEqual([path.join("src", "eitri-env.d.ts")]);
66
+ // The pre-existing tsconfig is left untouched.
67
+ expect(JSON.parse(fs.readFileSync(path.join(dir, "tsconfig.json"), "utf-8"))).toEqual({
68
+ custom: true,
69
+ });
70
+ });
71
+ });
package/src/init.ts ADDED
@@ -0,0 +1,69 @@
1
+ /**
2
+ * `deco-eitri init` — scaffold the two files an Eitri app needs to generate a
3
+ * `.deco`, without touching anything that already exists (idempotent):
4
+ *
5
+ * 1. `tsconfig.json` extending `@decocms/eitri/tsconfig` — required by
6
+ * generate-schema (ts-morph needs a tsconfig to build the program).
7
+ * 2. `src/eitri-env.d.ts` — a copy of the eitri-luminus/bifrost ambient
8
+ * shims, dropped inside `src/` so the app's `include: ["src"]` picks it up
9
+ * with no node_modules path gymnastics. Purely editor/tsc ergonomics —
10
+ * generation works without it.
11
+ *
12
+ * Never overwrites an existing file, so it's safe to re-run.
13
+ */
14
+ import fs from "node:fs";
15
+ import path from "node:path";
16
+ import { fileURLToPath } from "node:url";
17
+
18
+ export interface EitriInitOptions {
19
+ /** App root to scaffold into. Defaults to process.cwd(). */
20
+ root?: string;
21
+ }
22
+
23
+ export interface EitriInitResult {
24
+ /** Files created this run (relative to root). */
25
+ created: string[];
26
+ /** Files that already existed and were left untouched (relative to root). */
27
+ skipped: string[];
28
+ }
29
+
30
+ const APP_TSCONFIG = `${JSON.stringify(
31
+ {
32
+ extends: "@decocms/eitri/tsconfig",
33
+ include: ["src"],
34
+ },
35
+ null,
36
+ 2,
37
+ )}\n`;
38
+
39
+ /** Absolute path to the packaged ambient shim (../types/eitri-luminus.d.ts). */
40
+ function packagedShimPath(): string {
41
+ const here = path.dirname(fileURLToPath(import.meta.url));
42
+ return path.resolve(here, "..", "types", "eitri-luminus.d.ts");
43
+ }
44
+
45
+ export function runEitriInit(opts: EitriInitOptions = {}): EitriInitResult {
46
+ const root = path.resolve(opts.root ?? process.cwd());
47
+ const created: string[] = [];
48
+ const skipped: string[] = [];
49
+
50
+ const write = (rel: string, contents: string) => {
51
+ const abs = path.join(root, rel);
52
+ if (fs.existsSync(abs)) {
53
+ skipped.push(rel);
54
+ return;
55
+ }
56
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
57
+ fs.writeFileSync(abs, contents);
58
+ created.push(rel);
59
+ };
60
+
61
+ write("tsconfig.json", APP_TSCONFIG);
62
+
63
+ // Copy the shim content (not a reference) so the app file is self-contained
64
+ // and immune to node_modules path resolution quirks.
65
+ const shim = fs.readFileSync(packagedShimPath(), "utf-8");
66
+ write(path.join("src", "eitri-env.d.ts"), shim);
67
+
68
+ return { created, skipped };
69
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,7 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist"
5
+ },
6
+ "include": ["src/**/*"]
7
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Ambient shims for the Eitri runtime libraries.
3
+ *
4
+ * Eitri apps import UI primitives from `eitri-luminus` and the native SDK from
5
+ * `eitri-bifrost`/`eitri-commons`, but those packages are resolved by the
6
+ * external `eitri-cli` toolchain, not installed in the app's node_modules. When
7
+ * generating a `.deco` we only need the section `Props` interfaces (which use
8
+ * plain primitives), so these shims exist purely to keep an author's editor and
9
+ * `tsc` from erroring on the imports — they are intentionally loose (`any`).
10
+ *
11
+ * `deco-eitri init` copies this file into your app's `src/` (as
12
+ * `eitri-env.d.ts`) so it is picked up by `include: ["src"]` with no path
13
+ * gymnastics. If you install the real `eitri-luminus` types, delete the copy.
14
+ */
15
+ declare module "eitri-luminus" {
16
+ // Every luminus component (View, Text, Image, Page, Button, ...) is exposed
17
+ // as a permissive component type. Named + default imports both resolve.
18
+ const luminus: Record<string, any>;
19
+ export = luminus;
20
+ }
21
+
22
+ declare module "eitri-bifrost" {
23
+ const Eitri: any;
24
+ export default Eitri;
25
+ export = Eitri;
26
+ }
27
+
28
+ declare module "eitri-commons" {
29
+ const commons: any;
30
+ export = commons;
31
+ }