@timeax/zipper
Version:
Config-driven project zipper with .zipconfig / zip.json (include/exclude, presets, dry-run, gitignore support).
917 lines (908 loc) • 31.5 kB
JavaScript
// src/config.ts
import { cosmiconfig } from "cosmiconfig";
import fs3 from "fs";
import path4 from "path";
import YAML2 from "yaml";
import Ajv from "ajv/dist/2020.js";
// src/utils.ts
import os from "os";
import path from "path";
import fsSync from "fs";
import { fileURLToPath } from "url";
function getBuiltinStubDir() {
const here = path.dirname(fileURLToPath(import.meta.url));
return path.resolve(here, "../stubs");
}
// src/config.ts
import pc2 from "picocolors";
import { pathToFileURL } from "url";
// src/smart-merge.ts
import path3 from "path";
import fs2 from "fs";
import picomatch from "picomatch";
import { globby } from "globby";
// src/user-presets.ts
import fs from "fs";
import fsp from "fs/promises";
import path2 from "path";
import os2 from "os";
import YAML from "yaml";
function isDir(p) {
try {
return fs.statSync(p).isDirectory();
} catch {
return false;
}
}
function getPresetDirs(extra = []) {
const env = (process.env.ZIPPER_PRESETS ?? "").split(path2.delimiter).map((s) => s.trim()).filter(Boolean);
const home = os2.homedir();
const defaults = [
path2.join(home, ".config", "zipper", "presets"),
path2.join(home, ".zipper", "presets")
];
const all = [...extra, ...env, ...defaults];
const seen = /* @__PURE__ */ new Set();
const out = [];
for (const d of all) {
const abs = path2.isAbsolute(d) ? d : path2.resolve(process.cwd(), d);
if (!seen.has(abs) && isDir(abs)) {
seen.add(abs);
out.push(abs);
}
}
return out;
}
async function loadUserPresets(extraDirs = []) {
const dirs = getPresetDirs(extraDirs);
const out = {};
for (const d of dirs) {
const entries = await fsp.readdir(d, { withFileTypes: true }).catch(() => []);
for (const e of entries) {
if (!e.isFile()) continue;
if (!/\.(ya?ml|json)$/i.test(e.name)) continue;
const full = path2.join(d, e.name);
try {
const content = await fsp.readFile(full, "utf8");
const data = e.name.endsWith(".json") ? JSON.parse(content) : YAML.parse(content);
const name = path2.basename(e.name).replace(/\.(ya?ml|json)$/i, "");
if (data && typeof data === "object") out[name] = data;
} catch {
}
}
}
return out;
}
// src/presets.ts
var PRESETS = {
"laravel-basic": {
include: [
"app/**",
"bootstrap/**",
"config/**",
"database/**",
"public/**",
"resources/**",
"routes/**",
"artisan",
"composer.*",
"package.json"
],
exclude: [
"node_modules/**",
"vendor/**",
"storage/logs/**/*",
"storage/framework/**/*",
// files only; dirs can be kept via placeholders
".env*",
".git/**",
"**/*.map"
]
},
"laravel-no-vendor": {
include: [
"app/**",
"bootstrap/**",
"config/**",
"database/**",
"public/**",
"resources/**",
"routes/**",
"artisan",
"composer.*",
"package.json"
],
exclude: [
"vendor/**",
"node_modules/**",
"storage/**/*",
// if you want folders kept, switch to logs/**/* + framework/**/* like above
".env*",
".git/**"
]
},
"node-module": {
include: ["dist/**", "package.json", "README*", "LICENSE*"],
exclude: ["**/*.map", ".git/**", "node_modules/**"]
},
// NEW
"inertia": {
include: [
// server-side
"app/**",
"bootstrap/**",
"config/**",
"database/**",
"routes/**",
"artisan",
"composer.*",
".env.example",
// front-end source
"resources/js/**",
"resources/ts/**",
"resources/css/**",
"resources/sass/**",
"resources/views/**",
"resources/lang/**",
"vite.config.*",
"postcss.config.*",
"tailwind.config.*",
"package.json",
"pnpm-lock.yaml",
"yarn.lock",
"package-lock.json",
".eslintrc.*",
".prettierrc*",
".editorconfig",
// public runtime / built assets
"public/index.php",
"public/.htaccess",
"public/build/**"
],
exclude: [
// deps
"node_modules/**",
"vendor/**",
// storage (drop files but allow dirs to survive if you include placeholders)
"storage/logs/**/*",
"storage/framework/**/*",
// public stuff you usually don’t want
"public/hot",
"public/storage/**",
"public/*.map",
"public/**/*.map",
// sourcemaps anywhere
"**/*.map",
// VCS
".git/**"
]
},
/**
* Laravel + Inertia with grouped archive layout:
* - server/ → backend + blade/lang
* - source/ → frontend sources + tooling
* - public/* → kept as-is (no double "public/public")
*/
"inertia-grouped": {
groups: {
server: {
target: "server/",
priority: 10,
include: [
"app/**",
"bootstrap/**",
"config/**",
"database/**",
"routes/**",
"artisan",
"composer.*",
".env",
"resources/views/**",
"resources/lang/**",
"vendor/**",
"storage/logs/**",
"storage/framework/**"
]
},
source: {
target: "source/",
priority: 15,
include: [
"resources/js/**",
"resources/ts/**",
"resources/css/**",
"resources/sass/**",
"vite.config.*",
"postcss.config.*",
"tailwind.config.*",
"package.json",
"pnpm-lock.yaml",
"yarn.lock",
"package-lock.json",
".eslintrc.*",
".prettierrc*",
".editorconfig"
],
exclude: ["resources/views/**", "resources/lang/**"]
},
public: {
// keep existing public/* paths as-is (avoid public/public/**)
target: "",
priority: 1,
include: ["public/**"],
exclude: ["public/hot", "public/storage/**", "public/*.map", "public/**/*.map", "public/index.php", "public/.htaccess"]
},
root: {
target: "",
priority: 100,
files: [
"public/index.php",
"public/.htaccess"
]
}
}
}
};
function resolvePresets(names) {
if (!names?.length) return {};
const merged = {};
const accInclude = [];
const accExclude = [];
const accIgnoreFiles = [];
const accGroups = {};
const accPre = {
includes: [],
excludes: [],
files: [],
modules: []
};
for (const n of names) {
const p = PRESETS[n];
if (!p) continue;
if (p.include?.length) accInclude.push(...p.include);
if (p.exclude?.length) accExclude.push(...p.exclude);
if (p.ignoreFiles?.length) accIgnoreFiles.push(...p.ignoreFiles);
if (p.groups) {
for (const [gname, g] of Object.entries(p.groups)) {
const cur = accGroups[gname];
if (!cur) {
accGroups[gname] = {
target: g.target ?? "",
include: [...g.include ?? []],
exclude: g.exclude?.length ? [...g.exclude] : void 0,
files: g.files?.length ? [...g.files] : void 0,
// NEW
priority: g.priority ?? 0
};
} else {
if (g.target != null) cur.target = g.target;
if (g.priority != null) cur.priority = g.priority;
if (g.include?.length) cur.include?.push(...g.include);
if (g.exclude?.length) cur.exclude = [...cur.exclude ?? [], ...g.exclude];
if (g.files?.length) cur.files = [...cur.files ?? [], ...g.files];
}
}
}
if (p.preprocess) {
const pr = p.preprocess;
if (pr.includes?.length) accPre.includes.push(...pr.includes);
if (pr.excludes?.length) accPre.excludes.push(...pr.excludes);
if (pr.files?.length) accPre.files.push(...pr.files);
if (pr.modules?.length) accPre.modules.push(...pr.modules);
if (pr.module) accPre.modules.push(pr.module);
if (typeof pr.maxBytes === "number") accPre.maxBytes = pr.maxBytes;
if (typeof pr.timeoutMs === "number") accPre.timeoutMs = pr.timeoutMs;
if (typeof pr.binaryMode === "string") accPre.binaryMode = pr.binaryMode;
}
}
merged.include = uniqKeepLast(accInclude);
merged.exclude = uniqKeepLast(accExclude);
if (accIgnoreFiles.length) merged.ignoreFiles = uniqKeepLast(accIgnoreFiles);
if (Object.keys(accGroups).length) {
const outGroups = {};
for (const [k, g] of Object.entries(accGroups)) {
outGroups[k] = {
target: g.target,
priority: g.priority,
include: uniqKeepLast(g.include ?? []),
...g.exclude?.length ? { exclude: uniqKeepLast(g.exclude) } : {},
...g.files?.length ? { files: uniqKeepLast(g.files) } : {}
// NEW
};
}
merged.groups = outGroups;
}
const hasPre = (accPre.includes?.length ?? 0) || (accPre.excludes?.length ?? 0) || (accPre.files?.length ?? 0) || (accPre.modules?.length ?? 0) || accPre.maxBytes != null || accPre.timeoutMs != null || accPre.binaryMode != null;
if (hasPre) {
const preOut = {};
if (accPre.includes?.length) preOut.includes = uniqKeepLast(accPre.includes);
if (accPre.excludes?.length) preOut.excludes = uniqKeepLast(accPre.excludes);
if (accPre.files?.length) preOut.files = uniqKeepLast(accPre.files);
if (accPre.modules?.length) preOut.modules = uniqKeepLast(accPre.modules);
if (accPre.maxBytes != null) preOut.maxBytes = accPre.maxBytes;
if (accPre.timeoutMs != null) preOut.timeoutMs = accPre.timeoutMs;
if (accPre.binaryMode != null) preOut.binaryMode = accPre.binaryMode;
merged.preprocess = preOut;
}
return merged;
}
function uniqKeepLast(arr) {
const res = [];
const seen = /* @__PURE__ */ new Set();
for (let i = arr.length - 1; i >= 0; i--) {
const key = String(arr[i]);
if (!seen.has(key)) {
seen.add(key);
res.unshift(arr[i]);
}
}
return res;
}
async function getAllPresets(extraDirs = []) {
const user = await loadUserPresets(extraDirs);
return { ...PRESETS, ...user };
}
// src/smart-merge.ts
import pc from "picocolors";
import cliProgress from "cli-progress";
var normRel = (p) => p.replaceAll("\\", "/").replace(/^\.?\//, "");
var normArr = (a) => (a ?? []).map(normRel);
var uniq = (xs) => Array.from(new Set(xs ?? []));
var union = (...arrs) => uniq(arrs.flatMap((a) => a ?? []));
function mergeGroups(a, b) {
const out = {};
const names = uniq([...a ? Object.keys(a) : [], ...b ? Object.keys(b) : []]);
for (const name of names) {
const ga = a?.[name] ?? {};
const gb = b?.[name] ?? {};
const target = gb.target ?? ga.target ?? "";
const priority = gb.priority ?? ga.priority ?? 0;
const include = union(ga.include, gb.include);
const exclude = union(ga.exclude, gb.exclude);
const files = union(ga.files, gb.files);
const g = { target, priority };
if (include.length) g.include = include;
if (exclude.length) g.exclude = exclude;
if (files.length) g.files = files;
if (!g.include && !g.files) continue;
out[name] = g;
}
return Object.keys(out).length ? out : void 0;
}
function sourcesFromPresetsAndUser(cfg, root) {
const presetSources = (cfg.presets ?? []).map((pname, i) => {
const p = PRESETS[pname] ?? {};
return {
name: `preset:${pname}`,
tier: i,
order: p.order ?? cfg.order ?? ["include", "exclude"],
include: normArr(p.include),
exclude: normArr(p.exclude),
files: normArr(p.files),
groups: p.groups
};
});
const userTier = cfg.presets?.length ?? 0;
const groupFiles = (() => {
const g = cfg.groups ?? {};
const all = /* @__PURE__ */ new Set();
for (const k of Object.keys(g)) for (const f of g[k].files ?? []) all.add(normRel(f));
return Array.from(all);
})();
const userSource = {
name: "user",
tier: userTier,
order: cfg.order,
include: normArr(cfg.include),
exclude: normArr(cfg.exclude),
files: groupFiles,
groups: cfg.groups
};
return [...presetSources, userSource];
}
function compileSignals(sources) {
const sigs = [];
for (const src of sources) {
let seq = 0;
for (const g of src.include) {
const m = picomatch(g, { dot: true });
sigs.push({ kind: "include", tier: src.tier, seq: seq++, order: src.order, name: src.name, match: m });
}
for (const g of src.exclude) {
const m = picomatch(g, { dot: true });
sigs.push({ kind: "exclude", tier: src.tier, seq: seq++, order: src.order, name: src.name, match: m });
}
for (const f of src.files) {
const rel = normRel(f);
const m = (x) => normRel(x) === rel;
sigs.push({ kind: "include", tier: src.tier, seq: seq++, order: src.order, name: src.name, match: m });
}
}
sigs.sort((a, b) => a.tier - b.tier || a.seq - b.seq);
return sigs;
}
function decideFor(rel, signals) {
let winner;
for (const s of signals) {
if (!s.match(rel)) continue;
if (!winner) {
winner = s;
continue;
}
if (s.tier > winner.tier) {
winner = s;
continue;
}
if (s.tier === winner.tier) {
if (s.seq > winner.seq) winner = s;
else if (s.seq === winner.seq) {
if (s.order[0] === "exclude" && s.kind === "include" && winner.kind === "exclude") winner = s;
else if (s.order[0] === "include" && s.kind === "exclude" && winner.kind === "include") winner = s;
}
}
}
return winner;
}
async function buildUniverse(root, sources, opts, log) {
const includeGlobs = /* @__PURE__ */ new Set();
const explicitFiles = /* @__PURE__ */ new Set();
for (const s of sources) {
for (const g of s.include) includeGlobs.add(g);
for (const f of s.files) explicitFiles.add(f);
}
if (includeGlobs.size === 0 && explicitFiles.size === 0) includeGlobs.add("**/*");
const t0 = Date.now();
log(`scan: globby(${includeGlobs.size} globs) \u2026`);
const fromGlobs = includeGlobs.size ? await globby(Array.from(includeGlobs), {
cwd: root,
dot: opts.dot,
followSymbolicLinks: opts.followSymlinks,
onlyFiles: true
}) : [];
const t1 = Date.now();
const fromFiles = Array.from(explicitFiles).filter((f) => fs2.existsSync(path3.join(root, f)));
const universe = Array.from(/* @__PURE__ */ new Set([...fromGlobs.map(normRel), ...fromFiles.map(normRel)]));
log(`scan: ${pc.bold(String(universe.length))} candidates (globs: ${fromGlobs.length}, explicit: ${fromFiles.length}) in ${t1 - t0}ms`);
return { universe, stats: { includeGlobs: includeGlobs.size, explicitFiles: explicitFiles.size } };
}
async function applySmartMergeToConfig(cfg, rootAbs, opts = {}) {
const log = opts.log ?? ((m) => console.log(pc.dim(`[smart-merge]`), m));
const label = opts.label ? `[${opts.label}] ` : "";
const root = rootAbs ?? path3.resolve(process.cwd(), cfg.root ?? ".");
log(`${label}start (presets: ${cfg.presets?.length ?? 0})`);
const tSrc0 = Date.now();
const sources = sourcesFromPresetsAndUser(cfg, root);
const tSrc1 = Date.now();
log(`${label}sources: ${sources.length} tier(s) in ${tSrc1 - tSrc0}ms`);
const g0 = Date.now();
const mergedPresetGroups = sources.filter((s) => s.name.startsWith("preset:")).map((s) => s.groups).reduce((acc, g) => mergeGroups(acc, g), void 0);
cfg.groups = mergeGroups(mergedPresetGroups, sources[sources.length - 1]?.groups) ?? cfg.groups;
const g1 = Date.now();
log(`${label}groups: merged in ${g1 - g0}ms (${Object.keys(cfg.groups ?? {}).length} group(s))`);
const u = await buildUniverse(root, sources, { dot: !!cfg.dot, followSymlinks: !!cfg.followSymlinks }, log);
const s0 = Date.now();
const signals = compileSignals(sources);
const s1 = Date.now();
log(`${label}signals: ${signals.length} rule(s) in ${s1 - s0}ms`);
const bar = new cliProgress.SingleBar({ hideCursor: true }, cliProgress.Presets.shades_classic);
const total = u.universe.length;
const tickEvery = Math.max(1, opts.tickEvery ?? 500);
const decideT0 = Date.now();
const selectedSet = /* @__PURE__ */ new Set();
if (opts.progress && total > 0) bar.start(total, 0);
for (let i = 0; i < total; i++) {
const rel = u.universe[i];
const w = decideFor(rel, signals);
if (w?.kind === "include") selectedSet.add(rel);
if (opts.progress && (i + 1 === total || (i + 1) % tickEvery === 0)) bar.update(i + 1);
}
if (opts.progress && bar.isActive) bar.stop();
const decideT1 = Date.now();
const selected = Array.from(selectedSet);
if (cfg.deterministic) selected.sort((a, b) => a.localeCompare(b));
cfg.backup = {
include: cfg.include ?? void 0,
exclude: cfg.exclude ?? void 0,
presets: cfg.presets ?? void 0,
groups: cfg.groups ?? void 0,
respectGitignore: cfg.respectGitignore ?? void 0,
ignoreFiles: cfg.ignoreFiles ?? void 0,
order: cfg.order ?? void 0
};
cfg.include = selected;
cfg.exclude = [];
cfg.respectGitignore = false;
cfg.ignoreFiles = [];
cfg.order = ["include", "exclude"];
log(`${label}decide: kept ${pc.bold(String(selected.length))} / ${total} in ${decideT1 - decideT0}ms`);
log(`${label}done`);
return selected;
}
// src/config.ts
var MODULE_NAME = "zipper";
var SCHEMA_PATH = new URL("../schema/zipconfig.schema.json", import.meta.url);
function appendStubIfNoExt(p) {
return path4.extname(p) ? p : `${p}.stub`;
}
function envExpand(s) {
return s.replace(/\$\{?([A-Z0-9_]+)\}?/gi, (_, k) => process.env[k] ?? "");
}
function parseUnknownConfig(content) {
try {
return JSON.parse(content);
} catch {
return YAML2.parse(content);
}
}
async function loadConfig(explicitPath) {
const explorer = cosmiconfig(MODULE_NAME, {
searchPlaces: [
".zipconfig",
"zip.json",
".zipconfig.json",
".zipconfig.yaml",
".zipconfig.yml",
"package.json"
],
loaders: {
".yaml": (_fp, content) => YAML2.parse(content),
".yml": (_fp, content) => YAML2.parse(content),
noExt: (_fp, content) => parseUnknownConfig(content)
// extensionless .zipconfig
}
});
let result;
if (explicitPath) {
let fp = appendStubIfNoExt(explicitPath);
if (fp.endsWith(".stub")) {
const abs = path4.isAbsolute(fp) ? fp : path4.resolve(process.cwd(), fp);
let content = null;
if (fs3.existsSync(abs)) content = fs3.readFileSync(abs, "utf8");
if (content === null) {
const local = path4.join(process.cwd(), "stubs", path4.basename(abs));
if (fs3.existsSync(local)) content = fs3.readFileSync(local, "utf8");
}
if (content === null) {
const builtin = path4.join(getBuiltinStubDir(), path4.basename(abs));
if (fs3.existsSync(builtin)) content = fs3.readFileSync(builtin, "utf8");
}
if (content !== null) {
result = { config: parseUnknownConfig(content), filepath: abs };
} else {
throw new Error(`Config not found: ${explicitPath} (.stub assumed). Looked in: ${abs}, ./stubs, and built-in stubs.`);
}
} else {
const r = await explorer.load(fp);
if (r) result = { config: r.config, filepath: r.filepath };
}
} else {
const r = await explorer.search();
if (r) result = { config: r.config, filepath: r.filepath };
}
const raw = result?.config && result.config.zipper ? result.config.zipper : result?.config;
const defaults = {
out: "dist.zip",
root: ".",
include: ["**/*"],
// will be replaced by smart-merge materialization
exclude: [],
// ditto
dot: true,
followSymlinks: false,
order: ["include", "exclude"],
presets: [],
respectGitignore: false,
ignoreFiles: [".zipignore"],
deterministic: true,
manifest: true
};
const merged = { ...defaults, ...raw ?? {} };
merged.out = envExpand(merged.out);
if (merged.root) merged.root = envExpand(merged.root);
try {
const schema = JSON.parse(fs3.readFileSync(SCHEMA_PATH, "utf8"));
const ajv = new Ajv({ allowUnionTypes: true, allErrors: true, strict: false });
const authored = JSON.parse(JSON.stringify(merged));
delete authored.$schema;
const validate = ajv.compile(schema);
if (!validate(authored)) {
const msgs = (validate.errors ?? []).map((e) => `${e.instancePath || "<root>"} ${e.message}`).join("; ");
throw new Error(`Invalid .zipconfig: ${msgs}`);
}
} catch (e) {
console.warn(`[zipper] config validation warning: ${e.message}`);
}
const rootAbs = path4.resolve(process.cwd(), merged.root ?? ".");
try {
const extraIg = await readIgnoreFiles(rootAbs, merged.ignoreFiles);
if (extraIg?.length) {
merged.exclude = [...merged.exclude ?? [], ...extraIg];
}
} catch {
}
const progress = process.env.ZIPPER_SMARTMERGE_PROGRESS === "1" || process.env.ZIPPER_DEBUG === "1";
await applySmartMergeToConfig(merged, rootAbs, {
progress,
label: "smart-merge",
log: (m) => console.log(pc2.dim(m)),
// or keep default
tickEvery: 1e3
// optional: fewer updates for very large trees
});
return { cfg: merged, filepath: result?.filepath };
}
function loadListFile(root, listPath) {
if (!listPath) return [];
const full = path4.isAbsolute(listPath) ? listPath : path4.join(root, listPath);
if (!fs3.existsSync(full)) return [];
return fs3.readFileSync(full, "utf8").split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
}
function readIgnoreFiles(root, files) {
const out = [];
for (const rel of files ?? []) {
const fp = path4.isAbsolute(rel) ? rel : path4.join(root, rel);
if (fs3.existsSync(fp)) {
out.push(...fs3.readFileSync(fp, "utf8").split(/\r?\n/));
}
}
return out;
}
async function loadPreprocessHandlers(cfg, root, extraModuleFromCli) {
const modulesFromCfg = Array.isArray(cfg.preprocess?.modules) ? cfg.preprocess.modules : cfg.preprocess?.module ? [cfg.preprocess.module] : [];
const modulesFromCli = Array.isArray(extraModuleFromCli) ? extraModuleFromCli : extraModuleFromCli ? [extraModuleFromCli] : [];
const modules = [...modulesFromCfg, ...modulesFromCli];
if (!modules.length) return [];
const out = [];
for (const modPath of modules) {
const abs = path4.isAbsolute(modPath) ? modPath : path4.resolve(root, String(modPath));
if (!fs3.existsSync(abs)) {
console.warn(pc2.yellow(`preprocess: module not found: ${modPath}`));
continue;
}
const handlers = await importHandlers(abs);
if (!handlers.length) {
console.warn(pc2.yellow(`preprocess: no handlers exported by ${modPath}`));
continue;
}
out.push(...handlers);
}
return out;
}
async function importHandlers(absFile) {
const lower = absFile.toLowerCase();
let mod;
if (lower.endsWith(".ts")) {
let esbuild;
try {
esbuild = await import("esbuild");
} catch {
throw new Error(`To use TS preprocess module (${absFile}), install "esbuild".`);
}
const src = await fs3.promises.readFile(absFile, "utf8");
const out = await esbuild.transform(src, {
loader: "ts",
format: "esm",
sourcemap: "inline",
sourcefile: absFile,
target: "es2020"
});
const dataUrl = "data:text/javascript;base64," + Buffer.from(out.code).toString("base64");
mod = await import(dataUrl);
} else if (lower.endsWith(".mjs") || lower.endsWith(".js")) {
mod = await import(pathToFileURL(absFile).href);
} else {
throw new Error(`Unsupported preprocess module extension: ${absFile}`);
}
const cand = mod?.default ?? mod?.handlers ?? [];
if (Array.isArray(cand)) return cand;
if (typeof cand === "function") return [cand];
return [];
}
function mergeGroupFilesIntoInclude(cfg) {
const files = Object.values(cfg.groups ?? {}).flatMap((g) => g.files ?? []).map((s) => s.replaceAll("\\", "/").replace(/^\.?\//, ""));
if (!files.length) return;
cfg.include = [.../* @__PURE__ */ new Set([...cfg.include ?? [], ...files])];
}
// src/pack.ts
import fs4 from "fs";
import path5 from "path";
import archiver from "archiver";
import { globby as globby2 } from "globby";
import ignore from "ignore";
import pc3 from "picocolors";
import crypto from "crypto";
import cliProgress2 from "cli-progress";
function stableSort(arr) {
return [...arr].sort((a, b) => a.localeCompare(b));
}
function readGitignore(root) {
const gi = path5.join(root, ".gitignore");
if (!fs4.existsSync(gi)) return [];
return fs4.readFileSync(gi, "utf8").split(/\r?\n/);
}
var GLOB_CHARS = /[*?[\]{}()!+@]|\\|[/]?\*\*[/]?/;
var looksLikeGlob = (p) => GLOB_CHARS.test(p);
var hasAnyGlob = (patterns) => !!patterns?.some(looksLikeGlob);
var normRel2 = (p) => p.replaceAll("\\", "/").replace(/^\.?\//, "");
async function buildFileListWithGlob(cfg, extraList, extraIgnore) {
const root = path5.resolve(process.cwd(), cfg.root ?? ".");
const include = (cfg.include?.length ? cfg.include : ["**/*"]).map(normRel2);
if (!hasAnyGlob(include)) {
const merged2 = Array.from(/* @__PURE__ */ new Set([...include, ...extraList.map(normRel2)]));
const existing = merged2.filter((rel) => {
try {
return fs4.statSync(path5.join(root, rel)).isFile();
} catch {
return false;
}
});
const ig2 = ignore();
if (cfg.respectGitignore) ig2.add(readGitignore(root));
ig2.add(cfg.exclude ?? []);
ig2.add(extraIgnore);
let final2 = existing.filter((f) => !ig2.ignores(f));
if (cfg.order?.join(",") === "exclude,include") {
final2 = existing;
}
return cfg.deterministic ? stableSort(final2) : final2;
}
console.log(include.filter(looksLikeGlob));
const candidates = await globby2(include, {
cwd: root,
dot: !!cfg.dot,
followSymbolicLinks: !!cfg.followSymlinks,
onlyFiles: true
});
const ig = ignore();
if (cfg.respectGitignore) ig.add(readGitignore(root));
ig.add(cfg.exclude ?? []);
ig.add(extraIgnore);
const merged = Array.from(/* @__PURE__ */ new Set([...candidates, ...extraList.map(normRel2)]));
let final = merged.filter((f) => !ig.ignores(f));
if (cfg.order?.join(",") === "exclude,include" && cfg.include?.length) {
const reincluded = await globby2(cfg.include, {
cwd: root,
dot: !!cfg.dot,
followSymbolicLinks: !!cfg.followSymlinks,
onlyFiles: true
});
const set = new Set(final);
for (const f of reincluded) set.add(normRel2(f));
final = [...set];
}
return cfg.deterministic ? stableSort(final) : final;
}
async function buildFileList(cfg, extraList, extraIgnore) {
const root = path5.resolve(process.cwd(), cfg.root ?? ".");
const include = (cfg.include ?? []).map(normRel2);
const merged = Array.from(/* @__PURE__ */ new Set([
...include,
...extraList.map(normRel2)
]));
const ig = ignore();
if (cfg.respectGitignore) ig.add(readGitignore(root));
ig.add(cfg.exclude ?? []);
ig.add(extraIgnore);
let final = merged.filter((f) => !ig.ignores(f));
if (cfg.order?.join(",") === "exclude,include") {
final = merged;
}
return cfg.deterministic ? stableSort(final) : final;
}
function zipPathNormalize(p) {
return String(p).replaceAll("\\", "/").replace(/^\.?\//, "");
}
function isContentLike(x) {
return Buffer.isBuffer(x) || x instanceof Uint8Array || typeof x === "string";
}
function hashBuffer(buf) {
return crypto.createHash("sha256").update(buf).digest("hex");
}
async function computeManifestFromEntries(entries) {
const sorted = [...entries].sort(
(a, b) => zipPathNormalize(a.zipPath).localeCompare(zipPathNormalize(b.zipPath))
);
const files = [];
for (const e of sorted) {
if ("content" in e && isContentLike(e.content)) {
const c = Buffer.isBuffer(e.content) ? e.content : Buffer.from(e.content);
files.push({ path: e.zipPath, size: c.length, sha256: hashBuffer(c) });
} else if ("sourcePath" in e && typeof e.sourcePath === "string") {
const data = await fs4.promises.readFile(e.sourcePath);
files.push({ path: e.zipPath, size: data.length, sha256: hashBuffer(data) });
}
}
return {
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
files,
totalFiles: files.length,
totalBytes: files.reduce((acc, f) => acc + f.size, 0),
algorithm: "sha256",
version: 1
};
}
async function writeZip(cfg, files) {
const root = path5.resolve(process.cwd(), cfg.root ?? ".");
const outPath = path5.resolve(process.cwd(), cfg.out);
fs4.mkdirSync(path5.dirname(outPath), { recursive: true });
const rawEntries = Array.isArray(files) && typeof files[0] === "string" ? files.map((rel) => ({
zipPath: zipPathNormalize(rel),
sourcePath: path5.join(root, rel)
})) : files.map((e) => {
const zipPath = zipPathNormalize(e.zipPath);
if ("content" in e && isContentLike(e.content)) {
return { zipPath, content: e.content };
}
let sp = e.sourcePath;
if (typeof sp !== "string") {
throw new TypeError(`Invalid sourcePath for ${zipPath}: expected string, got ${typeof sp}`);
}
const abs = path5.isAbsolute(sp) ? sp : path5.join(root, sp);
return { zipPath, sourcePath: abs };
});
const lastWins = /* @__PURE__ */ new Map();
for (const e of rawEntries) lastWins.set(e.zipPath, e);
const entries = Array.from(lastWins.values());
let manifest;
if (cfg.manifest !== false) {
manifest = await computeManifestFromEntries(entries);
}
const output = fs4.createWriteStream(outPath);
const archive = archiver("zip", { zlib: { level: 9 } });
const bar = new cliProgress2.SingleBar({ hideCursor: true }, cliProgress2.Presets.shades_classic);
archive.on("progress", (p) => {
const total = p.entries.total || entries.length;
const processed = p.entries.processed || 0;
if (!bar.isActive) bar.start(total, processed);
else bar.update(processed);
});
archive.on("warning", (err) => console.warn(pc3.yellow("archiver:"), err.message));
archive.on("error", (err) => {
throw err;
});
archive.pipe(output);
if (manifest) {
archive.append(JSON.stringify(manifest, null, 2), { name: "MANIFEST.json" });
}
for (const e of entries) {
if ("content" in e && isContentLike(e.content)) {
const c = Buffer.isBuffer(e.content) ? e.content : Buffer.from(e.content);
archive.append(c, { name: e.zipPath });
} else {
if (typeof e.sourcePath !== "string") {
throw new TypeError(`Invalid sourcePath for ${e.zipPath}: not a string`);
}
try {
archive.file(e.sourcePath, { name: e.zipPath, stats: fs4.statSync(e.sourcePath) });
} catch (err) {
console.warn(pc3.yellow(`skipping missing file: ${e.zipPath}`));
}
}
}
await archive.finalize();
bar.stop();
if (manifest) {
const manifestOut = cfg.manifestPath ? path5.resolve(process.cwd(), cfg.manifestPath) : path5.join(path5.dirname(outPath), path5.basename(outPath, ".zip") + ".manifest.json");
fs4.writeFileSync(manifestOut, JSON.stringify(manifest, null, 2));
const zhash = crypto.createHash("sha256").update(fs4.readFileSync(outPath)).digest("hex");
fs4.writeFileSync(outPath + ".sha256", `${zhash} ${path5.basename(outPath)}
`);
}
return outPath;
}
export {
PRESETS,
appendStubIfNoExt,
buildFileList,
buildFileListWithGlob,
getAllPresets,
loadConfig,
loadListFile,
loadPreprocessHandlers,
mergeGroupFilesIntoInclude,
readIgnoreFiles,
resolvePresets,
writeZip
};
//# sourceMappingURL=index.js.map