Skip to content

Commit 096f87c

Browse files
committed
fix(build): make gen tasks and tests fully cacheable; format generated files with oxfmt
- Define gen:examples/gen:docs as Vite+ tasks with pinned input + output globs so they no longer hit read-and-wrote cache misses - Add cacheable test tasks for xl-ai and tests packages, excluding self-written .vite-temp and .next artifacts from input tracking - Remove prettier from dev-scripts; generators now collect written files and format them via 'vp fmt' (oxfmt), keeping output in sync with repo - Stop ignoring playground/src/examples.gen.tsx in fmt config - Switch VSCode config from prettier to Oxc formatter
1 parent 5f81a33 commit 096f87c

12 files changed

Lines changed: 170 additions & 66 deletions

File tree

.vscode/extensions.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
22
// See http://go.microsoft.com/fwlink/?LinkId=827846
33
// for the documentation about the extensions.json format
4-
"recommendations": ["esbenp.prettier-vscode", "dbaeumer.vscode-eslint"]
4+
"recommendations": ["VoidZero.vite-plus-extension-pack"]
55
}

.vscode/settings.json

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
{
2-
"editor.defaultFormatter": "esbenp.prettier-vscode",
2+
"editor.defaultFormatter": "oxc.oxc-vscode",
33
"editor.formatOnSave": true,
4-
"[javascript]": { "editor.defaultFormatter": "esbenp.prettier-vscode" },
5-
"[typescriptreact]": {
6-
"editor.defaultFormatter": "esbenp.prettier-vscode"
4+
"editor.formatOnSaveMode": "file",
5+
"editor.codeActionsOnSave": {
6+
"source.fixAll.oxc": "explicit"
77
},
8+
"oxc.fmt.configPath": "./vite.config.ts",
9+
"npm.scriptRunner": "vp",
10+
"[javascript]": { "editor.defaultFormatter": "oxc.oxc-vscode" },
11+
"[javascriptreact]": { "editor.defaultFormatter": "oxc.oxc-vscode" },
12+
"[typescript]": { "editor.defaultFormatter": "oxc.oxc-vscode" },
13+
"[typescriptreact]": { "editor.defaultFormatter": "oxc.oxc-vscode" },
814
"search.exclude": {
915
"packages/editor/public/types": true,
1016
"packages/website/docs/.vitepress": false,
@@ -15,11 +21,5 @@
1521
"editor.defaultFormatter": "redhat.vscode-xml"
1622
},
1723
"scm.defaultViewMode": "tree",
18-
"search.defaultViewMode": "tree",
19-
"[typescript]": {
20-
"editor.defaultFormatter": "esbenp.prettier-vscode"
21-
},
22-
"[mdx]": {
23-
"editor.defaultFormatter": "esbenp.prettier-vscode"
24-
}
24+
"search.defaultViewMode": "tree"
2525
}
Lines changed: 17 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
import * as glob from "glob";
2-
import * as fs from "node:fs";
32
import * as path from "node:path";
43
import { fileURLToPath, pathToFileURL } from "node:url";
5-
import prettier from "prettier";
64
import React from "react";
75

86
import ReactDOM from "react-dom/server";
97
import {
108
Project,
9+
formatFiles,
1110
getExampleProjects,
1211
groupProjects,
1312
replacePathSepToSlash,
13+
writeGeneratedFile,
1414
} from "./util.js";
1515

1616
/**
@@ -26,7 +26,11 @@ import {
2626
*/
2727
const dir = path.parse(fileURLToPath(import.meta.url)).dir;
2828

29-
async function writeTemplate(project: Project, templateFile: string) {
29+
async function writeTemplate(
30+
project: Project,
31+
templateFile: string,
32+
written: string[],
33+
) {
3034
const template = await import(pathToFileURL(templateFile).toString());
3135
if (
3236
project.config.tailwind !== true &&
@@ -45,12 +49,6 @@ async function writeTemplate(project: Project, templateFile: string) {
4549
let stringOutput: string | undefined = undefined;
4650
if (React.isValidElement(ret)) {
4751
stringOutput = ReactDOM.renderToString(ret);
48-
49-
const prettierConfig = await prettier.resolveConfig(targetFilePath);
50-
stringOutput = await prettier.format(stringOutput, {
51-
...prettierConfig,
52-
parser: "html",
53-
});
5452
} else if (typeof ret === "string") {
5553
stringOutput = ret;
5654
} else if (typeof ret === "object") {
@@ -59,20 +57,20 @@ async function writeTemplate(project: Project, templateFile: string) {
5957
throw new Error("unsupported template");
6058
}
6159

62-
fs.writeFileSync(targetFilePath, stringOutput);
60+
writeGeneratedFile(targetFilePath, stringOutput, written);
6361
}
6462

65-
async function generateCodeForExample(project: Project) {
63+
async function generateCodeForExample(project: Project, written: string[]) {
6664
const templates = glob.sync(
6765
replacePathSepToSlash(path.resolve(dir, "./template-react/*.template.tsx")),
6866
);
6967

7068
for (const template of templates) {
71-
await writeTemplate(project, template);
69+
await writeTemplate(project, template, written);
7270
}
7371
}
7472

75-
async function generateExamplesData(projects: Project[]) {
73+
async function generateExamplesData(projects: Project[], written: string[]) {
7674
// TODO: fix playground?
7775
const target = path.resolve(dir, "../../../playground/src/examples.gen.tsx");
7876

@@ -84,29 +82,21 @@ async function generateExamplesData(projects: Project[]) {
8482
// add as any after deps, otherwise const type inference will be too complex for TS
8583
code = code.replace(/("dependencies":\s*{[^}]*})/g, "$1 as any");
8684

87-
code = await prettier.format(code, {
88-
parser: "typescript",
89-
semi: true,
90-
singleQuote: false,
91-
tabWidth: 2,
92-
printWidth: 80,
93-
trailingComma: "all",
94-
bracketSpacing: true,
95-
arrowParens: "always",
96-
endOfLine: "lf",
97-
});
98-
99-
fs.writeFileSync(target, code);
85+
writeGeneratedFile(target, code, written);
10086
}
10187

10288
const projects = getExampleProjects();
89+
const writtenFiles: string[] = [];
10390

10491
for (const project of projects) {
10592
// eslint-disable-next-line no-console
10693
console.log("generating code for example", project.projectSlug);
107-
await generateCodeForExample(project);
94+
await generateCodeForExample(project, writtenFiles);
10895
}
10996

11097
await generateExamplesData(
11198
projects.filter((p) => p.config?.playground === true),
99+
writtenFiles,
112100
);
101+
102+
formatFiles(writtenFiles);

packages/dev-scripts/examples/genDocs.ts

Lines changed: 36 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ import * as path from "node:path";
33
import { fileURLToPath } from "node:url";
44
import {
55
addTitleToGroups,
6+
formatFiles,
67
getExampleProjects,
78
getProjectFiles,
89
groupProjects,
910
Project,
11+
writeGeneratedFile,
1012
} from "./util.js";
1113

1214
/*
@@ -23,7 +25,7 @@ const EXAMPLES_PAGES_DIR = path.resolve(DOCS_DIR, "./content/examples/");
2325
* Generates the <ExampleBlock> component that has all the source code of the example
2426
* This block can be used both in the /docs and in the /example page
2527
*/
26-
async function generateCodeForExample(project: Project) {
28+
async function generateCodeForExample(project: Project, written: string[]) {
2729
const projectFiles = getProjectFiles(project);
2830
const componentTarget = path.join(
2931
COMPONENT_DIR,
@@ -33,18 +35,18 @@ async function generateCodeForExample(project: Project) {
3335
const indexFile = path.join(componentTarget, "index.tsx");
3436
fs.rmSync(componentTarget, { recursive: true, force: true });
3537
fs.mkdirSync(componentTarget, { recursive: true });
36-
fs.writeFileSync(
38+
writeGeneratedFile(
3739
indexFile,
3840
`"use client";
3941
import Component from "./App";
4042
4143
export default Component;`,
44+
written,
4245
);
4346

4447
projectFiles.forEach(({ filename, code }) => {
4548
const target = path.join(componentTarget, filename);
46-
fs.mkdirSync(path.dirname(target), { recursive: true });
47-
fs.writeFileSync(target, code);
49+
writeGeneratedFile(target, code, written);
4850
});
4951
}
5052

@@ -61,22 +63,25 @@ ${project.readme}
6163
*
6264
* Consists of the contents of the readme
6365
*/
64-
async function generatePageForExample(project: Project) {
66+
async function generatePageForExample(project: Project, written: string[]) {
6567
const code = templatePageForExample(project);
6668

6769
const target = path.join(EXAMPLES_PAGES_DIR, project.fullSlug + ".mdx");
6870

69-
fs.writeFileSync(target, code);
71+
writeGeneratedFile(target, code, written);
7072
}
7173

7274
/**
7375
* generates meta.json file for each example group, so that order is preserved
7476
*/
75-
async function generateMetaForExampleGroup(group: {
76-
title: string;
77-
slug: string;
78-
projects: Project[];
79-
}) {
77+
async function generateMetaForExampleGroup(
78+
group: {
79+
title: string;
80+
slug: string;
81+
projects: Project[];
82+
},
83+
written: string[],
84+
) {
8085
if (!fs.existsSync(path.join(EXAMPLES_PAGES_DIR, group.slug))) {
8186
fs.mkdirSync(path.join(EXAMPLES_PAGES_DIR, group.slug));
8287
}
@@ -90,15 +95,18 @@ async function generateMetaForExampleGroup(group: {
9095

9196
const code = JSON.stringify(meta, undefined, 2);
9297

93-
fs.writeFileSync(target, code);
98+
writeGeneratedFile(target, code, written);
9499
}
95100

96101
/**
97102
* Generates the exampleGroups.gen.ts file, which contains all the necessary
98103
* data about the examples & their groups for the components we use in the
99104
* docs. E.g. the interactive demos and example cards.
100105
*/
101-
async function generateExampleGroupsData(projects: Project[]) {
106+
async function generateExampleGroupsData(
107+
projects: Project[],
108+
written: string[],
109+
) {
102110
const target = path.join(COMPONENT_DIR, "exampleGroupsData.gen.ts");
103111

104112
const groups = addTitleToGroups(groupProjects(projects));
@@ -146,10 +154,10 @@ export type ExampleData = ExampleGroupsData[number]["examplesData"][number];
146154
export const exampleGroupsData: ExampleGroupsData = ${JSON.stringify(exampleGroupsData, undefined, 2)};
147155
`;
148156

149-
fs.writeFileSync(target, code);
157+
writeGeneratedFile(target, code, written);
150158
}
151159

152-
async function addDependenciesToExample(project: Project) {
160+
async function addDependenciesToExample(project: Project, written: string[]) {
153161
const dependencies = project.config.dependencies || {};
154162
const devDependencies = project.config.devDependencies || {};
155163
if (
@@ -178,7 +186,11 @@ async function addDependenciesToExample(project: Project) {
178186
packageJsonObject.devDependencies[key] = "workspace:*";
179187
}
180188
});
181-
fs.writeFileSync(packageJson, JSON.stringify(packageJsonObject, null, 2));
189+
writeGeneratedFile(
190+
packageJson,
191+
JSON.stringify(packageJsonObject, null, 2),
192+
written,
193+
);
182194
}
183195
}
184196

@@ -197,17 +209,20 @@ fs.readdirSync(EXAMPLES_PAGES_DIR, { withFileTypes: true }).forEach((file) => {
197209
// generate new files
198210
const projects = getExampleProjects().filter((p) => p.config?.docs === true);
199211
const groups = addTitleToGroups(groupProjects(projects));
212+
const writtenFiles: string[] = [];
200213

201214
for (const group of Object.values(groups)) {
202-
await generateMetaForExampleGroup(group);
215+
await generateMetaForExampleGroup(group, writtenFiles);
203216

204217
for (const project of group.projects) {
205218
// eslint-disable-next-line no-console
206219
console.log("generating docs for", project.fullSlug);
207-
await generateCodeForExample(project);
208-
await generatePageForExample(project);
209-
await addDependenciesToExample(project);
220+
await generateCodeForExample(project, writtenFiles);
221+
await generatePageForExample(project, writtenFiles);
222+
await addDependenciesToExample(project, writtenFiles);
210223
}
211224
}
212225

213-
await generateExampleGroupsData(projects);
226+
await generateExampleGroupsData(projects, writtenFiles);
227+
228+
formatFiles(writtenFiles);

packages/dev-scripts/examples/util.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,58 @@
1+
import { execFileSync } from "node:child_process";
12
import { globSync } from "tinyglobby";
23
import * as fs from "node:fs";
34
import * as path from "node:path";
45
import { fileURLToPath } from "node:url";
56

67
const dir = path.parse(fileURLToPath(import.meta.url)).dir;
8+
const workspaceRoot = path.resolve(dir, "../../..");
9+
10+
/**
11+
* Writes a generated file, creating parent directories as needed, and records
12+
* the absolute path in `written` so it can be formatted afterwards.
13+
*/
14+
export function writeGeneratedFile(
15+
target: string,
16+
content: string,
17+
written: string[],
18+
) {
19+
const absolute = path.resolve(target);
20+
fs.mkdirSync(path.dirname(absolute), { recursive: true });
21+
fs.writeFileSync(absolute, content);
22+
written.push(absolute);
23+
}
24+
25+
/**
26+
* Formats the given files in-place using `vp fmt`. Files that are excluded by
27+
* oxfmt ignore rules (e.g. *.mdx) are tolerated. Runs in chunks to avoid argv
28+
* length limits.
29+
*/
30+
export function formatFiles(files: string[]) {
31+
const unique = [...new Set(files.map((file) => path.resolve(file)))];
32+
if (unique.length === 0) {
33+
return;
34+
}
35+
36+
const chunkSize = 200;
37+
for (let i = 0; i < unique.length; i += chunkSize) {
38+
const chunk = unique.slice(i, i + chunkSize);
39+
try {
40+
execFileSync("vp", ["fmt", ...chunk, "--write"], {
41+
encoding: "utf-8",
42+
cwd: workspaceRoot,
43+
});
44+
} catch (err: any) {
45+
const output = `${err?.stdout ?? ""}${err?.stderr ?? ""}`;
46+
if (output.includes("Expected at least one target file")) {
47+
// All files in this chunk were excluded by oxfmt ignore rules.
48+
continue;
49+
}
50+
// eslint-disable-next-line no-console
51+
console.error(output);
52+
throw err;
53+
}
54+
}
55+
}
756

857
export type Project = {
958
/**

packages/dev-scripts/package.json

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,13 @@
1313
"type": "module",
1414
"scripts": {
1515
"gen": "vp run gen:examples && vp run gen:docs",
16-
"gen:examples": "tsx examples/gen.ts",
17-
"gen:docs": "tsx examples/genDocs.ts",
1816
"lint": "vp lint examples",
1917
"clean": "rimraf dist && rimraf types"
2018
},
2119
"devDependencies": {
2220
"@types/react": "^19.2.3",
2321
"@types/react-dom": "^19.2.3",
2422
"glob": "^10.5.0",
25-
"prettier": "3.6.2",
2623
"react": "^19.2.5",
2724
"react-dom": "^19.2.5",
2825
"rimraf": "^5.0.10",

0 commit comments

Comments
 (0)