@usequeek/theme-check 0.5.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/context.d.ts CHANGED
@@ -9,7 +9,7 @@ export declare function localEnv(dir: string, cwd?: string): CheckEnv;
9
9
  * the theme's own project — the kit the developer installed, not ours.
10
10
  */
11
11
  export declare function loadContext(themeDir: string, env?: Partial<CheckEnv>): Promise<ThemeContext>;
12
- /** Every .ts/.tsx under a theme, for the whole-tree scans. */
12
+ /** Every JS/TS source file under a theme, for the whole-tree scans. */
13
13
  export declare function themeSourceFiles(dir: string): string[];
14
14
  /**
15
15
  * Load a module of the kit the theme's project installed. The kit ships
package/dist/context.js CHANGED
@@ -80,13 +80,13 @@ export async function loadContext(themeDir, env = {}) {
80
80
  read,
81
81
  };
82
82
  }
83
- /** Every .ts/.tsx under a theme, for the whole-tree scans. */
83
+ /** Every JS/TS source file under a theme, for the whole-tree scans. */
84
84
  export function themeSourceFiles(dir) {
85
85
  return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
86
86
  const full = join(dir, entry.name);
87
87
  if (entry.isDirectory())
88
88
  return entry.name === 'node_modules' ? [] : themeSourceFiles(full);
89
- return /\.tsx?$/.test(entry.name) ? [full] : [];
89
+ return /\.[mc]?[jt]sx?$/.test(entry.name) ? [full] : [];
90
90
  });
91
91
  }
92
92
  /**
package/dist/index.d.ts CHANGED
@@ -13,5 +13,5 @@ export { TEMPLATE_COPY_PLACES, copyViolations, isTestimonialSection, storeNameFo
13
13
  export { PRIMARY_DEMO_ID, DEMO_ID_FORMAT, demoFilesOf } from './utils/theme-demos.js';
14
14
  export { designsOf, groupTemplates, mainTemplateKey, composeLabel, type DesignDeclaration, type ThemeDesignsConfig, type ThemeDesign, type ThemeTemplate, } from './utils/theme-designs.js';
15
15
  export { TEMPLATE_DESCRIPTION_MAX, screenshotFile, sectionStyle, sectionCopy, declaredFieldsByVariant } from './utils/theme-templates.js';
16
- export { STATIC_RULES, moduleContractRule, structureRule, demoStoreRule, demoStoresRule, demoArtRule, codeQualityRule, sdkBoundaryRule, selectionMetadataRule, demoCompletenessRule, subscribeScopeRule, demoBlockTypesRule, compositionVariantsRule, 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';
16
+ export { STATIC_RULES, moduleContractRule, structureRule, demoStoreRule, demoStoresRule, demoArtRule, codeQualityRule, sdkBoundaryRule, selectionMetadataRule, demoCompletenessRule, subscribeScopeRule, demoBlockTypesRule, compositionVariantsRule, identityRule, productMetafieldsRule, poweredByRule, fontsSelfHostedRule, templateDescriptionRule, templateScreenshotRule, templateChromeRule, templateStyleRule, templateBusinessRule, templateVersionsRule, templateDesignsRule, TEMPLATE_DESIGNS_MAX, templatePagesRule, templateCopyRule, vendorFactsRule, placeholderContentRule, frameworkImport, moduleSpecifiers, STARTER_PLACEHOLDER_IMAGES, } from './rules/static.js';
17
17
  export { ANALYSIS_RULES, variantParityRule, fieldParityRule, designTokensRule } from './rules/analysis.js';
package/dist/index.js CHANGED
@@ -17,5 +17,5 @@ export { TEMPLATE_DESCRIPTION_MAX, screenshotFile, sectionStyle, sectionCopy, de
17
17
  // Per-kind rule arrays and the individual rule constants, for rule-level unit
18
18
  // tests that want to run one rule against a hand-built ThemeContext instead
19
19
  // of a whole theme directory through checkTheme().
20
- export { STATIC_RULES, moduleContractRule, structureRule, demoStoreRule, demoStoresRule, demoArtRule, codeQualityRule, sdkBoundaryRule, selectionMetadataRule, demoCompletenessRule, subscribeScopeRule, demoBlockTypesRule, compositionVariantsRule, 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';
20
+ export { STATIC_RULES, moduleContractRule, structureRule, demoStoreRule, demoStoresRule, demoArtRule, codeQualityRule, sdkBoundaryRule, selectionMetadataRule, demoCompletenessRule, subscribeScopeRule, demoBlockTypesRule, compositionVariantsRule, identityRule, productMetafieldsRule, poweredByRule, fontsSelfHostedRule, templateDescriptionRule, templateScreenshotRule, templateChromeRule, templateStyleRule, templateBusinessRule, templateVersionsRule, templateDesignsRule, TEMPLATE_DESIGNS_MAX, templatePagesRule, templateCopyRule, vendorFactsRule, placeholderContentRule, frameworkImport, moduleSpecifiers, STARTER_PLACEHOLDER_IMAGES, } from './rules/static.js';
21
21
  export { ANALYSIS_RULES, variantParityRule, fieldParityRule, designTokensRule } from './rules/analysis.js';
@@ -16,6 +16,13 @@ export declare const codeQualityRule: Rule;
16
16
  * import Next, a theme may not.
17
17
  */
18
18
  export declare function frameworkImport(source: string): string | null;
19
+ /**
20
+ * Every static module specifier in a source file: `import … from`,
21
+ * `export … from`, side-effect `import '…'`, dynamic `import('…')` and
22
+ * `require('…')`. Parsed like frameworkImport — regexes, not a bundler — so
23
+ * a theme whose module graph will not load still gets checked.
24
+ */
25
+ export declare function moduleSpecifiers(source: string): string[];
19
26
  export declare const sdkBoundaryRule: Rule;
20
27
  /**
21
28
  * The ThemeModule shape (Layout, Header, Footer, blocks, getBlock, pages…) is
@@ -1,4 +1,4 @@
1
- import { join, relative } from 'node:path';
1
+ import { dirname, join, relative, resolve, sep } 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';
@@ -308,9 +308,29 @@ export function frameworkImport(source) {
308
308
  const match = source.match(/(?:\bfrom\s*|\bimport\s*\(?\s*|\brequire\s*\(\s*)['"](next(?:\/[\w./-]+)?)['"]/);
309
309
  return match ? match[1] : null;
310
310
  }
311
+ /**
312
+ * Every static module specifier in a source file: `import … from`,
313
+ * `export … from`, side-effect `import '…'`, dynamic `import('…')` and
314
+ * `require('…')`. Parsed like frameworkImport — regexes, not a bundler — so
315
+ * a theme whose module graph will not load still gets checked.
316
+ */
317
+ export function moduleSpecifiers(source) {
318
+ const found = [];
319
+ const patterns = [
320
+ /(?:import|export)[^'"]*?from\s*['"]([^'"]+)['"]/g,
321
+ /\bimport\s*['"]([^'"]+)['"]/g,
322
+ /\bimport\s*\(\s*['"]([^'"]+)['"]\s*\)/g,
323
+ /\brequire\s*\(\s*['"]([^'"]+)['"]\s*\)/g,
324
+ ];
325
+ for (const pattern of patterns) {
326
+ for (const match of source.matchAll(pattern))
327
+ found.push(match[1]);
328
+ }
329
+ return found;
330
+ }
311
331
  export const sdkBoundaryRule = {
312
332
  id: 'theme/core-boundary',
313
- summary: 'No SDK or framework imports, no direct API calls, no store mutations',
333
+ summary: 'No SDK or framework imports, no app-code imports or escapes, no direct API calls, no store mutations',
314
334
  kind: 'static',
315
335
  run(context) {
316
336
  const findings = [];
@@ -334,6 +354,33 @@ export const sdkBoundaryRule = {
334
354
  docs: `${context.env.docs}#what-themes-must-not-do`,
335
355
  }));
336
356
  }
357
+ // A theme is a self-contained folder: it imports packages and its own
358
+ // files, never the app around it. `@/` only resolves inside the
359
+ // storefront repo, and a relative import past the theme root only
360
+ // resolves there too — both break in a developer's `npm create`
361
+ // project and in `theme:pull`.
362
+ const root = resolve(context.dir);
363
+ for (const spec of moduleSpecifiers(source)) {
364
+ if (spec.startsWith('@/')) {
365
+ findings.push(finding(context, 'theme/core-boundary', 'reject', {
366
+ where,
367
+ found: `imports app code ('${spec}')`,
368
+ fix: "Move the code into the theme's own folder, or import it from '@usequeek/theme-kit'.",
369
+ docs: `${context.env.docs}#what-themes-must-not-do`,
370
+ }));
371
+ }
372
+ else if (spec.startsWith('.')) {
373
+ const resolved = resolve(dirname(path), spec);
374
+ if (resolved !== root && !resolved.startsWith(root + sep)) {
375
+ findings.push(finding(context, 'theme/core-boundary', 'reject', {
376
+ where,
377
+ found: `relative import '${spec}' escapes the theme (resolves to ${relative(root, resolved)})`,
378
+ fix: "Move the code into the theme's own folder, or import it from '@usequeek/theme-kit'.",
379
+ docs: `${context.env.docs}#what-themes-must-not-do`,
380
+ }));
381
+ }
382
+ }
383
+ }
337
384
  if (/\b(?:fetch|axios)\s*\(\s*['"`]https?:/.test(source)) {
338
385
  findings.push(finding(context, 'theme/core-boundary', 'reject', {
339
386
  where,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usequeek/theme-check",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "The rules a Queek storefront theme is checked against, as a library.",
5
5
  "license": "MIT",
6
6
  "type": "module",