|
| 1 | +import type { ParsedTarFileItem } from 'nanotar' |
1 | 2 | import type { DownloadTemplateOptions, DownloadTemplateResult, ExtractOptions as GitItExtractOptions, Hooks, InstallOptions, TemplateProvider } from './types'
|
2 | 3 | import { spawn } from 'node:child_process'
|
3 | 4 | import { existsSync, readdirSync } from 'node:fs'
|
4 |
| -import { mkdir, rm } from 'node:fs/promises' |
5 |
| -import { dirname, resolve } from 'node:path' |
| 5 | +import { mkdir, readFile, rm, writeFile } from 'node:fs/promises' |
| 6 | +import { dirname, join, resolve } from 'node:path' |
6 | 7 | import process from 'node:process'
|
| 8 | +import { gunzipSync } from 'node:zlib' |
7 | 9 | import defu from 'defu'
|
| 10 | +import { parseTar } from 'nanotar' |
8 | 11 | import { providers } from './providers'
|
9 | 12 | import { registryProvider } from './registry'
|
10 | 13 | import { cacheDirectory, debug, download, normalizeHeaders } from './utils'
|
@@ -58,34 +61,91 @@ async function installDependencies(options: InstallOptions): Promise<void> {
|
58 | 61 | }
|
59 | 62 |
|
60 | 63 | /**
|
61 |
| - * Extract a tarball using the native tar command |
| 64 | + * Extract a tarball using nanotar (cross-platform) |
62 | 65 | */
|
63 | 66 | async function extractTar(options: GitItExtractOptions): Promise<void> {
|
64 |
| - const { file, cwd } = options |
| 67 | + const { file, cwd, onentry } = options |
65 | 68 |
|
66 | 69 | debug(`Extracting tarball ${file} to ${cwd}`)
|
67 | 70 |
|
68 |
| - // Simple case: extract all files from tarball to cwd |
69 |
| - return new Promise<void>((resolve, reject) => { |
70 |
| - // Use --strip-components=1 to remove the first directory level (e.g., stacks-main/) |
71 |
| - const child = spawn('tar', ['-xzf', file, '--strip-components=1', '-C', cwd], { |
72 |
| - stdio: 'inherit', |
73 |
| - }) |
| 71 | + try { |
| 72 | + // Read the tar file |
| 73 | + const tarData = await readFile(file) |
74 | 74 |
|
75 |
| - child.on('close', (code) => { |
76 |
| - if (code === 0) { |
77 |
| - debug(`Extracted tarball ${file} to ${cwd}`) |
78 |
| - resolve() |
| 75 | + // Determine if it's gzipped |
| 76 | + const isGzipped = file.endsWith('.gz') || file.endsWith('.tgz') |
| 77 | + |
| 78 | + // Process the tar data |
| 79 | + let tarBuffer: Uint8Array |
| 80 | + if (isGzipped) { |
| 81 | + debug('Decompressing gzipped tarball using zlib') |
| 82 | + tarBuffer = gunzipSync(tarData) |
| 83 | + } |
| 84 | + else { |
| 85 | + tarBuffer = tarData |
| 86 | + } |
| 87 | + |
| 88 | + // Parse the tar file |
| 89 | + const entries = parseTar(tarBuffer) |
| 90 | + debug(`Parsed ${entries.length} entries from tarball`) |
| 91 | + |
| 92 | + // Identify the root directory to strip |
| 93 | + let rootDir: string | null = null |
| 94 | + for (const entry of entries) { |
| 95 | + if (entry.type === 'directory' && !rootDir && entry.name.indexOf('/') === entry.name.length - 1) { |
| 96 | + rootDir = entry.name.slice(0, -1) // Remove trailing slash |
| 97 | + debug(`Identified root directory to strip: ${rootDir}`) |
| 98 | + break |
| 99 | + } |
| 100 | + } |
| 101 | + |
| 102 | + // Process entries |
| 103 | + for (const entry of entries) { |
| 104 | + let targetPath = entry.name |
| 105 | + |
| 106 | + // Apply onentry function if provided (for custom subdir handling) |
| 107 | + if (typeof onentry === 'function') { |
| 108 | + const entryForHook = { path: targetPath } |
| 109 | + onentry(entryForHook) |
| 110 | + targetPath = entryForHook.path |
| 111 | + |
| 112 | + // Skip entries filtered out by onentry |
| 113 | + if (!targetPath) { |
| 114 | + debug(`Skipping ${entry.name} (filtered by onentry)`) |
| 115 | + continue |
| 116 | + } |
| 117 | + } |
| 118 | + // Default behavior: strip root directory (equivalent to tar --strip-components=1) |
| 119 | + else if (rootDir && targetPath.startsWith(`${rootDir}/`)) { |
| 120 | + targetPath = targetPath.slice(rootDir.length + 1) |
| 121 | + if (!targetPath) { |
| 122 | + debug(`Skipping ${entry.name} (root directory)`) |
| 123 | + continue |
| 124 | + } |
| 125 | + } |
| 126 | + |
| 127 | + const fullPath = join(cwd, targetPath) |
| 128 | + |
| 129 | + if (entry.type === 'directory') { |
| 130 | + debug(`Creating directory: ${fullPath}`) |
| 131 | + await mkdir(fullPath, { recursive: true }) |
| 132 | + } |
| 133 | + else if (entry.type === 'file' && entry.data) { |
| 134 | + debug(`Writing file: ${fullPath} (${entry.size} bytes)`) |
| 135 | + // Ensure parent directory exists |
| 136 | + await mkdir(dirname(fullPath), { recursive: true }) |
| 137 | + await writeFile(fullPath, entry.data) |
79 | 138 | }
|
80 | 139 | else {
|
81 |
| - reject(new Error(`tar -xzf exited with code ${code}`)) |
| 140 | + debug(`Skipping unsupported entry type: ${entry.type} for ${entry.name}`) |
82 | 141 | }
|
83 |
| - }) |
| 142 | + } |
84 | 143 |
|
85 |
| - child.on('error', (err) => { |
86 |
| - reject(new Error(`Failed to run tar: ${err.message}`)) |
87 |
| - }) |
88 |
| - }) |
| 144 | + debug(`Successfully extracted ${entries.length} entries to ${cwd}`) |
| 145 | + } |
| 146 | + catch (error) { |
| 147 | + throw new Error(`Failed to extract tarball: ${error instanceof Error ? error.message : String(error)}`) |
| 148 | + } |
89 | 149 | }
|
90 | 150 |
|
91 | 151 | /**
|
|
0 commit comments