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
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,8 @@ Multi-entrypoint packages can specify subpaths or wildcard exports in `package.j
"exports": {
".": "./src/index.ts", // root entrypoint
"./utils": "./src/utils.ts", // subpath
"./plugins/*": "./src/plugins/*" // wildcard
"./plugins/*": "./src/plugins/*", // wildcard
"./components/*": "./src/components/**/*" // deep wildcard
}
}
}
Expand Down Expand Up @@ -327,7 +328,7 @@ The `"bin"` field is automatically written into your `package.json`:
"bin": "./src/cli.ts"
},
+ "bin": {
+ "my-cli": "./dist/cli.cjs" // CLI entrypoint)
+ "my-cli": "./dist/cli.cjs" // CLI entrypoint
+ }
}
```
Expand Down
2 changes: 1 addition & 1 deletion VERSION.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.4.1
0.4.2
75 changes: 74 additions & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,7 @@ Examples:
// Extract entry points from zshy exports config
emojiLog("➡️", "Determining entrypoints...");
const entryPoints: string[] = [];
const assetEntrypoints: Array<{ exportPath: string; sourcePath: string }> = [];

const rows: string[][] = [["Subpath", "Entrypoint"]];

Expand Down Expand Up @@ -468,6 +469,10 @@ Examples:
entryPoints.push(sourcePath);

rows.push([`"${cleanExportPath}"`, sourcePath]);
} else {
// Any non-compilable file should be treated as an asset
assetEntrypoints.push({ exportPath, sourcePath });
rows.push([`"${cleanExportPath}"`, `${sourcePath}`]);
}
}
}
Expand Down Expand Up @@ -719,6 +724,41 @@ Examples:
buildContext
);

///////////////////////////////////
/// copy asset entrypoints ///
///////////////////////////////////

// Copy asset entrypoints to output directory
if (assetEntrypoints.length > 0) {
emojiLog("📄", `${prefix}Copying ${assetEntrypoints.length} asset${assetEntrypoints.length === 1 ? "" : "s"}...`);

for (const { sourcePath } of assetEntrypoints) {
const sourceFile = path.resolve(pkgJsonDir, sourcePath);
const relativePath = path.relative(rootDir, path.resolve(pkgJsonDir, sourcePath));
const destFile = path.resolve(outDir, relativePath);
const destDir = path.dirname(destFile);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Asset Path Calculation Error

The asset copying logic incorrectly calculates destination paths. Using path.relative(rootDir, ...) and then resolving against outDir can result in asset entrypoints being copied outside the intended output directory. This could lead to path traversal vulnerabilities, overwriting arbitrary files, or even overwriting the original source files.

Fix in Cursor Fix in Web


if (!fs.existsSync(sourceFile)) {
emojiLog("⚠️", `Asset not found: ${sourcePath}`, "warn");
continue;
}

if (!isDryRun) {
fs.mkdirSync(destDir, { recursive: true });
fs.copyFileSync(sourceFile, destFile);
}

// Track the copied file
buildContext.copiedAssets.add(toPosix(path.relative(pkgJsonDir, destFile)));

if (isVerbose) {
const relativeSource = toPosix(path.relative(pkgJsonDir, sourceFile));
const relativeDest = toPosix(path.relative(pkgJsonDir, destFile));
emojiLog("📋", `${isDryRun ? "[dryrun] " : ""}Copied asset: ./${relativeSource} → ./${relativeDest}`);
}
}
}

///////////////////////////////////
/// display written files ///
///////////////////////////////////
Expand Down Expand Up @@ -860,6 +900,39 @@ Examples:
}
}

// Handle asset entrypoints (only those that don't already have exports from TypeScript compilation)
for (const { exportPath, sourcePath } of assetEntrypoints) {
// Skip if this export path was already handled by TypeScript compilation
if (newExports[exportPath]) {
continue;
}

const absSourcePath = path.resolve(pkgJsonDir, sourcePath);
const relSourcePath = path.relative(rootDir, absSourcePath);
const absAssetPath = path.resolve(outDir, relSourcePath);
const relAssetPath = "./" + relativePosix(pkgJsonDir, absAssetPath);

// Assets are not source code - they just get copied and referenced with a simple path
newExports[exportPath] = relAssetPath;

// Handle root export special fields (only if no TypeScript root export exists)
if (exportPath === ".") {
if (!skipCjs) {
pkgJson.main = relAssetPath;
pkgJson.module = relAssetPath;
pkgJson.types = relAssetPath;
} else {
pkgJson.module = relAssetPath;
pkgJson.types = relAssetPath;
}
if (isVerbose) {
emojiLog("🔧", `Setting "main": ${formatForLog(relAssetPath)}`);
emojiLog("🔧", `Setting "module": ${formatForLog(relAssetPath)}`);
emojiLog("🔧", `Setting "types": ${formatForLog(relAssetPath)}`);
}
}
}

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Asset Configuration Errors Break TypeScript and Runtime

Asset entrypoints incorrectly configure package.json exports conditions and root fields (main, module, types). This causes types to reference non-declaration files and main/module to point to non-JavaScript assets, leading to TypeScript resolution issues and runtime errors. Furthermore, exports are generated for assets even if they failed to copy, referencing non-existent files.

Fix in Cursor Fix in Web

pkgJson.exports = newExports;
if (isVerbose) {
emojiLog("🔧", `Setting "exports": ${formatForLog(newExports)}`);
Expand Down Expand Up @@ -962,7 +1035,7 @@ Examples:
if (buildContext.errorCount > 0 || buildContext.warningCount > 0) {
emojiLog(
"📊",
`Compilation finished with ${buildContext.errorCount} error(s) and ${buildContext.warningCount} warning(s)`
`Compilation finished with ${buildContext.errorCount} error${buildContext.errorCount === 1 ? "" : "s"} and ${buildContext.warningCount} warning${buildContext.warningCount === 1 ? "" : "s"}`
);

// Apply threshold rules for exit code
Expand Down
6 changes: 6 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ export function emojiLog(_emoji: string, content: string, level: "log" | "warn"
}

export function isSourceFile(filePath: string): boolean {
// Declaration files are not source files
if (filePath.endsWith(".d.ts") || filePath.endsWith(".d.mts") || filePath.endsWith(".d.cts")) {
return false;
}

// TypeScript source files
return (
filePath.endsWith(".ts") || filePath.endsWith(".mts") || filePath.endsWith(".cts") || filePath.endsWith(".tsx")
);
Expand Down
Loading