@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 +8 -1
- package/drawing-registry.mjs +145 -0
- package/lib/index.js +4 -3
- package/lib/{storybook-CrdwuS_Y.js → storybook-CSzjegfP.js} +2 -0
- package/lib/storybook.js +1 -1
- package/lib/tamaguiWorkspacePaths-ZIcqybtn.js +58 -0
- package/lib/{vite-C7884n3X.js → vite-W9hIeg2_.js} +2 -0
- package/lib/vite.js +1 -1
- package/lib/vitest.js +3 -1
- package/lint.d.ts +2 -0
- package/lint.mjs +129 -1
- package/oxlint.d.ts +1 -0
- package/oxlint.mjs +275 -7
- package/package.json +4 -2
- package/src/drawing-registry.spec.ts +69 -0
- package/src/index.ts +6 -0
- package/src/lint.spec.ts +122 -0
- package/src/oxlint.spec.ts +180 -1
- package/src/storybook.ts +3 -0
- package/src/tamaguiWorkspacePaths.spec.ts +113 -0
- package/src/tamaguiWorkspacePaths.ts +85 -0
- package/src/vite.ts +4 -0
- package/src/vitest.ts +4 -0
- package/types/index.d.ts +1 -0
- package/types/index.d.ts.map +1 -1
- package/types/storybook.d.ts.map +1 -1
- package/types/tamaguiWorkspacePaths.d.ts +35 -0
- package/types/tamaguiWorkspacePaths.d.ts.map +1 -0
- package/types/vite.d.ts.map +1 -1
- package/types/vitest.d.ts.map +1 -1
package/oxlint.mjs
CHANGED
|
@@ -14,12 +14,19 @@
|
|
|
14
14
|
* Tamagui*-prefixed typography escape hatches (TamaguiText, TamaguiHeading,
|
|
15
15
|
* TamaguiH1…, TamaguiParagraph, TamaguiSizableText, TamaguiAnchor) outside
|
|
16
16
|
* public/. House Text/Heading shadows are the bare names (MPO-20).
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
17
|
+
* mpo-conventions/no-chrome-props — ban chrome (background, radius, type,
|
|
18
|
+
* shadow, elevation) as props on imported @multiplatform.one/* components,
|
|
19
|
+
* even at a valid token. Layout props (flex, alignSelf, sibling margins)
|
|
20
|
+
* stay legal. Exceptions come from the spec DRAWING table's CHROME-ALLOW
|
|
21
|
+
* row, not from the code (MPO-17).
|
|
22
|
+
* mpo-conventions/no-raw-geometry — ban numeric height/width/padding/fontSize
|
|
23
|
+
* on those same imports. Silent behind sizeRecipeEscape() and inside a
|
|
24
|
+
* file listed as DRAWING. An undeclared `// mpo-drawing` pragma is a
|
|
25
|
+
* lint.mjs failure, not a skip here.
|
|
21
26
|
*/
|
|
22
27
|
|
|
28
|
+
import { isDrawingFile, readDrawingRegistry } from "./drawing-registry.mjs";
|
|
29
|
+
|
|
23
30
|
/** Hex + functional CSS colour literals. AST-visited only (not a text scan). */
|
|
24
31
|
export const COLOR_LITERAL =
|
|
25
32
|
/#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})\b|(?:rgba?|hsla?)\([^)]*\)/i;
|
|
@@ -29,11 +36,12 @@ export const PALETTE_FILE =
|
|
|
29
36
|
/(?:^|\/)themes\/(base|accent)\.(tsx?|jsx?)$|(?:^|\/)[^/]*palette[^/]*\.(tsx?|jsx?)$/i;
|
|
30
37
|
|
|
31
38
|
const RAW_TYPOGRAPHY_HATCH = /^Tamagui(?:Text|SizableText|Paragraph|Heading|H[1-6]|Anchor)$/;
|
|
39
|
+
const HOUSE_PACKAGE = /^@multiplatform\.one\//;
|
|
32
40
|
|
|
33
41
|
/**
|
|
34
|
-
* Chrome props
|
|
35
|
-
*
|
|
36
|
-
*
|
|
42
|
+
* Chrome props banned on imported `@multiplatform.one/*` components
|
|
43
|
+
* (including Tamagui shorthands). Layout composition through `{...props}`
|
|
44
|
+
* stays legal.
|
|
37
45
|
*/
|
|
38
46
|
export const CHROME_PROPS = Object.freeze([
|
|
39
47
|
"backgroundColor",
|
|
@@ -79,6 +87,206 @@ export const LAYOUT_PROPS = Object.freeze([
|
|
|
79
87
|
"marginVertical",
|
|
80
88
|
]);
|
|
81
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Numeric geometry no-raw-geometry flags on imported house components.
|
|
92
|
+
* Token strings (`"$4"`) and identifiers do not match; only a numeric
|
|
93
|
+
* literal (or one wrapped in sizeRecipeEscape) is in scope.
|
|
94
|
+
*/
|
|
95
|
+
export const GEOMETRY_PROPS = Object.freeze([
|
|
96
|
+
"height",
|
|
97
|
+
"minHeight",
|
|
98
|
+
"maxHeight",
|
|
99
|
+
"width",
|
|
100
|
+
"minWidth",
|
|
101
|
+
"maxWidth",
|
|
102
|
+
"paddingHorizontal",
|
|
103
|
+
"paddingVertical",
|
|
104
|
+
"padding",
|
|
105
|
+
"fontSize",
|
|
106
|
+
]);
|
|
107
|
+
|
|
108
|
+
const CHROME_PROP_SET = new Set(CHROME_PROPS);
|
|
109
|
+
const GEOMETRY_PROP_SET = new Set(GEOMETRY_PROPS);
|
|
110
|
+
|
|
111
|
+
const RULE_OPTIONS_SCHEMA = [
|
|
112
|
+
{
|
|
113
|
+
type: "object",
|
|
114
|
+
additionalProperties: false,
|
|
115
|
+
properties: {
|
|
116
|
+
drawings: { type: "array", items: { type: "string" } },
|
|
117
|
+
chromeAllow: { type: "array", items: { type: "string" } },
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* @param {unknown} context
|
|
124
|
+
* @returns {{ drawings: string[], chromeAllow: Set<string> }}
|
|
125
|
+
*/
|
|
126
|
+
function resolveAllowlists(context) {
|
|
127
|
+
const opt = context?.options?.[0] ?? {};
|
|
128
|
+
if (Array.isArray(opt.drawings) || Array.isArray(opt.chromeAllow)) {
|
|
129
|
+
return {
|
|
130
|
+
drawings: Array.isArray(opt.drawings) ? opt.drawings : [],
|
|
131
|
+
chromeAllow: new Set(Array.isArray(opt.chromeAllow) ? opt.chromeAllow : []),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
const registry = readDrawingRegistry();
|
|
136
|
+
return { drawings: registry.files, chromeAllow: new Set(registry.chromeAllow) };
|
|
137
|
+
} catch {
|
|
138
|
+
return { drawings: [], chromeAllow: new Set() };
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** @param {string | undefined} source */
|
|
143
|
+
function isHousePackage(source) {
|
|
144
|
+
return typeof source === "string" && HOUSE_PACKAGE.test(source);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** @param {{ type?: string, imported?: { name?: string, value?: string }, local?: { name?: string } }} specifier */
|
|
148
|
+
function specifierLocalName(specifier) {
|
|
149
|
+
return specifier.local?.name ?? specifier.imported?.name ?? specifier.imported?.value;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Walk a JSX name node into "Card" or "Menu.Content".
|
|
154
|
+
* @param {unknown} name
|
|
155
|
+
* @returns {string}
|
|
156
|
+
*/
|
|
157
|
+
function jsxName(name) {
|
|
158
|
+
if (!name || typeof name !== "object") return "";
|
|
159
|
+
const node =
|
|
160
|
+
/** @type {{ type?: string, name?: string, object?: unknown, property?: { name?: string } }} */ (
|
|
161
|
+
name
|
|
162
|
+
);
|
|
163
|
+
if (node.type === "JSXIdentifier") return node.name ?? "";
|
|
164
|
+
if (node.type === "JSXMemberExpression") {
|
|
165
|
+
const object = jsxName(node.object);
|
|
166
|
+
const property = node.property?.name ?? "";
|
|
167
|
+
return object && property ? `${object}.${property}` : object || property;
|
|
168
|
+
}
|
|
169
|
+
return "";
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** @param {unknown} attr */
|
|
173
|
+
function jsxAttrName(attr) {
|
|
174
|
+
if (!attr || typeof attr !== "object") return "";
|
|
175
|
+
const node = /** @type {{ type?: string, name?: { type?: string, name?: string } }} */ (attr);
|
|
176
|
+
if (node.type !== "JSXAttribute") return "";
|
|
177
|
+
return node.name?.name ?? "";
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* @param {unknown} value
|
|
182
|
+
* @returns {unknown}
|
|
183
|
+
*/
|
|
184
|
+
function unwrapJsxValue(value) {
|
|
185
|
+
if (!value || typeof value !== "object") return value;
|
|
186
|
+
const node = /** @type {{ type?: string, expression?: unknown }} */ (value);
|
|
187
|
+
if (node.type === "JSXExpressionContainer") return unwrapJsxValue(node.expression);
|
|
188
|
+
return value;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** @param {unknown} value */
|
|
192
|
+
function isNumericLiteral(value) {
|
|
193
|
+
const node = unwrapJsxValue(value);
|
|
194
|
+
if (!node || typeof node !== "object") return false;
|
|
195
|
+
const expr =
|
|
196
|
+
/** @type {{ type?: string, value?: unknown, operator?: string, argument?: unknown }} */ (node);
|
|
197
|
+
if (expr.type === "Literal" && typeof expr.value === "number") return true;
|
|
198
|
+
if (expr.type === "UnaryExpression" && expr.operator === "-" && isNumericLiteral(expr.argument)) {
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** @param {unknown} value */
|
|
205
|
+
function isSizeRecipeEscape(value) {
|
|
206
|
+
const node = unwrapJsxValue(value);
|
|
207
|
+
if (!node || typeof node !== "object") return false;
|
|
208
|
+
const expr =
|
|
209
|
+
/** @type {{ type?: string, callee?: { type?: string, name?: string, property?: { name?: string } } }} */ (
|
|
210
|
+
node
|
|
211
|
+
);
|
|
212
|
+
if (expr.type !== "CallExpression") return false;
|
|
213
|
+
const callee = expr.callee;
|
|
214
|
+
if (!callee) return false;
|
|
215
|
+
if (callee.type === "Identifier" && callee.name === "sizeRecipeEscape") return true;
|
|
216
|
+
if (callee.type === "MemberExpression" && callee.property?.name === "sizeRecipeEscape") {
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* @param {unknown} importNode
|
|
224
|
+
* @param {Set<string>} locals
|
|
225
|
+
* @param {Set<string>} namespaces
|
|
226
|
+
*/
|
|
227
|
+
function collectHouseLocals(importNode, locals, namespaces) {
|
|
228
|
+
const node = /** @type {{ source?: { value?: string }, specifiers?: unknown[] }} */ (importNode);
|
|
229
|
+
if (!isHousePackage(node.source?.value)) return;
|
|
230
|
+
for (const specifier of node.specifiers ?? []) {
|
|
231
|
+
const spec = /** @type {{ type?: string, local?: { name?: string } }} */ (specifier);
|
|
232
|
+
if (spec.type === "ImportNamespaceSpecifier" && spec.local?.name) {
|
|
233
|
+
namespaces.add(spec.local.name);
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
const local = specifierLocalName(spec);
|
|
237
|
+
if (typeof local === "string" && local) locals.add(local);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* @param {string} name
|
|
243
|
+
* @param {Set<string>} locals
|
|
244
|
+
* @param {Set<string>} namespaces
|
|
245
|
+
*/
|
|
246
|
+
function isHouseElement(name, locals, namespaces) {
|
|
247
|
+
if (!name) return false;
|
|
248
|
+
if (locals.has(name)) return true;
|
|
249
|
+
const dot = name.indexOf(".");
|
|
250
|
+
if (dot === -1) return false;
|
|
251
|
+
return namespaces.has(name.slice(0, dot));
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Bare component name for CHROME-ALLOW matching (`Menu.Item` → `Item` and `Menu.Item`).
|
|
256
|
+
* @param {string} name
|
|
257
|
+
*/
|
|
258
|
+
function elementAllowKeys(name) {
|
|
259
|
+
const keys = [name];
|
|
260
|
+
const dot = name.lastIndexOf(".");
|
|
261
|
+
if (dot !== -1) keys.push(name.slice(dot + 1));
|
|
262
|
+
return keys;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* @param {unknown} context
|
|
267
|
+
*/
|
|
268
|
+
function houseJsxVisitors(context, onHouseElement) {
|
|
269
|
+
const filename = context.filename ?? context.getFilename?.() ?? "";
|
|
270
|
+
const { drawings, chromeAllow } = resolveAllowlists(context);
|
|
271
|
+
const locals = new Set();
|
|
272
|
+
const namespaces = new Set();
|
|
273
|
+
return {
|
|
274
|
+
drawings,
|
|
275
|
+
chromeAllow,
|
|
276
|
+
filename,
|
|
277
|
+
visitors: {
|
|
278
|
+
ImportDeclaration(node) {
|
|
279
|
+
collectHouseLocals(node, locals, namespaces);
|
|
280
|
+
},
|
|
281
|
+
JSXOpeningElement(node) {
|
|
282
|
+
const name = jsxName(node.name);
|
|
283
|
+
if (!isHouseElement(name, locals, namespaces)) return;
|
|
284
|
+
onHouseElement({ node, name, filename, drawings, chromeAllow });
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
82
290
|
/**
|
|
83
291
|
* @param {string | undefined} filename
|
|
84
292
|
* @returns {boolean}
|
|
@@ -177,6 +385,66 @@ export const plugin = {
|
|
|
177
385
|
};
|
|
178
386
|
},
|
|
179
387
|
},
|
|
388
|
+
"no-chrome-props": {
|
|
389
|
+
meta: {
|
|
390
|
+
type: "problem",
|
|
391
|
+
docs: {
|
|
392
|
+
description:
|
|
393
|
+
"Ban chrome props (background, radius, type, shadow, elevation) on imported @multiplatform.one/* components. Layout props stay legal.",
|
|
394
|
+
},
|
|
395
|
+
schema: RULE_OPTIONS_SCHEMA,
|
|
396
|
+
messages: {
|
|
397
|
+
chrome:
|
|
398
|
+
"{{prop}} is chrome, not layout — it belongs to the system, not the caller. Pass layout (flex, alignSelf, sibling margin) or a named knob; do not restyle {{name}}.",
|
|
399
|
+
},
|
|
400
|
+
},
|
|
401
|
+
create(context) {
|
|
402
|
+
const { visitors } = houseJsxVisitors(context, ({ node, name, chromeAllow }) => {
|
|
403
|
+
if (elementAllowKeys(name).some((key) => chromeAllow.has(key))) return;
|
|
404
|
+
for (const attr of node.attributes ?? []) {
|
|
405
|
+
const prop = jsxAttrName(attr);
|
|
406
|
+
if (!CHROME_PROP_SET.has(prop)) continue;
|
|
407
|
+
context.report({
|
|
408
|
+
node: attr,
|
|
409
|
+
messageId: "chrome",
|
|
410
|
+
data: { prop, name },
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
return visitors;
|
|
415
|
+
},
|
|
416
|
+
},
|
|
417
|
+
"no-raw-geometry": {
|
|
418
|
+
meta: {
|
|
419
|
+
type: "problem",
|
|
420
|
+
docs: {
|
|
421
|
+
description:
|
|
422
|
+
"Ban numeric geometry on imported @multiplatform.one/* components unless wrapped in sizeRecipeEscape or the file is a declared drawing.",
|
|
423
|
+
},
|
|
424
|
+
schema: RULE_OPTIONS_SCHEMA,
|
|
425
|
+
messages: {
|
|
426
|
+
geometry:
|
|
427
|
+
"Numeric {{prop}} on {{name}} bypasses the size recipe. Wrap with sizeRecipeEscape() or move the drawing into a file listed under DRAWING in the spec.",
|
|
428
|
+
},
|
|
429
|
+
},
|
|
430
|
+
create(context) {
|
|
431
|
+
const { visitors } = houseJsxVisitors(context, ({ node, name, filename, drawings }) => {
|
|
432
|
+
if (isDrawingFile(filename, drawings)) return;
|
|
433
|
+
for (const attr of node.attributes ?? []) {
|
|
434
|
+
const prop = jsxAttrName(attr);
|
|
435
|
+
if (!GEOMETRY_PROP_SET.has(prop)) continue;
|
|
436
|
+
if (isSizeRecipeEscape(attr.value)) continue;
|
|
437
|
+
if (!isNumericLiteral(attr.value)) continue;
|
|
438
|
+
context.report({
|
|
439
|
+
node: attr,
|
|
440
|
+
messageId: "geometry",
|
|
441
|
+
data: { prop, name },
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
return visitors;
|
|
446
|
+
},
|
|
447
|
+
},
|
|
180
448
|
},
|
|
181
449
|
};
|
|
182
450
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@multiplatform.one/config",
|
|
3
|
-
"version": "7.6.
|
|
3
|
+
"version": "7.6.1",
|
|
4
4
|
"description": "Shared build and test configuration presets for multiplatform.one",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"config",
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
"oxlint.d.ts",
|
|
37
37
|
"lint.mjs",
|
|
38
38
|
"lint.d.ts",
|
|
39
|
+
"drawing-registry.mjs",
|
|
39
40
|
"!tsconfig/dist",
|
|
40
41
|
"!tsconfig/types",
|
|
41
42
|
"types",
|
|
@@ -83,6 +84,7 @@
|
|
|
83
84
|
"./tsconfig/app.json": "./tsconfig/app.json",
|
|
84
85
|
"./react-native-config": "./src/react-native.config.cjs",
|
|
85
86
|
"./oxlint": "./oxlint.mjs",
|
|
87
|
+
"./drawing-registry": "./drawing-registry.mjs",
|
|
86
88
|
"./lint": {
|
|
87
89
|
"types": "./lint.d.ts",
|
|
88
90
|
"default": "./lint.mjs",
|
|
@@ -100,7 +102,7 @@
|
|
|
100
102
|
"vite-plugin-external": "^6.2.2",
|
|
101
103
|
"vite-plugin-i18next-loader": "^3.1.3",
|
|
102
104
|
"vitest": "^4.1.5",
|
|
103
|
-
"@multiplatform.one/utils": "7.6.
|
|
105
|
+
"@multiplatform.one/utils": "7.6.1"
|
|
104
106
|
},
|
|
105
107
|
"devDependencies": {
|
|
106
108
|
"tsdown": "^0.21.10",
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
5
|
+
import { DRAWING_PRAGMA, isDrawingFile, readDrawingRegistry } from "../drawing-registry.mjs";
|
|
6
|
+
|
|
7
|
+
const fixtures: string[] = [];
|
|
8
|
+
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
for (const dir of fixtures.splice(0)) {
|
|
11
|
+
rmSync(dir, { recursive: true, force: true });
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
function specWith(members: { drawing?: string; chrome?: string }) {
|
|
16
|
+
const root = mkdtempSync(join(tmpdir(), "mpo-drawing-"));
|
|
17
|
+
fixtures.push(root);
|
|
18
|
+
const drawingMembers = members.drawing ? `Members: **${members.drawing}**` : "Members: *(none)*";
|
|
19
|
+
const chromeMembers = members.chrome ? `Members: **${members.chrome}**` : "Members: *(none)*";
|
|
20
|
+
const markdown = `# Theme
|
|
21
|
+
|
|
22
|
+
## Drawing files
|
|
23
|
+
|
|
24
|
+
| Class | Normative meaning |
|
|
25
|
+
| ------------ | ----------------- |
|
|
26
|
+
| DRAWING | Glyphs. ${drawingMembers} |
|
|
27
|
+
| CHROME-ALLOW | Exceptions. ${chromeMembers} |
|
|
28
|
+
`;
|
|
29
|
+
const specPath = join(root, "spec.md");
|
|
30
|
+
writeFileSync(specPath, markdown);
|
|
31
|
+
return specPath;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe("readDrawingRegistry", () => {
|
|
35
|
+
it("parses DRAWING and CHROME-ALLOW members from the spec table", () => {
|
|
36
|
+
const specPath = specWith({
|
|
37
|
+
drawing: "listing/glyphs/CanvasGlyph.tsx",
|
|
38
|
+
chrome: "Card",
|
|
39
|
+
});
|
|
40
|
+
const registry = readDrawingRegistry(specPath);
|
|
41
|
+
expect(registry.present).toBe(true);
|
|
42
|
+
expect(registry.files).toEqual(["listing/glyphs/CanvasGlyph.tsx"]);
|
|
43
|
+
expect(registry.chromeAllow).toEqual(["Card"]);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("yields empty lists when the spec file is missing", () => {
|
|
47
|
+
const registry = readDrawingRegistry(join(tmpdir(), "no-such-spec.md"));
|
|
48
|
+
expect(registry.present).toBe(false);
|
|
49
|
+
expect(registry.files).toEqual([]);
|
|
50
|
+
expect(registry.chromeAllow).toEqual([]);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe("isDrawingFile / DRAWING_PRAGMA", () => {
|
|
55
|
+
it("matches a registered path suffix", () => {
|
|
56
|
+
expect(
|
|
57
|
+
isDrawingFile("packages/marketplace/src/listing/glyphs/CanvasGlyph.tsx", [
|
|
58
|
+
"listing/glyphs/CanvasGlyph.tsx",
|
|
59
|
+
]),
|
|
60
|
+
).toBe(true);
|
|
61
|
+
expect(isDrawingFile("features/lookout/src/WallScreen.tsx", ["CanvasGlyph.tsx"])).toBe(false);
|
|
62
|
+
expect(isDrawingFile("features/demo/FooCanvasGlyph.tsx", ["CanvasGlyph.tsx"])).toBe(false);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("matches the file pragma", () => {
|
|
66
|
+
expect(DRAWING_PRAGMA.test("// mpo-drawing — canvas mark\nexport const G = 1;\n")).toBe(true);
|
|
67
|
+
expect(DRAWING_PRAGMA.test("export const G = 1;\n")).toBe(false);
|
|
68
|
+
});
|
|
69
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
1
|
export { createStorybookViteConfig, type CreateStorybookViteConfigOptions } from "./storybook";
|
|
2
2
|
export { createViteConfig, type CreateViteConfigOptions } from "./vite";
|
|
3
3
|
export { createVitestConfig, type CreateVitestConfigOptions } from "./vitest";
|
|
4
|
+
export {
|
|
5
|
+
ensureTamaguiWorkspacePaths,
|
|
6
|
+
tamaguiWorkspacePathsFile,
|
|
7
|
+
type EnsureTamaguiWorkspacePathsOptions,
|
|
8
|
+
type EnsureTamaguiWorkspacePathsResult,
|
|
9
|
+
} from "./tamaguiWorkspacePaths";
|
package/src/lint.spec.ts
CHANGED
|
@@ -50,3 +50,125 @@ describe("runConventionChecks", () => {
|
|
|
50
50
|
expect(code).toBe(0);
|
|
51
51
|
});
|
|
52
52
|
});
|
|
53
|
+
|
|
54
|
+
describe("story file shape (MPO-93)", () => {
|
|
55
|
+
function storyTree() {
|
|
56
|
+
const root = makeTree();
|
|
57
|
+
mkdirSync(join(root, "packages", "ui", "src"), { recursive: true });
|
|
58
|
+
return root;
|
|
59
|
+
}
|
|
60
|
+
function run(root: string) {
|
|
61
|
+
const error = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
62
|
+
const log = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
63
|
+
const code = runConventionChecks({ roots: { root } });
|
|
64
|
+
const lines = error.mock.calls.map((c) => String(c[0]));
|
|
65
|
+
error.mockRestore();
|
|
66
|
+
log.mockRestore();
|
|
67
|
+
return { code, lines };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
it("fails a story that exports a named `meta` beside `export default`", () => {
|
|
71
|
+
const root = storyTree();
|
|
72
|
+
writeFileSync(
|
|
73
|
+
join(root, "packages", "ui", "src", "Navbar.stories.tsx"),
|
|
74
|
+
"export const meta = { title: 'Navbar' };\nexport default meta;\nexport const Default = {};\n",
|
|
75
|
+
);
|
|
76
|
+
const { code, lines } = run(root);
|
|
77
|
+
expect(code).toBe(1);
|
|
78
|
+
expect(lines.join("\n")).toMatch(
|
|
79
|
+
/Navbar\.stories\.tsx:1 — exports a named `meta` beside `export default`/,
|
|
80
|
+
);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("fails a story that re-exports `meta` through `export { meta }`", () => {
|
|
84
|
+
const root = storyTree();
|
|
85
|
+
writeFileSync(
|
|
86
|
+
join(root, "packages", "ui", "src", "Carousel.stories.tsx"),
|
|
87
|
+
"const meta = { title: 'Carousel' };\nexport { meta };\nexport default meta;\n",
|
|
88
|
+
);
|
|
89
|
+
const { code, lines } = run(root);
|
|
90
|
+
expect(code).toBe(1);
|
|
91
|
+
expect(lines.join("\n")).toMatch(/Carousel\.stories\.tsx:2 — exports a named `meta`/);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("fails a story with no default export", () => {
|
|
95
|
+
const root = storyTree();
|
|
96
|
+
writeFileSync(
|
|
97
|
+
join(root, "apps", "demo", "Orphan.stories.tsx"),
|
|
98
|
+
"const meta = { title: 'Orphan' };\nexport const Default = {};\n",
|
|
99
|
+
);
|
|
100
|
+
const { code, lines } = run(root);
|
|
101
|
+
expect(code).toBe(1);
|
|
102
|
+
expect(lines.join("\n")).toMatch(/Orphan\.stories\.tsx — no default export/);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("passes classic CSF and a CSF factory file, and ignores non-story modules", () => {
|
|
106
|
+
const root = storyTree();
|
|
107
|
+
writeFileSync(
|
|
108
|
+
join(root, "packages", "ui", "src", "Button.stories.tsx"),
|
|
109
|
+
"const meta = { title: 'Button' };\nexport default meta;\nexport const Default = {};\n",
|
|
110
|
+
);
|
|
111
|
+
writeFileSync(
|
|
112
|
+
join(root, "packages", "ui", "src", "Factory.stories.tsx"),
|
|
113
|
+
"import preview from '#.storybook/preview';\nexport const meta = preview.meta({ title: 'Factory' });\nexport const Default = meta.story({});\n",
|
|
114
|
+
);
|
|
115
|
+
writeFileSync(join(root, "packages", "ui", "src", "meta.ts"), "export const meta = 1;\n");
|
|
116
|
+
const { code } = run(root);
|
|
117
|
+
expect(code).toBe(0);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe("undeclared drawing file (MPO-17)", () => {
|
|
122
|
+
function drawingSpec(root: string, drawingMember: string) {
|
|
123
|
+
mkdirSync(join(root, "docs"), { recursive: true });
|
|
124
|
+
writeFileSync(
|
|
125
|
+
join(root, "docs", "theme-propagation-spec.md"),
|
|
126
|
+
[
|
|
127
|
+
"# Theme",
|
|
128
|
+
"",
|
|
129
|
+
"## Drawing files",
|
|
130
|
+
"",
|
|
131
|
+
"| Class | Normative meaning |",
|
|
132
|
+
"| ------------ | ----------------- |",
|
|
133
|
+
`| DRAWING | Glyphs. Members: ${drawingMember} |`,
|
|
134
|
+
"| CHROME-ALLOW | Exceptions. Members: *(none)* |",
|
|
135
|
+
"",
|
|
136
|
+
].join("\n"),
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function run(root: string) {
|
|
141
|
+
const error = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
142
|
+
const log = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
143
|
+
const code = runConventionChecks({ roots: { root } });
|
|
144
|
+
const lines = error.mock.calls.map((c) => String(c[0]));
|
|
145
|
+
error.mockRestore();
|
|
146
|
+
log.mockRestore();
|
|
147
|
+
return { code, lines };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
it("fails a // mpo-drawing file that is not a DRAWING member", () => {
|
|
151
|
+
const root = makeTree();
|
|
152
|
+
drawingSpec(root, "*(none)*");
|
|
153
|
+
writeFileSync(
|
|
154
|
+
join(root, "features", "demo", "CanvasGlyph.tsx"),
|
|
155
|
+
"// mpo-drawing\nexport const Glyph = () => null;\n",
|
|
156
|
+
);
|
|
157
|
+
const { code, lines } = run(root);
|
|
158
|
+
expect(code).toBe(1);
|
|
159
|
+
expect(lines.join("\n")).toMatch(
|
|
160
|
+
/CanvasGlyph\.tsx — \/\/ mpo-drawing without a DRAWING Members row/,
|
|
161
|
+
);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("passes a pragma whose path is listed as a DRAWING member", () => {
|
|
165
|
+
const root = makeTree();
|
|
166
|
+
drawingSpec(root, "**features/demo/CanvasGlyph.tsx**");
|
|
167
|
+
writeFileSync(
|
|
168
|
+
join(root, "features", "demo", "CanvasGlyph.tsx"),
|
|
169
|
+
"// mpo-drawing\nexport const Glyph = () => null;\n",
|
|
170
|
+
);
|
|
171
|
+
const { code } = run(root);
|
|
172
|
+
expect(code).toBe(0);
|
|
173
|
+
});
|
|
174
|
+
});
|