Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion .github/workflows/actions.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ on:
pull_request:
branches:
- main
- master
push:
branches:
- main
- master

jobs:
build:
Expand All @@ -24,7 +26,12 @@ jobs:
cache: npm
- run: npm ci
- run: npm run build
# - run: npm test
- name: Upload site for preview validation
uses: actions/upload-artifact@v4
with:
name: site-dist
path: dist/
retention-days: 1

check:
runs-on: ubuntu-latest
Expand All @@ -37,3 +44,15 @@ jobs:
cache: npm
- run: npm ci
- run: npm run check
- run: npm run check:security-compat
- run: npm run check:braces-backport
- name: Audit all installed dependencies
shell: bash
run: npm audit --json | tee audit.json
- name: Upload full audit report
if: always()
uses: actions/upload-artifact@v4
with:
name: dependency-audit
path: audit.json
retention-days: 1
6 changes: 5 additions & 1 deletion .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,8 @@ node_modules/
*.min.css

# Dependency files
package-lock.json
package-lock.json

# Preserve upstream source/test formatting for backport review
vendor/braces-backport/source/
vendor/braces-backport/upstream-tests/
2 changes: 2 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ export default [
"node_modules",
".github",
"types.generated.d.ts",
"vendor/braces-backport/source/**",
"vendor/braces-backport/upstream-tests/**",
".astro",
],
},
Expand Down
945 changes: 509 additions & 436 deletions package-lock.json

Large diffs are not rendered by default.

47 changes: 32 additions & 15 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@
"check:prettier": "prettier --check .",
"fix": "npm run fix:eslint && npm run fix:prettier",
"fix:eslint": "eslint --fix .",
"fix:prettier": "prettier -w ."
"fix:prettier": "prettier -w .",
"check:security-compat": "node scripts/check-security-compat.mjs",
"check:braces-backport": "python3 scripts/package-braces-backport.py && node scripts/check-braces-backport.mjs && node scripts/test-braces-upstream.mjs"
},
"dependencies": {
"@astrojs/rss": "^4.0.19",
Expand All @@ -28,7 +30,7 @@
"@astrolib/seo": "^1.0.0-beta.8",
"@fontsource-variable/inter": "^5.2.6",
"@fontsource-variable/outfit": "^5.2.8",
"astro": "^7.2.2",
"astro": "7.2.8",
"astro-embed": "^0.13.1",
"astro-icon": "^1.1.5",
"limax": "4.1.0",
Expand All @@ -48,48 +50,63 @@
"@types/js-yaml": "^4.0.9",
"@types/lodash.merge": "^4.6.9",
"@types/mdx": "^2.0.13",
"@typescript-eslint/eslint-plugin": "^8.39.0",
"@typescript-eslint/parser": "^8.39.0",
"@typescript-eslint/eslint-plugin": "8.71.1",
"@typescript-eslint/parser": "8.71.1",
"astro-compress": "2.4.3",
"astro-eslint-parser": "^1.2.2",
"eslint": "^9.33.0",
"eslint-plugin-astro": "^1.3.1",
"globals": "^16.3.0",
"js-yaml": "4.3.1",
"js-yaml": "4.3.2",
"mdast-util-to-string": "^4.0.0",
"prettier": "^3.6.2",
"prettier-plugin-astro": "^0.14.1",
"reading-time": "^1.5.0",
"sharp": "0.35.3",
"sharp": "0.35.5",
"tailwind-merge": "^2.6.0",
"tailwindcss": "^3.4.17",
"typescript": "^5.8.3",
"typescript-eslint": "^8.39.0",
"unist-util-visit": "^5.0.0"
"typescript-eslint": "8.71.1",
"unist-util-visit": "^5.0.0",
"braces": "file:vendor/braces-backport/braces-3.0.3-depth-guard.1.tgz"
},
"overrides": {
"@iconify/tools": "5.0.12",
"brace-expansion": "5.0.9",
"brace-expansion": "5.0.12",
"deepmerge-ts": "8.0.1",
"fast-uri": "3.1.6",
"fast-uri": "3.1.8",
"h3": ">=1.15.6",
"nanoid": "3.3.18",
"undici": "7.29.0",
"undici": "7.29.1",
"fast-xml-parser": ">=5.5.7",
"tar": "7.5.22",
"minimatch": ">=9.0.7",
"rollup": ">=4.59.0",
"axios": ">=1.13.5",
"svgo": "4.0.2",
"svgo": "4.1.0",
"flatted": ">=3.4.2",
"devalue": ">=5.6.4",
"devalue": "5.9.4",
"lodash": ">=4.17.23",
"diff": ">=5.2.2",
"mdast-util-to-hast": ">=13.2.1",
"picomatch": ">=4.0.4",
"yaml": ">=2.8.3",
"esbuild": "0.28.1",
"js-yaml": "4.3.1",
"postcss-selector-parser@^6": "6.1.3"
"js-yaml": "4.3.2",
"sharp": "$sharp",
"http-cache-semantics": "4.3.0",
"smol-toml": "1.9.0",
"source-map-js": "1.2.2",
"postcss-selector-parser@^7": "7.1.6",
"tailwindcss": {
"postcss-selector-parser": "7.1.6"
},
"postcss-nested": {
"postcss-selector-parser": "7.1.6"
},
"@tailwindcss/typography": {
"postcss-selector-parser": "7.1.6"
},
"braces": "$braces"
}
}
132 changes: 132 additions & 0 deletions scripts/braces-depth-probe.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import assert from "node:assert/strict";
import fs from "node:fs";
import { createRequire } from "node:module";
const require = createRequire(import.meta.url);
const { originalPath, patchedPath } = JSON.parse(fs.readFileSync(0, "utf8"));
const original = require(originalPath);
const patched = require(patchedPath);
const deep = "{".repeat(4000) + "x" + "}".repeat(4000);
assert.ok(deep.length < 10000);
const evidence = { inputLength: deep.length, original: {}, patched: {} };
for (const method of ["compile", "expand"]) {
assert.throws(
() => original[method](deep),
(error) => {
evidence.original[method] = { name: error.name, message: error.message };
return (
error instanceof RangeError &&
/call stack/i.test(error.message) &&
!error.code
);
},
);
}
const rejected = (fn, code = "ERR_BRACES_DEPTH") =>
assert.throws(
fn,
(error) =>
error instanceof RangeError &&
error.code === code &&
!/call stack/i.test(error.message),
);
for (const pattern of [
deep,
"(".repeat(4000) + "x" + ")".repeat(4000),
"{(".repeat(1500) + "x" + ")}".repeat(1500),
"{".repeat(101) + "x",
]) {
for (const method of ["parse", "compile", "expand", "stringify"])
rejected(() => patched[method](pattern));
}
// Bypass parsing intentionally: all public and internal AST entry points must guard.
for (const method of ["compile", "expand", "stringify"]) {
rejected(() => patched[method](original.parse(deep)));
const internal = require(patchedPath + "/lib/" + method);
rejected(() => internal(original.parse(deep)));
evidence.patched[method] = "ERR_BRACES_DEPTH for string and caller AST";
}
for (const opening of ["{", "(", "{("]) {
const depth = opening.length === 2 ? 50 : 100;
const closing = opening === "{" ? "}" : opening === "(" ? ")" : ")}";
const pattern = opening.repeat(depth) + "x" + closing.repeat(depth);
for (const method of ["compile", "expand", "stringify"])
assert.deepEqual(patched[method](pattern), original[method](pattern));
for (const method of ["parse", "compile", "expand", "stringify"])
rejected(() => patched[method](opening + pattern + closing));
}
for (const pattern of [
"\\{".repeat(200) + "x" + "\\}".repeat(200),
'"' + deep + '"',
"[" + deep + "]",
]) {
for (const method of ["compile", "expand", "stringify"])
assert.deepEqual(patched[method](pattern), original[method](pattern));
}
for (const maxDepth of [false, 0, -1, NaN, Infinity, 101, "100"])
rejected(
() => patched.parse("{a,b}", { maxDepth }),
"ERR_BRACES_DEPTH_OPTION",
);
rejected(() => patched.compile("{{a,b}}", { maxDepth: 1 }));
const cycle = { type: "root", nodes: [] };
cycle.nodes.push(cycle);
const parentCycle = { type: "root", nodes: [] };
parentCycle.parent = parentCycle;
const wide = {
type: "root",
nodes: Array.from({ length: 20011 }, () => ({ type: "text", value: "x" })),
};
for (const method of ["compile", "expand", "stringify"]) {
rejected(() => patched[method](cycle), "ERR_BRACES_AST");
rejected(() => patched[method](parentCycle), "ERR_BRACES_AST");
rejected(() => patched[method](wide), "ERR_BRACES_NODES");
}
// Differential corpus preserves options and ordinary/incomplete pattern outputs.
const patterns = [
"",
"x",
"a/{b,c}/d",
"{a,a,,b}",
"{01..05}",
"{5..1..2}",
"{a..e}",
"${a,b}",
"a{b{c,d}e",
"{a,b}}",
"\\{a,b}",
"(a|{b,c})",
"{1..x}",
"[{}]",
'"{a,b}"',
];
for (const first of ["a", "{b,c}", "{1..3}", "{,x}", "{foo,bar}"])
for (const second of ["", "{d,e}", "/{a,b}/x", "\\{x,y}"])
patterns.push(first + second);
const options = [
{},
{ expand: true },
{ expand: true, nodupes: true },
{ expand: true, noempty: true },
{ keepEscaping: true },
{ keepQuotes: true },
{ escapeInvalid: true },
{ rangeLimit: 50 },
];
let comparisons = 0;
for (const pattern of patterns)
for (const option of options) {
for (const method of ["compile", "expand", "stringify", "create"]) {
assert.deepEqual(
patched[method](pattern, option),
original[method](pattern, option),
);
comparisons++;
}
assert.deepEqual(
patched([pattern, pattern], option),
original([pattern, pattern], option),
);
comparisons++;
}
evidence.differentialComparisons = comparisons;
process.stdout.write(JSON.stringify(evidence));
Loading
Loading