@multiplatform.one/config 7.5.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 +149 -3
- 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 +256 -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/src/oxlint.spec.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { fileURLToPath } from "node:url";
|
|
|
4
4
|
import { describe, expect, it } from "vitest";
|
|
5
5
|
import {
|
|
6
6
|
CHROME_PROPS,
|
|
7
|
+
GEOMETRY_PROPS,
|
|
7
8
|
LAYOUT_PROPS,
|
|
8
9
|
isPaletteModule,
|
|
9
10
|
matchColorLiteral,
|
|
@@ -56,6 +57,260 @@ describe("MPO-17 export surface", () => {
|
|
|
56
57
|
expect(plugin.meta?.name).toBe("mpo-conventions");
|
|
57
58
|
expect(plugin.rules?.["no-hex-literals"]).toBeDefined();
|
|
58
59
|
expect(plugin.rules?.["no-raw-typography"]).toBeDefined();
|
|
59
|
-
expect(plugin.rules?.["no-chrome-props"]).
|
|
60
|
+
expect(plugin.rules?.["no-chrome-props"]).toBeDefined();
|
|
61
|
+
expect(plugin.rules?.["no-raw-geometry"]).toBeDefined();
|
|
62
|
+
expect(GEOMETRY_PROPS).toContain("height");
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* AN-8 behavioural teeth (MPO-20 AC-4). The block above only asserts the rule
|
|
68
|
+
* is DEFINED — a rule that never reports would pass it. These cases drive the
|
|
69
|
+
* rule's own visitor and assert it reports on every raw typography hatch and
|
|
70
|
+
* stays silent on the house names, so "the lint fails on raw typography in app
|
|
71
|
+
* code" is a checked claim rather than a registration.
|
|
72
|
+
*/
|
|
73
|
+
describe("mpo-conventions/no-raw-typography (AN-8)", () => {
|
|
74
|
+
const rule = plugin.rules?.["no-raw-typography"];
|
|
75
|
+
|
|
76
|
+
type Report = { data?: { name?: string } };
|
|
77
|
+
type ImportVisitor = { ImportDeclaration: (node: unknown) => void };
|
|
78
|
+
type Creatable = { create: (context: unknown) => ImportVisitor };
|
|
79
|
+
|
|
80
|
+
/** Minimal ESTree ImportDeclaration — the only node shape the rule visits. */
|
|
81
|
+
function importDeclaration(source: string, names: string[]) {
|
|
82
|
+
return {
|
|
83
|
+
type: "ImportDeclaration",
|
|
84
|
+
source: { value: source },
|
|
85
|
+
specifiers: names.map((name) => ({ type: "ImportSpecifier", imported: { name } })),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function reportedNames(source: string, names: string[]): string[] {
|
|
90
|
+
const reports: Report[] = [];
|
|
91
|
+
const context = {
|
|
92
|
+
filename: "apps/one/app/index.tsx",
|
|
93
|
+
report: (report: Report) => reports.push(report),
|
|
94
|
+
};
|
|
95
|
+
const visitors = (rule as unknown as Creatable).create(context);
|
|
96
|
+
visitors.ImportDeclaration(importDeclaration(source, names));
|
|
97
|
+
return reports.map((report) => report.data?.name ?? "");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const hatches = [
|
|
101
|
+
"TamaguiText",
|
|
102
|
+
"TamaguiSizableText",
|
|
103
|
+
"TamaguiParagraph",
|
|
104
|
+
"TamaguiHeading",
|
|
105
|
+
"TamaguiH1",
|
|
106
|
+
"TamaguiH6",
|
|
107
|
+
"TamaguiAnchor",
|
|
108
|
+
];
|
|
109
|
+
|
|
110
|
+
it.each(hatches)("reports %s imported from the components barrel", (name) => {
|
|
111
|
+
expect(reportedNames("@multiplatform.one/components", [name])).toEqual([name]);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("stays silent on the house shadows — the bare names ARE the fix", () => {
|
|
115
|
+
expect(
|
|
116
|
+
reportedNames("@multiplatform.one/components", [
|
|
117
|
+
"Text",
|
|
118
|
+
"SizableText",
|
|
119
|
+
"Paragraph",
|
|
120
|
+
"Heading",
|
|
121
|
+
"H1",
|
|
122
|
+
"H6",
|
|
123
|
+
"Anchor",
|
|
124
|
+
]),
|
|
125
|
+
).toEqual([]);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("stays silent on non-typography hatches and on other packages", () => {
|
|
129
|
+
expect(
|
|
130
|
+
reportedNames("@multiplatform.one/components", ["TamaguiButton", "TamaguiSeparator"]),
|
|
131
|
+
).toEqual([]);
|
|
132
|
+
expect(reportedNames("some-other-package", ["TamaguiText"])).toEqual([]);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("reports every offending specifier in one import, not just the first", () => {
|
|
136
|
+
expect(
|
|
137
|
+
reportedNames("@multiplatform.one/components", ["Text", "TamaguiText", "TamaguiHeading"]),
|
|
138
|
+
).toEqual(["TamaguiText", "TamaguiHeading"]);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
type Report = { messageId?: string; data?: Record<string, string> };
|
|
143
|
+
type HouseVisitors = {
|
|
144
|
+
ImportDeclaration: (node: unknown) => void;
|
|
145
|
+
JSXOpeningElement: (node: unknown) => void;
|
|
146
|
+
};
|
|
147
|
+
type Creatable = { create: (context: unknown) => HouseVisitors };
|
|
148
|
+
|
|
149
|
+
function houseImport(source: string, names: string[]) {
|
|
150
|
+
return {
|
|
151
|
+
type: "ImportDeclaration",
|
|
152
|
+
source: { value: source },
|
|
153
|
+
specifiers: names.map((name) => ({
|
|
154
|
+
type: "ImportSpecifier",
|
|
155
|
+
imported: { name },
|
|
156
|
+
local: { name },
|
|
157
|
+
})),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function jsxOpen(name: string, attrs: { prop: string; value: unknown }[]) {
|
|
162
|
+
return {
|
|
163
|
+
type: "JSXOpeningElement",
|
|
164
|
+
name: { type: "JSXIdentifier", name },
|
|
165
|
+
attributes: attrs.map((attr) => ({
|
|
166
|
+
type: "JSXAttribute",
|
|
167
|
+
name: { type: "JSXIdentifier", name: attr.prop },
|
|
168
|
+
value: attr.value,
|
|
169
|
+
})),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function token(value: string) {
|
|
174
|
+
return { type: "Literal", value };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function numeric(value: number) {
|
|
178
|
+
return { type: "JSXExpressionContainer", expression: { type: "Literal", value } };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function escaped(value: number) {
|
|
182
|
+
return {
|
|
183
|
+
type: "JSXExpressionContainer",
|
|
184
|
+
expression: {
|
|
185
|
+
type: "CallExpression",
|
|
186
|
+
callee: { type: "Identifier", name: "sizeRecipeEscape" },
|
|
187
|
+
arguments: [{ type: "Literal", value }],
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function driveHouseRule(
|
|
193
|
+
ruleName: "no-chrome-props" | "no-raw-geometry",
|
|
194
|
+
filename: string,
|
|
195
|
+
nodes: unknown[],
|
|
196
|
+
options?: { drawings?: string[]; chromeAllow?: string[] },
|
|
197
|
+
): Report[] {
|
|
198
|
+
const reports: Report[] = [];
|
|
199
|
+
const rule = plugin.rules?.[ruleName] as unknown as Creatable;
|
|
200
|
+
const visitors = rule.create({
|
|
201
|
+
filename,
|
|
202
|
+
options: options ? [options] : [],
|
|
203
|
+
report: (report: Report) => reports.push(report),
|
|
204
|
+
});
|
|
205
|
+
for (const node of nodes) {
|
|
206
|
+
const typed = node as { type?: string };
|
|
207
|
+
if (typed.type === "ImportDeclaration") visitors.ImportDeclaration(node);
|
|
208
|
+
if (typed.type === "JSXOpeningElement") visitors.JSXOpeningElement(node);
|
|
209
|
+
}
|
|
210
|
+
return reports;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
describe("mpo-conventions/no-chrome-props (MPO-17)", () => {
|
|
214
|
+
const filename = "features/lookout/src/WallScreen.tsx";
|
|
215
|
+
const importCard = houseImport("@multiplatform.one/components", ["Card"]);
|
|
216
|
+
|
|
217
|
+
it("errors on Card backgroundColor=$color5 and stays silent on Card flex={1}", () => {
|
|
218
|
+
const chrome = driveHouseRule("no-chrome-props", filename, [
|
|
219
|
+
importCard,
|
|
220
|
+
jsxOpen("Card", [{ prop: "backgroundColor", value: token("$color5") }]),
|
|
221
|
+
]);
|
|
222
|
+
expect(chrome.map((r) => r.data?.prop)).toEqual(["backgroundColor"]);
|
|
223
|
+
|
|
224
|
+
const layout = driveHouseRule("no-chrome-props", filename, [
|
|
225
|
+
importCard,
|
|
226
|
+
jsxOpen("Card", [{ prop: "flex", value: numeric(1) }]),
|
|
227
|
+
]);
|
|
228
|
+
expect(layout).toEqual([]);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("stays silent on a CHROME-ALLOW member and on a non-house import", () => {
|
|
232
|
+
expect(
|
|
233
|
+
driveHouseRule(
|
|
234
|
+
"no-chrome-props",
|
|
235
|
+
filename,
|
|
236
|
+
[importCard, jsxOpen("Card", [{ prop: "bg", value: token("$color2") }])],
|
|
237
|
+
{ chromeAllow: ["Card"] },
|
|
238
|
+
),
|
|
239
|
+
).toEqual([]);
|
|
240
|
+
expect(
|
|
241
|
+
driveHouseRule("no-chrome-props", filename, [
|
|
242
|
+
houseImport("somewhere-else", ["Card"]),
|
|
243
|
+
jsxOpen("Card", [{ prop: "backgroundColor", value: token("$color5") }]),
|
|
244
|
+
]),
|
|
245
|
+
).toEqual([]);
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
describe("mpo-conventions/no-raw-geometry (MPO-17)", () => {
|
|
250
|
+
const filename = "features/lookout/src/WallScreen.tsx";
|
|
251
|
+
const importCard = houseImport("@multiplatform.one/components", ["Card"]);
|
|
252
|
+
|
|
253
|
+
it("errors on a numeric height and stays silent behind sizeRecipeEscape()", () => {
|
|
254
|
+
const raw = driveHouseRule("no-raw-geometry", filename, [
|
|
255
|
+
importCard,
|
|
256
|
+
jsxOpen("Card", [{ prop: "height", value: numeric(20) }]),
|
|
257
|
+
]);
|
|
258
|
+
expect(raw.map((r) => r.data?.prop)).toEqual(["height"]);
|
|
259
|
+
|
|
260
|
+
const wrapped = driveHouseRule("no-raw-geometry", filename, [
|
|
261
|
+
importCard,
|
|
262
|
+
jsxOpen("Card", [{ prop: "height", value: escaped(20) }]),
|
|
263
|
+
]);
|
|
264
|
+
expect(wrapped).toEqual([]);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("stays silent inside a registered drawing file", () => {
|
|
268
|
+
expect(
|
|
269
|
+
driveHouseRule(
|
|
270
|
+
"no-raw-geometry",
|
|
271
|
+
"packages/springboard-ui/src/StatusGlyphs.tsx",
|
|
272
|
+
[importCard, jsxOpen("Card", [{ prop: "height", value: numeric(14) }])],
|
|
273
|
+
{ drawings: ["springboard-ui/src/StatusGlyphs.tsx"] },
|
|
274
|
+
),
|
|
275
|
+
).toEqual([]);
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
describe("no-hex-literals comments and palette (MPO-17 AC-4)", () => {
|
|
280
|
+
it("does not visit comments, so a JSDoc Pokedex #025 never reaches the matcher", () => {
|
|
281
|
+
const rule = plugin.rules?.["no-hex-literals"] as { create: (c: unknown) => object };
|
|
282
|
+
const visitors = rule.create({
|
|
283
|
+
filename: "features/pokemon/logic.ts",
|
|
284
|
+
report() {},
|
|
285
|
+
});
|
|
286
|
+
expect(visitors).not.toHaveProperty("Comment");
|
|
287
|
+
expect(visitors).not.toHaveProperty("Block");
|
|
288
|
+
expect(visitors).toHaveProperty("Literal");
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it("stays silent in packages/themes/base.ts even for rgba", () => {
|
|
292
|
+
const reports: Report[] = [];
|
|
293
|
+
const rule = plugin.rules?.["no-hex-literals"] as Creatable & {
|
|
294
|
+
create: (c: unknown) => { Literal?: (n: unknown) => void };
|
|
295
|
+
};
|
|
296
|
+
const visitors = rule.create({
|
|
297
|
+
filename: "packages/themes/base.ts",
|
|
298
|
+
report: (report: Report) => reports.push(report),
|
|
299
|
+
});
|
|
300
|
+
visitors.Literal?.({ type: "Literal", value: "rgba(0,0,0,0.35)" });
|
|
301
|
+
expect(reports).toEqual([]);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it("errors on an rgba() literal in features/**", () => {
|
|
305
|
+
const reports: Report[] = [];
|
|
306
|
+
const rule = plugin.rules?.["no-hex-literals"] as {
|
|
307
|
+
create: (c: unknown) => { Literal: (n: unknown) => void };
|
|
308
|
+
};
|
|
309
|
+
const visitors = rule.create({
|
|
310
|
+
filename: "features/console/src/DeskShell.tsx",
|
|
311
|
+
report: (report: Report) => reports.push(report),
|
|
312
|
+
});
|
|
313
|
+
visitors.Literal({ type: "Literal", value: "rgba(0,0,0,0.35)" });
|
|
314
|
+
expect(reports.map((r) => r.data?.value ?? r.messageId)).toEqual(["rgba(0,0,0,0.35)"]);
|
|
60
315
|
});
|
|
61
316
|
});
|
package/src/storybook.ts
CHANGED
|
@@ -3,6 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import type { Plugin, UserConfig } from "vite";
|
|
4
4
|
import { unexportedDepAliases } from "./unexportedDepAliases.js";
|
|
5
5
|
import { discoverPublicPackageRoots, resolvePackageMainSource } from "./workspacePublicPackages.js";
|
|
6
|
+
import { ensureTamaguiWorkspacePaths } from "./tamaguiWorkspacePaths.js";
|
|
6
7
|
|
|
7
8
|
export interface CreateStorybookViteConfigOptions {
|
|
8
9
|
/**
|
|
@@ -49,6 +50,8 @@ export interface CreateStorybookViteConfigOptions {
|
|
|
49
50
|
export function createStorybookViteConfig(
|
|
50
51
|
options: CreateStorybookViteConfigOptions = {},
|
|
51
52
|
): UserConfig {
|
|
53
|
+
// MPO-79: see tamaguiWorkspacePaths.ts.
|
|
54
|
+
ensureTamaguiWorkspacePaths();
|
|
52
55
|
const workspaceRoot = options.workspaceRoot || findWorkspaceRoot();
|
|
53
56
|
const packagesDir = path.join(workspaceRoot, "packages");
|
|
54
57
|
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ensureTamaguiWorkspacePaths — regression guard for MPO-79.
|
|
3
|
+
*
|
|
4
|
+
* `tsconfig.base.json` extends a gitignored generated file that only the root
|
|
5
|
+
* `prepare` script writes. With a global `ignore-scripts` that script never
|
|
6
|
+
* runs, the extends chain dangles, and every vitest transform in the workspace
|
|
7
|
+
* dies with `[TSCONFIG_ERROR] ... Tsconfig not found`. The config factories
|
|
8
|
+
* must regenerate the file themselves when it is missing.
|
|
9
|
+
*
|
|
10
|
+
* The real generator is exercised against a throwaway repo layout so the test
|
|
11
|
+
* never touches the checkout's own generated file.
|
|
12
|
+
*/
|
|
13
|
+
import fs from "node:fs";
|
|
14
|
+
import os from "node:os";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
17
|
+
import { ensureTamaguiWorkspacePaths, tamaguiWorkspacePathsFile } from "./tamaguiWorkspacePaths";
|
|
18
|
+
|
|
19
|
+
const realGenerator = path.resolve(
|
|
20
|
+
__dirname,
|
|
21
|
+
"../../../scripts/generate-tamagui-workspace-paths.mjs",
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
const roots: string[] = [];
|
|
25
|
+
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
for (const root of roots.splice(0)) fs.rmSync(root, { recursive: true, force: true });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* A minimal repo: `scripts/` holding the generator, `public/config/tsconfig/`,
|
|
32
|
+
* and one public package with a declared TypeScript source entry.
|
|
33
|
+
*/
|
|
34
|
+
function fakeRepo(generatorSource: string | null): { root: string; configDir: string } {
|
|
35
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "mpo-97-"));
|
|
36
|
+
roots.push(root);
|
|
37
|
+
const configDir = path.join(root, "public/config");
|
|
38
|
+
fs.mkdirSync(path.join(configDir, "tsconfig"), { recursive: true });
|
|
39
|
+
fs.mkdirSync(path.join(root, "public/widget/src"), { recursive: true });
|
|
40
|
+
fs.writeFileSync(
|
|
41
|
+
path.join(root, "public/widget/package.json"),
|
|
42
|
+
JSON.stringify({ name: "@fake/widget", exports: { ".": { source: "./src/index.ts" } } }),
|
|
43
|
+
);
|
|
44
|
+
fs.writeFileSync(path.join(root, "public/widget/src/index.ts"), "export const widget = 1;\n");
|
|
45
|
+
if (generatorSource !== null) {
|
|
46
|
+
fs.mkdirSync(path.join(root, "scripts"), { recursive: true });
|
|
47
|
+
fs.writeFileSync(
|
|
48
|
+
path.join(root, "scripts/generate-tamagui-workspace-paths.mjs"),
|
|
49
|
+
generatorSource,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
return { root, configDir };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
describe("ensureTamaguiWorkspacePaths", () => {
|
|
56
|
+
it("regenerates the file with the real generator when it is missing", () => {
|
|
57
|
+
const { configDir } = fakeRepo(fs.readFileSync(realGenerator, "utf8"));
|
|
58
|
+
const generatedPath = path.join(configDir, tamaguiWorkspacePathsFile);
|
|
59
|
+
expect(fs.existsSync(generatedPath)).toBe(false);
|
|
60
|
+
const notices: string[] = [];
|
|
61
|
+
const result = ensureTamaguiWorkspacePaths({
|
|
62
|
+
configPackageDir: configDir,
|
|
63
|
+
log: (message) => notices.push(message),
|
|
64
|
+
});
|
|
65
|
+
expect(result.status).toBe("generated");
|
|
66
|
+
expect(result.generatedPath).toBe(generatedPath);
|
|
67
|
+
const doc = JSON.parse(fs.readFileSync(generatedPath, "utf8"));
|
|
68
|
+
expect(doc.compilerOptions.paths["@fake/widget"]).toEqual(["../../widget/src/index.ts"]);
|
|
69
|
+
expect(notices).toHaveLength(1);
|
|
70
|
+
expect(notices[0]).toContain("was missing");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("leaves an existing file alone and stays silent", () => {
|
|
74
|
+
const { configDir } = fakeRepo(fs.readFileSync(realGenerator, "utf8"));
|
|
75
|
+
const generatedPath = path.join(configDir, tamaguiWorkspacePathsFile);
|
|
76
|
+
fs.writeFileSync(generatedPath, '{"compilerOptions":{"paths":{"@kept/as-is":["./x.ts"]}}}\n');
|
|
77
|
+
const notices: string[] = [];
|
|
78
|
+
const result = ensureTamaguiWorkspacePaths({
|
|
79
|
+
configPackageDir: configDir,
|
|
80
|
+
log: (message) => notices.push(message),
|
|
81
|
+
});
|
|
82
|
+
expect(result.status).toBe("present");
|
|
83
|
+
expect(JSON.parse(fs.readFileSync(generatedPath, "utf8")).compilerOptions.paths).toEqual({
|
|
84
|
+
"@kept/as-is": ["./x.ts"],
|
|
85
|
+
});
|
|
86
|
+
expect(notices).toEqual([]);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("does nothing outside the monorepo, where no generator exists", () => {
|
|
90
|
+
const { configDir } = fakeRepo(null);
|
|
91
|
+
const notices: string[] = [];
|
|
92
|
+
const result = ensureTamaguiWorkspacePaths({
|
|
93
|
+
configPackageDir: configDir,
|
|
94
|
+
log: (message) => notices.push(message),
|
|
95
|
+
});
|
|
96
|
+
expect(result.status).toBe("no-generator");
|
|
97
|
+
expect(fs.existsSync(path.join(configDir, tamaguiWorkspacePathsFile))).toBe(false);
|
|
98
|
+
expect(notices).toEqual([]);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("reports a crashing generator instead of throwing", () => {
|
|
102
|
+
const { configDir } = fakeRepo("process.stderr.write('boom\\n'); process.exit(3);\n");
|
|
103
|
+
const notices: string[] = [];
|
|
104
|
+
const result = ensureTamaguiWorkspacePaths({
|
|
105
|
+
configPackageDir: configDir,
|
|
106
|
+
log: (message) => notices.push(message),
|
|
107
|
+
});
|
|
108
|
+
expect(result.status).toBe("failed");
|
|
109
|
+
expect(fs.existsSync(path.join(configDir, tamaguiWorkspacePathsFile))).toBe(false);
|
|
110
|
+
expect(notices).toHaveLength(1);
|
|
111
|
+
expect(notices[0]).toContain("could not regenerate");
|
|
112
|
+
});
|
|
113
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The repo `tsconfig.base.json` extends
|
|
8
|
+
* `public/config/tsconfig/tamagui-workspace-paths.generated.json`. That file is
|
|
9
|
+
* gitignored and written by the root `prepare` script. A global
|
|
10
|
+
* `ignore-scripts=true` in `~/.npmrc` (or `pnpm install --ignore-scripts`)
|
|
11
|
+
* skips `prepare`, so a fresh checkout carries an extends chain that points at
|
|
12
|
+
* a file which does not exist. Vite's tsconfig loader then fails EVERY
|
|
13
|
+
* TypeScript transform with the misleading
|
|
14
|
+
* `[TSCONFIG_ERROR] Failed to load tsconfig for '<file>': Tsconfig not found`
|
|
15
|
+
* (MPO-79: all 91 forms specs at once, blamed on tests/setup-animations.ts).
|
|
16
|
+
*
|
|
17
|
+
* The config factories run before any transform, so they regenerate the file
|
|
18
|
+
* here when it is absent. A published consumer outside the monorepo has no
|
|
19
|
+
* generator and no such extends entry, so there is nothing to do there.
|
|
20
|
+
*/
|
|
21
|
+
export interface EnsureTamaguiWorkspacePathsOptions {
|
|
22
|
+
/**
|
|
23
|
+
* Directory of the @multiplatform.one/config package (the one holding
|
|
24
|
+
* `tsconfig/`). Defaults to the package this module ships in.
|
|
25
|
+
*/
|
|
26
|
+
configPackageDir?: string;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Where the one-line notice goes when the file had to be regenerated.
|
|
30
|
+
* Defaults to console.warn. Pass null to silence.
|
|
31
|
+
*/
|
|
32
|
+
log?: ((message: string) => void) | null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface EnsureTamaguiWorkspacePathsResult {
|
|
36
|
+
generatedPath: string;
|
|
37
|
+
generatorPath: string;
|
|
38
|
+
status: "present" | "generated" | "no-generator" | "failed";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const tamaguiWorkspacePathsFile = "tsconfig/tamagui-workspace-paths.generated.json";
|
|
42
|
+
const generatorFile = "../../scripts/generate-tamagui-workspace-paths.mjs";
|
|
43
|
+
|
|
44
|
+
function thisPackageDir(): string {
|
|
45
|
+
// `src/` (vitest source condition) and `lib/` (tsdown output) both sit one
|
|
46
|
+
// level below the package root, same trick vitest.ts uses for
|
|
47
|
+
// pin-react-require.cjs.
|
|
48
|
+
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function ensureTamaguiWorkspacePaths(
|
|
52
|
+
options: EnsureTamaguiWorkspacePathsOptions = {},
|
|
53
|
+
): EnsureTamaguiWorkspacePathsResult {
|
|
54
|
+
// oxlint-disable-next-line no-console -- the default sink for the one-line regenerate notice
|
|
55
|
+
const { configPackageDir = thisPackageDir(), log = console.warn } = options;
|
|
56
|
+
const generatedPath = path.resolve(configPackageDir, tamaguiWorkspacePathsFile);
|
|
57
|
+
const generatorPath = path.resolve(configPackageDir, generatorFile);
|
|
58
|
+
if (fs.existsSync(generatedPath)) {
|
|
59
|
+
return { generatedPath, generatorPath, status: "present" };
|
|
60
|
+
}
|
|
61
|
+
if (!fs.existsSync(generatorPath)) {
|
|
62
|
+
return { generatedPath, generatorPath, status: "no-generator" };
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
// The generator locates the repo root from its own path, so no cwd or
|
|
66
|
+
// argument is needed; it writes exactly `generatedPath`.
|
|
67
|
+
execFileSync(process.execPath, [generatorPath], { stdio: ["ignore", "ignore", "pipe"] });
|
|
68
|
+
} catch (error) {
|
|
69
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
70
|
+
log?.(
|
|
71
|
+
`[@multiplatform.one/config] could not regenerate ${tamaguiWorkspacePathsFile}: ${message}`,
|
|
72
|
+
);
|
|
73
|
+
return { generatedPath, generatorPath, status: "failed" };
|
|
74
|
+
}
|
|
75
|
+
if (!fs.existsSync(generatedPath)) {
|
|
76
|
+
log?.(
|
|
77
|
+
`[@multiplatform.one/config] ${path.basename(generatorPath)} ran but did not write ${tamaguiWorkspacePathsFile}`,
|
|
78
|
+
);
|
|
79
|
+
return { generatedPath, generatorPath, status: "failed" };
|
|
80
|
+
}
|
|
81
|
+
log?.(
|
|
82
|
+
`[@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.`,
|
|
83
|
+
);
|
|
84
|
+
return { generatedPath, generatorPath, status: "generated" };
|
|
85
|
+
}
|
package/src/vite.ts
CHANGED
|
@@ -2,6 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { lookupTamaguiModules, resolveConfig } from "@multiplatform.one/utils/dev";
|
|
4
4
|
import { discoverPublicPackageRoots } from "./workspacePublicPackages.js";
|
|
5
|
+
import { ensureTamaguiWorkspacePaths } from "./tamaguiWorkspacePaths.js";
|
|
5
6
|
import { tamaguiPlugin } from "@tamagui/vite-plugin";
|
|
6
7
|
import dotenv from "dotenv";
|
|
7
8
|
import type { Plugin, UserConfig } from "vite";
|
|
@@ -173,6 +174,9 @@ export interface CreateViteConfigOptions {
|
|
|
173
174
|
* ```
|
|
174
175
|
*/
|
|
175
176
|
export function createViteConfig(options: CreateViteConfigOptions = {}): UserConfig {
|
|
177
|
+
// MPO-79: see tamaguiWorkspacePaths.ts; the tsconfig chain must resolve
|
|
178
|
+
// before vite (and tamagui's esbuild) read compilerOptions.paths.
|
|
179
|
+
ensureTamaguiWorkspacePaths();
|
|
176
180
|
const {
|
|
177
181
|
projectRoot: _projectRoot,
|
|
178
182
|
publicConfigKeys = [],
|
package/src/vitest.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { fileURLToPath } from "node:url";
|
|
|
5
5
|
import { tamaguiPlugin } from "@tamagui/vite-plugin";
|
|
6
6
|
import react from "@vitejs/plugin-react";
|
|
7
7
|
import type { UserConfig } from "vite";
|
|
8
|
+
import { ensureTamaguiWorkspacePaths } from "./tamaguiWorkspacePaths.js";
|
|
8
9
|
|
|
9
10
|
const pinReactRequireSetup = path.resolve(
|
|
10
11
|
path.dirname(fileURLToPath(import.meta.url)),
|
|
@@ -159,6 +160,9 @@ export interface CreateVitestConfigOptions {
|
|
|
159
160
|
* ```
|
|
160
161
|
*/
|
|
161
162
|
export function createVitestConfig(options: CreateVitestConfigOptions = {}): UserConfig {
|
|
163
|
+
// MPO-79: the tsconfig chain below extends a generated file that a skipped
|
|
164
|
+
// root `prepare` never wrote; regenerate it before vite loads any tsconfig.
|
|
165
|
+
ensureTamaguiWorkspacePaths();
|
|
162
166
|
const {
|
|
163
167
|
tamaguiConfig = "./tests/tamagui.config.ts",
|
|
164
168
|
environment = "jsdom",
|
package/types/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
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 { ensureTamaguiWorkspacePaths, tamaguiWorkspacePathsFile, type EnsureTamaguiWorkspacePathsOptions, type EnsureTamaguiWorkspacePathsResult, } from "./tamaguiWorkspacePaths";
|
|
4
5
|
//# sourceMappingURL=index.d.ts.map
|
package/types/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,yBAAyB,EAAE,KAAK,gCAAgC,EAAE,MAAM,aAAa,CAAC;AAC/F,OAAO,EAAE,gBAAgB,EAAE,KAAK,uBAAuB,EAAE,MAAM,QAAQ,CAAC;AACxE,OAAO,EAAE,kBAAkB,EAAE,KAAK,yBAAyB,EAAE,MAAM,UAAU,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,yBAAyB,EAAE,KAAK,gCAAgC,EAAE,MAAM,aAAa,CAAC;AAC/F,OAAO,EAAE,gBAAgB,EAAE,KAAK,uBAAuB,EAAE,MAAM,QAAQ,CAAC;AACxE,OAAO,EAAE,kBAAkB,EAAE,KAAK,yBAAyB,EAAE,MAAM,UAAU,CAAC;AAC9E,OAAO,EACL,2BAA2B,EAC3B,yBAAyB,EACzB,KAAK,kCAAkC,EACvC,KAAK,iCAAiC,GACvC,MAAM,yBAAyB,CAAC"}
|
package/types/storybook.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"storybook.d.ts","sourceRoot":"","sources":["../src/storybook.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAU,UAAU,EAAE,MAAM,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"storybook.d.ts","sourceRoot":"","sources":["../src/storybook.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAU,UAAU,EAAE,MAAM,MAAM,CAAC;AAK/C,MAAM,WAAW,gCAAgC;IAC/C;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAEjC;;OAEG;IACH,OAAO,CAAC,EAAE,UAAU,CAAC,SAAS,CAAC,CAAC;IAEhC;;OAEG;IACH,MAAM,CAAC,EAAE,UAAU,CAAC,QAAQ,CAAC,CAAC;CAC/B;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,yBAAyB,CACvC,OAAO,GAAE,gCAAqC,GAC7C,UAAU,CA4HZ"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The repo `tsconfig.base.json` extends
|
|
3
|
+
* `public/config/tsconfig/tamagui-workspace-paths.generated.json`. That file is
|
|
4
|
+
* gitignored and written by the root `prepare` script. A global
|
|
5
|
+
* `ignore-scripts=true` in `~/.npmrc` (or `pnpm install --ignore-scripts`)
|
|
6
|
+
* skips `prepare`, so a fresh checkout carries an extends chain that points at
|
|
7
|
+
* a file which does not exist. Vite's tsconfig loader then fails EVERY
|
|
8
|
+
* TypeScript transform with the misleading
|
|
9
|
+
* `[TSCONFIG_ERROR] Failed to load tsconfig for '<file>': Tsconfig not found`
|
|
10
|
+
* (MPO-79: all 91 forms specs at once, blamed on tests/setup-animations.ts).
|
|
11
|
+
*
|
|
12
|
+
* The config factories run before any transform, so they regenerate the file
|
|
13
|
+
* here when it is absent. A published consumer outside the monorepo has no
|
|
14
|
+
* generator and no such extends entry, so there is nothing to do there.
|
|
15
|
+
*/
|
|
16
|
+
export interface EnsureTamaguiWorkspacePathsOptions {
|
|
17
|
+
/**
|
|
18
|
+
* Directory of the @multiplatform.one/config package (the one holding
|
|
19
|
+
* `tsconfig/`). Defaults to the package this module ships in.
|
|
20
|
+
*/
|
|
21
|
+
configPackageDir?: string;
|
|
22
|
+
/**
|
|
23
|
+
* Where the one-line notice goes when the file had to be regenerated.
|
|
24
|
+
* Defaults to console.warn. Pass null to silence.
|
|
25
|
+
*/
|
|
26
|
+
log?: ((message: string) => void) | null;
|
|
27
|
+
}
|
|
28
|
+
export interface EnsureTamaguiWorkspacePathsResult {
|
|
29
|
+
generatedPath: string;
|
|
30
|
+
generatorPath: string;
|
|
31
|
+
status: "present" | "generated" | "no-generator" | "failed";
|
|
32
|
+
}
|
|
33
|
+
export declare const tamaguiWorkspacePathsFile = "tsconfig/tamagui-workspace-paths.generated.json";
|
|
34
|
+
export declare function ensureTamaguiWorkspacePaths(options?: EnsureTamaguiWorkspacePathsOptions): EnsureTamaguiWorkspacePathsResult;
|
|
35
|
+
//# sourceMappingURL=tamaguiWorkspacePaths.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tamaguiWorkspacePaths.d.ts","sourceRoot":"","sources":["../src/tamaguiWorkspacePaths.ts"],"names":[],"mappings":"AAKA;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,kCAAkC;IACjD;;;OAGG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAE1B;;;OAGG;IACH,GAAG,CAAC,EAAE,CAAC,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC,GAAG,IAAI,CAAC;CAC1C;AAED,MAAM,WAAW,iCAAiC;IAChD,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,SAAS,GAAG,WAAW,GAAG,cAAc,GAAG,QAAQ,CAAC;CAC7D;AAED,eAAO,MAAM,yBAAyB,oDAAoD,CAAC;AAU3F,wBAAgB,2BAA2B,CACzC,OAAO,GAAE,kCAAuC,GAC/C,iCAAiC,CAgCnC"}
|
package/types/vite.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vite.d.ts","sourceRoot":"","sources":["../src/vite.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"vite.d.ts","sourceRoot":"","sources":["../src/vite.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAU,UAAU,EAAE,MAAM,MAAM,CAAC;AAI/C,MAAM,WAAW,uBAAuB;IACtC;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;;OAGG;IACH,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;IAE5B;;OAEG;IACH,OAAO,CAAC,EACJ,OAAO,GACP;QACE;;;WAGG;QACH,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;QAEtB;;;WAGG;QACH,MAAM,CAAC,EAAE,MAAM,CAAC;QAEhB;;WAEG;QACH,SAAS,CAAC,EAAE,MAAM,CAAC;KACpB,CAAC;IAEN;;OAEG;IACH,IAAI,CAAC,EACD,OAAO,GACP;QACE;;;WAGG;QACH,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;QAEjB;;WAEG;QACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;KAC9B,CAAC;IAEN;;;OAGG;IACH,GAAG,CAAC,EACA,OAAO,GACP;QACE,MAAM,CAAC,EAAE;YAAE,IAAI,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;QAC3B,GAAG,CAAC,EAAE;YAAE,MAAM,CAAC,EAAE,MAAM,CAAC;YAAC,iBAAiB,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;QACtD,KAAK,CAAC,EAAE;YAAE,QAAQ,CAAC,EAAE,OAAO,CAAA;SAAE,CAAC;QAC/B,MAAM,CAAC,EAAE;YAAE,GAAG,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;QAC1B,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KAChC,CAAC;IAEN;;OAEG;IACH,GAAG,CAAC,EAAE,UAAU,CAAC,KAAK,CAAC,CAAC;IAExB;;OAEG;IACH,MAAM,CAAC,EAAE,UAAU,CAAC,QAAQ,CAAC,CAAC;IAE9B;;OAEG;IACH,OAAO,CAAC,EAAE,UAAU,CAAC,SAAS,CAAC,CAAC;IAEhC;;OAEG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAE9B;;OAEG;IACH,OAAO,CAAC,EAAE;QACR,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;QACnB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;QACnB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;KACrB,CAAC;IAEF;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IAEtB;;;;;;;;OAQG;IACH,cAAc,CAAC,EAAE,MAAM,EAAE,GAAG,KAAK,CAAC;IAElC;;;OAGG;IACH,WAAW,CAAC,EAAE;QACZ;;;;;;WAMG;QACH,WAAW,CAAC,EAAE,OAAO,CAAC;QACtB;;;;;;;WAOG;QACH,QAAQ,CAAC,EAAE,OAAO,CAAC;KACpB,CAAC;CACH;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,GAAE,uBAA4B,GAAG,UAAU,CAmSlF;AAED,OAAO,EACL,0BAA0B,EAC1B,8BAA8B,GAC/B,MAAM,8BAA8B,CAAC"}
|
package/types/vitest.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vitest.d.ts","sourceRoot":"","sources":["../src/vitest.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"vitest.d.ts","sourceRoot":"","sources":["../src/vitest.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,MAAM,CAAC;AAqDvC,MAAM,WAAW,yBAAyB;IACxC;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;IAE/B;;OAEG;IACH,WAAW,CAAC,EAAE,OAAO,GAAG,WAAW,GAAG,MAAM,CAAC;IAE7C;;OAEG;IACH,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IAEtB;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;OAEG;IACH,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IAEtB;;OAEG;IACH,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IAExB;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAEjC;;OAEG;IACH,QAAQ,CAAC,EAAE;QACT,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;KACpB,CAAC;IAEF;;;OAGG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAE7B;;;OAGG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IAEtB;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IAEzB;;OAEG;IACH,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IAEtB;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IAEnB;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IAEnB;;OAEG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,GAAE,yBAA8B,GAAG,UAAU,CAiNtF"}
|