Skip to content
Merged
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "srcpack",
"version": "0.1.14",
"version": "0.1.15",
"description": "Zero-config CLI for bundling code into LLM-optimized context files",
"keywords": [
"llm",
Expand Down
39 changes: 35 additions & 4 deletions src/bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,11 +161,22 @@ async function loadGitignore(cwd: string): Promise<GitignoreResult> {
return { ignore: ig, globPatterns };
}

/**
* Check if a glob pattern references paths outside cwd.
* Patterns traversing to parent directories start with ../ (or ./../).
*/
function isExternalPattern(pattern: string): boolean {
// Handle redundant ./ prefix (e.g., ./../other)
const normalized = pattern.startsWith("./") ? pattern.slice(2) : pattern;
return normalized.startsWith("../");
}

/**
* Resolve bundle config to a list of file paths.
* - Regular patterns respect .gitignore
* - Force patterns (+prefix) bypass .gitignore
* - Exclude patterns (!prefix) filter both
* - External patterns (../) skip .gitignore entirely
*/
export async function resolvePatterns(
config: BundleConfigInput,
Expand All @@ -176,10 +187,13 @@ export async function resolvePatterns(
const { ignore: gitignore, globPatterns } = await loadGitignore(cwd);
const files = new Set<string>();

// Regular includes: respect .gitignore
// Pass gitignore patterns to fast-glob to skip ignored directories during traversal
if (include.length > 0) {
const matches = await glob(include, {
// Split patterns into internal (within cwd) and external (../ prefixed)
const internalPatterns = include.filter((p) => !isExternalPattern(p));
const externalPatterns = include.filter(isExternalPattern);

// Internal patterns: respect .gitignore
if (internalPatterns.length > 0) {
const matches = await glob(internalPatterns, {
cwd,
onlyFiles: true,
dot: true,
Expand All @@ -195,6 +209,23 @@ export async function resolvePatterns(
}
}

// External patterns: skip .gitignore (it doesn't apply outside cwd)
if (externalPatterns.length > 0) {
const matches = await glob(externalPatterns, {
cwd,
onlyFiles: true,
dot: true,
});
for (const match of matches) {
if (!isExcluded(match, excludeMatchers)) {
const fullPath = join(cwd, match);
if (!(await isBinary(fullPath))) {
files.add(match);
}
}
}
}

// Force includes: bypass .gitignore (no ignore patterns passed to glob)
if (force.length > 0) {
const matches = await glob(force, { cwd, onlyFiles: true, dot: true });
Expand Down
1 change: 1 addition & 0 deletions tests/fixtures/gitignore-project/dist/bundle.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
// compiled output
34 changes: 34 additions & 0 deletions tests/unit/bundle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,40 @@ describe("resolvePatterns", () => {
expect(files).toContain("build/keep.txt"); // Re-included by negation
expect(files).not.toContain("build/bundle.js"); // Still ignored
});

test("should handle patterns pointing outside cwd", async () => {
// Use sample-project as cwd and resolve pattern pointing to gitignore-project
// External patterns skip .gitignore entirely (it doesn't apply to external files)
const files = await resolvePatterns(
"../gitignore-project/src/**/*.ts",
fixturesDir,
);

// Should include files from the external directory
expect(files).toContain("../gitignore-project/src/index.ts");
expect(files).toContain("../gitignore-project/src/utils.ts");
});

test("should not apply cwd gitignore to external patterns", async () => {
// fixturesDir is sample-project; external pattern points to gitignore-project
// Even though "dist" is a common gitignore pattern, external paths skip .gitignore
const files = await resolvePatterns(
"../gitignore-project/dist/**/*.js",
fixturesDir,
);

expect(files).toContain("../gitignore-project/dist/bundle.js");
});

test("should handle ./../ prefix as external pattern", async () => {
// Redundant ./ prefix should still be recognized as external
const files = await resolvePatterns(
"./../gitignore-project/src/**/*.ts",
fixturesDir,
);

expect(files).toContain("./../gitignore-project/src/index.ts");
});
});

describe("formatIndex", () => {
Expand Down