@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.
- package/dist/ai-files.d.ts +18 -0
- package/dist/ai-files.js +47 -0
- package/dist/block-allowlist.d.ts +27 -0
- package/dist/block-allowlist.js +21 -0
- package/dist/closest-scope.d.ts +51 -0
- package/dist/closest-scope.js +66 -0
- package/dist/editor-tools/apply.d.ts +85 -0
- package/dist/editor-tools/apply.js +544 -0
- package/dist/editor-tools/args.d.ts +153 -0
- package/dist/editor-tools/args.js +145 -0
- package/dist/editor-tools/coerce.d.ts +43 -0
- package/dist/editor-tools/coerce.js +200 -0
- package/dist/editor-tools/digest.d.ts +75 -0
- package/dist/editor-tools/digest.js +99 -0
- package/dist/editor-tools/index.d.ts +21 -0
- package/dist/editor-tools/index.js +24 -0
- package/dist/editor-tools/recipes.d.ts +122 -0
- package/dist/editor-tools/recipes.js +192 -0
- package/dist/fonts.d.ts +9 -0
- package/dist/fonts.js +11 -0
- package/dist/google-fonts.generated.d.ts +7 -0
- package/dist/google-fonts.generated.js +753 -0
- package/dist/index.d.ts +14 -5
- package/dist/index.js +14 -5
- package/dist/link-url.d.ts +9 -0
- package/dist/link-url.js +33 -0
- package/dist/metafield-binding.d.ts +89 -9
- package/dist/metafield-binding.js +254 -43
- package/dist/ops.d.ts +126 -0
- package/dist/ops.js +453 -0
- package/dist/richtext.d.ts +23 -0
- package/dist/richtext.js +115 -1
- package/dist/schemas.d.ts +325 -0
- package/dist/schemas.js +57 -7
- package/dist/section-scope.d.ts +17 -0
- package/dist/section-scope.js +22 -0
- package/dist/store-path.d.ts +9 -0
- package/dist/store-path.js +13 -1
- package/dist/template-limits.d.ts +34 -0
- package/dist/template-limits.js +71 -0
- package/dist/video-url.d.ts +10 -0
- package/dist/video-url.js +36 -0
- 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
|
package/dist/ai-files.js
ADDED
|
@@ -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 {};
|