srcpack 0.1.6 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -11
- package/dist/cli.js +78 -12
- package/dist/config.d.ts +24 -0
- package/dist/index.js +3 -1
- package/package.json +1 -1
- package/src/bundle.ts +71 -6
- package/src/cli.ts +75 -8
- package/src/config.ts +38 -3
package/README.md
CHANGED
|
@@ -48,11 +48,14 @@ Or add to `package.json`:
|
|
|
48
48
|
|
|
49
49
|
### Options
|
|
50
50
|
|
|
51
|
-
| Option
|
|
52
|
-
|
|
|
53
|
-
| `outDir`
|
|
54
|
-
| `
|
|
55
|
-
| `
|
|
51
|
+
| Option | Default | Description |
|
|
52
|
+
| ------------- | ---------- | -------------------------------------- |
|
|
53
|
+
| `outDir` | `.srcpack` | Output directory for bundles |
|
|
54
|
+
| `emptyOutDir` | `true`\* | Empty output directory before bundling |
|
|
55
|
+
| `bundles` | — | Named bundles with glob patterns |
|
|
56
|
+
| `upload` | — | Upload destination(s) |
|
|
57
|
+
|
|
58
|
+
\*`emptyOutDir` defaults to `true` when `outDir` is inside project root. When `outDir` is outside root, a warning is emitted unless explicitly set.
|
|
56
59
|
|
|
57
60
|
### Bundle Config
|
|
58
61
|
|
|
@@ -90,6 +93,7 @@ export default defineConfig({
|
|
|
90
93
|
folderId: "1ABC...", // Google Drive folder ID (from URL)
|
|
91
94
|
clientId: "...",
|
|
92
95
|
clientSecret: "...",
|
|
96
|
+
exclude: ["local"], // skip specific bundles
|
|
93
97
|
},
|
|
94
98
|
});
|
|
95
99
|
```
|
|
@@ -127,12 +131,14 @@ export function utils() {
|
|
|
127
131
|
## CLI
|
|
128
132
|
|
|
129
133
|
```bash
|
|
130
|
-
npx srcpack
|
|
131
|
-
npx srcpack web api
|
|
132
|
-
npx srcpack --dry-run
|
|
133
|
-
npx srcpack --
|
|
134
|
-
npx srcpack
|
|
135
|
-
npx srcpack
|
|
134
|
+
npx srcpack # Bundle all, upload if configured
|
|
135
|
+
npx srcpack web api # Bundle specific bundles only
|
|
136
|
+
npx srcpack --dry-run # Preview without writing files
|
|
137
|
+
npx srcpack --emptyOutDir # Empty output directory before bundling
|
|
138
|
+
npx srcpack --no-emptyOutDir # Keep existing files in output directory
|
|
139
|
+
npx srcpack --no-upload # Bundle only, skip upload
|
|
140
|
+
npx srcpack init # Interactive config setup
|
|
141
|
+
npx srcpack login # Authenticate with Google Drive
|
|
136
142
|
```
|
|
137
143
|
|
|
138
144
|
## API
|
|
@@ -159,6 +165,8 @@ const config = await loadConfig();
|
|
|
159
165
|
- [Discord](https://discord.com/invite/aG83xEb6RX) — Questions, feedback, and discussion
|
|
160
166
|
- [GitHub Issues](https://github.com/kriasoft/srcpack/issues) — Bug reports and feature requests
|
|
161
167
|
|
|
168
|
+
New contributors and OSS maintainers are welcome — join us on Discord or open an issue / PR.
|
|
169
|
+
|
|
162
170
|
## Backers
|
|
163
171
|
|
|
164
172
|
<a href="https://reactstarter.com/b/1"><img src="https://reactstarter.com/b/1.png" height="60" /></a> <a href="https://reactstarter.com/b/2"><img src="https://reactstarter.com/b/2.png" height="60" /></a> <a href="https://reactstarter.com/b/3"><img src="https://reactstarter.com/b/3.png" height="60" /></a> <a href="https://reactstarter.com/b/4"><img src="https://reactstarter.com/b/4.png" height="60" /></a> <a href="https://reactstarter.com/b/5"><img src="https://reactstarter.com/b/5.png" height="60" /></a> <a href="https://reactstarter.com/b/6"><img src="https://reactstarter.com/b/6.png" height="60" /></a> <a href="https://reactstarter.com/b/7"><img src="https://reactstarter.com/b/7.png" height="60" /></a> <a href="https://reactstarter.com/b/8"><img src="https://reactstarter.com/b/8.png" height="60" /></a>
|
package/dist/cli.js
CHANGED
|
@@ -204450,8 +204450,8 @@ var require_src8 = __commonJS((exports, module) => {
|
|
|
204450
204450
|
});
|
|
204451
204451
|
|
|
204452
204452
|
// src/cli.ts
|
|
204453
|
-
import { mkdir as mkdir3, writeFile as writeFile4 } from "node:fs/promises";
|
|
204454
|
-
import { dirname as dirname3, join as join6 } from "node:path";
|
|
204453
|
+
import { mkdir as mkdir3, readdir, rm, writeFile as writeFile4 } from "node:fs/promises";
|
|
204454
|
+
import { dirname as dirname3, isAbsolute, join as join6, relative, resolve } from "node:path";
|
|
204455
204455
|
|
|
204456
204456
|
// node_modules/ora/index.js
|
|
204457
204457
|
import process8 from "node:process";
|
|
@@ -207577,22 +207577,52 @@ function normalizePatterns(config) {
|
|
|
207577
207577
|
function isExcluded(filePath, matchers) {
|
|
207578
207578
|
return matchers.some((match) => match(filePath));
|
|
207579
207579
|
}
|
|
207580
|
+
function gitignoreToGlobPatterns(lines) {
|
|
207581
|
+
const hasNegation = lines.some((line) => {
|
|
207582
|
+
const trimmed = line.trim();
|
|
207583
|
+
return trimmed.startsWith("!");
|
|
207584
|
+
});
|
|
207585
|
+
if (hasNegation)
|
|
207586
|
+
return [];
|
|
207587
|
+
const patterns = [];
|
|
207588
|
+
for (const line of lines) {
|
|
207589
|
+
const trimmed = line.trim();
|
|
207590
|
+
if (!trimmed || trimmed.startsWith("#"))
|
|
207591
|
+
continue;
|
|
207592
|
+
if (trimmed.startsWith("/") || trimmed.includes("*") || trimmed.includes("?") || trimmed.includes("[") || trimmed.includes("/") || trimmed.includes("\\")) {
|
|
207593
|
+
continue;
|
|
207594
|
+
}
|
|
207595
|
+
const name = trimmed.endsWith("/") ? trimmed.slice(0, -1) : trimmed;
|
|
207596
|
+
if (name && /^[\w.-]+$/.test(name)) {
|
|
207597
|
+
patterns.push(`**/${name}/**`);
|
|
207598
|
+
}
|
|
207599
|
+
}
|
|
207600
|
+
return patterns;
|
|
207601
|
+
}
|
|
207580
207602
|
async function loadGitignore(cwd) {
|
|
207581
207603
|
const ig = import_ignore.default();
|
|
207582
207604
|
const gitignorePath = join(cwd, ".gitignore");
|
|
207605
|
+
let globPatterns = [];
|
|
207583
207606
|
try {
|
|
207584
207607
|
const content = await readFile(gitignorePath, "utf-8");
|
|
207585
207608
|
ig.add(content);
|
|
207609
|
+
globPatterns = gitignoreToGlobPatterns(content.split(`
|
|
207610
|
+
`));
|
|
207586
207611
|
} catch {}
|
|
207587
|
-
return ig;
|
|
207612
|
+
return { ignore: ig, globPatterns };
|
|
207588
207613
|
}
|
|
207589
207614
|
async function resolvePatterns(config, cwd) {
|
|
207590
207615
|
const { include, exclude, force } = normalizePatterns(config);
|
|
207591
207616
|
const excludeMatchers = exclude.map((p) => import_picomatch.default(p));
|
|
207592
|
-
const gitignore = await loadGitignore(cwd);
|
|
207617
|
+
const { ignore: gitignore, globPatterns } = await loadGitignore(cwd);
|
|
207593
207618
|
const files = new Set;
|
|
207594
207619
|
if (include.length > 0) {
|
|
207595
|
-
const matches = await import_fast_glob.glob(include, {
|
|
207620
|
+
const matches = await import_fast_glob.glob(include, {
|
|
207621
|
+
cwd,
|
|
207622
|
+
onlyFiles: true,
|
|
207623
|
+
dot: true,
|
|
207624
|
+
ignore: globPatterns
|
|
207625
|
+
});
|
|
207596
207626
|
for (const match of matches) {
|
|
207597
207627
|
if (!isExcluded(match, excludeMatchers) && !gitignore.ignores(match)) {
|
|
207598
207628
|
const fullPath = join(cwd, match);
|
|
@@ -221253,10 +221283,12 @@ var UploadConfigSchema = exports_external.object({
|
|
|
221253
221283
|
provider: exports_external.literal("gdrive"),
|
|
221254
221284
|
folderId: exports_external.string().optional(),
|
|
221255
221285
|
clientId: exports_external.string().min(1),
|
|
221256
|
-
clientSecret: exports_external.string().min(1)
|
|
221286
|
+
clientSecret: exports_external.string().min(1),
|
|
221287
|
+
exclude: exports_external.array(exports_external.string()).optional()
|
|
221257
221288
|
});
|
|
221258
221289
|
var ConfigSchema = exports_external.object({
|
|
221259
221290
|
outDir: exports_external.string().default(".srcpack"),
|
|
221291
|
+
emptyOutDir: exports_external.boolean().optional(),
|
|
221260
221292
|
upload: exports_external.union([UploadConfigSchema, exports_external.array(UploadConfigSchema).min(1)]).optional(),
|
|
221261
221293
|
bundles: exports_external.record(exports_external.string(), BundleConfigSchema)
|
|
221262
221294
|
});
|
|
@@ -223363,6 +223395,21 @@ function formatNumber(n) {
|
|
|
223363
223395
|
function plural(n, singular, pluralForm) {
|
|
223364
223396
|
return n === 1 ? singular : pluralForm ?? singular + "s";
|
|
223365
223397
|
}
|
|
223398
|
+
function isOutDirInsideRoot(outDir, root) {
|
|
223399
|
+
const absoluteOutDir = isAbsolute(outDir) ? outDir : resolve(root, outDir);
|
|
223400
|
+
const rel = relative(root, absoluteOutDir);
|
|
223401
|
+
return !rel.startsWith("..") && !isAbsolute(rel);
|
|
223402
|
+
}
|
|
223403
|
+
async function emptyDirectory(dir, skip = []) {
|
|
223404
|
+
let entries;
|
|
223405
|
+
try {
|
|
223406
|
+
entries = await readdir(dir);
|
|
223407
|
+
} catch {
|
|
223408
|
+
return;
|
|
223409
|
+
}
|
|
223410
|
+
const skipSet = new Set(skip);
|
|
223411
|
+
await Promise.all(entries.filter((entry) => !skipSet.has(entry)).map((entry) => rm(join6(dir, entry), { recursive: true, force: true })));
|
|
223412
|
+
}
|
|
223366
223413
|
async function main() {
|
|
223367
223414
|
const args = process.argv.slice(2);
|
|
223368
223415
|
if (args.includes("--help") || args.includes("-h")) {
|
|
@@ -223378,9 +223425,11 @@ Usage:
|
|
|
223378
223425
|
npx srcpack login Authenticate with Google Drive
|
|
223379
223426
|
|
|
223380
223427
|
Options:
|
|
223381
|
-
--dry-run
|
|
223382
|
-
--
|
|
223383
|
-
-
|
|
223428
|
+
--dry-run Preview bundles without writing files
|
|
223429
|
+
--emptyOutDir Empty output directory before bundling
|
|
223430
|
+
--no-emptyOutDir Keep existing files in output directory
|
|
223431
|
+
--no-upload Skip uploading to cloud storage
|
|
223432
|
+
-h, --help Show this help message
|
|
223384
223433
|
`);
|
|
223385
223434
|
return;
|
|
223386
223435
|
}
|
|
@@ -223394,6 +223443,7 @@ Options:
|
|
|
223394
223443
|
}
|
|
223395
223444
|
const dryRun = args.includes("--dry-run");
|
|
223396
223445
|
const noUpload = args.includes("--no-upload");
|
|
223446
|
+
const emptyOutDirFlag = args.includes("--emptyOutDir") ? true : args.includes("--no-emptyOutDir") ? false : undefined;
|
|
223397
223447
|
const subcommands = ["init", "login"];
|
|
223398
223448
|
const requestedBundles = args.filter((arg) => !arg.startsWith("-") && !subcommands.includes(arg));
|
|
223399
223449
|
const config2 = await loadConfig();
|
|
@@ -223413,6 +223463,15 @@ Options:
|
|
|
223413
223463
|
return;
|
|
223414
223464
|
}
|
|
223415
223465
|
const cwd = process.cwd();
|
|
223466
|
+
const outDirInsideRoot = isOutDirInsideRoot(config2.outDir, cwd);
|
|
223467
|
+
const emptyOutDir = emptyOutDirFlag ?? config2.emptyOutDir ?? outDirInsideRoot;
|
|
223468
|
+
if (!outDirInsideRoot && emptyOutDirFlag === undefined && config2.emptyOutDir === undefined) {
|
|
223469
|
+
console.warn(`Warning: outDir "${config2.outDir}" is outside project root. ` + "Use --emptyOutDir to suppress this warning and empty the directory.");
|
|
223470
|
+
}
|
|
223471
|
+
if (emptyOutDir && !dryRun) {
|
|
223472
|
+
const outDirPath = isAbsolute(config2.outDir) ? config2.outDir : resolve(cwd, config2.outDir);
|
|
223473
|
+
await emptyDirectory(outDirPath, [".git"]);
|
|
223474
|
+
}
|
|
223416
223475
|
const outputs = [];
|
|
223417
223476
|
const bundleSpinner = ora({
|
|
223418
223477
|
text: `Bundling ${bundleNames[0]}...`,
|
|
@@ -223525,6 +223584,13 @@ function printUploadConfigHelp() {
|
|
|
223525
223584
|
`);
|
|
223526
223585
|
}
|
|
223527
223586
|
async function handleGdriveUpload(uploadConfig, outputs, cwd) {
|
|
223587
|
+
const excludeSet = new Set(uploadConfig.exclude ?? []);
|
|
223588
|
+
const toUpload = outputs.filter((o2) => !excludeSet.has(o2.name));
|
|
223589
|
+
if (toUpload.length === 0) {
|
|
223590
|
+
console.log(`
|
|
223591
|
+
No bundles to upload (all excluded).`);
|
|
223592
|
+
return;
|
|
223593
|
+
}
|
|
223528
223594
|
try {
|
|
223529
223595
|
await ensureAuthenticated(uploadConfig);
|
|
223530
223596
|
const uploadSpinner = ora({
|
|
@@ -223532,10 +223598,10 @@ async function handleGdriveUpload(uploadConfig, outputs, cwd) {
|
|
|
223532
223598
|
color: "cyan"
|
|
223533
223599
|
}).start();
|
|
223534
223600
|
const results = [];
|
|
223535
|
-
for (let i2 = 0;i2 <
|
|
223536
|
-
const output =
|
|
223601
|
+
for (let i2 = 0;i2 < toUpload.length; i2++) {
|
|
223602
|
+
const output = toUpload[i2];
|
|
223537
223603
|
const filePath = join6(cwd, output.outfile);
|
|
223538
|
-
uploadSpinner.text = `Uploading ${output.name}... (${i2 + 1}/${
|
|
223604
|
+
uploadSpinner.text = `Uploading ${output.name}... (${i2 + 1}/${toUpload.length})`;
|
|
223539
223605
|
const result = await uploadFile(filePath, uploadConfig);
|
|
223540
223606
|
results.push(result);
|
|
223541
223607
|
}
|
package/dist/config.d.ts
CHANGED
|
@@ -1,28 +1,52 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
export declare function expandPath(p: string): string;
|
|
3
|
+
/**
|
|
4
|
+
* Bundle configuration. Accepts a string pattern, array of patterns, or object.
|
|
5
|
+
* Patterns prefixed with `!` are exclusions. Patterns prefixed with `+` force
|
|
6
|
+
* inclusion (bypass .gitignore).
|
|
7
|
+
*/
|
|
3
8
|
declare const BundleConfigSchema: z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>, z.ZodObject<{
|
|
4
9
|
include: z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>;
|
|
5
10
|
outfile: z.ZodOptional<z.ZodString>;
|
|
6
11
|
index: z.ZodDefault<z.ZodBoolean>;
|
|
7
12
|
}, z.core.$strip>]>;
|
|
13
|
+
/**
|
|
14
|
+
* Upload destination configuration.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* upload: {
|
|
19
|
+
* provider: "gdrive",
|
|
20
|
+
* clientId: process.env.GDRIVE_CLIENT_ID,
|
|
21
|
+
* clientSecret: process.env.GDRIVE_CLIENT_SECRET,
|
|
22
|
+
* folderId: "1abc...",
|
|
23
|
+
* exclude: ["local", "debug"],
|
|
24
|
+
* }
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
8
27
|
declare const UploadConfigSchema: z.ZodObject<{
|
|
9
28
|
provider: z.ZodLiteral<"gdrive">;
|
|
10
29
|
folderId: z.ZodOptional<z.ZodString>;
|
|
11
30
|
clientId: z.ZodString;
|
|
12
31
|
clientSecret: z.ZodString;
|
|
32
|
+
exclude: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
13
33
|
}, z.core.$strip>;
|
|
34
|
+
/** Root configuration for srcpack. */
|
|
14
35
|
declare const ConfigSchema: z.ZodObject<{
|
|
15
36
|
outDir: z.ZodDefault<z.ZodString>;
|
|
37
|
+
emptyOutDir: z.ZodOptional<z.ZodBoolean>;
|
|
16
38
|
upload: z.ZodOptional<z.ZodUnion<readonly [z.ZodObject<{
|
|
17
39
|
provider: z.ZodLiteral<"gdrive">;
|
|
18
40
|
folderId: z.ZodOptional<z.ZodString>;
|
|
19
41
|
clientId: z.ZodString;
|
|
20
42
|
clientSecret: z.ZodString;
|
|
43
|
+
exclude: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
21
44
|
}, z.core.$strip>, z.ZodArray<z.ZodObject<{
|
|
22
45
|
provider: z.ZodLiteral<"gdrive">;
|
|
23
46
|
folderId: z.ZodOptional<z.ZodString>;
|
|
24
47
|
clientId: z.ZodString;
|
|
25
48
|
clientSecret: z.ZodString;
|
|
49
|
+
exclude: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
26
50
|
}, z.core.$strip>>]>>;
|
|
27
51
|
bundles: z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>, z.ZodObject<{
|
|
28
52
|
include: z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>;
|
package/dist/index.js
CHANGED
|
@@ -187020,10 +187020,12 @@ var UploadConfigSchema = exports_external.object({
|
|
|
187020
187020
|
provider: exports_external.literal("gdrive"),
|
|
187021
187021
|
folderId: exports_external.string().optional(),
|
|
187022
187022
|
clientId: exports_external.string().min(1),
|
|
187023
|
-
clientSecret: exports_external.string().min(1)
|
|
187023
|
+
clientSecret: exports_external.string().min(1),
|
|
187024
|
+
exclude: exports_external.array(exports_external.string()).optional()
|
|
187024
187025
|
});
|
|
187025
187026
|
var ConfigSchema = exports_external.object({
|
|
187026
187027
|
outDir: exports_external.string().default(".srcpack"),
|
|
187028
|
+
emptyOutDir: exports_external.boolean().optional(),
|
|
187027
187029
|
upload: exports_external.union([UploadConfigSchema, exports_external.array(UploadConfigSchema).min(1)]).optional(),
|
|
187028
187030
|
bundles: exports_external.record(exports_external.string(), BundleConfigSchema)
|
|
187029
187031
|
});
|
package/package.json
CHANGED
package/src/bundle.ts
CHANGED
|
@@ -86,20 +86,79 @@ function isExcluded(filePath: string, matchers: Matcher[]): boolean {
|
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
/**
|
|
89
|
-
*
|
|
89
|
+
* Convert gitignore patterns to glob ignore patterns for fast-glob.
|
|
90
|
+
* This prevents traversing into ignored directories (performance optimization).
|
|
91
|
+
*
|
|
92
|
+
* Conservative approach: only convert simple, unambiguous directory patterns.
|
|
93
|
+
* Complex patterns (negations, root-anchored, globs) are left to the ignore filter.
|
|
90
94
|
*/
|
|
91
|
-
|
|
95
|
+
function gitignoreToGlobPatterns(lines: string[]): string[] {
|
|
96
|
+
// If any negation patterns exist, skip optimization entirely
|
|
97
|
+
// (negations could re-include files in otherwise-ignored directories)
|
|
98
|
+
const hasNegation = lines.some((line) => {
|
|
99
|
+
const trimmed = line.trim();
|
|
100
|
+
// Any line starting with ! is a negation (including !#file which negates "#file")
|
|
101
|
+
return trimmed.startsWith("!");
|
|
102
|
+
});
|
|
103
|
+
if (hasNegation) return [];
|
|
104
|
+
|
|
105
|
+
const patterns: string[] = [];
|
|
106
|
+
|
|
107
|
+
for (const line of lines) {
|
|
108
|
+
const trimmed = line.trim();
|
|
109
|
+
// Skip empty lines and comments
|
|
110
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
111
|
+
|
|
112
|
+
// Skip patterns with special gitignore features we can't safely convert:
|
|
113
|
+
// - Root-anchored (starts with /)
|
|
114
|
+
// - Contains globs (*, ?, [)
|
|
115
|
+
// - Contains path separators (complex paths)
|
|
116
|
+
// - Escaped characters
|
|
117
|
+
if (
|
|
118
|
+
trimmed.startsWith("/") ||
|
|
119
|
+
trimmed.includes("*") ||
|
|
120
|
+
trimmed.includes("?") ||
|
|
121
|
+
trimmed.includes("[") ||
|
|
122
|
+
trimmed.includes("/") ||
|
|
123
|
+
trimmed.includes("\\")
|
|
124
|
+
) {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Only convert simple directory names (e.g., "node_modules", "dist")
|
|
129
|
+
// These are safe to prune at any depth
|
|
130
|
+
const name = trimmed.endsWith("/") ? trimmed.slice(0, -1) : trimmed;
|
|
131
|
+
if (name && /^[\w.-]+$/.test(name)) {
|
|
132
|
+
patterns.push(`**/${name}/**`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return patterns;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
interface GitignoreResult {
|
|
140
|
+
ignore: Ignore;
|
|
141
|
+
globPatterns: string[];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Load and parse .gitignore file from a directory.
|
|
146
|
+
* Returns both an Ignore instance for filtering and glob patterns for fast-glob.
|
|
147
|
+
*/
|
|
148
|
+
async function loadGitignore(cwd: string): Promise<GitignoreResult> {
|
|
92
149
|
const ig = ignore();
|
|
93
150
|
const gitignorePath = join(cwd, ".gitignore");
|
|
151
|
+
let globPatterns: string[] = [];
|
|
94
152
|
|
|
95
153
|
try {
|
|
96
154
|
const content = await readFile(gitignorePath, "utf-8");
|
|
97
155
|
ig.add(content);
|
|
156
|
+
globPatterns = gitignoreToGlobPatterns(content.split("\n"));
|
|
98
157
|
} catch {
|
|
99
158
|
// No .gitignore file, return empty ignore instance
|
|
100
159
|
}
|
|
101
160
|
|
|
102
|
-
return ig;
|
|
161
|
+
return { ignore: ig, globPatterns };
|
|
103
162
|
}
|
|
104
163
|
|
|
105
164
|
/**
|
|
@@ -114,12 +173,18 @@ export async function resolvePatterns(
|
|
|
114
173
|
): Promise<string[]> {
|
|
115
174
|
const { include, exclude, force } = normalizePatterns(config);
|
|
116
175
|
const excludeMatchers = exclude.map((p) => picomatch(p));
|
|
117
|
-
const gitignore = await loadGitignore(cwd);
|
|
176
|
+
const { ignore: gitignore, globPatterns } = await loadGitignore(cwd);
|
|
118
177
|
const files = new Set<string>();
|
|
119
178
|
|
|
120
179
|
// Regular includes: respect .gitignore
|
|
180
|
+
// Pass gitignore patterns to fast-glob to skip ignored directories during traversal
|
|
121
181
|
if (include.length > 0) {
|
|
122
|
-
const matches = await glob(include, {
|
|
182
|
+
const matches = await glob(include, {
|
|
183
|
+
cwd,
|
|
184
|
+
onlyFiles: true,
|
|
185
|
+
dot: true,
|
|
186
|
+
ignore: globPatterns,
|
|
187
|
+
});
|
|
123
188
|
for (const match of matches) {
|
|
124
189
|
if (!isExcluded(match, excludeMatchers) && !gitignore.ignores(match)) {
|
|
125
190
|
const fullPath = join(cwd, match);
|
|
@@ -130,7 +195,7 @@ export async function resolvePatterns(
|
|
|
130
195
|
}
|
|
131
196
|
}
|
|
132
197
|
|
|
133
|
-
// Force includes: bypass .gitignore
|
|
198
|
+
// Force includes: bypass .gitignore (no ignore patterns passed to glob)
|
|
134
199
|
if (force.length > 0) {
|
|
135
200
|
const matches = await glob(force, { cwd, onlyFiles: true, dot: true });
|
|
136
201
|
for (const match of matches) {
|
package/src/cli.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// SPDX-License-Identifier: MIT
|
|
3
3
|
|
|
4
|
-
import { mkdir, writeFile } from "node:fs/promises";
|
|
5
|
-
import { dirname, join } from "node:path";
|
|
4
|
+
import { mkdir, readdir, rm, writeFile } from "node:fs/promises";
|
|
5
|
+
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
6
6
|
import ora from "ora";
|
|
7
7
|
import { bundleOne, type BundleResult } from "./bundle.ts";
|
|
8
8
|
import {
|
|
@@ -38,6 +38,31 @@ function plural(n: number, singular: string, pluralForm?: string): string {
|
|
|
38
38
|
return n === 1 ? singular : (pluralForm ?? singular + "s");
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
function isOutDirInsideRoot(outDir: string, root: string): boolean {
|
|
42
|
+
const absoluteOutDir = isAbsolute(outDir) ? outDir : resolve(root, outDir);
|
|
43
|
+
const rel = relative(root, absoluteOutDir);
|
|
44
|
+
return !rel.startsWith("..") && !isAbsolute(rel);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Empty a directory while preserving specified entries (e.g., `.git`).
|
|
49
|
+
* Uses `force: true` to handle read-only or in-use files.
|
|
50
|
+
*/
|
|
51
|
+
async function emptyDirectory(dir: string, skip: string[] = []): Promise<void> {
|
|
52
|
+
let entries: string[];
|
|
53
|
+
try {
|
|
54
|
+
entries = await readdir(dir);
|
|
55
|
+
} catch {
|
|
56
|
+
return; // Directory doesn't exist, nothing to empty
|
|
57
|
+
}
|
|
58
|
+
const skipSet = new Set(skip);
|
|
59
|
+
await Promise.all(
|
|
60
|
+
entries
|
|
61
|
+
.filter((entry) => !skipSet.has(entry))
|
|
62
|
+
.map((entry) => rm(join(dir, entry), { recursive: true, force: true })),
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
41
66
|
async function main() {
|
|
42
67
|
const args = process.argv.slice(2);
|
|
43
68
|
|
|
@@ -54,9 +79,11 @@ Usage:
|
|
|
54
79
|
npx srcpack login Authenticate with Google Drive
|
|
55
80
|
|
|
56
81
|
Options:
|
|
57
|
-
--dry-run
|
|
58
|
-
--
|
|
59
|
-
-
|
|
82
|
+
--dry-run Preview bundles without writing files
|
|
83
|
+
--emptyOutDir Empty output directory before bundling
|
|
84
|
+
--no-emptyOutDir Keep existing files in output directory
|
|
85
|
+
--no-upload Skip uploading to cloud storage
|
|
86
|
+
-h, --help Show this help message
|
|
60
87
|
`);
|
|
61
88
|
return;
|
|
62
89
|
}
|
|
@@ -73,6 +100,12 @@ Options:
|
|
|
73
100
|
|
|
74
101
|
const dryRun = args.includes("--dry-run");
|
|
75
102
|
const noUpload = args.includes("--no-upload");
|
|
103
|
+
// CLI flags: --emptyOutDir forces true, --no-emptyOutDir forces false
|
|
104
|
+
const emptyOutDirFlag = args.includes("--emptyOutDir")
|
|
105
|
+
? true
|
|
106
|
+
: args.includes("--no-emptyOutDir")
|
|
107
|
+
? false
|
|
108
|
+
: undefined;
|
|
76
109
|
const subcommands = ["init", "login"];
|
|
77
110
|
const requestedBundles = args.filter(
|
|
78
111
|
(arg) => !arg.startsWith("-") && !subcommands.includes(arg),
|
|
@@ -106,6 +139,31 @@ Options:
|
|
|
106
139
|
}
|
|
107
140
|
|
|
108
141
|
const cwd = process.cwd();
|
|
142
|
+
|
|
143
|
+
// Resolve emptyOutDir: CLI flag > config > auto (true if inside root)
|
|
144
|
+
const outDirInsideRoot = isOutDirInsideRoot(config.outDir, cwd);
|
|
145
|
+
const emptyOutDir = emptyOutDirFlag ?? config.emptyOutDir ?? outDirInsideRoot;
|
|
146
|
+
|
|
147
|
+
// Warn if outDir is outside root and emptyOutDir is not explicitly set
|
|
148
|
+
if (
|
|
149
|
+
!outDirInsideRoot &&
|
|
150
|
+
emptyOutDirFlag === undefined &&
|
|
151
|
+
config.emptyOutDir === undefined
|
|
152
|
+
) {
|
|
153
|
+
console.warn(
|
|
154
|
+
`Warning: outDir "${config.outDir}" is outside project root. ` +
|
|
155
|
+
"Use --emptyOutDir to suppress this warning and empty the directory.",
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Empty outDir before bundling (unless dry-run)
|
|
160
|
+
if (emptyOutDir && !dryRun) {
|
|
161
|
+
const outDirPath = isAbsolute(config.outDir)
|
|
162
|
+
? config.outDir
|
|
163
|
+
: resolve(cwd, config.outDir);
|
|
164
|
+
await emptyDirectory(outDirPath, [".git"]);
|
|
165
|
+
}
|
|
166
|
+
|
|
109
167
|
const outputs: BundleOutput[] = [];
|
|
110
168
|
|
|
111
169
|
// Process all bundles with progress
|
|
@@ -279,6 +337,15 @@ async function handleGdriveUpload(
|
|
|
279
337
|
outputs: BundleOutput[],
|
|
280
338
|
cwd: string,
|
|
281
339
|
): Promise<void> {
|
|
340
|
+
// Filter out excluded bundles
|
|
341
|
+
const excludeSet = new Set(uploadConfig.exclude ?? []);
|
|
342
|
+
const toUpload = outputs.filter((o) => !excludeSet.has(o.name));
|
|
343
|
+
|
|
344
|
+
if (toUpload.length === 0) {
|
|
345
|
+
console.log("\nNo bundles to upload (all excluded).");
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
|
|
282
349
|
try {
|
|
283
350
|
await ensureAuthenticated(uploadConfig);
|
|
284
351
|
|
|
@@ -289,10 +356,10 @@ async function handleGdriveUpload(
|
|
|
289
356
|
|
|
290
357
|
const results: UploadResult[] = [];
|
|
291
358
|
|
|
292
|
-
for (let i = 0; i <
|
|
293
|
-
const output =
|
|
359
|
+
for (let i = 0; i < toUpload.length; i++) {
|
|
360
|
+
const output = toUpload[i]!;
|
|
294
361
|
const filePath = join(cwd, output.outfile);
|
|
295
|
-
uploadSpinner.text = `Uploading ${output.name}... (${i + 1}/${
|
|
362
|
+
uploadSpinner.text = `Uploading ${output.name}... (${i + 1}/${toUpload.length})`;
|
|
296
363
|
const result = await uploadFile(filePath, uploadConfig);
|
|
297
364
|
results.push(result);
|
|
298
365
|
}
|
package/src/config.ts
CHANGED
|
@@ -12,33 +12,68 @@ export function expandPath(p: string): string {
|
|
|
12
12
|
return p;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
/** Glob patterns for file matching. Single pattern or array of patterns. */
|
|
15
16
|
const PatternsSchema = z.union([
|
|
16
17
|
z.string().min(1),
|
|
17
18
|
z.array(z.string().min(1)).min(1),
|
|
18
19
|
]);
|
|
19
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Bundle configuration. Accepts a string pattern, array of patterns, or object.
|
|
23
|
+
* Patterns prefixed with `!` are exclusions. Patterns prefixed with `+` force
|
|
24
|
+
* inclusion (bypass .gitignore).
|
|
25
|
+
*/
|
|
20
26
|
const BundleConfigSchema = z.union([
|
|
21
|
-
z.string().min(1),
|
|
22
|
-
z.array(z.string().min(1)).min(1),
|
|
27
|
+
z.string().min(1),
|
|
28
|
+
z.array(z.string().min(1)).min(1),
|
|
23
29
|
z.object({
|
|
30
|
+
/** Glob patterns to include in the bundle. */
|
|
24
31
|
include: PatternsSchema,
|
|
32
|
+
/** Custom output file path. Defaults to `<outDir>/<bundleName>.txt`. */
|
|
25
33
|
outfile: z.string().optional(),
|
|
26
|
-
|
|
34
|
+
/** Include file index header in output. Defaults to true. */
|
|
35
|
+
index: z.boolean().default(true),
|
|
27
36
|
}),
|
|
28
37
|
]);
|
|
29
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Upload destination configuration.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```ts
|
|
44
|
+
* upload: {
|
|
45
|
+
* provider: "gdrive",
|
|
46
|
+
* clientId: process.env.GDRIVE_CLIENT_ID,
|
|
47
|
+
* clientSecret: process.env.GDRIVE_CLIENT_SECRET,
|
|
48
|
+
* folderId: "1abc...",
|
|
49
|
+
* exclude: ["local", "debug"],
|
|
50
|
+
* }
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
30
53
|
const UploadConfigSchema = z.object({
|
|
54
|
+
/** Upload provider. Currently only "gdrive" is supported. */
|
|
31
55
|
provider: z.literal("gdrive"),
|
|
56
|
+
/** Google Drive folder ID to upload files to. If omitted, uploads to root. */
|
|
32
57
|
folderId: z.string().optional(),
|
|
58
|
+
/** OAuth 2.0 client ID from Google Cloud Console. */
|
|
33
59
|
clientId: z.string().min(1),
|
|
60
|
+
/** OAuth 2.0 client secret from Google Cloud Console. */
|
|
34
61
|
clientSecret: z.string().min(1),
|
|
62
|
+
/** Bundle names to skip during upload. Supports exact names only. */
|
|
63
|
+
exclude: z.array(z.string()).optional(),
|
|
35
64
|
});
|
|
36
65
|
|
|
66
|
+
/** Root configuration for srcpack. */
|
|
37
67
|
const ConfigSchema = z.object({
|
|
68
|
+
/** Output directory for bundle files. Defaults to ".srcpack". */
|
|
38
69
|
outDir: z.string().default(".srcpack"),
|
|
70
|
+
/** Empty outDir before bundling. Auto-enabled when outDir is inside project root. */
|
|
71
|
+
emptyOutDir: z.boolean().optional(),
|
|
72
|
+
/** Upload configuration for cloud storage. Single destination or array. */
|
|
39
73
|
upload: z
|
|
40
74
|
.union([UploadConfigSchema, z.array(UploadConfigSchema).min(1)])
|
|
41
75
|
.optional(),
|
|
76
|
+
/** Named bundles mapping bundle name to glob patterns or config object. */
|
|
42
77
|
bundles: z.record(z.string(), BundleConfigSchema),
|
|
43
78
|
});
|
|
44
79
|
|