Skip to content

Commit d505d50

Browse files
authored
feat: add force-include patterns and Node.js compatibility (#4)
1 parent 935dc07 commit d505d50

14 files changed

Lines changed: 167 additions & 45 deletions

File tree

.vitepress/config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,9 @@ export default defineConfig({
113113

114114
socialLinks: [
115115
{ icon: "github", link: "https://github.com/kriasoft/srcpack" },
116+
{ icon: "discord", link: "https://discord.com/invite/aG83xEb6RX" },
117+
{ icon: "x", link: "https://x.com/kriasoft" },
118+
{ icon: "bluesky", link: "https://bsky.app/profile/kriasoft.com" },
116119
],
117120

118121
footer: {

README.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,12 @@ Or add to `package.json`:
6060
// Simple glob
6161
"src/**/*"
6262

63-
// Array with exclusions
63+
// Array with exclusions (! prefix)
6464
["src/**/*", "!src/**/*.test.ts"]
6565

66+
// Force-include gitignored files (+ prefix)
67+
["docs/**/*", "+docs/**/*.local.md"]
68+
6669
// Full options
6770
{
6871
include: "src/**/*",
@@ -71,7 +74,7 @@ Or add to `package.json`:
7174
}
7275
```
7376

74-
Patterns follow glob syntax. Prefix with `!` to exclude. `.gitignore` patterns are respected automatically. Binary files are excluded.
77+
Patterns follow glob syntax. Prefix with `!` to exclude, `+` to force-include (bypasses `.gitignore`). Binary files are excluded.
7578

7679
### Google Drive Upload
7780

@@ -151,6 +154,13 @@ const config = await loadConfig();
151154
- https://kriasoft.com/srcpack/llms.txt
152155
- https://kriasoft.com/srcpack/llms-full.txt
153156

157+
## Community
158+
159+
- [Discord](https://discord.com/invite/aG83xEb6RX) — Questions, feedback, and discussion
160+
- [GitHub Issues](https://github.com/kriasoft/srcpack/issues) — Bug reports and feature requests
161+
162+
New contributors and OSS maintainers are welcome — join us on Discord or open an issue / PR.
163+
154164
## Backers
155165

156166
<a href="https://reactstarter.com/b/1"><img src="https://reactstarter.com/b/1.png" height="60" /></a>&nbsp;&nbsp;<a href="https://reactstarter.com/b/2"><img src="https://reactstarter.com/b/2.png" height="60" /></a>&nbsp;&nbsp;<a href="https://reactstarter.com/b/3"><img src="https://reactstarter.com/b/3.png" height="60" /></a>&nbsp;&nbsp;<a href="https://reactstarter.com/b/4"><img src="https://reactstarter.com/b/4.png" height="60" /></a>&nbsp;&nbsp;<a href="https://reactstarter.com/b/5"><img src="https://reactstarter.com/b/5.png" height="60" /></a>&nbsp;&nbsp;<a href="https://reactstarter.com/b/6"><img src="https://reactstarter.com/b/6.png" height="60" /></a>&nbsp;&nbsp;<a href="https://reactstarter.com/b/7"><img src="https://reactstarter.com/b/7.png" height="60" /></a>&nbsp;&nbsp;<a href="https://reactstarter.com/b/8"><img src="https://reactstarter.com/b/8.png" height="60" /></a>

bun.lock

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/configuration.md

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -74,15 +74,29 @@ bundles: {
7474

7575
## Pattern Syntax
7676

77-
Patterns follow standard glob syntax:
78-
79-
| Pattern | Matches |
80-
| ---------------- | ------------------------- |
81-
| `src/**/*` | All files under `src/` |
82-
| `*.ts` | TypeScript files in root |
83-
| `**/*.ts` | TypeScript files anywhere |
84-
| `!**/*.test.ts` | Exclude test files |
85-
| `{src,lib}/**/*` | Files in `src/` or `lib/` |
77+
Patterns follow standard glob syntax with special prefixes:
78+
79+
| Pattern | Matches |
80+
| ---------------- | ---------------------------------- |
81+
| `src/**/*` | All files under `src/` |
82+
| `*.ts` | TypeScript files in root |
83+
| `**/*.ts` | TypeScript files anywhere |
84+
| `!**/*.test.ts` | Exclude test files |
85+
| `+**/*.local.md` | Force-include, bypass `.gitignore` |
86+
| `{src,lib}/**/*` | Files in `src/` or `lib/` |
87+
88+
### Force-Include (`+` prefix)
89+
90+
Use `+` to include files that would normally be excluded by `.gitignore`:
91+
92+
```ts
93+
bundles: {
94+
docs: [
95+
"docs/**/*", // all docs (respects .gitignore)
96+
"+docs/**/*.local.md", // force-include local notes
97+
],
98+
}
99+
```
86100

87101
## Automatic Exclusions
88102

docs/getting-started.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,3 +198,4 @@ yarn dlx srcpack --no-upload # Skip upload even if configured
198198
- [Configuration Reference](./configuration.md) — All options explained
199199
- [Google Drive Upload](./upload.md) — Auto-sync bundles to the cloud
200200
- [CLI Reference](./cli.md) — Full command documentation
201+
- [Discord](https://discord.com/invite/aG83xEb6RX) — Questions and discussion

package.json

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "srcpack",
3-
"version": "0.1.4",
3+
"version": "0.1.6",
44
"description": "Zero-config CLI for bundling code into LLM-optimized context files",
55
"keywords": [
66
"llm",
@@ -30,7 +30,16 @@
3030
"author": "Konstantin Tarkus <koistya@kriasoft.com>",
3131
"license": "MIT",
3232
"homepage": "https://kriasoft.com/srcpack/",
33-
"repository": "github:kriasoft/srcpack",
33+
"repository": {
34+
"type": "git",
35+
"url": "https://github.com/kriasoft/srcpack.git"
36+
},
37+
"bugs": {
38+
"url": "https://github.com/kriasoft/srcpack/issues"
39+
},
40+
"engines": {
41+
"node": ">=18.0.0"
42+
},
3443
"type": "module",
3544
"types": "./dist/index.d.ts",
3645
"exports": {
@@ -48,8 +57,7 @@
4857
"schema.json"
4958
],
5059
"scripts": {
51-
"build": "bun build ./src/cli.ts ./src/index.ts --outdir ./dist --target bun && tsc",
52-
"typecheck": "tsc -p tsconfig.check.json",
60+
"build": "bun build ./src/cli.ts ./src/index.ts --outdir ./dist --target node && tsc",
5361
"check": "tsc -p tsconfig.check.json",
5462
"test": "bun test tests/unit/ tests/e2e/",
5563
"test:unit": "bun test tests/unit/",
@@ -67,14 +75,17 @@
6775
"@clack/prompts": "^0.11.0",
6876
"@googleapis/drive": "^20.0.0",
6977
"cosmiconfig": "^9.0.0",
78+
"fast-glob": "^3.3.3",
7079
"google-auth-library": "^10.5.0",
7180
"ignore": "^7.0.5",
7281
"oauth-callback": "^1.2.5",
7382
"ora": "^9.0.0",
83+
"picomatch": "^4.0.2",
7484
"zod": "^4.3.5"
7585
},
7686
"devDependencies": {
7787
"@types/bun": "^1.3.6",
88+
"@types/picomatch": "^4.0.2",
7889
"gh-pages": "^6.3.0",
7990
"prettier": "^3.8.0",
8091
"typescript": "^5.9.3",

src/bundle.ts

Lines changed: 52 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,27 @@
11
// SPDX-License-Identifier: MIT
22

3+
import { open, readFile, stat } from "node:fs/promises";
34
import { join } from "node:path";
4-
import { Glob } from "bun";
5+
import { glob } from "fast-glob";
6+
import picomatch from "picomatch";
57
import ignore, { type Ignore } from "ignore";
68
import type { BundleConfigInput } from "./config.ts";
79

810
// Binary file detection: check first 8KB for null bytes (same heuristic as git)
911
const BINARY_CHECK_SIZE = 8192;
1012

1113
async function isBinary(filePath: string): Promise<boolean> {
12-
const file = Bun.file(filePath);
13-
const size = file.size;
14-
if (size === 0) return false;
14+
const stats = await stat(filePath);
15+
if (stats.size === 0) return false;
1516

16-
const chunk = await file.slice(0, Math.min(size, BINARY_CHECK_SIZE)).bytes();
17-
return chunk.includes(0);
17+
const fd = await open(filePath, "r");
18+
try {
19+
const buffer = Buffer.alloc(Math.min(stats.size, BINARY_CHECK_SIZE));
20+
await fd.read(buffer, 0, buffer.length, 0);
21+
return buffer.includes(0);
22+
} finally {
23+
await fd.close();
24+
}
1825
}
1926

2027
export interface FileEntry {
@@ -30,11 +37,15 @@ export interface BundleResult {
3037
}
3138

3239
/**
33-
* Normalize BundleConfig to arrays of include/exclude patterns
40+
* Normalize BundleConfig to arrays of include/exclude/force patterns.
41+
* - Regular patterns: included, filtered by .gitignore
42+
* - `!pattern`: excluded from results
43+
* - `+pattern`: force-included, bypasses .gitignore
3444
*/
3545
function normalizePatterns(config: BundleConfigInput): {
3646
include: string[];
3747
exclude: string[];
48+
force: string[];
3849
} {
3950
let patterns: string[];
4051

@@ -50,29 +61,28 @@ function normalizePatterns(config: BundleConfigInput): {
5061

5162
const include: string[] = [];
5263
const exclude: string[] = [];
64+
const force: string[] = [];
5365

5466
for (const p of patterns) {
5567
if (p.startsWith("!")) {
5668
exclude.push(p.slice(1));
69+
} else if (p.startsWith("+")) {
70+
force.push(p.slice(1));
5771
} else {
5872
include.push(p);
5973
}
6074
}
6175

62-
return { include, exclude };
76+
return { include, exclude, force };
6377
}
6478

79+
type Matcher = (path: string) => boolean;
80+
6581
/**
66-
* Check if a path matches any of the exclusion patterns
82+
* Check if a path matches any of the exclusion matchers
6783
*/
68-
function isExcluded(filePath: string, excludePatterns: string[]): boolean {
69-
for (const pattern of excludePatterns) {
70-
const glob = new Glob(pattern);
71-
if (glob.match(filePath)) {
72-
return true;
73-
}
74-
}
75-
return false;
84+
function isExcluded(filePath: string, matchers: Matcher[]): boolean {
85+
return matchers.some((match) => match(filePath));
7686
}
7787

7888
/**
@@ -83,7 +93,7 @@ async function loadGitignore(cwd: string): Promise<Ignore> {
8393
const gitignorePath = join(cwd, ".gitignore");
8494

8595
try {
86-
const content = await Bun.file(gitignorePath).text();
96+
const content = await readFile(gitignorePath, "utf-8");
8797
ig.add(content);
8898
} catch {
8999
// No .gitignore file, return empty ignore instance
@@ -94,20 +104,37 @@ async function loadGitignore(cwd: string): Promise<Ignore> {
94104

95105
/**
96106
* Resolve bundle config to a list of file paths.
97-
* Respects .gitignore patterns in the working directory.
107+
* - Regular patterns respect .gitignore
108+
* - Force patterns (+prefix) bypass .gitignore
109+
* - Exclude patterns (!prefix) filter both
98110
*/
99111
export async function resolvePatterns(
100112
config: BundleConfigInput,
101113
cwd: string,
102114
): Promise<string[]> {
103-
const { include, exclude } = normalizePatterns(config);
115+
const { include, exclude, force } = normalizePatterns(config);
116+
const excludeMatchers = exclude.map((p) => picomatch(p));
104117
const gitignore = await loadGitignore(cwd);
105118
const files = new Set<string>();
106119

107-
for (const pattern of include) {
108-
const glob = new Glob(pattern);
109-
for await (const match of glob.scan({ cwd, onlyFiles: true })) {
110-
if (!isExcluded(match, exclude) && !gitignore.ignores(match)) {
120+
// Regular includes: respect .gitignore
121+
if (include.length > 0) {
122+
const matches = await glob(include, { cwd, onlyFiles: true, dot: true });
123+
for (const match of matches) {
124+
if (!isExcluded(match, excludeMatchers) && !gitignore.ignores(match)) {
125+
const fullPath = join(cwd, match);
126+
if (!(await isBinary(fullPath))) {
127+
files.add(match);
128+
}
129+
}
130+
}
131+
}
132+
133+
// Force includes: bypass .gitignore
134+
if (force.length > 0) {
135+
const matches = await glob(force, { cwd, onlyFiles: true, dot: true });
136+
for (const match of matches) {
137+
if (!isExcluded(match, excludeMatchers)) {
111138
const fullPath = join(cwd, match);
112139
if (!(await isBinary(fullPath))) {
113140
files.add(match);
@@ -184,7 +211,7 @@ export async function createBundle(
184211
for (let i = 0; i < files.length; i++) {
185212
const filePath = files[i]!;
186213
const fullPath = join(cwd, filePath);
187-
const content = await Bun.file(fullPath).text();
214+
const content = await readFile(fullPath, "utf-8");
188215
const lines = countLines(content);
189216

190217
// Separator takes 1 line, then content starts on next line

src/cli.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
#!/usr/bin/env bun
1+
#!/usr/bin/env node
22
// SPDX-License-Identifier: MIT
33

4-
import { mkdir } from "node:fs/promises";
4+
import { mkdir, writeFile } from "node:fs/promises";
55
import { dirname, join } from "node:path";
66
import ora from "ora";
77
import { bundleOne, type BundleResult } from "./bundle.ts";
@@ -154,7 +154,7 @@ Options:
154154
}
155155
} else {
156156
await mkdir(dirname(outPath), { recursive: true });
157-
await Bun.write(outPath, result.content);
157+
await writeFile(outPath, result.content);
158158
console.log(
159159
` ${nameCol} ${filesCol} ${plural(fileCount, "file")} ${linesCol} ${plural(lineCount, "line")}${outfile}`,
160160
);

src/init.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// SPDX-License-Identifier: MIT
22

33
import * as p from "@clack/prompts";
4-
import { appendFile, readFile } from "node:fs/promises";
4+
import { appendFile, readFile, writeFile } from "node:fs/promises";
55
import { existsSync } from "node:fs";
66
import { join } from "node:path";
77

@@ -70,7 +70,7 @@ export async function runInit(): Promise<void> {
7070

7171
// Generate and write config
7272
const config = generateConfig(bundles, outDirValue);
73-
await Bun.write(configPath, config);
73+
await writeFile(configPath, config);
7474

7575
// Add output directory to .gitignore
7676
await addToGitignore(cwd, outDirValue);
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
*.local.md

0 commit comments

Comments
 (0)