Skip to content
Open
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
4 changes: 4 additions & 0 deletions packages/bun-types/scripts/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ let claude = Bun.file(join(import.meta.dir, "../../../src/cli/init/rule.md"));
if (await claude.exists()) {
let original = await claude.text();
const endOfFrontMatter = original.lastIndexOf("---\n");
// The template locates the docs through the project's node_modules, once per install
// layout (hoisted path, then the isolated store path in parentheses). Inside this package
// they are just ./docs.
original = original.replace(/ \(`node_modules\/\.bun\/[^`]*`[^)]*\)/, "");
original = original.replaceAll("node_modules/bun-types/", "");
if (endOfFrontMatter > -1) {
original = original.slice(endOfFrontMatter + "---\n".length).trim() + "\n";
Expand Down
13 changes: 8 additions & 5 deletions scripts/glob-sources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,14 +79,16 @@ const patterns = {
* all `*.rs` + workspace manifests — implicit inputs to the cargo step.
* `rust-toolchain.toml` is included so a nightly bump invalidates the
* staticlib (cargo's own fingerprinting then forces a full rebuild).
* `.html` under `src/runtime/` is embedded with `include_bytes!` (e.g. the
* dev error page template), so edits to it must re-run cargo too.
* `.html` under `src/runtime/` and the `bun init` templates are embedded
* with `include_bytes!` (e.g. the dev error page template, `init/rule.md`),
* so edits to them must re-run cargo too.
*/
rust: {
paths: [
"src/**/*.rs",
"src/**/Cargo.toml",
"src/runtime/**/*.html",
"src/runtime/cli/init/**/*.*",
"Cargo.toml",
"Cargo.lock",
"rust-toolchain.toml",
Expand Down Expand Up @@ -151,17 +153,18 @@ export function globAllSources(): Sources {
excludePrefix.push(ex.slice(0, -2)); // keep trailing '/'
else excludeExact.add(ex);
}
const files: string[] = [];
// A Set: patterns in one list may overlap (the `bun init` templates include `.html`).
const matched = new Set<string>();
for (const pattern of spec.paths) {
for (const rel of globSync(pattern, { cwd: root })) {
const normalized = normalize(rel);
if (excludeExact.has(normalized)) continue;
if (excludePrefix.some(p => normalized.startsWith(p))) continue;
files.push(resolve(root, normalized));
matched.add(resolve(root, normalized));
}
}

files.sort((a, b) => a.localeCompare(b));
const files = [...matched].sort((a, b) => a.localeCompare(b));
assert(files.length > 0, `Source list '${field}' matched nothing`, {
file: import.meta.url,
hint: `Patterns: ${spec.paths.join(", ")}`,
Expand Down
2 changes: 1 addition & 1 deletion src/runtime/cli/init/rule.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,4 +108,4 @@ Then, run index.ts
bun --hot ./index.ts
```

For more information, read the Bun API docs in `node_modules/bun-types/docs/**.mdx`.
For more information, read the Bun API docs in `node_modules/bun-types/docs/**/*.mdx` (`node_modules/.bun/bun-types@*/node_modules/bun-types/docs/**/*.mdx` when the project uses the isolated linker).
47 changes: 47 additions & 0 deletions test/cli/init/init.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -425,4 +425,51 @@ const initEnv = { ...bunEnv, BUN_AGENT_RULE_DISABLED: "1" };
expect(fs.existsSync(path.join(temp, "CLAUDE.md"))).toBe(false);
expect(fs.existsSync(path.join(temp, ".cursor"))).toBe(false);
});

// The agent rule ends by pointing at the docs that bun-types ships. bun-types is
// only a transitive dependency (of @types/bun), so where `bun install` puts it
// depends on the linker: top-level node_modules/bun-types when hoisted, the
// node_modules/.bun store when isolated. The rule has to name a path that exists
// either way.
test.each(["hoisted", "isolated"])(
"agent rule points at the installed bun-types docs (%s linker)",
async linker => {
await using temp = tempDir(`bun-init-agent-rule-docs-${linker}`, {
"bunfig.toml": `[install]\nlinker = "${linker}"\n`,
});

await using proc = Bun.spawn({
cmd: [bunExe(), "init", "-y"],
cwd: temp,
stdio: ["ignore", "pipe", "pipe"],
// CURSOR_TRACE_ID makes `bun init` write the cursor rule on every platform
// (CLAUDE.md additionally needs a `claude` binary, which CI machines may not have).
env: { ...bunEnv, CURSOR_TRACE_ID: "1", CLAUDE_CODE_AGENT_RULE_DISABLED: "1" },
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect({ stdout, stderr, exitCode }).toMatchObject({ exitCode: 0 });

const rule = fs.readFileSync(path.join(temp, ".cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc"), "utf8");
const docGlobs = Array.from(rule.matchAll(/`([^`]*\.mdx)`/g), m => m[1]);
expect(docGlobs.length).toBeGreaterThan(0);

// Identify a doc by its path inside docs/ so the comparison does not depend on
// which node_modules directory the linker installed the package into.
const docName = (file: string) => file.replaceAll("\\", "/").replace(/^.*\/docs\//, "");
const hinted = new Set(
docGlobs.flatMap(glob => Array.from(new Bun.Glob(glob).scanSync({ cwd: String(temp) }), docName)),
);
const installed = new Set(
Array.from(
new Bun.Glob("node_modules/**/bun-types/docs/**/*.mdx").scanSync({ cwd: String(temp), dot: true }),
docName,
),
);

expect(installed.size).toBeGreaterThan(0);
expect(hinted.size).toBe(installed.size);
expect(hinted).toEqual(installed);
},
30_000,
);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});
30 changes: 28 additions & 2 deletions test/integration/bun-types/bun-types.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { $ as Shell, fileURLToPath } from "bun";
import { fileURLToPath, $ as Shell } from "bun";
import { afterAll, beforeAll, describe, expect, setDefaultTimeout, test } from "bun:test";
import { bunEnv, bunExe, isDebug, makeTree } from "harness";
import { existsSync, readFileSync } from "node:fs";
import { existsSync, readdirSync, readFileSync } from "node:fs";
import { cp, mkdir, mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { basename, dirname, join, relative } from "node:path";
Expand Down Expand Up @@ -326,6 +326,32 @@ describe("@types/bun integration test", () => {
expect((await claude.text()).length).toBeGreaterThan(0);
});

// CLAUDE.md is the `bun init` agent rule, which locates the docs through a project's
// node_modules. The copy packed into this package has to point at the package's own
// docs/ directory instead, and what it points at has to cover every packed doc page.
test("packed CLAUDE.md points at the packed docs", async () => {
const packageDir = join(BASE_FIXTURE_DIR, "node_modules", "bun-types");
const claude = await Bun.file(join(packageDir, "CLAUDE.md")).text();

const docGlobs = Array.from(claude.matchAll(/`([^`]*\.mdx)`/g), m => m[1]);
expect(docGlobs.length).toBeGreaterThan(0);
expect(docGlobs.filter(glob => glob.includes("node_modules"))).toEqual([]);

const slash = (file: string) => file.replaceAll("\\", "/");
const hinted = new Set(
docGlobs.flatMap(glob => Array.from(new Bun.Glob(glob).scanSync({ cwd: packageDir }), slash)),
);
const packed = new Set(
readdirSync(join(packageDir, "docs"), { recursive: true, encoding: "utf8" })
.filter(file => file.endsWith(".mdx"))
.map(file => `docs/${slash(file)}`),
);

expect(packed.size).toBeGreaterThan(0);
expect(hinted.size).toBe(packed.size);
expect(hinted).toEqual(packed);
});

describe("basic type checks", () => {
typeTest("checks without lib.dom.d.ts", {
emptyInterfaces: expectedEmptyInterfacesWhenNoDOM,
Expand Down