fix(build): make a fresh clone build on Windows

A fresh clone could not build on Windows at all. Three independent causes,
reported by @kevin101681 in #11:

- check-css.mjs resolved the styles directory with `new URL(...).pathname`,
  which yields `/C:/Users/...`; readdirSync resolves that against the current
  drive and looks for `C:\C:\Users\...`. fileURLToPath is also the fix for a
  latent bug everywhere else: `.pathname` stays percent-encoded, so a checkout
  under a directory with a space in it fails on macOS and Linux too.
- vendor-assets.mjs derived asset names with `from.split("/").pop()`, a no-op
  on the backslash paths require.resolve returns, so the whole absolute source
  path was appended to the destination. path.basename handles both separators.
- The front-matter regexes in the three gen.mjs scripts were anchored to
  `^---\n`, and with core.autocrlf=true — the Git-for-Windows default — the
  markdown specs check out as CRLF, so they reported a missing front-matter
  block rather than a wrong one.

.gitattributes pins every checkout to LF, which prevents the third from
recurring. The regexes take `\r?\n` anyway: .gitattributes only applies on
checkout, so it does nothing for a clone made before it existed, a zip
download, a patch, or an editor configured to write CRLF.

sync-readme.mjs had the same defect one step further on, and it was the worse
one — it splits on "\n" but rejoins the banner with LF, so on a CRLF tree the
--check comparison could never match the file on disk and a Windows
contributor had no way to make that CI gate pass.
This commit is contained in:
Nayan
2026-08-11 23:10:43 +05:30
parent c1417e3504
commit 83b1ab0520
7 changed files with 36 additions and 9 deletions
+4
View File
@@ -0,0 +1,4 @@
# Check out everything with LF, even on Windows (core.autocrlf=true would
# otherwise write CRLF, which breaks the front-matter regexes in the
# editor-tokens/editor-icons/site-tokens gen scripts).
* text=auto eol=lf
+6 -3
View File
@@ -17,7 +17,7 @@
import { copyFileSync, existsSync, mkdirSync } from "node:fs";
import { createRequire } from "node:module";
import { dirname, join } from "node:path";
import { basename, join } from "node:path";
import { fileURLToPath } from "node:url";
const require = createRequire(import.meta.url);
@@ -50,7 +50,10 @@ function resolve(specifier, what) {
// serves `${bundle}.map` derived from whatever path it resolved, so the map has
// to land beside its bundle here too.
function copyWithMap(from, toDir) {
const name = from.split("/").pop();
// basename, not split("/"): require.resolve returns a native path, so on
// Windows there is no "/" to split on and `name` would come back as the whole
// absolute path — which join() then appends to the destination wholesale.
const name = basename(from);
copyFileSync(from, join(toDir, name));
if (existsSync(`${from}.map`)) {
copyFileSync(`${from}.map`, join(toDir, `${name}.map`));
@@ -87,5 +90,5 @@ for (const name of expected) {
}
console.log(
`vendor-assets: ${expected.length} bundles + ${FONTS.length} fonts -> ${dirname(vendorDir)}/vendor`
`vendor-assets: ${expected.length} bundles + ${FONTS.length} fonts -> ${vendorDir}`
);
+3 -1
View File
@@ -39,7 +39,9 @@ if (!existsSync(specPath)) {
}
const raw = readFileSync(specPath, "utf8");
const match = raw.match(/^---\n([\s\S]*?)\n---/);
// \r? because a Windows checkout with core.autocrlf=true hands us CRLF, and a
// front-matter block that opens `---\r\n` would otherwise read as absent.
const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/);
if (!match) {
throw new Error("gen: ICONS.md is missing its YAML front-matter block");
}
+3 -1
View File
@@ -16,7 +16,9 @@ if (!existsSync(specPath)) {
}
const raw = readFileSync(specPath, "utf8");
const match = raw.match(/^---\n([\s\S]*?)\n---/);
// \r? because a Windows checkout with core.autocrlf=true hands us CRLF, and a
// front-matter block that opens `---\r\n` would otherwise read as absent.
const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/);
if (!match) {
throw new Error("gen: EDITOR.md is missing its YAML front-matter block");
}
+7 -1
View File
@@ -16,8 +16,14 @@
*/
import { readdirSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
const DIR = new URL("../src/styles/", import.meta.url).pathname;
// fileURLToPath, not `.pathname`. `.pathname` is a URL component, not a path:
// on Windows it yields `/C:/…`, which readdirSync resolves against the current
// drive as `C:\C:\…`, and on every platform it stays percent-encoded, so a
// checkout under a directory with a space in it fails too. `join(DIR, file)`
// below needs a string, so this cannot stay a URL.
const DIR = fileURLToPath(new URL("../src/styles/", import.meta.url));
const START = /export const css\s*=\s*`/;
const problems = [];
+3 -1
View File
@@ -16,7 +16,9 @@ if (!existsSync(designPath)) {
}
const raw = readFileSync(designPath, "utf8");
const match = raw.match(/^---\n([\s\S]*?)\n---/);
// \r? because a Windows checkout with core.autocrlf=true hands us CRLF, and a
// front-matter block that opens `---\r\n` would otherwise read as absent.
const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/);
if (!match) {
throw new Error("gen: DESIGN.md is missing its YAML front-matter block");
}
+10 -2
View File
@@ -33,6 +33,10 @@ const LINK = /(!?)\[([^\]]*)\]\(([^)\s]+)\)/g;
/** ``` or ~~~ opening or closing a fenced block, which is never rewritten. */
const FENCE = /^\s*(?:```|~~~)/;
/** A scheme, a protocol-relative host, or an anchor — already resolvable. */
// Matches CRLF as readily as LF: a Windows working tree would otherwise leave a
// trailing \r on every line, and the rejoin below emits LF, so the --check
// comparison could never match the file on disk.
const LINE_BREAK = /\r?\n/;
const ABSOLUTE = /^(?:[a-z][a-z0-9+.-]*:|\/\/|#)/i;
const LEADING_DOT_SLASH = /^\.\//;
const REPO_URL = /github\.com[/:]([^/]+)\/([^/.]+)/;
@@ -94,7 +98,7 @@ function render(source, slug) {
return `${bang}[${label}](${absolutize(target, bang === "!", slug)})`;
});
for (const line of source.split("\n")) {
for (const line of source.split(LINE_BREAK)) {
if (FENCE.test(line)) {
inFence = !inFence;
out.push(line);
@@ -115,7 +119,11 @@ if (process.argv.includes("--check")) {
} catch {
die("apps/cli/README.md is missing — run `node scripts/sync-readme.mjs`");
}
if (current !== text) {
// Compare on content, not line endings. .gitattributes pins a fresh checkout
// to LF, but a file that arrived some other way — an existing clone, a zip, a
// patch, an editor configured for CRLF — would otherwise report as eternally
// stale with no way for the contributor to make it pass.
if (current.replace(/\r\n/g, "\n") !== text) {
die(
"apps/cli/README.md is stale — run `node scripts/sync-readme.mjs` and commit the result"
);