@shop-on/theme-contract 0.5.0 → 0.7.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.
Files changed (43) hide show
  1. package/dist/ai-files.d.ts +18 -0
  2. package/dist/ai-files.js +47 -0
  3. package/dist/block-allowlist.d.ts +27 -0
  4. package/dist/block-allowlist.js +21 -0
  5. package/dist/closest-scope.d.ts +51 -0
  6. package/dist/closest-scope.js +66 -0
  7. package/dist/editor-tools/apply.d.ts +85 -0
  8. package/dist/editor-tools/apply.js +544 -0
  9. package/dist/editor-tools/args.d.ts +153 -0
  10. package/dist/editor-tools/args.js +145 -0
  11. package/dist/editor-tools/coerce.d.ts +43 -0
  12. package/dist/editor-tools/coerce.js +200 -0
  13. package/dist/editor-tools/digest.d.ts +75 -0
  14. package/dist/editor-tools/digest.js +99 -0
  15. package/dist/editor-tools/index.d.ts +21 -0
  16. package/dist/editor-tools/index.js +24 -0
  17. package/dist/editor-tools/recipes.d.ts +122 -0
  18. package/dist/editor-tools/recipes.js +192 -0
  19. package/dist/fonts.d.ts +9 -0
  20. package/dist/fonts.js +11 -0
  21. package/dist/google-fonts.generated.d.ts +7 -0
  22. package/dist/google-fonts.generated.js +753 -0
  23. package/dist/index.d.ts +14 -5
  24. package/dist/index.js +14 -5
  25. package/dist/link-url.d.ts +9 -0
  26. package/dist/link-url.js +33 -0
  27. package/dist/metafield-binding.d.ts +89 -9
  28. package/dist/metafield-binding.js +254 -43
  29. package/dist/ops.d.ts +126 -0
  30. package/dist/ops.js +453 -0
  31. package/dist/richtext.d.ts +23 -0
  32. package/dist/richtext.js +115 -1
  33. package/dist/schemas.d.ts +325 -0
  34. package/dist/schemas.js +57 -7
  35. package/dist/section-scope.d.ts +17 -0
  36. package/dist/section-scope.js +22 -0
  37. package/dist/store-path.d.ts +9 -0
  38. package/dist/store-path.js +13 -1
  39. package/dist/template-limits.d.ts +34 -0
  40. package/dist/template-limits.js +71 -0
  41. package/dist/video-url.d.ts +10 -0
  42. package/dist/video-url.js +36 -0
  43. package/package.json +8 -1
@@ -0,0 +1,18 @@
1
+ import { AiEntry, LoadedRecipe } from "./editor-tools/recipes.js";
2
+ //#region src/ai-files.d.ts
3
+ /** Every entry `aiManifest` judges: the top level, and `recipes/` when it is
4
+ * a directory. Nothing deeper is listed: a subdir is refused as itself. */
5
+ export declare function listAiDir(dir: string): Promise<AiEntry[]>;
6
+ export interface ThemeAiFiles {
7
+ guide: string;
8
+ recipes: LoadedRecipe[];
9
+ /** Every path left out, and why: refused by the rule, or a recipe that didn't parse. */
10
+ skipped: {
11
+ path: string;
12
+ reason: string;
13
+ }[];
14
+ /** The bytes read, guide and recipes. */
15
+ bytes: number;
16
+ }
17
+ export declare function readThemeAiDir(dir: string): Promise<ThemeAiFiles>;
18
+ //#endregion
@@ -0,0 +1,47 @@
1
+ import { aiManifest, loadRecipes } from "./editor-tools/recipes.js";
2
+ import { readFile, readdir, stat } from "node:fs/promises";
3
+ import path from "node:path";
4
+ //#region src/ai-files.ts
5
+ async function entriesOf(dir, prefix) {
6
+ const dirents = await readdir(dir, { withFileTypes: true }).catch((err) => {
7
+ if (err.code === "ENOENT") return [];
8
+ throw err;
9
+ });
10
+ const entries = [];
11
+ for (const d of dirents) {
12
+ const rel = prefix + d.name;
13
+ const kind = d.isFile() ? "file" : d.isDirectory() ? "dir" : "other";
14
+ const bytes = kind === "file" ? (await stat(path.join(dir, d.name))).size : 0;
15
+ entries.push({
16
+ path: rel,
17
+ kind,
18
+ bytes
19
+ });
20
+ }
21
+ return entries;
22
+ }
23
+ /** Every entry `aiManifest` judges: the top level, and `recipes/` when it is
24
+ * a directory. Nothing deeper is listed: a subdir is refused as itself. */
25
+ async function listAiDir(dir) {
26
+ const top = await entriesOf(dir, "");
27
+ const recipes = top.some((e) => e.path === "recipes" && e.kind === "dir") ? await entriesOf(path.join(dir, "recipes"), "recipes/") : [];
28
+ return [...top, ...recipes];
29
+ }
30
+ async function readThemeAiDir(dir) {
31
+ const manifest = aiManifest(await listAiDir(dir));
32
+ const read = (rel) => readFile(path.join(dir, rel), "utf-8");
33
+ const [guide, bodies] = await Promise.all([manifest.guide ? read(manifest.guide.path) : "", Promise.all(manifest.recipes.map((e) => read(e.path)))]);
34
+ const files = Object.fromEntries(manifest.recipes.map((e, i) => [e.path.slice(8), bodies[i]]));
35
+ const loaded = loadRecipes(files);
36
+ return {
37
+ guide,
38
+ recipes: loaded.recipes,
39
+ skipped: [...manifest.refused, ...loaded.skipped.map(({ name, reason }) => ({
40
+ path: `recipes/${name}`,
41
+ reason: `recipes/${reason}`
42
+ }))],
43
+ bytes: Buffer.byteLength(guide) + bodies.reduce((n, b) => n + Buffer.byteLength(b), 0)
44
+ };
45
+ }
46
+ //#endregion
47
+ export { listAiDir, readThemeAiDir };
@@ -0,0 +1,27 @@
1
+ //#region src/block-allowlist.d.ts
2
+ type ContainerSchema = {
3
+ blocks?: {
4
+ type: string;
5
+ static?: boolean;
6
+ }[];
7
+ } | undefined;
8
+ /**
9
+ * The block types that may be inserted into a container: its schema's
10
+ * non-static `blocks`, without the `@app` wildcard. A container that lists
11
+ * `@app` at `depth` 0 (a section) or 1 (a block directly in a section) also
12
+ * takes every installed app block (the editor-loader serves those as
13
+ * `source:"app"` block schemas); deeper, `@app` means nothing, as publish lets
14
+ * no app block stand there.
15
+ *
16
+ * Every insert path (picker, paste, AI, the editor tools) goes through this,
17
+ * so none can offer a type publish would drop or the storefront would fail to
18
+ * render.
19
+ */
20
+ export declare function allowedChildBlockTypes(containerSchema: ContainerSchema, blockSchemas: readonly {
21
+ type?: string;
22
+ source?: "app";
23
+ }[], { depth }: {
24
+ depth: number;
25
+ }): string[];
26
+ //#endregion
27
+ export {};
@@ -0,0 +1,21 @@
1
+ import { acceptsAppBlocks } from "./document.js";
2
+ //#region src/block-allowlist.ts
3
+ /**
4
+ * The block types that may be inserted into a container: its schema's
5
+ * non-static `blocks`, without the `@app` wildcard. A container that lists
6
+ * `@app` at `depth` 0 (a section) or 1 (a block directly in a section) also
7
+ * takes every installed app block (the editor-loader serves those as
8
+ * `source:"app"` block schemas); deeper, `@app` means nothing, as publish lets
9
+ * no app block stand there.
10
+ *
11
+ * Every insert path (picker, paste, AI, the editor tools) goes through this,
12
+ * so none can offer a type publish would drop or the storefront would fail to
13
+ * render.
14
+ */
15
+ function allowedChildBlockTypes(containerSchema, blockSchemas, { depth }) {
16
+ const types = (containerSchema?.blocks ?? []).filter((b) => !b.static && b.type !== "@app").map((b) => b.type);
17
+ if (depth > 1 || !containerSchema || !acceptsAppBlocks(containerSchema)) return types;
18
+ return [...types, ...blockSchemas.flatMap((b) => b.source === "app" && b.type ? [b.type] : [])];
19
+ }
20
+ //#endregion
21
+ export { allowedChildBlockTypes };
@@ -0,0 +1,51 @@
1
+ import { ClosestResource, Provides } from "./schemas.js";
2
+ //#region src/closest-scope.d.ts
3
+ /** A node's providers: single ones inferred from its pickers, list ones
4
+ * declared under `provides`. `each` absent ⇒ single. */
5
+ export type Providers = Partial<Record<ClosestResource, {
6
+ from: string;
7
+ each?: readonly string[];
8
+ }>>;
9
+ /** One link of a root-first node chain: the node's providers and the id it
10
+ * hangs under in its parent (a section's instance id, a block's id). The
11
+ * root link stands for the route — its providers name the route resources
12
+ * the page has — and its key is never read. */
13
+ export interface ScopeLink {
14
+ providers: Providers;
15
+ key: string;
16
+ }
17
+ export interface ClosestScope {
18
+ /** Index into the chain of the providing link; 0 is the route. */
19
+ index: number;
20
+ decl: {
21
+ from: string;
22
+ each?: readonly string[];
23
+ };
24
+ }
25
+ export declare const PICKER_RESOURCES: Readonly<Record<string, ClosestResource>>;
26
+ /** The resources a schema provides: the FIRST picker of each resource type in
27
+ * schema order (Shopify's rule — `closest.product` is the nearest `product`
28
+ * setting), overridden per resource by an explicit `provides` entry. A second
29
+ * picker of the same type never provides; `provides` is how a schema says
30
+ * which one does. */
31
+ export declare function providersOf(schema: {
32
+ settings?: ReadonlyArray<{
33
+ type?: string;
34
+ id?: string;
35
+ }>;
36
+ provides?: Provides;
37
+ } | undefined): Providers;
38
+ /** The nearest link that provides `resource` to the LAST link of `chain`,
39
+ * or null when none does (the binding keeps its literal; a token blanks).
40
+ *
41
+ * - A single provider (a picker) scopes itself and every descendant.
42
+ * - An `each` provider scopes only the subtrees rooted at the block ids it
43
+ * names — never itself, never its other children.
44
+ * - The nearest provider wins per resource, even when its picker is unset:
45
+ * an empty `featured-product` shows literals rather than borrowing the
46
+ * route product from further up.
47
+ * - A mount carries ONE item id (the nearest `each`'s), so an `each` scope
48
+ * for one resource that sits outside a nearer `each` for another cannot
49
+ * be resolved per item: null, not the outer list's items. */
50
+ export declare function closestScopeAt(chain: readonly ScopeLink[], resource: ClosestResource): ClosestScope | null;
51
+ //#endregion
@@ -0,0 +1,66 @@
1
+ //#region src/closest-scope.ts
2
+ const PICKER_RESOURCES = {
3
+ product: "product",
4
+ collection: "collection",
5
+ page: "page",
6
+ blog: "blog",
7
+ article: "article"
8
+ };
9
+ /** The resources a schema provides: the FIRST picker of each resource type in
10
+ * schema order (Shopify's rule — `closest.product` is the nearest `product`
11
+ * setting), overridden per resource by an explicit `provides` entry. A second
12
+ * picker of the same type never provides; `provides` is how a schema says
13
+ * which one does. */
14
+ function providersOf(schema) {
15
+ const out = {};
16
+ for (const setting of schema?.settings ?? []) {
17
+ const resource = setting.type ? PICKER_RESOURCES[setting.type] : void 0;
18
+ if (resource && setting.id && !out[resource]) out[resource] = { from: setting.id };
19
+ }
20
+ for (const [resource, decl] of Object.entries(schema?.provides ?? {})) out[resource] = decl;
21
+ return out;
22
+ }
23
+ /** True when `link` (at chain index `i`) scopes the next link through an
24
+ * `each` declaration for some resource — i.e. the next link is one of the
25
+ * per-item subtrees the list renders. */
26
+ function eachChildAt(chain, i) {
27
+ const next = chain[i + 1];
28
+ if (!next) return false;
29
+ for (const decl of Object.values(chain[i].providers)) if (decl?.each?.includes(next.key)) return true;
30
+ return false;
31
+ }
32
+ /** The nearest link that provides `resource` to the LAST link of `chain`,
33
+ * or null when none does (the binding keeps its literal; a token blanks).
34
+ *
35
+ * - A single provider (a picker) scopes itself and every descendant.
36
+ * - An `each` provider scopes only the subtrees rooted at the block ids it
37
+ * names — never itself, never its other children.
38
+ * - The nearest provider wins per resource, even when its picker is unset:
39
+ * an empty `featured-product` shows literals rather than borrowing the
40
+ * route product from further up.
41
+ * - A mount carries ONE item id (the nearest `each`'s), so an `each` scope
42
+ * for one resource that sits outside a nearer `each` for another cannot
43
+ * be resolved per item: null, not the outer list's items. */
44
+ function closestScopeAt(chain, resource) {
45
+ const last = chain.length - 1;
46
+ let nearestEach = -1;
47
+ for (let i = last - 1; i >= 0; i--) if (eachChildAt(chain, i)) {
48
+ nearestEach = i;
49
+ break;
50
+ }
51
+ for (let i = last; i >= 0; i--) {
52
+ const decl = chain[i].providers[resource];
53
+ if (!decl) continue;
54
+ if (!decl.each) return {
55
+ index: i,
56
+ decl
57
+ };
58
+ if (i < last && decl.each.includes(chain[i + 1].key)) return i === nearestEach ? {
59
+ index: i,
60
+ decl
61
+ } : null;
62
+ }
63
+ return null;
64
+ }
65
+ //#endregion
66
+ export { PICKER_RESOURCES, closestScopeAt, providersOf };
@@ -0,0 +1,85 @@
1
+ import { BlockDefinition, SchemaSetting, TemplateSection } from "../schemas.js";
2
+ import { Op, ThemeDocument } from "../ops.js";
3
+ import { IdPolicy } from "./coerce.js";
4
+ import { EditorScope } from "./digest.js";
5
+ import { LoadedRecipe, Recipe } from "./recipes.js";
6
+ //#region src/editor-tools/apply.d.ts
7
+ type Placement = {
8
+ enabled_on?: {
9
+ groups?: string[];
10
+ templates?: string[];
11
+ };
12
+ disabled_on?: {
13
+ groups?: string[];
14
+ templates?: string[];
15
+ };
16
+ };
17
+ type AppFields = {
18
+ source?: "app";
19
+ appId?: string;
20
+ tag?: string;
21
+ scriptUrl?: string;
22
+ };
23
+ type Preset = {
24
+ settings?: Record<string, unknown>;
25
+ blocks?: BlockDefinition[];
26
+ };
27
+ export type EditorBlockSchema = AppFields & {
28
+ type: string;
29
+ name?: string;
30
+ settings?: SchemaSetting[];
31
+ blocks?: BlockDefinition[];
32
+ max_blocks?: number;
33
+ presets?: Preset[];
34
+ };
35
+ export type EditorSectionSchema = EditorBlockSchema & Placement;
36
+ export interface EditorSchemas {
37
+ /** `config/settings-schema.json`, t: resolved. */
38
+ settingsSchema: readonly {
39
+ settings?: SchemaSetting[];
40
+ }[];
41
+ /** Theme and app sections, as the editor-loader lists them. */
42
+ sections: EditorSectionSchema[];
43
+ blocks: EditorBlockSchema[];
44
+ }
45
+ export interface EngineContext {
46
+ doc: ThemeDocument;
47
+ schemas: EditorSchemas;
48
+ recipes: LoadedRecipe[];
49
+ /** `config/ai/guide.md`; empty when the theme ships none. */
50
+ guide?: string;
51
+ sanitizeRichText: (html: string) => string;
52
+ images: IdPolicy;
53
+ fonts?: IdPolicy;
54
+ /** Mints a new instance id; `${type}_${8 hex}` when absent, as the editor does. */
55
+ idFor?: (type: string) => string;
56
+ }
57
+ export type EditorToolResult = {
58
+ ok: true;
59
+ ops: Op[];
60
+ result: Record<string, unknown>;
61
+ } | {
62
+ ok: false;
63
+ reason: string;
64
+ };
65
+ /**
66
+ * A recipe's sections, built and coerced as `add_section` and
67
+ * `update_*_setting` would build and coerce them, keyed by their new ids in
68
+ * recipe order. Refuses the whole recipe at the first bad section, block,
69
+ * setting or param.
70
+ */
71
+ export declare function expandRecipe(recipe: Recipe, params: Record<string, string | number>, ctx: EngineContext, scope?: EditorScope): {
72
+ ok: true;
73
+ sections: [string, TemplateSection][];
74
+ } | {
75
+ ok: false;
76
+ reason: string;
77
+ };
78
+ /**
79
+ * Runs one editor tool against a document: the ops that make the edit (what
80
+ * the admin would have sent for it, via `diffDocument`) and the result the
81
+ * model sees, or a refusal. Pure: no I/O, and `ctx.doc` is never mutated.
82
+ */
83
+ export declare function applyEditorTool(name: string, args: unknown, ctx: EngineContext): EditorToolResult;
84
+ //#endregion
85
+ export {};