@publint/pack
Advanced tools
+1
-1
| MIT License | ||
| Copyright (c) 2025 Bjorn Lu | ||
| Copyright (c) 2025 Bjorn Lu and publint contributors | ||
@@ -5,0 +5,0 @@ Permission is hereby granted, free of charge, to any person obtaining a copy |
+4
-4
| { | ||
| "name": "@publint/pack", | ||
| "version": "0.1.0", | ||
| "version": "0.1.1", | ||
| "description": "Utilities for packing and unpacking npm packages", | ||
@@ -22,7 +22,7 @@ "type": "module", | ||
| "type": "git", | ||
| "url": "git+https://github.com/bluwy/publint.git", | ||
| "url": "git+https://github.com/publint/publint.git", | ||
| "directory": "packages/pack" | ||
| }, | ||
| "bugs": { | ||
| "url": "https://github.com/bluwy/publint/issues" | ||
| "url": "https://github.com/publint/publint/issues" | ||
| }, | ||
@@ -37,3 +37,3 @@ "keywords": [ | ||
| "fs-fixture": "^2.6.0", | ||
| "vitest": "^3.0.0-beta.3" | ||
| "vitest": "^3.0.0-beta.4" | ||
| }, | ||
@@ -40,0 +40,0 @@ "scripts": { |
+6
-4
@@ -12,2 +12,4 @@ # @publint/pack | ||
| NOTE: All `pack*` APIs support passing `opts.packageManager` to specify the package manager to use for packing, and `opts.ignoreScripts` to skip running lifecycle scripts. | ||
| ### `pack()` | ||
@@ -17,3 +19,3 @@ | ||
| Packs the given directory and returns the packed tarball path. Pass `opts.packageManager` to specify the package manager to use for packing. | ||
| Packs the given directory and returns the packed tarball path. Pass `opts.destination` to change the output directory of the tarball. | ||
@@ -32,3 +34,3 @@ ```js | ||
| Packs the given directory and returns a list of relative file paths that were packed. Pass `opts.packageManager` to specify the package manager to use for packing. | ||
| Packs the given directory and returns a list of relative file paths that were packed. | ||
@@ -52,3 +54,3 @@ > [!NOTE] | ||
| Packs the given directory with the `--json` flag and returns its stdout as JSON. You can run the `<pm> pack --json` command manually to inspect the output shape. Pass `opts.packageManager` to specify the package manager to use for packing. | ||
| Packs the given directory with the `--json` flag and returns its stdout as JSON. You can run the `<pm> pack --json` command manually to inspect the output shape. | ||
@@ -78,3 +80,3 @@ > [!NOTE] | ||
| const response = await fetch( | ||
| 'https://registry.npmjs.org/mylib/-/mylib-1.0.0.tgz' | ||
| 'https://registry.npmjs.org/mylib/-/mylib-1.0.0.tgz', | ||
| ) | ||
@@ -81,0 +83,0 @@ if (!response.body) throw new Error('Failed to fetch tarball') |
| import { | ||
| arrayBufferToReadableStream, | ||
| readableStreamToArrayBuffer | ||
| readableStreamToArrayBuffer, | ||
| } from '../shared/buffer-stream.js' | ||
@@ -14,3 +14,3 @@ import { getFilesRootDir, parseTar } from '../shared/parse-tar.js' | ||
| const buffer = await readableStreamToArrayBuffer( | ||
| stream.pipeThrough(new DecompressionStream('gzip')) | ||
| stream.pipeThrough(new DecompressionStream('gzip')), | ||
| ) | ||
@@ -17,0 +17,0 @@ const files = parseTar(buffer) |
+9
-2
@@ -8,2 +8,9 @@ interface SharedPackOptions { | ||
| packageManager?: 'npm' | 'yarn' | 'pnpm' | 'bun' | ||
| /** | ||
| * Whether to ignore lifecycle scripts during packing, i.e. `prepare`, `prepack`, | ||
| * and `postpack`. (Does not work with yarn as it does not support ignoring scripts) | ||
| * | ||
| * @default false | ||
| */ | ||
| ignoreScripts?: boolean | ||
| } | ||
@@ -28,3 +35,3 @@ | ||
| dir: string, | ||
| opts?: PackAsListOptions | ||
| opts?: PackAsListOptions, | ||
| ): Promise<string[]> | ||
@@ -69,3 +76,3 @@ | ||
| export function unpack( | ||
| tarball: ArrayBuffer | ReadableStream<Uint8Array> | ||
| tarball: ArrayBuffer | ReadableStream<Uint8Array>, | ||
| ): Promise<UnpackResult> |
@@ -15,2 +15,4 @@ import cp from 'node:child_process' | ||
| // Handle tarball output. Try `--dry-run` if possible to not output any tarball, | ||
| // otherwise pack to a temporary directory and delete it later | ||
| const supportsDryRun = packageManager === 'npm' || packageManager === 'yarn' | ||
@@ -22,4 +24,2 @@ /** @type {string | undefined} */ | ||
| } else { | ||
| // For package managers that don't support `--dry-run`, we need to pack to a temporary directory | ||
| // and delete it later | ||
| packDestination = await getTempPackDir() | ||
@@ -29,8 +29,27 @@ command += ` --pack-destination ${packDestination}` | ||
| const { stdout } = await util.promisify(cp.exec)(command, { cwd: dir }) | ||
| // Handle ignore-scripts | ||
| if (opts?.ignoreScripts) { | ||
| switch (packageManager) { | ||
| case 'pnpm': | ||
| command += ' --config.ignore-scripts=true' | ||
| break | ||
| case 'yarn': | ||
| // yarn does not support ignoring scripts | ||
| break | ||
| default: | ||
| command += ' --ignore-scripts' | ||
| break | ||
| } | ||
| } | ||
| let { stdout } = await util.promisify(cp.exec)(command, { cwd: dir }) | ||
| try { | ||
| return JSON.parse( | ||
| packageManager === 'yarn' ? fixYarnStdout(stdout) : stdout | ||
| ) | ||
| stdout = stdout.trim() | ||
| if (packageManager === 'pnpm') { | ||
| stdout = fixPnpmStdout(stdout) | ||
| } else if (packageManager === 'yarn') { | ||
| stdout = fixYarnStdout(stdout) | ||
| } | ||
| return JSON.parse(stdout) | ||
| } finally { | ||
@@ -43,2 +62,22 @@ if (!supportsDryRun && packDestination) { | ||
| // pnpm outputs lifecycle script logs if not ignoring scripts | ||
| /** | ||
| * @param {string} stdout | ||
| */ | ||
| function fixPnpmStdout(stdout) { | ||
| // If starts with `{`, it's likely a valid JSON | ||
| if (stdout.startsWith('{')) return stdout | ||
| // Otherwise try to find its usual output format, `{\n "name": ...` | ||
| const usualStartIndex = /\{\s*"name"/.exec(stdout)?.index | ||
| if (usualStartIndex != null) return stdout.slice(usualStartIndex) | ||
| // Otherwise, simply try to find the first `{` character | ||
| const firstBraceIndex = stdout.indexOf('{') | ||
| if (firstBraceIndex !== -1) return stdout.slice(firstBraceIndex) | ||
| // If all fails, return the original stdout | ||
| return stdout | ||
| } | ||
| // yarn outputs invalid json for some reason | ||
@@ -45,0 +84,0 @@ /** |
@@ -13,5 +13,5 @@ import fs from 'node:fs/promises' | ||
| try { | ||
| return await packAsListWithJson(dir, packageManager) | ||
| return await packAsListWithJson(dir, packageManager, opts?.ignoreScripts) | ||
| } catch { | ||
| return await packAsListWithPack(dir, packageManager) | ||
| return await packAsListWithPack(dir, packageManager, opts?.ignoreScripts) | ||
| } | ||
@@ -25,6 +25,7 @@ } | ||
| * @param {NonNullable<import('../index.d.ts').PackAsListOptions['packageManager']>} packageManager | ||
| * @param {import('../index.d.ts').PackAsListOptions['ignoreScripts']} ignoreScripts | ||
| * @returns {Promise<string[]>} | ||
| */ | ||
| export async function packAsListWithJson(dir, packageManager) { | ||
| const stdoutJson = await packAsJson(dir, { packageManager }) | ||
| export async function packAsListWithJson(dir, packageManager, ignoreScripts) { | ||
| const stdoutJson = await packAsJson(dir, { packageManager, ignoreScripts }) | ||
| switch (packageManager) { | ||
@@ -47,7 +48,12 @@ case 'npm': | ||
| * @param {NonNullable<import('../index.d.ts').PackAsListOptions['packageManager']>} packageManager | ||
| * @param {import('../index.d.ts').PackAsListOptions['ignoreScripts']} ignoreScripts | ||
| * @returns {Promise<string[]>} | ||
| */ | ||
| export async function packAsListWithPack(dir, packageManager) { | ||
| export async function packAsListWithPack(dir, packageManager, ignoreScripts) { | ||
| const destination = await getTempPackDir() | ||
| const tarballPath = await pack(dir, { packageManager, destination }) | ||
| const tarballPath = await pack(dir, { | ||
| packageManager, | ||
| ignoreScripts, | ||
| destination, | ||
| }) | ||
@@ -54,0 +60,0 @@ try { |
+18
-2
@@ -15,2 +15,3 @@ import cp from 'node:child_process' | ||
| // Handle tarball output | ||
| const packDestination = opts?.destination ?? dir | ||
@@ -31,2 +32,17 @@ if (opts?.destination) { | ||
| // Handle ignore-scripts | ||
| if (opts?.ignoreScripts) { | ||
| switch (packageManager) { | ||
| case 'pnpm': | ||
| command += ' --config.ignore-scripts=true' | ||
| break | ||
| case 'yarn': | ||
| // yarn does not support ignoring scripts | ||
| break | ||
| default: | ||
| command += ' --ignore-scripts' | ||
| break | ||
| } | ||
| } | ||
| const output = await util.promisify(cp.exec)(command, { cwd: dir }) | ||
@@ -39,3 +55,3 @@ | ||
| return files.find( | ||
| (file) => file.endsWith('.tgz') && output.stdout.includes(file) | ||
| (file) => file.endsWith('.tgz') && output.stdout.includes(file), | ||
| ) | ||
@@ -45,3 +61,3 @@ }) | ||
| throw new Error( | ||
| `Failed to find packed tarball file in ${packDestination}. Command output:\n${JSON.stringify(output, null, 2)}` | ||
| `Failed to find packed tarball file in ${packDestination}. Command output:\n${JSON.stringify(output, null, 2)}`, | ||
| ) | ||
@@ -48,0 +64,0 @@ } |
@@ -12,3 +12,3 @@ import util from 'node:util' | ||
| buffer = await readableStreamToArrayBuffer( | ||
| tarball.pipeThrough(new DecompressionStream('gzip')) | ||
| tarball.pipeThrough(new DecompressionStream('gzip')), | ||
| ) | ||
@@ -15,0 +15,0 @@ } else { |
@@ -12,3 +12,3 @@ // Utilities between ArrayBuffer and ReadableStream | ||
| controller.close() | ||
| } | ||
| }, | ||
| }) | ||
@@ -15,0 +15,0 @@ } |
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
19420
11.89%423
17.17%88
2.33%5
25%