@usequeek/theme-check 0.3.6 → 0.4.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/context.js +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +4 -1
- package/dist/rules/static.d.ts +10 -1
- package/dist/rules/static.js +145 -24
- package/dist/types.d.ts +21 -5
- package/dist/utils/business-vocabulary.json +1 -1
- package/dist/utils/theme-designs.d.ts +142 -0
- package/dist/utils/theme-designs.js +183 -0
- package/package.json +5 -1
package/dist/context.js
CHANGED
|
@@ -68,6 +68,7 @@ export async function loadContext(themeDir, env = {}) {
|
|
|
68
68
|
retired: config?.active === false,
|
|
69
69
|
demo: demos.find((store) => store.id === 'default')?.data ?? null,
|
|
70
70
|
demos,
|
|
71
|
+
themeConfig: config,
|
|
71
72
|
declaredDemos: config === null ? null : Array.isArray(declared) ? declared : [],
|
|
72
73
|
defaultDescription: typeof primary?.description === 'string' ? primary.description : null,
|
|
73
74
|
defaultFor: primary?.for ?? null,
|
package/dist/index.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ export type { CheckEnv, Finding, Rule, Severity, ThemeContext, DemoStore, Declar
|
|
|
10
10
|
export { BUSINESS_KEYS, SERVICE_SLUGS, CATALOGUE, SUBCATEGORIES, isBusinessKey, businessRoot } from './utils/business-vocabulary.js';
|
|
11
11
|
export { TEMPLATE_COPY_PLACES, copyViolations, isTestimonialSection, storeNameForms } from './utils/template-copy.js';
|
|
12
12
|
export { PRIMARY_DEMO_ID, DEMO_ID_FORMAT, demoFilesOf } from './utils/theme-demos.js';
|
|
13
|
+
export { designsOf, groupTemplates, mainTemplateKey, composeLabel, type DesignDeclaration, type ThemeDesignsConfig, type ThemeDesign, type ThemeTemplate, } from './utils/theme-designs.js';
|
|
13
14
|
export { TEMPLATE_DESCRIPTION_MAX, screenshotFile, sectionStyle, sectionCopy, declaredFieldsByVariant } from './utils/theme-templates.js';
|
|
14
|
-
export { STATIC_RULES, moduleContractRule, structureRule, demoStoreRule, demoStoresRule, demoArtRule, codeQualityRule, sdkBoundaryRule, selectionMetadataRule, demoCompletenessRule, subscribeScopeRule, demoBlockTypesRule, identityRule, productMetafieldsRule, poweredByRule, fontsSelfHostedRule, templateDescriptionRule, templateScreenshotRule, templateChromeRule, templateStyleRule, templateBusinessRule, templateVersionsRule, templatePagesRule, templateCopyRule, vendorFactsRule, placeholderContentRule, frameworkImport, STARTER_PLACEHOLDER_IMAGES, } from './rules/static.js';
|
|
15
|
+
export { STATIC_RULES, moduleContractRule, structureRule, demoStoreRule, demoStoresRule, demoArtRule, codeQualityRule, sdkBoundaryRule, selectionMetadataRule, demoCompletenessRule, subscribeScopeRule, demoBlockTypesRule, identityRule, productMetafieldsRule, poweredByRule, fontsSelfHostedRule, templateDescriptionRule, templateScreenshotRule, templateChromeRule, templateStyleRule, templateBusinessRule, templateVersionsRule, templateDesignsRule, TEMPLATE_DESIGNS_MAX, templatePagesRule, templateCopyRule, vendorFactsRule, placeholderContentRule, frameworkImport, STARTER_PLACEHOLDER_IMAGES, } from './rules/static.js';
|
|
15
16
|
export { ANALYSIS_RULES, variantParityRule, fieldParityRule, designTokensRule } from './rules/analysis.js';
|
package/dist/index.js
CHANGED
|
@@ -9,9 +9,12 @@ export { formatJson, formatGithubActions, formatStylish, summarize, levelOf, fil
|
|
|
9
9
|
export { BUSINESS_KEYS, SERVICE_SLUGS, CATALOGUE, SUBCATEGORIES, isBusinessKey, businessRoot } from './utils/business-vocabulary.js';
|
|
10
10
|
export { TEMPLATE_COPY_PLACES, copyViolations, isTestimonialSection, storeNameForms } from './utils/template-copy.js';
|
|
11
11
|
export { PRIMARY_DEMO_ID, DEMO_ID_FORMAT, demoFilesOf } from './utils/theme-demos.js';
|
|
12
|
+
// Theme → template → design (contract R2.8): the resolver the registry, the
|
|
13
|
+
// preview and these rules share. Also published alone as `@usequeek/theme-check/designs`.
|
|
14
|
+
export { designsOf, groupTemplates, mainTemplateKey, composeLabel, } from './utils/theme-designs.js';
|
|
12
15
|
export { TEMPLATE_DESCRIPTION_MAX, screenshotFile, sectionStyle, sectionCopy, declaredFieldsByVariant } from './utils/theme-templates.js';
|
|
13
16
|
// Per-kind rule arrays and the individual rule constants, for rule-level unit
|
|
14
17
|
// tests that want to run one rule against a hand-built ThemeContext instead
|
|
15
18
|
// of a whole theme directory through checkTheme().
|
|
16
|
-
export { STATIC_RULES, moduleContractRule, structureRule, demoStoreRule, demoStoresRule, demoArtRule, codeQualityRule, sdkBoundaryRule, selectionMetadataRule, demoCompletenessRule, subscribeScopeRule, demoBlockTypesRule, identityRule, productMetafieldsRule, poweredByRule, fontsSelfHostedRule, templateDescriptionRule, templateScreenshotRule, templateChromeRule, templateStyleRule, templateBusinessRule, templateVersionsRule, templatePagesRule, templateCopyRule, vendorFactsRule, placeholderContentRule, frameworkImport, STARTER_PLACEHOLDER_IMAGES, } from './rules/static.js';
|
|
19
|
+
export { STATIC_RULES, moduleContractRule, structureRule, demoStoreRule, demoStoresRule, demoArtRule, codeQualityRule, sdkBoundaryRule, selectionMetadataRule, demoCompletenessRule, subscribeScopeRule, demoBlockTypesRule, identityRule, productMetafieldsRule, poweredByRule, fontsSelfHostedRule, templateDescriptionRule, templateScreenshotRule, templateChromeRule, templateStyleRule, templateBusinessRule, templateVersionsRule, templateDesignsRule, TEMPLATE_DESIGNS_MAX, templatePagesRule, templateCopyRule, vendorFactsRule, placeholderContentRule, frameworkImport, STARTER_PLACEHOLDER_IMAGES, } from './rules/static.js';
|
|
17
20
|
export { ANALYSIS_RULES, variantParityRule, fieldParityRule, designTokensRule } from './rules/analysis.js';
|
package/dist/rules/static.d.ts
CHANGED
|
@@ -41,7 +41,16 @@ export declare const templateChromeRule: Rule;
|
|
|
41
41
|
export declare const templateStyleRule: Rule;
|
|
42
42
|
export declare const templateBusinessRule: Rule;
|
|
43
43
|
export declare const templateVersionsRule: Rule;
|
|
44
|
-
/**
|
|
44
|
+
/** A template has at most this many designs (contract R2.2). */
|
|
45
|
+
export declare const TEMPLATE_DESIGNS_MAX = 3;
|
|
46
|
+
/**
|
|
47
|
+
* Theme → template → design (contract R2.8). Designs are grouped by the
|
|
48
|
+
* `template` key each one declares, exactly as the registry and the preview
|
|
49
|
+
* group them (utils/theme-designs.ts). The resolver is lenient, so a config
|
|
50
|
+
* written before R2.8 still previews; this rule is where it is strict.
|
|
51
|
+
*/
|
|
52
|
+
export declare const templateDesignsRule: Rule;
|
|
53
|
+
/** The designed pages every template ships (contract R2.3); page versions `-2`… count. */
|
|
45
54
|
export declare const TEMPLATE_PAGES: readonly ["about", "sales", "landing"];
|
|
46
55
|
export declare const templatePagesRule: Rule;
|
|
47
56
|
export declare const templateCopyRule: Rule;
|
package/dist/rules/static.js
CHANGED
|
@@ -2,6 +2,7 @@ import { join, relative } from 'node:path';
|
|
|
2
2
|
import { readdirSync, existsSync, readFileSync } from 'node:fs';
|
|
3
3
|
import { foreignImageRefs } from '../utils/theme-demo-images.js';
|
|
4
4
|
import { DEMO_ID_FORMAT, PRIMARY_DEMO_ID } from '../utils/theme-demos.js';
|
|
5
|
+
import { designsOf, groupTemplates, mainTemplateKey } from '../utils/theme-designs.js';
|
|
5
6
|
import { SCREENSHOT_LOCK, TEMPLATE_DESCRIPTION_MAX, TEMPLATE_DESCRIPTION_PLACEHOLDER, declaredFieldsByVariant, isPresentationalField, screenshotFile, screenshotUrl, sectionCopy } from '../utils/theme-templates.js';
|
|
6
7
|
import { copyViolations, isTestimonialSection } from '../utils/template-copy.js';
|
|
7
8
|
import { BUSINESS_KEYS, SERVICE_SLUGS, isBusinessKey } from '../utils/business-vocabulary.js';
|
|
@@ -66,6 +67,26 @@ function storesOf(context) {
|
|
|
66
67
|
stores.unshift({ id: PRIMARY_DEMO_ID, file: 'demo.json', data: null });
|
|
67
68
|
return stores;
|
|
68
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* theme.config.ts as the design resolver reads it (contract R2.8): its own
|
|
72
|
+
* fields, with `demos` as the context declares them. Never null, so a config
|
|
73
|
+
* that did not load resolves to demo.json alone.
|
|
74
|
+
*/
|
|
75
|
+
function designsConfig(context) {
|
|
76
|
+
const config = context.themeConfig !== null && typeof context.themeConfig === 'object' ? context.themeConfig : {};
|
|
77
|
+
return { ...config, demos: (context.declaredDemos ?? []) };
|
|
78
|
+
}
|
|
79
|
+
/** Every design, grouped by its explicit `template` exactly as the registry groups it, in declaration order. */
|
|
80
|
+
function designsOfContext(context) {
|
|
81
|
+
return designsOf(designsConfig(context));
|
|
82
|
+
}
|
|
83
|
+
/** A design's declaration as written: `default_demo`, or its (first) `demos[]` entry. */
|
|
84
|
+
function declarationOf(context, id) {
|
|
85
|
+
const config = designsConfig(context);
|
|
86
|
+
if (id === PRIMARY_DEMO_ID)
|
|
87
|
+
return config.default_demo ?? {};
|
|
88
|
+
return config.demos?.find((demo) => demo?.id === id) ?? {};
|
|
89
|
+
}
|
|
69
90
|
export const demoStoreRule = {
|
|
70
91
|
id: 'theme/demo-store',
|
|
71
92
|
summary: 'Every demo store is a believable store that identifies itself as this theme',
|
|
@@ -139,6 +160,9 @@ export const demoStoresRule = {
|
|
|
139
160
|
};
|
|
140
161
|
const declared = context.declaredDemos ?? [];
|
|
141
162
|
const secondaries = context.demos.filter((store) => store.id !== PRIMARY_DEMO_ID);
|
|
163
|
+
// `label` and `for` are the template's, declared once on its design 1; a
|
|
164
|
+
// later design inherits them (theme/template-designs checks a repeat).
|
|
165
|
+
const firsts = new Set(designsOfContext(context).filter((design) => design.designIndex === 1).map((design) => design.id));
|
|
142
166
|
for (const demo of declared) {
|
|
143
167
|
const id = String(demo?.id ?? '');
|
|
144
168
|
if (id === PRIMARY_DEMO_ID) {
|
|
@@ -152,6 +176,8 @@ export const demoStoresRule = {
|
|
|
152
176
|
if (!secondaries.some((store) => store.id === id)) {
|
|
153
177
|
add(at, `demos[] declares "${id}" but there is no file ${context.env.root}demos/${id}.json`, 'Add the store, or drop the declaration. The registry advertises a preview URL for every declared store.');
|
|
154
178
|
}
|
|
179
|
+
if (!firsts.has(id))
|
|
180
|
+
continue;
|
|
155
181
|
if (typeof demo.label !== 'string' || demo.label.trim() === '') {
|
|
156
182
|
add(at, `demos[] "${id}" has no label`, 'Give it the name a merchant sees, e.g. "Restaurant & takeaway".');
|
|
157
183
|
}
|
|
@@ -169,7 +195,7 @@ export const demoStoresRule = {
|
|
|
169
195
|
continue;
|
|
170
196
|
}
|
|
171
197
|
if (!declared.some((demo) => demo?.id === store.id)) {
|
|
172
|
-
add(`${context.env.root}${store.file}`, `demos/${store.id}.json is not declared in theme.config.ts`, `Add \`{ id: '${store.id}', label, for
|
|
198
|
+
add(`${context.env.root}${store.file}`, `demos/${store.id}.json is not declared in theme.config.ts`, `Add \`{ id: '${store.id}', template, label, for, description }\` to \`demos\` in theme.config.ts, or remove the file. An undeclared store is previewable but never offered to anyone.`);
|
|
173
199
|
}
|
|
174
200
|
}
|
|
175
201
|
const seen = new Map();
|
|
@@ -835,9 +861,12 @@ export const templateBusinessRule = {
|
|
|
835
861
|
if (context.retired || context.declaredDemos === null)
|
|
836
862
|
return [];
|
|
837
863
|
const config = `${context.env.root}theme.config.ts`;
|
|
864
|
+
// Each template once, by its design 1: `for` is the template's, and a later
|
|
865
|
+
// design that repeats it is theme/template-designs' to compare.
|
|
866
|
+
const firsts = new Set(designsOfContext(context).filter((design) => design.designIndex === 1).map((design) => design.id));
|
|
838
867
|
const templates = [
|
|
839
868
|
{ id: PRIMARY_DEMO_ID, keys: context.defaultFor, where: `${config} → default_demo.for` },
|
|
840
|
-
...context.declaredDemos.map((demo) => ({ id: demo.id, keys: demo.for, where: `${config} → demos[${demo.id}].for` })),
|
|
869
|
+
...context.declaredDemos.filter((demo) => firsts.has(demo.id)).map((demo) => ({ id: demo.id, keys: demo.for, where: `${config} → demos[${demo.id}].for` })),
|
|
841
870
|
];
|
|
842
871
|
const findings = [];
|
|
843
872
|
for (const { id, keys, where } of templates) {
|
|
@@ -880,14 +909,20 @@ export const templateBusinessRule = {
|
|
|
880
909
|
return findings;
|
|
881
910
|
},
|
|
882
911
|
};
|
|
883
|
-
/**
|
|
884
|
-
|
|
885
|
-
|
|
912
|
+
/**
|
|
913
|
+
* A page slug's kind: `about-2` → `about`; null for a slug with no `-N`
|
|
914
|
+
* suffix. Only page slugs are numbered (R2.3). Designs are grouped by their
|
|
915
|
+
* explicit `template` (R2.8), never by an id's suffix.
|
|
916
|
+
*/
|
|
917
|
+
function pageBase(slug) {
|
|
918
|
+
const match = /^(.+)-([2-9])$/.exec(slug);
|
|
886
919
|
return match ? match[1] : null;
|
|
887
920
|
}
|
|
921
|
+
// Keeps its id (`theme/template-versions` is API: tools and to-do lists name
|
|
922
|
+
// it); the R2.8 grouping checks are theme/template-designs'.
|
|
888
923
|
export const templateVersionsRule = {
|
|
889
924
|
id: 'theme/template-versions',
|
|
890
|
-
summary: 'Every
|
|
925
|
+
summary: 'Every design has its own home: no two list the same sections in the same order',
|
|
891
926
|
kind: 'static',
|
|
892
927
|
run(context) {
|
|
893
928
|
if (context.retired)
|
|
@@ -896,7 +931,7 @@ export const templateVersionsRule = {
|
|
|
896
931
|
const sequence = (store) => (pagesOf(store).home?.content ?? []).map((section) => `${section?.type}/${section?.variant ?? 'default'}`).join(' → ');
|
|
897
932
|
const seen = new Map();
|
|
898
933
|
// The single-page default theme draws one fixed layout whatever its home
|
|
899
|
-
// lists, so its
|
|
934
|
+
// lists, so its designs differ by business, not composition.
|
|
900
935
|
for (const store of context.pageBased ? context.demos : []) {
|
|
901
936
|
const home = sequence(store);
|
|
902
937
|
if (!home)
|
|
@@ -905,8 +940,8 @@ export const templateVersionsRule = {
|
|
|
905
940
|
if (first) {
|
|
906
941
|
findings.push(finding(context, 'theme/template-versions', 'reject', {
|
|
907
942
|
where: `${context.env.root}${store.file} → pages.home`,
|
|
908
|
-
found: `
|
|
909
|
-
fix: '
|
|
943
|
+
found: `design "${store.id}" has the same home sections, in the same order, as "${first}"`,
|
|
944
|
+
fix: 'Each design must be its own, not a recolour: change the order and the section variants (and the header, footer and palette where the theme allows). Vendors of one business are spread across its template\'s designs; two identical ones make that a coin toss between the same page.',
|
|
910
945
|
docs: `${context.env.docs}#templates`,
|
|
911
946
|
}));
|
|
912
947
|
}
|
|
@@ -914,23 +949,109 @@ export const templateVersionsRule = {
|
|
|
914
949
|
seen.set(home, store.id);
|
|
915
950
|
}
|
|
916
951
|
}
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
952
|
+
return findings;
|
|
953
|
+
},
|
|
954
|
+
};
|
|
955
|
+
/** A template has at most this many designs (contract R2.2). */
|
|
956
|
+
export const TEMPLATE_DESIGNS_MAX = 3;
|
|
957
|
+
/**
|
|
958
|
+
* Theme → template → design (contract R2.8). Designs are grouped by the
|
|
959
|
+
* `template` key each one declares, exactly as the registry and the preview
|
|
960
|
+
* group them (utils/theme-designs.ts). The resolver is lenient, so a config
|
|
961
|
+
* written before R2.8 still previews; this rule is where it is strict.
|
|
962
|
+
*/
|
|
963
|
+
export const templateDesignsRule = {
|
|
964
|
+
id: 'theme/template-designs',
|
|
965
|
+
summary: "Every design names its template; a template's designs share its label and business and are told apart by design_label",
|
|
966
|
+
kind: 'static',
|
|
967
|
+
run(context) {
|
|
968
|
+
if (context.retired || context.declaredDemos === null)
|
|
969
|
+
return [];
|
|
970
|
+
const findings = [];
|
|
971
|
+
const config = designsConfig(context);
|
|
972
|
+
const where = (id, field) => `${context.env.root}theme.config.ts → ${id === PRIMARY_DEMO_ID ? 'default_demo' : `demos[${id}]`}.${field}`;
|
|
973
|
+
const add = (at, found, fix) => {
|
|
974
|
+
findings.push(finding(context, 'theme/template-designs', 'reject', { where: at, found, fix, docs: `${context.env.docs}#templates` }));
|
|
975
|
+
};
|
|
976
|
+
const designs = designsOfContext(context);
|
|
977
|
+
// Every design names its template, by a key that can be one.
|
|
978
|
+
for (const { id } of designs) {
|
|
979
|
+
const key = declarationOf(context, id).template;
|
|
980
|
+
if (key === undefined || key === null || key === '') {
|
|
981
|
+
if (id === PRIMARY_DEMO_ID) {
|
|
982
|
+
add(where(id, 'template'), 'the main template has no key (default_demo.template)', "Add `template` to `default_demo`: a lowercase slug naming its business (medley's main template is `beauty`). On a theme with two or more templates the main store moves to `/<slug>~<key>`, and `/<slug>` becomes the template gallery.");
|
|
983
|
+
}
|
|
984
|
+
else {
|
|
985
|
+
add(where(id, 'template'), `design "${id}" names no template`, `Add \`template\` to it: \`template: '${id}'\` when it is the first design of its business, or the key of the template it is another design of. Designs are grouped by this key, never by an id's \`-2\`.`);
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
else if (typeof key !== 'string' || !DEMO_ID_FORMAT.test(key)) {
|
|
989
|
+
add(where(id, 'template'), `template key "${String(key)}" is not a slug`, 'Use lowercase letters, digits and single hyphens: the key is a URL segment (`/<slug>~<key>`). Never rename a key once shipped; old links redirect by it.');
|
|
990
|
+
}
|
|
991
|
+
else if (key === PRIMARY_DEMO_ID) {
|
|
992
|
+
add(where(id, 'template'), `template key "${key}" is reserved`, '"default" is demo.json\'s design id, never a template key. Name the business: `beauty`, `food`, `laundry`.');
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
// Template keys and design ids share one namespace: `/<slug>~<segment>` opens exactly one store.
|
|
996
|
+
const main = mainTemplateKey(config);
|
|
997
|
+
if (main !== null && designs.some((design) => design.id === main)) {
|
|
998
|
+
add(where(PRIMARY_DEMO_ID, 'template'), `the main template's key "${main}" is also a design id`, `\`/<slug>~${main}\` must open one store. Give the main template another key; design ids are never renamed once shipped, keys neither, so settle it before the theme ships.`);
|
|
999
|
+
}
|
|
1000
|
+
for (const template of groupTemplates(designs)) {
|
|
1001
|
+
const { key } = template;
|
|
1002
|
+
if (key === null)
|
|
1003
|
+
continue; // a main template with no key: reported above
|
|
1004
|
+
const count = template.designs.length;
|
|
1005
|
+
const [first, ...later] = template.designs;
|
|
1006
|
+
if (first.id !== PRIMARY_DEMO_ID && !template.designs.some((design) => design.id === key)) {
|
|
1007
|
+
add(where(first.id, 'template'), `template "${key}" has no design 1 (a design whose id is "${key}")`, `Design 1 of a template is the design whose id is its key, so \`/<slug>~${key}\` opens the template. Declare \`{ id: '${key}', template: '${key}', label, for, description }\` with demos/${key}.json, or give "${first.id}" its own id as its \`template\`.`);
|
|
1008
|
+
}
|
|
1009
|
+
if (count > TEMPLATE_DESIGNS_MAX) {
|
|
1010
|
+
add(where(template.designs[TEMPLATE_DESIGNS_MAX].id, 'template'), `template "${key}" has ${count} designs; a template has at most ${TEMPLATE_DESIGNS_MAX}`, `Keep ${TEMPLATE_DESIGNS_MAX} designs of "${key}" (contract R2.2): drop the rest, or make one a template of another business.`);
|
|
1011
|
+
}
|
|
1012
|
+
// `label` and `for` are the template's, declared once on design 1. A later
|
|
1013
|
+
// design may repeat them unchanged, or leave them out and inherit them.
|
|
1014
|
+
for (const design of later) {
|
|
1015
|
+
const declared = declarationOf(context, design.id);
|
|
1016
|
+
const label = typeof declared.label === 'string' ? declared.label.trim() : '';
|
|
1017
|
+
if (label !== '' && label !== template.label) {
|
|
1018
|
+
add(where(design.id, 'label'), `design "${design.id}" relabels its template ("${label}"); a design is named by design_label`, `\`label\` is the template's ("${template.label}"), declared once on "${first.id}". Drop \`label\` from "${design.id}" (it inherits it), and say what tells this design apart in \`design_label\`.`);
|
|
1019
|
+
}
|
|
1020
|
+
if (declared.for !== undefined && JSON.stringify(declared.for) !== JSON.stringify(template.for)) {
|
|
1021
|
+
add(where(design.id, 'for'), `design "${design.id}" is for ${JSON.stringify(declared.for)}, its template "${key}" for ${JSON.stringify(template.for)}`, `Every design of a template is for the same business: drop \`for\` from "${design.id}" (it inherits "${first.id}"'s), or make it exactly ${JSON.stringify(template.for)}. The backend spreads a business's vendors across its template's designs.`);
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
if (count > 1) {
|
|
1025
|
+
const named = new Map();
|
|
1026
|
+
for (const design of template.designs) {
|
|
1027
|
+
if (design.designLabel === null) {
|
|
1028
|
+
add(where(design.id, 'design_label'), `design "${design.id}" has no design_label (template "${key}" has ${count} designs)`, `Name what tells "${design.id}" from the template's other designs ("Dining room", "Neighbourhood buka"). The registry, the gallery and the Designs dropdown show it.`);
|
|
1029
|
+
continue;
|
|
1030
|
+
}
|
|
1031
|
+
const other = named.get(design.designLabel.toLowerCase());
|
|
1032
|
+
if (other) {
|
|
1033
|
+
add(where(design.id, 'design_label'), `designs "${other}" and "${design.id}" share the design_label "${design.designLabel}"`, 'Give each design of a template its own `design_label`: it is how a merchant tells them apart.');
|
|
1034
|
+
}
|
|
1035
|
+
else {
|
|
1036
|
+
named.set(design.designLabel.toLowerCase(), design.id);
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
// A design_label is shown on every store built from the design (R2.6).
|
|
1042
|
+
for (const design of designs) {
|
|
1043
|
+
if (design.designLabel === null)
|
|
922
1044
|
continue;
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
}));
|
|
1045
|
+
const name = context.demos.find((store) => store.id === design.id)?.data?.profile?.name;
|
|
1046
|
+
const why = copyViolations(design.designLabel, typeof name === 'string' ? name : null);
|
|
1047
|
+
if (why.length === 0)
|
|
1048
|
+
continue;
|
|
1049
|
+
add(where(design.id, 'design_label'), `design "${design.id}" design_label: ${why.join('; ')}`, 'Write it true of any store in the business: no store name, place, naira amount, promise or date ("Neighbourhood buka", never "Lekki buka" or "Mama Tee\'s buka").');
|
|
929
1050
|
}
|
|
930
1051
|
return findings;
|
|
931
1052
|
},
|
|
932
1053
|
};
|
|
933
|
-
/** The designed pages every template ships (contract R2.3); versions `-2`… count. */
|
|
1054
|
+
/** The designed pages every template ships (contract R2.3); page versions `-2`… count. */
|
|
934
1055
|
export const TEMPLATE_PAGES = ['about', 'sales', 'landing'];
|
|
935
1056
|
export const templatePagesRule = {
|
|
936
1057
|
id: 'theme/template-pages',
|
|
@@ -945,7 +1066,7 @@ export const templatePagesRule = {
|
|
|
945
1066
|
const pages = pagesOf(store);
|
|
946
1067
|
const where = `${context.env.root}${store.file} → pages`;
|
|
947
1068
|
for (const slug of TEMPLATE_PAGES) {
|
|
948
|
-
if (Object.keys(pages).some((key) => key === slug ||
|
|
1069
|
+
if (Object.keys(pages).some((key) => key === slug || pageBase(key) === slug))
|
|
949
1070
|
continue;
|
|
950
1071
|
findings.push(finding(context, 'theme/template-pages', 'reject', {
|
|
951
1072
|
where,
|
|
@@ -957,7 +1078,7 @@ export const templatePagesRule = {
|
|
|
957
1078
|
}));
|
|
958
1079
|
}
|
|
959
1080
|
for (const [key, page] of Object.entries(pages)) {
|
|
960
|
-
const kind = TEMPLATE_PAGES.find((slug) => slug !== 'about' && (key === slug ||
|
|
1081
|
+
const kind = TEMPLATE_PAGES.find((slug) => slug !== 'about' && (key === slug || pageBase(key) === slug));
|
|
961
1082
|
if (!kind)
|
|
962
1083
|
continue;
|
|
963
1084
|
const content = page?.content ?? [];
|
|
@@ -1242,5 +1363,5 @@ export const placeholderContentRule = {
|
|
|
1242
1363
|
};
|
|
1243
1364
|
export const STATIC_RULES = [moduleContractRule, structureRule, demoStoreRule, demoStoresRule, demoArtRule, codeQualityRule, sdkBoundaryRule, selectionMetadataRule, demoCompletenessRule, subscribeScopeRule, demoBlockTypesRule, identityRule, productMetafieldsRule, poweredByRule, fontsSelfHostedRule,
|
|
1244
1365
|
templateDescriptionRule, templateScreenshotRule, templateChromeRule, templateStyleRule,
|
|
1245
|
-
templateBusinessRule, templateVersionsRule, templatePagesRule, templateCopyRule, vendorFactsRule, placeholderContentRule,
|
|
1366
|
+
templateBusinessRule, templateVersionsRule, templateDesignsRule, templatePagesRule, templateCopyRule, vendorFactsRule, placeholderContentRule,
|
|
1246
1367
|
];
|
package/dist/types.d.ts
CHANGED
|
@@ -64,13 +64,23 @@ export interface DemoStore {
|
|
|
64
64
|
/** Null when the file is missing or not valid JSON. */
|
|
65
65
|
data: Record<string, unknown> | null;
|
|
66
66
|
}
|
|
67
|
-
/**
|
|
67
|
+
/**
|
|
68
|
+
* A `demos[]` entry in theme.config.ts: one design (contract R2.8). A theme is
|
|
69
|
+
* the look, a template is a business it is dressed as, and a design is one
|
|
70
|
+
* concrete store of a template — one demo file.
|
|
71
|
+
*/
|
|
68
72
|
export interface DeclaredDemo {
|
|
73
|
+
/** The design id: demos/<id>.json. Never renamed once shipped. */
|
|
69
74
|
id: string;
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
75
|
+
/** The key of the template this design belongs to: `food`. Explicit on every design. */
|
|
76
|
+
template?: string;
|
|
77
|
+
/** The template's label, the business as a merchant sees it. Declared on the template's design 1; a later design inherits it. */
|
|
78
|
+
label?: string;
|
|
79
|
+
/** What tells this design from its template's others: `Neighbourhood buka`. Required when a template has 2+ designs. */
|
|
80
|
+
design_label?: string;
|
|
81
|
+
/** The template's business slugs (the vendor's service_slug/service_type vocabulary). Declared on design 1; a later design inherits them. */
|
|
82
|
+
for?: string[];
|
|
83
|
+
/** What an AI reads to choose this design for a merchant (≤ 300 chars). */
|
|
74
84
|
description?: string;
|
|
75
85
|
}
|
|
76
86
|
/**
|
|
@@ -106,6 +116,12 @@ export interface ThemeContext {
|
|
|
106
116
|
demo: Record<string, unknown> | null;
|
|
107
117
|
/** Every store on disk, the primary first. */
|
|
108
118
|
demos: DemoStore[];
|
|
119
|
+
/**
|
|
120
|
+
* theme.config.ts's default export, as loaded; null when it could not be
|
|
121
|
+
* loaded. The template and design rules read it through the design resolver
|
|
122
|
+
* (`designsOf`), so they group designs exactly as the registry does.
|
|
123
|
+
*/
|
|
124
|
+
themeConfig: unknown;
|
|
109
125
|
/** `demos` from theme.config.ts; null when the config could not be loaded. */
|
|
110
126
|
declaredDemos: DeclaredDemo[] | null;
|
|
111
127
|
/** `default_demo.description` from theme.config.ts — the primary template's description. */
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"foods", "local-meals", "shawarma-pizza-snacks", "suya", "boli", "fruits-fresh-natural",
|
|
5
5
|
"meat-fish", "supermarket", "groceries", "shop-groceries", "local-market", "pharmacy",
|
|
6
6
|
"health-wellness-store", "fashion", "beauty-cosmetics", "electronics", "phones-accessories",
|
|
7
|
-
"home-living", "baby-kids", "office-school", "shop", "
|
|
7
|
+
"home-living", "baby-kids", "office-school", "shop", "laundry", "gas-refill", "home-cleaning",
|
|
8
8
|
"car-wash"
|
|
9
9
|
],
|
|
10
10
|
"catalogue": {
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme → template → design (queek_backend contract R2.8).
|
|
3
|
+
*
|
|
4
|
+
* A theme is the look. A template is a business the theme is dressed as
|
|
5
|
+
* (medley's Food), and a design is one concrete store of it, one demo file:
|
|
6
|
+
* `food` and `food-2` are the Food template's two designs. theme.config.ts
|
|
7
|
+
* declares every design. `default_demo` is the main template's first design
|
|
8
|
+
* (demo.json, id `default`); each `demos[]` entry is one more (demos/<id>.json).
|
|
9
|
+
*
|
|
10
|
+
* default_demo: { template: 'beauty', label: 'Skincare & make-up', design_label: 'Photo collage', for: [...], description },
|
|
11
|
+
* demos: [
|
|
12
|
+
* { id: 'food', template: 'food', label: 'Restaurant & kitchen', design_label: 'Dining room', for: [...], description },
|
|
13
|
+
* { id: 'food-2', template: 'food', design_label: 'Neighbourhood buka', description },
|
|
14
|
+
* ],
|
|
15
|
+
*
|
|
16
|
+
* - `template` is the key designs are grouped by, explicit on every design
|
|
17
|
+
* (never inferred from an id's `-2`): a slug, never `default`, never renamed
|
|
18
|
+
* once shipped. Template keys and design ids share one namespace.
|
|
19
|
+
* - Design 1 of a template is the design whose id IS its key (`default` in the
|
|
20
|
+
* main template), so `/medley~food` opens the Food template. The others
|
|
21
|
+
* follow in `demos[]` order. The index is derived, never declared.
|
|
22
|
+
* - `label` (the business) and `for` belong to the template and are declared
|
|
23
|
+
* on design 1. A later design may omit them; it inherits design 1's.
|
|
24
|
+
* - `design_label` names a design within its template ("Neighbourhood buka").
|
|
25
|
+
*
|
|
26
|
+
* Tolerant where theme-check is strict: a design without a valid `template` is
|
|
27
|
+
* a template of its own, keyed by its id, and a main design without one has no
|
|
28
|
+
* key (so no gallery). A config written before R2.8 previews and publishes as
|
|
29
|
+
* it did.
|
|
30
|
+
*
|
|
31
|
+
* One resolver for every consumer: the registry, the design index the proxy
|
|
32
|
+
* and the preview read, both copies of theme-check, and theme-cli. So it
|
|
33
|
+
* imports nothing. `yarn tools:sync-lists` copies this file byte for byte to
|
|
34
|
+
* theme-tools (packages/theme-check/src/utils/theme-designs.ts), and a copy
|
|
35
|
+
* that drifts fails `yarn tools:sync-lists --check`. Change it here, then sync.
|
|
36
|
+
*/
|
|
37
|
+
/** The id `demo.json` goes by: the main template's first design. Equal to theme-demos.ts's PRIMARY_DEMO_ID. */
|
|
38
|
+
export declare const PRIMARY_DESIGN_ID = "default";
|
|
39
|
+
/** Between the theme and the design in a preview segment: `medley~food-2`. Equal to DEMO_SEPARATOR. */
|
|
40
|
+
export declare const DESIGN_SEPARATOR = "~";
|
|
41
|
+
/** A design id or a template key: both land in a URL segment and a filename. Equal to DEMO_ID_FORMAT. */
|
|
42
|
+
export declare const DESIGN_ID_FORMAT: RegExp;
|
|
43
|
+
/** One design as theme.config.ts declares it (`default_demo`, or a `demos[]` entry with its `id`). */
|
|
44
|
+
export interface DesignDeclaration {
|
|
45
|
+
/** The key of the template this design belongs to: `food`. */
|
|
46
|
+
template?: string;
|
|
47
|
+
/** The template's label, the business it is dressed as: `Restaurant & kitchen`. Declared on design 1. */
|
|
48
|
+
label?: string;
|
|
49
|
+
/** What tells this design from its template's others: `Neighbourhood buka`. */
|
|
50
|
+
design_label?: string;
|
|
51
|
+
/** The template's business, in lib/storefront/business-vocabulary.json. Declared on design 1. */
|
|
52
|
+
for?: string[];
|
|
53
|
+
/** What an AI reads to choose this design for a merchant (≤ 300 chars). */
|
|
54
|
+
description?: string;
|
|
55
|
+
}
|
|
56
|
+
/** The part of theme.config.ts this file reads. */
|
|
57
|
+
export interface ThemeDesignsConfig {
|
|
58
|
+
/** The theme's name: the main template's label when `default_demo` declares none. */
|
|
59
|
+
name?: string;
|
|
60
|
+
default_demo?: DesignDeclaration;
|
|
61
|
+
demos?: Array<DesignDeclaration & {
|
|
62
|
+
id: string;
|
|
63
|
+
}>;
|
|
64
|
+
}
|
|
65
|
+
/** One design, resolved: what the registry publishes for it. */
|
|
66
|
+
export interface ThemeDesign {
|
|
67
|
+
/** `default` for demo.json, else the name of its demos/<id>.json. Never renamed once shipped. */
|
|
68
|
+
id: string;
|
|
69
|
+
/** Its template's key. Null only for a main design that declares none (a config written before R2.8). */
|
|
70
|
+
template: string | null;
|
|
71
|
+
/** The template's label (design 1's `label`). */
|
|
72
|
+
templateLabel: string;
|
|
73
|
+
/** 1 for design 1 (the design whose id is the key; `default` in the main template), then `demos[]` order. */
|
|
74
|
+
designIndex: number;
|
|
75
|
+
designLabel: string | null;
|
|
76
|
+
/** For readers that know no templates: see composeLabel. */
|
|
77
|
+
label: string;
|
|
78
|
+
/** The template's business: design 1's `for`, the same on every design of it. */
|
|
79
|
+
for: string[];
|
|
80
|
+
description: string | null;
|
|
81
|
+
}
|
|
82
|
+
/** A template: its designs, design 1 first. */
|
|
83
|
+
export interface ThemeTemplate {
|
|
84
|
+
key: string | null;
|
|
85
|
+
label: string;
|
|
86
|
+
for: string[];
|
|
87
|
+
designs: ThemeDesign[];
|
|
88
|
+
}
|
|
89
|
+
/** One theme in themes/design-index.json. */
|
|
90
|
+
export interface DesignIndexEntry {
|
|
91
|
+
/** The main template's key: `/<slug>~<main>` is demo.json once the theme has a gallery. */
|
|
92
|
+
main: string | null;
|
|
93
|
+
/** The main template first, then in the order the config first names each. */
|
|
94
|
+
templates: DesignIndexTemplate[];
|
|
95
|
+
}
|
|
96
|
+
export interface DesignIndexTemplate {
|
|
97
|
+
key: string | null;
|
|
98
|
+
label: string;
|
|
99
|
+
/** Design 1 first. `label` is the design's `design_label` (its composed label when it declares none). */
|
|
100
|
+
designs: Array<{
|
|
101
|
+
id: string;
|
|
102
|
+
label: string;
|
|
103
|
+
}>;
|
|
104
|
+
}
|
|
105
|
+
/** themes/design-index.json: every active theme, by slug. */
|
|
106
|
+
export type DesignIndex = Record<string, DesignIndexEntry>;
|
|
107
|
+
/**
|
|
108
|
+
* The label a reader that knows no templates shows (the registry's `label`):
|
|
109
|
+
* the template's label when it has one design; `template — design` on every
|
|
110
|
+
* design, design 1 included, when it has more, so two cards never read alike.
|
|
111
|
+
*/
|
|
112
|
+
export declare function composeLabel(templateLabel: string, designLabel: string | null, designs: number): string;
|
|
113
|
+
/** Every design the config declares, resolved, in declaration order (demo.json first). */
|
|
114
|
+
export declare function designsOf(config: ThemeDesignsConfig | null | undefined): ThemeDesign[];
|
|
115
|
+
/** Designs grouped into templates: the main template (demo.json's) first, each template's design 1 first. */
|
|
116
|
+
export declare function groupTemplates(designs: readonly ThemeDesign[]): ThemeTemplate[];
|
|
117
|
+
/** The main template's key (`default_demo.template`), or null when it declares none. */
|
|
118
|
+
export declare function mainTemplateKey(config: ThemeDesignsConfig | null | undefined): string | null;
|
|
119
|
+
/** The theme's entry in themes/design-index.json. */
|
|
120
|
+
export declare function designIndexEntry(config: ThemeDesignsConfig | null | undefined): DesignIndexEntry;
|
|
121
|
+
/**
|
|
122
|
+
* Does `/<slug>` open the template gallery? Only for a theme with two or more
|
|
123
|
+
* templates and a main key to move the main store to (`/<slug>~<main>`). A
|
|
124
|
+
* single-template theme keeps `/<slug>` as its store.
|
|
125
|
+
*/
|
|
126
|
+
export declare function hasGallery(theme: {
|
|
127
|
+
main: string | null;
|
|
128
|
+
templates: readonly unknown[];
|
|
129
|
+
}): boolean;
|
|
130
|
+
/**
|
|
131
|
+
* The preview segment of a design: `medley~food-2`. demo.json's is the bare
|
|
132
|
+
* slug, unless the theme has a gallery there: then it is `<slug>~<main key>`.
|
|
133
|
+
*/
|
|
134
|
+
export declare function designParam(slug: string, id: string, main: string | null, gallery: boolean): string;
|
|
135
|
+
/**
|
|
136
|
+
* The design a preview segment's `~<segment>` names, or null for none: the
|
|
137
|
+
* inverse of designParam. The main key is demo.json (`default`) on a gallery
|
|
138
|
+
* theme; any other design id is itself. `default` is never a segment (one URL
|
|
139
|
+
* per design), and a single-template theme's key is none either: its store
|
|
140
|
+
* stays at the bare slug.
|
|
141
|
+
*/
|
|
142
|
+
export declare function designOfSegment(theme: DesignIndexEntry, segment: string): string | null;
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme → template → design (queek_backend contract R2.8).
|
|
3
|
+
*
|
|
4
|
+
* A theme is the look. A template is a business the theme is dressed as
|
|
5
|
+
* (medley's Food), and a design is one concrete store of it, one demo file:
|
|
6
|
+
* `food` and `food-2` are the Food template's two designs. theme.config.ts
|
|
7
|
+
* declares every design. `default_demo` is the main template's first design
|
|
8
|
+
* (demo.json, id `default`); each `demos[]` entry is one more (demos/<id>.json).
|
|
9
|
+
*
|
|
10
|
+
* default_demo: { template: 'beauty', label: 'Skincare & make-up', design_label: 'Photo collage', for: [...], description },
|
|
11
|
+
* demos: [
|
|
12
|
+
* { id: 'food', template: 'food', label: 'Restaurant & kitchen', design_label: 'Dining room', for: [...], description },
|
|
13
|
+
* { id: 'food-2', template: 'food', design_label: 'Neighbourhood buka', description },
|
|
14
|
+
* ],
|
|
15
|
+
*
|
|
16
|
+
* - `template` is the key designs are grouped by, explicit on every design
|
|
17
|
+
* (never inferred from an id's `-2`): a slug, never `default`, never renamed
|
|
18
|
+
* once shipped. Template keys and design ids share one namespace.
|
|
19
|
+
* - Design 1 of a template is the design whose id IS its key (`default` in the
|
|
20
|
+
* main template), so `/medley~food` opens the Food template. The others
|
|
21
|
+
* follow in `demos[]` order. The index is derived, never declared.
|
|
22
|
+
* - `label` (the business) and `for` belong to the template and are declared
|
|
23
|
+
* on design 1. A later design may omit them; it inherits design 1's.
|
|
24
|
+
* - `design_label` names a design within its template ("Neighbourhood buka").
|
|
25
|
+
*
|
|
26
|
+
* Tolerant where theme-check is strict: a design without a valid `template` is
|
|
27
|
+
* a template of its own, keyed by its id, and a main design without one has no
|
|
28
|
+
* key (so no gallery). A config written before R2.8 previews and publishes as
|
|
29
|
+
* it did.
|
|
30
|
+
*
|
|
31
|
+
* One resolver for every consumer: the registry, the design index the proxy
|
|
32
|
+
* and the preview read, both copies of theme-check, and theme-cli. So it
|
|
33
|
+
* imports nothing. `yarn tools:sync-lists` copies this file byte for byte to
|
|
34
|
+
* theme-tools (packages/theme-check/src/utils/theme-designs.ts), and a copy
|
|
35
|
+
* that drifts fails `yarn tools:sync-lists --check`. Change it here, then sync.
|
|
36
|
+
*/
|
|
37
|
+
/** The id `demo.json` goes by: the main template's first design. Equal to theme-demos.ts's PRIMARY_DEMO_ID. */
|
|
38
|
+
export const PRIMARY_DESIGN_ID = 'default';
|
|
39
|
+
/** Between the theme and the design in a preview segment: `medley~food-2`. Equal to DEMO_SEPARATOR. */
|
|
40
|
+
export const DESIGN_SEPARATOR = '~';
|
|
41
|
+
/** A design id or a template key: both land in a URL segment and a filename. Equal to DEMO_ID_FORMAT. */
|
|
42
|
+
export const DESIGN_ID_FORMAT = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
43
|
+
const text = (value) => (typeof value === 'string' && value.trim() !== '' ? value.trim() : null);
|
|
44
|
+
const businessKeys = (value) => {
|
|
45
|
+
if (!Array.isArray(value))
|
|
46
|
+
return null;
|
|
47
|
+
const keys = value.filter((key) => typeof key === 'string');
|
|
48
|
+
return keys.length > 0 ? keys : null;
|
|
49
|
+
};
|
|
50
|
+
/** A declared `template`, or null when it is missing or could not be a key. */
|
|
51
|
+
function templateKey(value) {
|
|
52
|
+
return typeof value === 'string' && DESIGN_ID_FORMAT.test(value) && value !== PRIMARY_DESIGN_ID ? value : null;
|
|
53
|
+
}
|
|
54
|
+
function declaredDesigns(config) {
|
|
55
|
+
const main = config?.default_demo ?? {};
|
|
56
|
+
const designs = [{
|
|
57
|
+
id: PRIMARY_DESIGN_ID,
|
|
58
|
+
primary: true,
|
|
59
|
+
key: templateKey(main.template),
|
|
60
|
+
label: text(main.label),
|
|
61
|
+
designLabel: text(main.design_label),
|
|
62
|
+
for: businessKeys(main.for),
|
|
63
|
+
description: text(main.description),
|
|
64
|
+
}];
|
|
65
|
+
const seen = new Set([PRIMARY_DESIGN_ID]);
|
|
66
|
+
const demos = config?.demos;
|
|
67
|
+
for (const demo of Array.isArray(demos) ? demos : []) {
|
|
68
|
+
// A missing, reserved or repeated id is theme-check's to name; it is no design.
|
|
69
|
+
if (typeof demo?.id !== 'string' || seen.has(demo.id))
|
|
70
|
+
continue;
|
|
71
|
+
seen.add(demo.id);
|
|
72
|
+
designs.push({
|
|
73
|
+
id: demo.id,
|
|
74
|
+
primary: false,
|
|
75
|
+
key: templateKey(demo.template) ?? demo.id,
|
|
76
|
+
label: text(demo.label),
|
|
77
|
+
designLabel: text(demo.design_label),
|
|
78
|
+
for: businessKeys(demo.for),
|
|
79
|
+
description: text(demo.description),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
return designs;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* The label a reader that knows no templates shows (the registry's `label`):
|
|
86
|
+
* the template's label when it has one design; `template — design` on every
|
|
87
|
+
* design, design 1 included, when it has more, so two cards never read alike.
|
|
88
|
+
*/
|
|
89
|
+
export function composeLabel(templateLabel, designLabel, designs) {
|
|
90
|
+
return designs > 1 && designLabel ? `${templateLabel} — ${designLabel}` : templateLabel;
|
|
91
|
+
}
|
|
92
|
+
/** Every design the config declares, resolved, in declaration order (demo.json first). */
|
|
93
|
+
export function designsOf(config) {
|
|
94
|
+
const declared = declaredDesigns(config);
|
|
95
|
+
const groups = new Map();
|
|
96
|
+
for (const design of declared)
|
|
97
|
+
groups.set(design.key, [...(groups.get(design.key) ?? []), design]);
|
|
98
|
+
const resolved = new Map();
|
|
99
|
+
for (const [key, members] of groups) {
|
|
100
|
+
const first = members.find((design) => design.primary) ?? members.find((design) => design.id === key) ?? members[0];
|
|
101
|
+
const ordered = [first, ...members.filter((design) => design !== first)];
|
|
102
|
+
const templateLabel = first.label
|
|
103
|
+
?? (first.primary ? text(config?.name) : null)
|
|
104
|
+
?? ordered.map((design) => design.label).find((label) => label !== null)
|
|
105
|
+
?? key
|
|
106
|
+
?? first.id;
|
|
107
|
+
const templateFor = first.for ?? ordered.map((design) => design.for).find((keys) => keys !== null) ?? [];
|
|
108
|
+
ordered.forEach((design, position) => {
|
|
109
|
+
resolved.set(design.id, {
|
|
110
|
+
id: design.id,
|
|
111
|
+
template: key,
|
|
112
|
+
templateLabel,
|
|
113
|
+
designIndex: position + 1,
|
|
114
|
+
designLabel: design.designLabel,
|
|
115
|
+
// A later design of a template that names no design_label keeps its own
|
|
116
|
+
// label rather than repeating the template's (theme-check rejects it).
|
|
117
|
+
label: ordered.length > 1 && !design.designLabel ? design.label ?? templateLabel : composeLabel(templateLabel, design.designLabel, ordered.length),
|
|
118
|
+
for: [...templateFor],
|
|
119
|
+
description: design.description,
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
return declared.map((design) => resolved.get(design.id));
|
|
124
|
+
}
|
|
125
|
+
/** Designs grouped into templates: the main template (demo.json's) first, each template's design 1 first. */
|
|
126
|
+
export function groupTemplates(designs) {
|
|
127
|
+
const groups = new Map();
|
|
128
|
+
const main = designs.find((design) => design.id === PRIMARY_DESIGN_ID);
|
|
129
|
+
if (main)
|
|
130
|
+
groups.set(main.template, []);
|
|
131
|
+
for (const design of designs)
|
|
132
|
+
groups.set(design.template, [...(groups.get(design.template) ?? []), design]);
|
|
133
|
+
return [...groups].map(([key, members]) => {
|
|
134
|
+
const ordered = [...members].sort((a, b) => a.designIndex - b.designIndex);
|
|
135
|
+
return { key, label: ordered[0].templateLabel, for: [...ordered[0].for], designs: ordered };
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
/** The main template's key (`default_demo.template`), or null when it declares none. */
|
|
139
|
+
export function mainTemplateKey(config) {
|
|
140
|
+
return templateKey(config?.default_demo?.template);
|
|
141
|
+
}
|
|
142
|
+
/** The theme's entry in themes/design-index.json. */
|
|
143
|
+
export function designIndexEntry(config) {
|
|
144
|
+
return {
|
|
145
|
+
main: mainTemplateKey(config),
|
|
146
|
+
templates: groupTemplates(designsOf(config)).map((template) => ({
|
|
147
|
+
key: template.key,
|
|
148
|
+
label: template.label,
|
|
149
|
+
designs: template.designs.map((design) => ({ id: design.id, label: design.designLabel ?? design.label })),
|
|
150
|
+
})),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Does `/<slug>` open the template gallery? Only for a theme with two or more
|
|
155
|
+
* templates and a main key to move the main store to (`/<slug>~<main>`). A
|
|
156
|
+
* single-template theme keeps `/<slug>` as its store.
|
|
157
|
+
*/
|
|
158
|
+
export function hasGallery(theme) {
|
|
159
|
+
return theme.main !== null && theme.templates.length >= 2;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* The preview segment of a design: `medley~food-2`. demo.json's is the bare
|
|
163
|
+
* slug, unless the theme has a gallery there: then it is `<slug>~<main key>`.
|
|
164
|
+
*/
|
|
165
|
+
export function designParam(slug, id, main, gallery) {
|
|
166
|
+
if (id !== PRIMARY_DESIGN_ID)
|
|
167
|
+
return `${slug}${DESIGN_SEPARATOR}${id}`;
|
|
168
|
+
return gallery && main !== null ? `${slug}${DESIGN_SEPARATOR}${main}` : slug;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* The design a preview segment's `~<segment>` names, or null for none: the
|
|
172
|
+
* inverse of designParam. The main key is demo.json (`default`) on a gallery
|
|
173
|
+
* theme; any other design id is itself. `default` is never a segment (one URL
|
|
174
|
+
* per design), and a single-template theme's key is none either: its store
|
|
175
|
+
* stays at the bare slug.
|
|
176
|
+
*/
|
|
177
|
+
export function designOfSegment(theme, segment) {
|
|
178
|
+
if (hasGallery(theme) && segment === theme.main)
|
|
179
|
+
return PRIMARY_DESIGN_ID;
|
|
180
|
+
if (segment === PRIMARY_DESIGN_ID)
|
|
181
|
+
return null;
|
|
182
|
+
return theme.templates.some((template) => template.designs.some((design) => design.id === segment)) ? segment : null;
|
|
183
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@usequeek/theme-check",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "The rules a Queek storefront theme is checked against, as a library.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -8,6 +8,10 @@
|
|
|
8
8
|
".": {
|
|
9
9
|
"types": "./dist/index.d.ts",
|
|
10
10
|
"default": "./dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"./designs": {
|
|
13
|
+
"types": "./dist/utils/theme-designs.d.ts",
|
|
14
|
+
"default": "./dist/utils/theme-designs.js"
|
|
11
15
|
}
|
|
12
16
|
},
|
|
13
17
|
"files": [
|