@multiplatform.one/config 7.6.0 → 7.6.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/README.md CHANGED
@@ -68,7 +68,14 @@ Convention rules travel with the package. Consuming apps add one line:
68
68
 
69
69
  Then enable `mpo-conventions/no-hex-literals` on the globs you want (mpo's
70
70
  own `oxlintrc.json` turns it on for `features/**`). Palette modules
71
- (`themes/base.ts`, `themes/accent.ts`, `*palette*`) are exempt.
71
+ (`themes/base.ts`, `themes/accent.ts`, `*palette*`) are exempt. Comments
72
+ are not visited, so a JSDoc `#025` is not a hit. Widening to `apps/**`
73
+ and `packages/**` is 182 hits on this tree (153 outside tests) — record
74
+ the count, then burn down; do not flip the glob until then.
75
+
76
+ `no-chrome-props` and `no-raw-geometry` ship in the same plugin (MPO-17).
77
+ Enable them the same way when you start that burndown. Drawing-file
78
+ exceptions come from the spec table `## Drawing files`, not from the code.
72
79
 
73
80
  ### Lint (node-script checks)
74
81
 
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Drawing / chrome-allow registry — parsed from the living rulebook.
3
+ *
4
+ * MPO-17: raw numeric geometry is legal only in files listed as DRAWING
5
+ * members, and chrome props on imported @multiplatform.one/* components are
6
+ * legal only for CHROME-ALLOW members. Both lists are parsed from
7
+ * `docs/theme-propagation-spec.md` `## Drawing files`, the same shape
8
+ * scripts/radius-identity-registry.mjs uses for LC-60. Writing more
9
+ * violations into the code cannot widen either list.
10
+ *
11
+ * A consumer tree with no spec (or no Drawing files section) yields empty
12
+ * lists rather than throwing, so shc can pick the oxlint rules up before it
13
+ * has copied this table.
14
+ *
15
+ * Usage:
16
+ * import { readDrawingRegistry } from "./drawing-registry.mjs";
17
+ * node drawing-registry.mjs [--json]
18
+ */
19
+ import { existsSync, readFileSync } from "node:fs";
20
+ import { join } from "node:path";
21
+ import { pathToFileURL } from "node:url";
22
+
23
+ export const DRAWING_HEADING = "## Drawing files";
24
+ export const DRAWING_CLASS = "DRAWING";
25
+ export const CHROME_ALLOW_CLASS = "CHROME-ALLOW";
26
+
27
+ /** `// mpo-drawing` or `/* mpo-drawing` at the start of a line. */
28
+ export const DRAWING_PRAGMA = /^\s*(?:\/\/|\/\*)\s*mpo-drawing\b/m;
29
+
30
+ /**
31
+ * @param {string} [cwd]
32
+ */
33
+ export function defaultSpecPath(cwd = process.cwd()) {
34
+ return join(cwd, "docs/theme-propagation-spec.md");
35
+ }
36
+
37
+ /** `Members: **Avatar**, **Radio disc**.` → ["Avatar", "Radio disc"]. */
38
+ function parseMembers(text) {
39
+ const match = text.match(/Members:\s*(.*)$/i);
40
+ if (!match) return [];
41
+ return [...match[1].matchAll(/\*\*([^*]+)\*\*/g)].map((m) => m[1].trim()).filter(Boolean);
42
+ }
43
+
44
+ /**
45
+ * @param {string} markdown
46
+ * @param {string} specPath
47
+ */
48
+ function parseDrawingTable(markdown, specPath) {
49
+ const lines = markdown.split("\n");
50
+ const heading = lines.findIndex((line) => /^##\s+Drawing files\s*$/.test(line));
51
+ if (heading === -1) return null;
52
+
53
+ /** @type {{ id: string, text: string }[]} */
54
+ const rows = [];
55
+ let seenTable = false;
56
+ for (let i = heading + 1; i < lines.length; i++) {
57
+ const line = lines[i].trim();
58
+ if (line.startsWith("##")) break;
59
+ if (!line.startsWith("|")) {
60
+ if (seenTable) break;
61
+ continue;
62
+ }
63
+ seenTable = true;
64
+ const cells = line
65
+ .split("|")
66
+ .slice(1, -1)
67
+ .map((c) => c.trim());
68
+ if (cells.length < 2) continue;
69
+ if (/^:?-{2,}/.test(cells[0])) continue;
70
+ if (cells[0].toLowerCase() === "class") continue;
71
+ rows.push({ id: cells[0], text: cells[1] });
72
+ }
73
+ if (rows.length === 0) {
74
+ throw new Error(`${specPath}: "${DRAWING_HEADING}" has no class rows`);
75
+ }
76
+ return rows;
77
+ }
78
+
79
+ /**
80
+ * @param {string} [specPath]
81
+ * @returns {{
82
+ * specPath: string,
83
+ * present: boolean,
84
+ * files: string[],
85
+ * chromeAllow: string[],
86
+ * }}
87
+ */
88
+ export function readDrawingRegistry(specPath = defaultSpecPath()) {
89
+ if (!existsSync(specPath)) {
90
+ return { specPath, present: false, files: [], chromeAllow: [] };
91
+ }
92
+ const rows = parseDrawingTable(readFileSync(specPath, "utf8"), specPath);
93
+ if (!rows) {
94
+ return { specPath, present: false, files: [], chromeAllow: [] };
95
+ }
96
+
97
+ /** @type {Record<string, { text: string, members: string[] }>} */
98
+ const classes = {};
99
+ for (const row of rows) {
100
+ classes[row.id] = { text: row.text, members: parseMembers(row.text) };
101
+ }
102
+ if (!classes[DRAWING_CLASS]) {
103
+ throw new Error(`${specPath}: "${DRAWING_HEADING}" has no ${DRAWING_CLASS} row`);
104
+ }
105
+ if (!classes[CHROME_ALLOW_CLASS]) {
106
+ throw new Error(`${specPath}: "${DRAWING_HEADING}" has no ${CHROME_ALLOW_CLASS} row`);
107
+ }
108
+
109
+ return {
110
+ specPath,
111
+ present: true,
112
+ files: classes[DRAWING_CLASS].members,
113
+ chromeAllow: classes[CHROME_ALLOW_CLASS].members,
114
+ };
115
+ }
116
+
117
+ /**
118
+ * True when `filename` is one of the registered drawing files.
119
+ * Members are path suffixes (`listing/glyphs/CanvasGlyph.tsx`) or basenames.
120
+ *
121
+ * @param {string | undefined} filename
122
+ * @param {string[]} files
123
+ */
124
+ export function isDrawingFile(filename, files) {
125
+ if (!filename || !files.length) return false;
126
+ const normalized = filename.replaceAll("\\", "/");
127
+ return files.some((entry) => {
128
+ const needle = entry.replaceAll("\\", "/").replace(/^\.\//, "");
129
+ return normalized === needle || normalized.endsWith(`/${needle}`);
130
+ });
131
+ }
132
+
133
+ const invokedDirectly =
134
+ typeof process.argv[1] === "string" && import.meta.url === pathToFileURL(process.argv[1]).href;
135
+
136
+ if (invokedDirectly) {
137
+ const registry = readDrawingRegistry();
138
+ if (process.argv.includes("--json")) {
139
+ console.log(JSON.stringify(registry, null, 2));
140
+ } else {
141
+ console.log(`drawing registry from ${registry.specPath} (present=${registry.present})`);
142
+ console.log(` DRAWING: ${registry.files.join(", ") || "(none)"}`);
143
+ console.log(` CHROME-ALLOW: ${registry.chromeAllow.join(", ") || "(none)"}`);
144
+ }
145
+ }
package/lib/index.js CHANGED
@@ -1,5 +1,6 @@
1
- import { t as createViteConfig } from "./vite-C7884n3X.js";
2
- import { t as createStorybookViteConfig } from "./storybook-CrdwuS_Y.js";
1
+ import { t as createViteConfig } from "./vite-W9hIeg2_.js";
2
+ import { t as createStorybookViteConfig } from "./storybook-CSzjegfP.js";
3
+ import { n as tamaguiWorkspacePathsFile, t as ensureTamaguiWorkspacePaths } from "./tamaguiWorkspacePaths-ZIcqybtn.js";
3
4
  import { createVitestConfig } from "./vitest.js";
4
5
 
5
- export { createStorybookViteConfig, createViteConfig, createVitestConfig };
6
+ export { createStorybookViteConfig, createViteConfig, createVitestConfig, ensureTamaguiWorkspacePaths, tamaguiWorkspacePathsFile };
@@ -1,4 +1,5 @@
1
1
  import { r as resolvePackageMainSource, t as discoverPublicPackageRoots } from "./workspacePublicPackages-COicQSj4.js";
2
+ import { t as ensureTamaguiWorkspacePaths } from "./tamaguiWorkspacePaths-ZIcqybtn.js";
2
3
  import { createRequire } from "node:module";
3
4
  import fs from "node:fs";
4
5
  import path from "node:path";
@@ -89,6 +90,7 @@ function resolvePackageDir(pkgName, root, requireFrom) {
89
90
  * ```
90
91
  */
91
92
  function createStorybookViteConfig(options = {}) {
93
+ ensureTamaguiWorkspacePaths();
92
94
  const workspaceRoot = options.workspaceRoot || findWorkspaceRoot();
93
95
  const aliases = {
94
96
  ...discoverPackageAliases(path.join(workspaceRoot, "packages")),
package/lib/storybook.js CHANGED
@@ -1,3 +1,3 @@
1
- import { t as createStorybookViteConfig } from "./storybook-CrdwuS_Y.js";
1
+ import { t as createStorybookViteConfig } from "./storybook-CSzjegfP.js";
2
2
 
3
3
  export { createStorybookViteConfig };
@@ -0,0 +1,58 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { execFileSync } from "node:child_process";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ //#region src/tamaguiWorkspacePaths.ts
7
+ const tamaguiWorkspacePathsFile = "tsconfig/tamagui-workspace-paths.generated.json";
8
+ const generatorFile = "../../scripts/generate-tamagui-workspace-paths.mjs";
9
+ function thisPackageDir() {
10
+ return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
11
+ }
12
+ function ensureTamaguiWorkspacePaths(options = {}) {
13
+ const { configPackageDir = thisPackageDir(), log = console.warn } = options;
14
+ const generatedPath = path.resolve(configPackageDir, tamaguiWorkspacePathsFile);
15
+ const generatorPath = path.resolve(configPackageDir, generatorFile);
16
+ if (fs.existsSync(generatedPath)) return {
17
+ generatedPath,
18
+ generatorPath,
19
+ status: "present"
20
+ };
21
+ if (!fs.existsSync(generatorPath)) return {
22
+ generatedPath,
23
+ generatorPath,
24
+ status: "no-generator"
25
+ };
26
+ try {
27
+ execFileSync(process.execPath, [generatorPath], { stdio: [
28
+ "ignore",
29
+ "ignore",
30
+ "pipe"
31
+ ] });
32
+ } catch (error) {
33
+ const message = error instanceof Error ? error.message : String(error);
34
+ log?.(`[@multiplatform.one/config] could not regenerate ${tamaguiWorkspacePathsFile}: ${message}`);
35
+ return {
36
+ generatedPath,
37
+ generatorPath,
38
+ status: "failed"
39
+ };
40
+ }
41
+ if (!fs.existsSync(generatedPath)) {
42
+ log?.(`[@multiplatform.one/config] ${path.basename(generatorPath)} ran but did not write ${tamaguiWorkspacePathsFile}`);
43
+ return {
44
+ generatedPath,
45
+ generatorPath,
46
+ status: "failed"
47
+ };
48
+ }
49
+ log?.(`[@multiplatform.one/config] ${tamaguiWorkspacePathsFile} was missing (the root prepare script did not run; a global ignore-scripts skips it). Regenerated it. Run \`pnpm generate:tamagui-paths\` after install to skip this step.`);
50
+ return {
51
+ generatedPath,
52
+ generatorPath,
53
+ status: "generated"
54
+ };
55
+ }
56
+
57
+ //#endregion
58
+ export { tamaguiWorkspacePathsFile as n, ensureTamaguiWorkspacePaths as t };
@@ -1,4 +1,5 @@
1
1
  import { t as discoverPublicPackageRoots } from "./workspacePublicPackages-COicQSj4.js";
2
+ import { t as ensureTamaguiWorkspacePaths } from "./tamaguiWorkspacePaths-ZIcqybtn.js";
2
3
  import { createRequire } from "node:module";
3
4
  import fs from "node:fs";
4
5
  import path from "node:path";
@@ -77,6 +78,7 @@ function lookupProjectRoot() {
77
78
  * ```
78
79
  */
79
80
  function createViteConfig(options = {}) {
81
+ ensureTamaguiWorkspacePaths();
80
82
  const { projectRoot: _projectRoot, publicConfigKeys = [], tamagui = true, i18n = false, one = false, ssr, server, plugins: extraPlugins = [], externalReactRouter, ssrDeps, ssrEntries, ssrCjsExternal, nativeFixes } = options;
81
83
  const projectRoot = _projectRoot || findProjectRoot();
82
84
  dotenv.config({ path: path.resolve(projectRoot, ".env") });
package/lib/vite.js CHANGED
@@ -1,4 +1,4 @@
1
- import { t as createViteConfig } from "./vite-C7884n3X.js";
1
+ import { t as createViteConfig } from "./vite-W9hIeg2_.js";
2
2
  import { n as publicPackageViteSourceAliases, t as discoverPublicPackageRoots } from "./workspacePublicPackages-COicQSj4.js";
3
3
 
4
4
  export { createViteConfig, discoverPublicPackageRoots, publicPackageViteSourceAliases };
package/lib/vitest.js CHANGED
@@ -1,8 +1,9 @@
1
+ import { t as ensureTamaguiWorkspacePaths } from "./tamaguiWorkspacePaths-ZIcqybtn.js";
1
2
  import Module from "node:module";
2
3
  import fs from "node:fs";
3
4
  import path from "node:path";
4
- import { tamaguiPlugin } from "@tamagui/vite-plugin";
5
5
  import { fileURLToPath } from "node:url";
6
+ import { tamaguiPlugin } from "@tamagui/vite-plugin";
6
7
  import react from "@vitejs/plugin-react";
7
8
 
8
9
  //#region src/vitest.ts
@@ -47,6 +48,7 @@ function pinNodeReactRequire(reactDir, reactDomDir, schedulerDir) {
47
48
  * ```
48
49
  */
49
50
  function createVitestConfig(options = {}) {
51
+ ensureTamaguiWorkspacePaths();
50
52
  const { tamaguiConfig = "./tests/tamagui.config.ts", environment = "jsdom", setupFiles = [], testTimeout = 3e4, inlineDeps = [], externalDeps = [], aliases = {}, coverage, replaceReactNative = true, dedupeReact = true, rootNodeModules: _rootNodeModules, conditions = [
51
53
  "source",
52
54
  "test",
package/lint.d.ts CHANGED
@@ -3,6 +3,7 @@ export type ConventionRoots = {
3
3
  apps?: string;
4
4
  features?: string;
5
5
  public?: string;
6
+ packages?: string;
6
7
  };
7
8
 
8
9
  export type RunConventionChecksOptions = {
@@ -14,6 +15,7 @@ export function resolveRoots(roots?: ConventionRoots): {
14
15
  apps: string;
15
16
  features: string;
16
17
  public: string;
18
+ packages: string;
17
19
  };
18
20
 
19
21
  export function runConventionChecks(options?: RunConventionChecksOptions): number;
package/lint.mjs CHANGED
@@ -18,6 +18,15 @@
18
18
  * `knobProps.control.height`) are fine. Escape with `sizeRecipeEscape` or
19
19
  * `size-recipe-escape:` on the previous line or same statement. Pilot:
20
20
  * Button/, InputParts/, fields/Select/ only.
21
+ * 5. Fail on a STORY FILE THE CSF INDEXER CANNOT READ (MPO-93) — a
22
+ * `*.stories.*` file that exports a named `meta` beside `export default`
23
+ * (`meta` is reserved for CSF Factories, so the indexer reports "missing
24
+ * default export"), or one with no default export at all. One such file
25
+ * kills the whole gallery and no package build ever sees it.
26
+ * 6. Fail on an UNDECLARED DRAWING FILE (MPO-17) — a `// mpo-drawing` pragma
27
+ * whose path is not a DRAWING member in docs/theme-propagation-spec.md.
28
+ * The oxlint rule skips registered drawings; this check is what makes
29
+ * adding an undeclared drawing file still fail.
21
30
  *
22
31
  * Usage:
23
32
  * import { runConventionChecks } from "@multiplatform.one/config/lint";
@@ -31,9 +40,10 @@ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
31
40
  import { createRequire } from "node:module";
32
41
  import { join, relative } from "node:path";
33
42
  import { pathToFileURL } from "node:url";
43
+ import { DRAWING_PRAGMA, isDrawingFile, readDrawingRegistry } from "./drawing-registry.mjs";
34
44
 
35
45
  /**
36
- * @typedef {{ root?: string, apps?: string, features?: string, public?: string }} ConventionRoots
46
+ * @typedef {{ root?: string, apps?: string, features?: string, public?: string, packages?: string }} ConventionRoots
37
47
  * @typedef {{ roots?: ConventionRoots }} RunConventionChecksOptions
38
48
  */
39
49
 
@@ -45,6 +55,8 @@ let APPS_DIR = join(ROOT, "apps");
45
55
  let FEATURES_DIR = join(ROOT, "features");
46
56
  /** @type {string} */
47
57
  let PUBLIC_DIR = join(ROOT, "public");
58
+ /** @type {string} */
59
+ let PACKAGES_DIR = join(ROOT, "packages");
48
60
 
49
61
  /**
50
62
  * @param {ConventionRoots} [roots]
@@ -56,6 +68,7 @@ export function resolveRoots(roots = {}) {
56
68
  apps: roots.apps ?? join(root, "apps"),
57
69
  features: roots.features ?? join(root, "features"),
58
70
  public: roots.public ?? join(root, "public"),
71
+ packages: roots.packages ?? join(root, "packages"),
59
72
  };
60
73
  }
61
74
 
@@ -68,6 +81,7 @@ function applyRoots(roots) {
68
81
  APPS_DIR = resolved.apps;
69
82
  FEATURES_DIR = resolved.features;
70
83
  PUBLIC_DIR = resolved.public;
84
+ PACKAGES_DIR = resolved.packages;
71
85
  sourceFileCache = undefined;
72
86
  requireFromRoot = createRequire(join(ROOT, "package.json"));
73
87
  }
@@ -670,6 +684,100 @@ function checkSizeRecipeEscape() {
670
684
  return violations;
671
685
  }
672
686
 
687
+ // ── Story file shape (MPO-93) ─────────────────────────────────────────────
688
+
689
+ const STORY_FILE_RE = /\.(stories|story)\.(js|jsx|mjs|ts|tsx)$/;
690
+
691
+ /**
692
+ * Names exported through `export { a, b as c }` statements.
693
+ * @param {string} src
694
+ * @returns {Set<string>}
695
+ */
696
+ function braceExportNames(src) {
697
+ const names = new Set();
698
+ for (const m of src.matchAll(/^export\s*\{([^}]*)\}/gm)) {
699
+ for (const item of m[1].split(",")) {
700
+ const parts = item.trim().split(/\s+as\s+/);
701
+ const name = (parts[1] ?? parts[0] ?? "").trim();
702
+ if (name) names.add(name);
703
+ }
704
+ }
705
+ return names;
706
+ }
707
+
708
+ /**
709
+ * Storybook's CSF indexer accepts a story file in exactly two shapes: classic
710
+ * CSF (`const meta = {...}; export default meta`) or CSF Factories
711
+ * (`export const meta = preview.meta({...})`, no default export). A file that
712
+ * exports a NAMED `meta` next to `export default` is neither — the indexer
713
+ * takes the named export as the factory shape, finds no factory call, and
714
+ * reports "CSF: missing default export (line 1, col 0)". One such file fails
715
+ * the whole index, the preview build, and `storybook dev` exits non-zero.
716
+ * Nothing else catches it: package builds exclude `.stories.` files, so the
717
+ * pipeline stays green. Three files did exactly this on main (MPO-93).
718
+ */
719
+ function checkStoryFileShape() {
720
+ /** @type {string[]} */
721
+ const violations = [];
722
+ const scan = (filePath) => {
723
+ if (!STORY_FILE_RE.test(filePath)) return;
724
+ const rel = relative(ROOT, filePath);
725
+ const src = readFileSync(filePath, "utf8");
726
+ const braced = braceExportNames(src);
727
+ const namedMeta =
728
+ src.match(/^export\s+(?:const|let|var|function|class)\s+meta\b/m) ??
729
+ (braced.has("meta") ? src.match(/^export\s*\{[^}]*\bmeta\b[^}]*\}/m) : null);
730
+ const hasDefault = /^export\s+default\b/m.test(src) || braced.has("default");
731
+ const isFactory = /^export\s+(?:const|let|var)\s+meta\b[^=\n]*=\s*[\w$.]+\.meta\s*\(/m.test(
732
+ src,
733
+ );
734
+ if (namedMeta && hasDefault) {
735
+ violations.push(
736
+ `${rel}:${lineAt(src, namedMeta.index ?? 0)} — exports a named \`meta\` beside \`export default\`; drop the \`export\` (\`meta\` is reserved for CSF Factories)`,
737
+ );
738
+ } else if (namedMeta && !isFactory) {
739
+ violations.push(
740
+ `${rel}:${lineAt(src, namedMeta.index ?? 0)} — exports a named \`meta\` that is not a CSF factory call (\`preview.meta(...)\`) and has no default export`,
741
+ );
742
+ } else if (!namedMeta && !hasDefault) {
743
+ violations.push(`${rel} — no default export (classic CSF needs \`export default meta\`)`);
744
+ }
745
+ };
746
+ for (const dir of [PUBLIC_DIR, PACKAGES_DIR, APPS_DIR, FEATURES_DIR]) walkFiles(dir, scan);
747
+ return violations;
748
+ }
749
+
750
+ /**
751
+ * MPO-17: a `// mpo-drawing` pragma is not itself an exemption. The file
752
+ * must also be a DRAWING member in the spec table. Missing spec → skip
753
+ * (shc can run the oxlint rules before it copies the table).
754
+ */
755
+ function checkUndeclaredDrawings() {
756
+ const specPath = join(ROOT, "docs/theme-propagation-spec.md");
757
+ let registry;
758
+ try {
759
+ registry = readDrawingRegistry(specPath);
760
+ } catch (error) {
761
+ return {
762
+ violations: [`drawing registry: ${error instanceof Error ? error.message : String(error)}`],
763
+ ok: false,
764
+ };
765
+ }
766
+ if (!registry.present) return { violations: [], ok: true };
767
+ /** @type {string[]} */
768
+ const violations = [];
769
+ const scan = (filePath) => {
770
+ if (!/\.(tsx|ts|jsx|js)$/.test(filePath)) return;
771
+ const src = readFileSync(filePath, "utf8");
772
+ if (!DRAWING_PRAGMA.test(src)) return;
773
+ const rel = relative(ROOT, filePath);
774
+ if (isDrawingFile(rel, registry.files) || isDrawingFile(filePath, registry.files)) return;
775
+ violations.push(`${rel} — // mpo-drawing without a DRAWING Members row in ## Drawing files`);
776
+ };
777
+ for (const dir of [PUBLIC_DIR, PACKAGES_DIR, APPS_DIR, FEATURES_DIR]) walkFiles(dir, scan);
778
+ return { violations, ok: true };
779
+ }
780
+
673
781
  /**
674
782
  * Run structural convention checks over a consumer tree.
675
783
  *
@@ -681,6 +789,8 @@ export function runConventionChecks(options = {}) {
681
789
  const css = checkCssInApps();
682
790
  const twins = checkFeaturesTwins();
683
791
  const sizeRecipeEscape = checkSizeRecipeEscape();
792
+ const storyShape = checkStoryFileShape();
793
+ const undeclaredDrawings = checkUndeclaredDrawings();
684
794
  const tokens = checkTransitionTokens();
685
795
  const styleTokens = checkStyleTokens();
686
796
  const themeNames = checkThemeNames();
@@ -729,6 +839,24 @@ export function runConventionChecks(options = {}) {
729
839
  console.error("");
730
840
  }
731
841
 
842
+ if (storyShape.length) {
843
+ failed = true;
844
+ console.error(
845
+ 'Convention (MPO-93 STORY FILE SHAPE): a story file must be classic CSF (`export default meta`, no named `meta` export) or a CSF factory (`export const meta = preview.meta(...)`). Anything else makes Storybook\'s indexer fail the WHOLE gallery with "CSF: missing default export", and no package build ever sees it.\n',
846
+ );
847
+ for (const msg of storyShape) console.error(` ${msg}`);
848
+ console.error("");
849
+ }
850
+
851
+ if (undeclaredDrawings.violations.length) {
852
+ failed = true;
853
+ console.error(
854
+ "Convention (MPO-17 DRAWING REGISTRY): `// mpo-drawing` is only legal on a file listed as a DRAWING member in docs/theme-propagation-spec.md. Adding an undeclared drawing file still fails.\n",
855
+ );
856
+ for (const msg of undeclaredDrawings.violations) console.error(` ${msg}`);
857
+ console.error("");
858
+ }
859
+
732
860
  if (tokens.violations.length) {
733
861
  failed = true;
734
862
  console.error(
package/oxlint.d.ts CHANGED
@@ -2,6 +2,7 @@ export const COLOR_LITERAL: RegExp;
2
2
  export const PALETTE_FILE: RegExp;
3
3
  export const CHROME_PROPS: readonly string[];
4
4
  export const LAYOUT_PROPS: readonly string[];
5
+ export const GEOMETRY_PROPS: readonly string[];
5
6
  export function isPaletteModule(filename: string | undefined): boolean;
6
7
  export function matchColorLiteral(value: unknown): string | undefined;
7
8
  export const plugin: {