@multiplatform.one/config 7.34.4 → 7.35.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/README.md +48 -0
- package/design-lint.mjs +524 -0
- package/lib/index.js +3 -3
- package/lib/{storybook-Dxwh_fl2.js → storybook-Ba0KCHBo.js} +9 -2
- package/lib/storybook.js +1 -1
- package/lib/{tamaguiWorkspacePaths-B6aRC2BU.js → tamaguiWorkspacePaths-DR3dud79.js} +3 -2
- package/lib/{vite-n3XSaftn.js → vite-DkidIRYX.js} +1 -1
- package/lib/vite.js +2 -2
- package/lib/vitest.js +1 -1
- package/lint.mjs +642 -69
- package/package.json +13 -7
- package/src/lint.spec.ts +511 -5
- package/src/phosphorTokenColor.spec.ts +7 -1
- package/src/phosphorTokenColor.ts +3 -2
- package/src/storybook.spec.ts +7 -0
- package/src/storybook.ts +11 -1
- package/types/phosphorTokenColor.d.ts.map +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@multiplatform.one/config",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.35.0",
|
|
4
4
|
"description": "Shared build and test configuration presets for multiplatform.one",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"config",
|
|
@@ -25,6 +25,9 @@
|
|
|
25
25
|
"url": "git+https://git.corp.bitspur.com/multiplatform.one/mpo.git",
|
|
26
26
|
"directory": "public/config"
|
|
27
27
|
},
|
|
28
|
+
"bin": {
|
|
29
|
+
"mpo-design-lint": "./design-lint.mjs"
|
|
30
|
+
},
|
|
28
31
|
"source": "src/index.ts",
|
|
29
32
|
"files": [
|
|
30
33
|
"src",
|
|
@@ -42,7 +45,8 @@
|
|
|
42
45
|
"!tsconfig/dist",
|
|
43
46
|
"!tsconfig/types",
|
|
44
47
|
"types",
|
|
45
|
-
"!types/.tsbuildinfo"
|
|
48
|
+
"!types/.tsbuildinfo",
|
|
49
|
+
"design-lint.mjs"
|
|
46
50
|
],
|
|
47
51
|
"type": "module",
|
|
48
52
|
"sideEffects": false,
|
|
@@ -93,25 +97,26 @@
|
|
|
93
97
|
"types": "./lint.d.ts",
|
|
94
98
|
"default": "./lint.mjs",
|
|
95
99
|
"import": "./lint.mjs"
|
|
96
|
-
}
|
|
100
|
+
},
|
|
101
|
+
"./design-lint": "./design-lint.mjs"
|
|
97
102
|
},
|
|
98
103
|
"publishConfig": {
|
|
99
104
|
"access": "public"
|
|
100
105
|
},
|
|
101
106
|
"dependencies": {
|
|
102
|
-
"@multiplatform.one/utils": "^7.
|
|
107
|
+
"@multiplatform.one/utils": "^7.35.0",
|
|
103
108
|
"@tamagui/vite-plugin": "2.7.6",
|
|
104
109
|
"@vitejs/plugin-react": "^6.1.2",
|
|
105
110
|
"dotenv": "^17.4.2",
|
|
106
111
|
"fast-flow-transform": "^0.0.3",
|
|
112
|
+
"typescript": "~5.9.3",
|
|
107
113
|
"vite": "^8.0.10",
|
|
108
114
|
"vite-plugin-external": "^6.2.2",
|
|
109
115
|
"vite-plugin-i18next-loader": "^3.1.3",
|
|
110
116
|
"vitest": "^4.1.5"
|
|
111
117
|
},
|
|
112
118
|
"devDependencies": {
|
|
113
|
-
"tsdown": "^0.21.10"
|
|
114
|
-
"typescript": "~5.9.3"
|
|
119
|
+
"tsdown": "^0.21.10"
|
|
115
120
|
},
|
|
116
121
|
"peerDependencies": {
|
|
117
122
|
"@playwright/test": ">=1.20.0",
|
|
@@ -139,6 +144,7 @@
|
|
|
139
144
|
"scripts": {
|
|
140
145
|
"typecheck": "tsgo --noEmit",
|
|
141
146
|
"build": "rm -rf lib types 2>/dev/null && tsc -p tsconfig.build.json && tsdown",
|
|
142
|
-
"test": "vitest run --coverage --coverage.provider=v8 --coverage.reporter=text --coverage.reporter=lcov --coverage.reporter=html"
|
|
147
|
+
"test": "pnpm test:design && vitest run --coverage --coverage.provider=v8 --coverage.reporter=text --coverage.reporter=lcov --coverage.reporter=html",
|
|
148
|
+
"test:design": "node --test design-lint.spec.mjs"
|
|
143
149
|
}
|
|
144
150
|
}
|
package/src/lint.spec.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
@@ -567,10 +567,10 @@ describe("a scaffolded project, where apps/one is renamed and public/ is pruned
|
|
|
567
567
|
}
|
|
568
568
|
|
|
569
569
|
function animationRegistry(root: string) {
|
|
570
|
-
mkdirSync(join(root, "public", "theme", "src", "theme"
|
|
570
|
+
mkdirSync(join(root, "public", "theme", "src", "theme"), { recursive: true });
|
|
571
571
|
writeFileSync(
|
|
572
|
-
join(root, "public", "theme", "src", "theme", "
|
|
573
|
-
'export const animationConfig = {\n medium: "ease-
|
|
572
|
+
join(root, "public", "theme", "src", "theme", "motionTiming.ts"),
|
|
573
|
+
'export const animationConfig = {\n medium: "300ms ease-out",\n "100ms": "100ms ease-out",\n quickest: "80ms ease-out",\n} as const;\n',
|
|
574
574
|
);
|
|
575
575
|
}
|
|
576
576
|
|
|
@@ -613,6 +613,13 @@ describe("a scaffolded project, where apps/one is renamed and public/ is pruned
|
|
|
613
613
|
expect(errors).toMatch(/apps\/demo\/App\.tsx:1 — transition="mediun"/);
|
|
614
614
|
});
|
|
615
615
|
|
|
616
|
+
it.each(["100ms", "quickest", "ease"])("validates v5 transition %s", (token) => {
|
|
617
|
+
const root = makeTree();
|
|
618
|
+
animationRegistry(root);
|
|
619
|
+
writeFileSync(join(root, "apps", "demo", "App.tsx"), `<View transition="${token}" />;\n`);
|
|
620
|
+
expect(run(root).code).toBe(token === "ease" ? 1 : 0);
|
|
621
|
+
});
|
|
622
|
+
|
|
616
623
|
it("passes a registered transition when the registry is readable", () => {
|
|
617
624
|
const root = makeTree();
|
|
618
625
|
animationRegistry(root);
|
|
@@ -915,7 +922,7 @@ describe("LC-24 enter/exit driver (MPO-327)", () => {
|
|
|
915
922
|
const root = makeTree();
|
|
916
923
|
writeFileSync(
|
|
917
924
|
join(root, "apps", "demo", "App.tsx"),
|
|
918
|
-
"export const App = () => <View enterStyle={{ opacity: 0 }} {...transitionProps(knobProps.
|
|
925
|
+
"export const App = () => <View enterStyle={{ opacity: 0 }} {...transitionProps(knobProps.motion.overlay)} />;\n",
|
|
919
926
|
);
|
|
920
927
|
const { code, errors } = run(root);
|
|
921
928
|
expect(errors).not.toMatch(/LC-24/);
|
|
@@ -1043,3 +1050,502 @@ ${csfTail}`,
|
|
|
1043
1050
|
expect(run(root).code).toBe(0);
|
|
1044
1051
|
});
|
|
1045
1052
|
});
|
|
1053
|
+
|
|
1054
|
+
describe("retired scale knobs (LC-100 TOKEN-SCALE, check 14)", () => {
|
|
1055
|
+
// Built, not written: scripts/mr-job-inputs reads a literal package import in
|
|
1056
|
+
// any file, fixtures included, as a workspace dependency of this package.
|
|
1057
|
+
const house = "@multiplatform.one";
|
|
1058
|
+
|
|
1059
|
+
function run(root: string) {
|
|
1060
|
+
const error = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
1061
|
+
const log = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
1062
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
1063
|
+
const code = runConventionChecks({ roots: { root } });
|
|
1064
|
+
const messages = error.mock.calls.map((call) => String(call[0])).join("\n");
|
|
1065
|
+
error.mockRestore();
|
|
1066
|
+
log.mockRestore();
|
|
1067
|
+
warn.mockRestore();
|
|
1068
|
+
return { code, messages };
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
function source(root: string, rel: string, body: string) {
|
|
1072
|
+
const full = join(root, rel);
|
|
1073
|
+
mkdirSync(join(full, ".."), { recursive: true });
|
|
1074
|
+
writeFileSync(full, body);
|
|
1075
|
+
return rel;
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
it("fails a planted density knob in <Preset overrides>, naming file and line", () => {
|
|
1079
|
+
const root = makeTree();
|
|
1080
|
+
const file = source(
|
|
1081
|
+
root,
|
|
1082
|
+
"features/demo/Dense.tsx",
|
|
1083
|
+
`import { Preset } from "${house}/theme";
|
|
1084
|
+
export const Dense = () => (
|
|
1085
|
+
<Preset overrides={{ borderRadius: "full", density: "compact" }}>{null}</Preset>
|
|
1086
|
+
);
|
|
1087
|
+
`,
|
|
1088
|
+
);
|
|
1089
|
+
const { code, messages } = run(root);
|
|
1090
|
+
expect(code).toBe(1);
|
|
1091
|
+
expect(messages).toContain("LC-100 TOKEN-SCALE");
|
|
1092
|
+
expect(messages).toContain(`${file}:3 — \`density\` knob in a knob literal`);
|
|
1093
|
+
});
|
|
1094
|
+
|
|
1095
|
+
it("fails size and space keys in a preset's knobs, its state knobs and a typed literal", () => {
|
|
1096
|
+
const root = makeTree();
|
|
1097
|
+
const file = source(
|
|
1098
|
+
root,
|
|
1099
|
+
"public/theme/src/theme/presets.ts",
|
|
1100
|
+
`import type { Knobs, Preset } from "./knobs";
|
|
1101
|
+
export const roomy: Preset = {
|
|
1102
|
+
theme: "light",
|
|
1103
|
+
knobs: {
|
|
1104
|
+
borderRadius: "medium",
|
|
1105
|
+
space: "large",
|
|
1106
|
+
hover: { size: "small" },
|
|
1107
|
+
},
|
|
1108
|
+
intents: {},
|
|
1109
|
+
tints: [],
|
|
1110
|
+
};
|
|
1111
|
+
export const loose: Partial<Knobs> = { size: "large" };
|
|
1112
|
+
`,
|
|
1113
|
+
);
|
|
1114
|
+
const { code, messages } = run(root);
|
|
1115
|
+
expect(code).toBe(1);
|
|
1116
|
+
expect(messages).toContain(`${file}:6 — \`space\` knob in a knob literal`);
|
|
1117
|
+
expect(messages).toContain(`${file}:7 — \`size\` knob in a knob literal`);
|
|
1118
|
+
expect(messages).toContain(`${file}:12 — \`size\` knob in a knob literal`);
|
|
1119
|
+
});
|
|
1120
|
+
|
|
1121
|
+
it("fails a compact prop on a house Button and a word size on <Surface>", () => {
|
|
1122
|
+
const root = makeTree();
|
|
1123
|
+
const app = source(
|
|
1124
|
+
root,
|
|
1125
|
+
"apps/demo/Toolbar.tsx",
|
|
1126
|
+
`import { Button } from "${house}/forms";
|
|
1127
|
+
import { Surface } from "${house}/theme";
|
|
1128
|
+
export const Toolbar = () => (
|
|
1129
|
+
<Surface size="small">
|
|
1130
|
+
<Button compact onPress={() => {}}>Save</Button>
|
|
1131
|
+
</Surface>
|
|
1132
|
+
);
|
|
1133
|
+
`,
|
|
1134
|
+
);
|
|
1135
|
+
const catalog = source(
|
|
1136
|
+
root,
|
|
1137
|
+
"public/components/src/Bar.tsx",
|
|
1138
|
+
`export const Bar = () => <Surface density="compact"><Chip compact={true} /></Surface>;
|
|
1139
|
+
`,
|
|
1140
|
+
);
|
|
1141
|
+
const { code, messages } = run(root);
|
|
1142
|
+
expect(code).toBe(1);
|
|
1143
|
+
expect(messages).toContain(`${app}:4 — \`<Surface size="small">\``);
|
|
1144
|
+
expect(messages).toContain(`${app}:5 — \`compact\` on <Button>`);
|
|
1145
|
+
expect(messages).toContain(`${catalog}:1 — \`<Surface density>\``);
|
|
1146
|
+
expect(messages).toContain(`${catalog}:1 — \`compact\` on <Chip>`);
|
|
1147
|
+
});
|
|
1148
|
+
|
|
1149
|
+
it("fails a data-density attribute and a density value outside a knob literal", () => {
|
|
1150
|
+
const root = makeTree();
|
|
1151
|
+
const file = source(
|
|
1152
|
+
root,
|
|
1153
|
+
"public/forms/src/Field.tsx",
|
|
1154
|
+
`const attrs = { "data-density": "compact" };
|
|
1155
|
+
export const mode = { density: "comfortable" };
|
|
1156
|
+
export const Field = () => <div data-density="compact" {...attrs} />;
|
|
1157
|
+
`,
|
|
1158
|
+
);
|
|
1159
|
+
const { code, messages } = run(root);
|
|
1160
|
+
expect(code).toBe(1);
|
|
1161
|
+
expect(messages).toContain(`${file}:1 — \`data-density\` attribute`);
|
|
1162
|
+
expect(messages).toContain(`${file}:2 — \`density: "comfortable"\``);
|
|
1163
|
+
expect(messages).toContain(`${file}:3 — \`data-density\` attribute`);
|
|
1164
|
+
});
|
|
1165
|
+
|
|
1166
|
+
it("passes size tokens, an app's own compact prop, comments, strings and specs", () => {
|
|
1167
|
+
const root = makeTree();
|
|
1168
|
+
source(
|
|
1169
|
+
root,
|
|
1170
|
+
"apps/demo/Panel.tsx",
|
|
1171
|
+
`import { Button } from "${house}/forms";
|
|
1172
|
+
import { Surface } from "${house}/theme";
|
|
1173
|
+
function Local({ compact }: { compact?: boolean }) {
|
|
1174
|
+
return compact ? null : <Button size="$3">Go</Button>;
|
|
1175
|
+
}
|
|
1176
|
+
export const Panel = () => (
|
|
1177
|
+
<Surface size="$3">
|
|
1178
|
+
<Local
|
|
1179
|
+
// the old compact prop is gone; density: "compact" lives in history
|
|
1180
|
+
compact
|
|
1181
|
+
/>
|
|
1182
|
+
<Button size={"$3"}>{"density: compact"}</Button>
|
|
1183
|
+
</Surface>
|
|
1184
|
+
);
|
|
1185
|
+
`,
|
|
1186
|
+
);
|
|
1187
|
+
source(
|
|
1188
|
+
root,
|
|
1189
|
+
"public/theme/src/theme/cookies.spec.ts",
|
|
1190
|
+
`export const stale = { density: "compact", size: "small", "data-density": 1 };
|
|
1191
|
+
`,
|
|
1192
|
+
);
|
|
1193
|
+
expect(run(root).code).toBe(0);
|
|
1194
|
+
});
|
|
1195
|
+
});
|
|
1196
|
+
|
|
1197
|
+
describe("the v5 theme registry", () => {
|
|
1198
|
+
it("accepts built v5 children and rejects retired v4 names and keys", () => {
|
|
1199
|
+
const root = makeTree();
|
|
1200
|
+
symlinkSync(join(process.cwd(), "../../node_modules"), join(root, "node_modules"), "dir");
|
|
1201
|
+
const app = join(root, "apps/demo/App.tsx");
|
|
1202
|
+
const error = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
1203
|
+
const log = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
1204
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
1205
|
+
try {
|
|
1206
|
+
writeFileSync(
|
|
1207
|
+
app,
|
|
1208
|
+
'export const App = () => <View theme="gray"><View theme="purple" color="$neutral9" /><View theme="SliderActive" color="$teal9" backgroundColor="$backgroundPress" borderColor="$borderColor" /><View theme="active" color="$color0" /><View theme="alt1" color="$accentColor" /><View color="$syntaxBlue" /><View color="$colorTransparent" /></View>;',
|
|
1209
|
+
);
|
|
1210
|
+
expect(runConventionChecks({ roots: { root } })).toBe(0);
|
|
1211
|
+
writeFileSync(
|
|
1212
|
+
app,
|
|
1213
|
+
'export const App = () => <View theme="SwitchThumb"><View theme="mauve" /><View theme="teal" /><View theme="neutral" /><View theme="surface1" color="$amber9" /><View backgroundColor="$backgroundStrong" /><View backgroundColor="$backgroundTransparent" /><View color="$colorTransparent" /></View>;',
|
|
1214
|
+
);
|
|
1215
|
+
expect(runConventionChecks({ roots: { root } })).toBe(1);
|
|
1216
|
+
const errors = error.mock.calls.flat().join("\n");
|
|
1217
|
+
expect(errors).toContain('theme="SwitchThumb"');
|
|
1218
|
+
expect(errors).toContain('theme="mauve"');
|
|
1219
|
+
expect(errors).toContain('theme="teal"');
|
|
1220
|
+
expect(errors).toContain('theme="neutral"');
|
|
1221
|
+
expect(errors).toContain('theme="surface1"');
|
|
1222
|
+
for (const key of ["amber9", "backgroundStrong", "backgroundTransparent"]) {
|
|
1223
|
+
expect(errors).toContain(`$${key}`);
|
|
1224
|
+
}
|
|
1225
|
+
} finally {
|
|
1226
|
+
error.mockRestore();
|
|
1227
|
+
log.mockRestore();
|
|
1228
|
+
warn.mockRestore();
|
|
1229
|
+
}
|
|
1230
|
+
});
|
|
1231
|
+
});
|
|
1232
|
+
|
|
1233
|
+
describe("layout rules (LC-101 ONE-SIDEBAR, LC-102 HUG-CONTENT, LC-103 RESPONSIVE-PARITY)", () => {
|
|
1234
|
+
function run(root: string) {
|
|
1235
|
+
const error = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
1236
|
+
const log = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
1237
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
1238
|
+
const code = runConventionChecks({ roots: { root } });
|
|
1239
|
+
const messages = error.mock.calls.map((call) => String(call[0])).join("\n");
|
|
1240
|
+
error.mockRestore();
|
|
1241
|
+
log.mockRestore();
|
|
1242
|
+
warn.mockRestore();
|
|
1243
|
+
return { code, messages };
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
function layoutTree(baselines: {
|
|
1247
|
+
secondSidebar?: string[];
|
|
1248
|
+
hugContent?: Record<string, number>;
|
|
1249
|
+
}) {
|
|
1250
|
+
const root = makeTree();
|
|
1251
|
+
mkdirSync(join(root, "docs"), { recursive: true });
|
|
1252
|
+
writeFileSync(
|
|
1253
|
+
join(root, "docs", "layout-rule-baselines.json"),
|
|
1254
|
+
JSON.stringify({ secondSidebar: [], hugContent: {}, ...baselines }),
|
|
1255
|
+
);
|
|
1256
|
+
return root;
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
const write = (root: string, rel: string, src: string) => {
|
|
1260
|
+
const path = join(root, rel);
|
|
1261
|
+
mkdirSync(join(path, ".."), { recursive: true });
|
|
1262
|
+
writeFileSync(path, src);
|
|
1263
|
+
};
|
|
1264
|
+
|
|
1265
|
+
it("fails a page that renders its own sidebar column beside the shell nav", () => {
|
|
1266
|
+
const root = layoutTree({});
|
|
1267
|
+
write(
|
|
1268
|
+
root,
|
|
1269
|
+
"features/demo/List.tsx",
|
|
1270
|
+
"export const L = () => <XStack><FacetSidebar /><Rows /></XStack>;\n",
|
|
1271
|
+
);
|
|
1272
|
+
const { code, messages } = run(root);
|
|
1273
|
+
expect(code).toBe(1);
|
|
1274
|
+
expect(messages).toContain("LC-101");
|
|
1275
|
+
expect(messages).toContain("features/demo/List.tsx:1 — <FacetSidebar>");
|
|
1276
|
+
});
|
|
1277
|
+
|
|
1278
|
+
it("passes a page that hands its panel to the shell, a shell, and a sidebar's own file", () => {
|
|
1279
|
+
const root = layoutTree({});
|
|
1280
|
+
write(
|
|
1281
|
+
root,
|
|
1282
|
+
"features/demo/List.tsx",
|
|
1283
|
+
"export const L = () => { useShellSidebarPanel({ title: 'x', content: <FacetSidebar fill /> }); return null; };\n",
|
|
1284
|
+
);
|
|
1285
|
+
write(root, "features/demo/AppShell.tsx", "export const S = () => <NavSidebar />;\n");
|
|
1286
|
+
write(root, "features/demo/FacetSidebar.tsx", "export const F = () => <InnerSidebar />;\n");
|
|
1287
|
+
write(root, "features/demo/Notes.tsx", "// <OldSidebar /> was removed\nexport const N = 1;\n");
|
|
1288
|
+
expect(run(root).code).toBe(0);
|
|
1289
|
+
});
|
|
1290
|
+
|
|
1291
|
+
it("holds pre-rule sidebar sites to the baseline and fails a stale row", () => {
|
|
1292
|
+
const root = layoutTree({
|
|
1293
|
+
secondSidebar: ["features/demo/Form.tsx", "features/demo/Gone.tsx"],
|
|
1294
|
+
});
|
|
1295
|
+
write(root, "features/demo/Form.tsx", "export const F = () => <FormSidebar />;\n");
|
|
1296
|
+
const { code, messages } = run(root);
|
|
1297
|
+
expect(code).toBe(1);
|
|
1298
|
+
expect(messages).not.toContain("features/demo/Form.tsx:1");
|
|
1299
|
+
expect(messages).toContain("features/demo/Gone.tsx — secondSidebar baseline row has no site");
|
|
1300
|
+
});
|
|
1301
|
+
|
|
1302
|
+
it("ratchets spread justification: over the baseline fails, under it asks to lower the row", () => {
|
|
1303
|
+
const root = layoutTree({
|
|
1304
|
+
hugContent: { "features/demo/Row.tsx": 1, "features/demo/Low.tsx": 2 },
|
|
1305
|
+
});
|
|
1306
|
+
write(
|
|
1307
|
+
root,
|
|
1308
|
+
"features/demo/Row.tsx",
|
|
1309
|
+
'export const R = () => <><XStack justifyContent="space-between" /><XStack justifyContent="space-around" /></>;\n',
|
|
1310
|
+
);
|
|
1311
|
+
write(root, "features/demo/Low.tsx", 'export const s = { justifyContent: "space-between" };\n');
|
|
1312
|
+
write(
|
|
1313
|
+
root,
|
|
1314
|
+
"features/demo/Fine.tsx",
|
|
1315
|
+
'// justifyContent="space-between" in a comment is not a layout\nexport const F = () => <XStack gap="$2" />;\n',
|
|
1316
|
+
);
|
|
1317
|
+
const { code, messages } = run(root);
|
|
1318
|
+
expect(code).toBe(1);
|
|
1319
|
+
expect(messages).toContain(
|
|
1320
|
+
'features/demo/Row.tsx:1 — justifyContent="space-around" (2 in file, baseline 1)',
|
|
1321
|
+
);
|
|
1322
|
+
expect(messages).toContain("features/demo/Low.tsx — 1 spread justifications, baseline says 2");
|
|
1323
|
+
expect(messages).not.toContain("Fine.tsx");
|
|
1324
|
+
});
|
|
1325
|
+
|
|
1326
|
+
it("fails content hidden on phones unless it names its phone twin", () => {
|
|
1327
|
+
const root = layoutTree({});
|
|
1328
|
+
write(
|
|
1329
|
+
root,
|
|
1330
|
+
"features/demo/Toolbar.tsx",
|
|
1331
|
+
[
|
|
1332
|
+
"export const T = () => (",
|
|
1333
|
+
" <XStack>",
|
|
1334
|
+
' <Button $max-sm={{ display: "none" }}>Export</Button>',
|
|
1335
|
+
' <Chip display="none" $md={{ display: "flex" }}>Draft</Chip>',
|
|
1336
|
+
" {/* responsive-parity: the ⋯ menu carries Print on phones */}",
|
|
1337
|
+
' <Button $max-sm={{ display: "none" }}>Print</Button>',
|
|
1338
|
+
' <Spacer display="none" />',
|
|
1339
|
+
" </XStack>",
|
|
1340
|
+
");",
|
|
1341
|
+
"",
|
|
1342
|
+
].join("\n"),
|
|
1343
|
+
);
|
|
1344
|
+
const { code, messages } = run(root);
|
|
1345
|
+
expect(code).toBe(1);
|
|
1346
|
+
expect(messages).toContain("features/demo/Toolbar.tsx:3 — hidden on phones");
|
|
1347
|
+
expect(messages).toContain("features/demo/Toolbar.tsx:4 — hidden on phones");
|
|
1348
|
+
expect(messages).not.toContain("Toolbar.tsx:6");
|
|
1349
|
+
expect(messages).not.toContain("Toolbar.tsx:7");
|
|
1350
|
+
});
|
|
1351
|
+
|
|
1352
|
+
it("skips every layout check in a project without the baselines (docs/ pruned)", () => {
|
|
1353
|
+
const root = makeTree();
|
|
1354
|
+
write(
|
|
1355
|
+
root,
|
|
1356
|
+
"features/demo/List.tsx",
|
|
1357
|
+
'export const L = () => <XStack justifyContent="space-between"><FacetSidebar /></XStack>;\n',
|
|
1358
|
+
);
|
|
1359
|
+
expect(run(root).code).toBe(0);
|
|
1360
|
+
});
|
|
1361
|
+
});
|
|
1362
|
+
|
|
1363
|
+
function runOverlayChecks(file: string, body: string) {
|
|
1364
|
+
const root = makeTree();
|
|
1365
|
+
const full = join(root, file);
|
|
1366
|
+
if (file.startsWith("apps/"))
|
|
1367
|
+
mkdirSync(join(root, "features", file.split("/")[1]), { recursive: true });
|
|
1368
|
+
mkdirSync(join(full, ".."), { recursive: true });
|
|
1369
|
+
writeFileSync(full, body);
|
|
1370
|
+
const error = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
1371
|
+
const log = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
1372
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
1373
|
+
try {
|
|
1374
|
+
const code = runConventionChecks({ roots: { root } });
|
|
1375
|
+
return { code, messages: error.mock.calls.map((call) => String(call[0])).join("\n") };
|
|
1376
|
+
} finally {
|
|
1377
|
+
error.mockRestore();
|
|
1378
|
+
log.mockRestore();
|
|
1379
|
+
warn.mockRestore();
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
describe("one shadow ladder (LC-85 ONE SHADOW LADDER, check 15)", () => {
|
|
1384
|
+
it.each([
|
|
1385
|
+
["public/forms/src/Panel.tsx", 'const style = { boxShadow: "0 1px 2px rgba(0,0,0,0.2)" };', 1],
|
|
1386
|
+
[
|
|
1387
|
+
"features/demo/Panel.tsx",
|
|
1388
|
+
'const S = "0px 8px 28px rgba(0,0,0,0.12)";\nexport const Panel = () => <View style={{ boxShadow: S }} />;',
|
|
1389
|
+
2,
|
|
1390
|
+
],
|
|
1391
|
+
[
|
|
1392
|
+
"packages/demo/Panel.tsx",
|
|
1393
|
+
"export const Panel = () => <View boxShadow={`0 0 2px ${color}`} />;",
|
|
1394
|
+
1,
|
|
1395
|
+
],
|
|
1396
|
+
["features/demo/Panel.tsx", 'export const Panel = () => <View boxShadow="0 0 red" />;', 1],
|
|
1397
|
+
[
|
|
1398
|
+
"features/demo/Panel.tsx",
|
|
1399
|
+
'const shadow: string = "0 1px 2px black";\nexport const Panel = () => <View style={{ boxShadow: shadow }} />;',
|
|
1400
|
+
2,
|
|
1401
|
+
],
|
|
1402
|
+
])("rejects owned shadows in %s", (file, body, line) => {
|
|
1403
|
+
const result = runOverlayChecks(file, body);
|
|
1404
|
+
expect(result.code).toBe(1);
|
|
1405
|
+
expect(result.messages).toContain("check 15");
|
|
1406
|
+
expect(result.messages).toContain(`${file}:${line}`);
|
|
1407
|
+
});
|
|
1408
|
+
it.each([
|
|
1409
|
+
["public/forms/src/Panel.tsx", 'const style = { boxShadow: "none" };'],
|
|
1410
|
+
["features/demo/Panel.tsx", 'const style = { boxShadow: "" };'],
|
|
1411
|
+
["features/demo/Panel.tsx", "const style = { boxShadow: chrome.shadow };"],
|
|
1412
|
+
["features/demo/Panel.tsx", "const style = { boxShadow: resolveShadow() };"],
|
|
1413
|
+
[
|
|
1414
|
+
"public/connectkit/src/Panel.tsx",
|
|
1415
|
+
'const style = { boxShadow: "0 1px 2px rgba(0,0,0,0.2)" };',
|
|
1416
|
+
],
|
|
1417
|
+
[
|
|
1418
|
+
"public/web3/src/connectkit/Panel.tsx",
|
|
1419
|
+
'const style = { boxShadow: "0 1px 2px rgba(0,0,0,0.2)" };',
|
|
1420
|
+
],
|
|
1421
|
+
[
|
|
1422
|
+
"public/theme/src/theme/resolveKnobs.ts",
|
|
1423
|
+
'const style = { boxShadow: "0 1px 2px rgba(0,0,0,0.2)" };',
|
|
1424
|
+
],
|
|
1425
|
+
[
|
|
1426
|
+
"apps/one/webext/content_scripts/Panel.tsx",
|
|
1427
|
+
'const style = { boxShadow: "0 1px 2px rgba(0,0,0,0.2)" };',
|
|
1428
|
+
],
|
|
1429
|
+
["features/demo/Panel.spec.tsx", 'const style = { boxShadow: "0 1px 2px rgba(0,0,0,0.2)" };'],
|
|
1430
|
+
["features/demo/Panel.tsx", '// boxShadow: "0 1px 2px rgba(0,0,0,0.2)"'],
|
|
1431
|
+
])("allows recipe paint and explicit exemptions in %s", (file, body) => {
|
|
1432
|
+
const result = runOverlayChecks(file, body);
|
|
1433
|
+
expect(result.code).toBe(0);
|
|
1434
|
+
expect(result.messages).not.toContain("check 15");
|
|
1435
|
+
});
|
|
1436
|
+
});
|
|
1437
|
+
|
|
1438
|
+
describe("one overlay frame (E-OVERLAY ONE FRAME, check 16)", () => {
|
|
1439
|
+
it.each([
|
|
1440
|
+
"export const Panel = () => <Popover.Content />;",
|
|
1441
|
+
"export const Panel = () => <Dialog.Content />;",
|
|
1442
|
+
"export const Panel = () => <AlertDialog.Content />;",
|
|
1443
|
+
"export const Panel = () => <DialogContent elevation={knobProps.elevation} />;",
|
|
1444
|
+
"export const Panel = () => <Sheet.Frame elevation={knobProps.overlayElevation} />;",
|
|
1445
|
+
"export const Panel = () => <TooltipContent elevation={open ? knobProps.elevation : undefined} />;",
|
|
1446
|
+
])("rejects %s", (body) => {
|
|
1447
|
+
const file = "features/demo/Panel.tsx";
|
|
1448
|
+
const result = runOverlayChecks(file, body);
|
|
1449
|
+
expect(result.code).toBe(1);
|
|
1450
|
+
expect(result.messages).toContain("check 16");
|
|
1451
|
+
expect(result.messages).toContain(`${file}:1`);
|
|
1452
|
+
});
|
|
1453
|
+
it.each([
|
|
1454
|
+
["features/demo/Panel.tsx", "export const Panel = () => <PopoverContent />;"],
|
|
1455
|
+
[
|
|
1456
|
+
"public/theme/src/theme/overlaySurfaces.tsx",
|
|
1457
|
+
"export const Panel = () => <Popover.Content />;",
|
|
1458
|
+
],
|
|
1459
|
+
["features/demo/Panel.tsx", "// <Popover.Content />"],
|
|
1460
|
+
["features/demo/Panel.spec.tsx", "export const Panel = () => <Popover.Content />;"],
|
|
1461
|
+
])("allows the shared frame in %s", (file, body) => {
|
|
1462
|
+
const result = runOverlayChecks(file, body);
|
|
1463
|
+
expect(result.code).toBe(0);
|
|
1464
|
+
});
|
|
1465
|
+
});
|
|
1466
|
+
|
|
1467
|
+
describe("accent solid owners (LC-05 ONE-EMPHASIS)", () => {
|
|
1468
|
+
function run(source: string, file = "public/components/src/Selected.tsx") {
|
|
1469
|
+
const root = makeTree();
|
|
1470
|
+
const full = join(root, file);
|
|
1471
|
+
mkdirSync(join(full, ".."), { recursive: true });
|
|
1472
|
+
writeFileSync(full, source);
|
|
1473
|
+
const error = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
1474
|
+
const log = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
1475
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
1476
|
+
const code = runConventionChecks({ roots: { root } });
|
|
1477
|
+
const messages = error.mock.calls.map((call) => String(call[0])).join("\n");
|
|
1478
|
+
error.mockRestore();
|
|
1479
|
+
log.mockRestore();
|
|
1480
|
+
warn.mockRestore();
|
|
1481
|
+
return { code, messages };
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
it.each([
|
|
1485
|
+
'export const Selected = () => <View backgroundColor="$accentBackground" />;',
|
|
1486
|
+
"export const fill = theme.accentBackground?.val;",
|
|
1487
|
+
])("fails an accent solid in a component: %s", (source) => {
|
|
1488
|
+
const { code, messages } = run(source);
|
|
1489
|
+
expect(code).toBe(1);
|
|
1490
|
+
expect(messages).toContain("LC-05 ONE-EMPHASIS");
|
|
1491
|
+
expect(messages).toContain("public/components/src/Selected.tsx:1");
|
|
1492
|
+
});
|
|
1493
|
+
|
|
1494
|
+
it.each(["apps/demo", "features/demo", "packages/ui/src"])(
|
|
1495
|
+
"scans %s as well as published sources",
|
|
1496
|
+
(dir) => {
|
|
1497
|
+
expect(run('export const fill = "$accentBackground";', `${dir}/fill.mjs`).code).toBe(1);
|
|
1498
|
+
},
|
|
1499
|
+
);
|
|
1500
|
+
|
|
1501
|
+
it.each([
|
|
1502
|
+
"public/components/src/componentColors.ts",
|
|
1503
|
+
"public/forms/src/shared/colorRamps.ts",
|
|
1504
|
+
"public/theme/src/theme/createThemes.ts",
|
|
1505
|
+
"public/theme/src/theme/chartPalette.ts",
|
|
1506
|
+
"public/theme/src/theme/readableColor.ts",
|
|
1507
|
+
])("allows the owner %s", (file) => {
|
|
1508
|
+
expect(run("export const fill = theme.accentBackground?.val;", file).code).toBe(0);
|
|
1509
|
+
});
|
|
1510
|
+
|
|
1511
|
+
it("keeps strings containing comment delimiters", () => {
|
|
1512
|
+
const { code, messages } = run(`const url = "https://example.invalid";
|
|
1513
|
+
const literal = "/* $accentBackground */";
|
|
1514
|
+
const value = "// $accentBackground";`);
|
|
1515
|
+
expect(code).toBe(1);
|
|
1516
|
+
expect(messages).toContain("public/components/src/Selected.tsx:2");
|
|
1517
|
+
expect(messages).toContain("public/components/src/Selected.tsx:3");
|
|
1518
|
+
});
|
|
1519
|
+
|
|
1520
|
+
it("ignores line, block and JSX comments", () => {
|
|
1521
|
+
expect(
|
|
1522
|
+
run(`// backgroundColor="$accentBackground"
|
|
1523
|
+
/* theme.accentBackground */
|
|
1524
|
+
export const Selected = () => <View>{/* "$accentBackground" */}</View>;`).code,
|
|
1525
|
+
).toBe(0);
|
|
1526
|
+
});
|
|
1527
|
+
|
|
1528
|
+
it("ends a quote at the line break, so a JSX apostrophe cannot open a comment", () => {
|
|
1529
|
+
expect(
|
|
1530
|
+
run(`export const Hint = () => <Text>Don't pick</Text>;
|
|
1531
|
+
// backgroundColor="$accentBackground"
|
|
1532
|
+
export const Other = () => <Text>It's fine</Text>;`).code,
|
|
1533
|
+
).toBe(0);
|
|
1534
|
+
});
|
|
1535
|
+
|
|
1536
|
+
it.each(["stories.tsx", "spec.tsx", "test.tsx", "d.ts"])("ignores %s", (suffix) => {
|
|
1537
|
+
expect(
|
|
1538
|
+
run(
|
|
1539
|
+
'const fill = "$accentBackground"; const meta = {};\nexport default meta;',
|
|
1540
|
+
`public/components/src/Selected.${suffix}`,
|
|
1541
|
+
).code,
|
|
1542
|
+
).toBe(0);
|
|
1543
|
+
});
|
|
1544
|
+
|
|
1545
|
+
it("reports every live occurrence with its line", () => {
|
|
1546
|
+
const { messages } = run(`const fill = "$accentBackground";
|
|
1547
|
+
const value = theme.accentBackground;`);
|
|
1548
|
+
expect(messages).toContain("public/components/src/Selected.tsx:1");
|
|
1549
|
+
expect(messages).toContain("public/components/src/Selected.tsx:2");
|
|
1550
|
+
});
|
|
1551
|
+
});
|
|
@@ -38,7 +38,7 @@ describe("phosphorTokenColorPlugin", () => {
|
|
|
38
38
|
const fromIcon = resolveId("../lib/IconBase.es.js", `${dist}/csr/Heart.es.js`);
|
|
39
39
|
const fromBarrel = resolveId("./lib/IconBase.es.js", `${dist}/index.es.js?v=1a2b`);
|
|
40
40
|
expect(fromIcon?.startsWith("\0mpo-phosphor-token-icon-base:")).toBe(true);
|
|
41
|
-
expect(fromIcon?.endsWith(iconBase)).toBe(true);
|
|
41
|
+
expect(fromIcon?.endsWith(`${iconBase}?mpo-token-icon`)).toBe(true);
|
|
42
42
|
expect(fromBarrel).toBe(fromIcon);
|
|
43
43
|
});
|
|
44
44
|
|
|
@@ -49,6 +49,12 @@ describe("phosphorTokenColorPlugin", () => {
|
|
|
49
49
|
expect(resolveId(iconBase, wrapper)).toBeNull();
|
|
50
50
|
});
|
|
51
51
|
|
|
52
|
+
it("keeps virtual modules out of file-only Flow loaders", () => {
|
|
53
|
+
const wrapper = resolveId("../lib/IconBase.es.js", `${dist}/csr/Heart.es.js`) ?? "";
|
|
54
|
+
expect(wrapper).not.toMatch(/\.(flow|jsx?)$/);
|
|
55
|
+
expect(load(wrapper)).toContain(`import IconBase from ${JSON.stringify(iconBase)}`);
|
|
56
|
+
});
|
|
57
|
+
|
|
52
58
|
it("loads a wrapper that resolves tokens through the theme and renders the original", () => {
|
|
53
59
|
const wrapper = resolveId("../lib/IconBase.es.js", `${dist}/csr/Heart.es.js`) ?? "";
|
|
54
60
|
const code = load(wrapper) ?? "";
|
|
@@ -4,6 +4,7 @@ import { pathToFileURL } from "node:url";
|
|
|
4
4
|
import type { Plugin } from "vite";
|
|
5
5
|
|
|
6
6
|
const virtualPrefix = "\0mpo-phosphor-token-icon-base:";
|
|
7
|
+
const virtualSuffix = "?mpo-token-icon";
|
|
7
8
|
const iconBaseFilter = /IconBase\.es\.js$/;
|
|
8
9
|
const phosphorLibFile = /\/@phosphor-icons\/react\/dist\/lib\/IconBase\.es\.js$/;
|
|
9
10
|
const phosphorBarrelFile = /\/@phosphor-icons\/react\/dist\/index\.es\.js$/;
|
|
@@ -59,12 +60,12 @@ function resolvePhosphorIconBase(id: string, importer: string | undefined): stri
|
|
|
59
60
|
const importerPath = toPosix(importer);
|
|
60
61
|
if (!importerPath.includes("/@phosphor-icons/react/dist/")) return null;
|
|
61
62
|
const target = toPosix(id.startsWith(".") ? path.resolve(path.dirname(importer), id) : id);
|
|
62
|
-
return phosphorLibFile.test(target) ? virtualPrefix + target : null;
|
|
63
|
+
return phosphorLibFile.test(target) ? virtualPrefix + target + virtualSuffix : null;
|
|
63
64
|
}
|
|
64
65
|
|
|
65
66
|
function loadTokenColorIconBase(id: string): string | null {
|
|
66
67
|
return id.startsWith(virtualPrefix)
|
|
67
|
-
? tokenColorIconBaseSource(id.slice(virtualPrefix.length))
|
|
68
|
+
? tokenColorIconBaseSource(id.slice(virtualPrefix.length, -virtualSuffix.length))
|
|
68
69
|
: null;
|
|
69
70
|
}
|
|
70
71
|
|
package/src/storybook.spec.ts
CHANGED
|
@@ -15,6 +15,13 @@ describe("createStorybookViteConfig", () => {
|
|
|
15
15
|
expect(include).toContain("d3-scale");
|
|
16
16
|
});
|
|
17
17
|
|
|
18
|
+
it("aliases an exported subpath whose name is not its path to the export's source file", () => {
|
|
19
|
+
const alias = config.resolve?.alias as Record<string, string>;
|
|
20
|
+
expect(alias["@multiplatform.one/theme/scale-attribute"]).toMatch(
|
|
21
|
+
/public\/theme\/src\/theme\/scale\/scaleAttribute\.ts$/,
|
|
22
|
+
);
|
|
23
|
+
});
|
|
24
|
+
|
|
18
25
|
it("pre-includes i18next so its late discovery cannot re-optimize", () => {
|
|
19
26
|
expect(include).toContain("i18next");
|
|
20
27
|
expect(include).toContain("react-i18next");
|