Skip to content

Commit b9598bc

Browse files
committed
chore: wip
1 parent bfa1e0b commit b9598bc

File tree

3 files changed

+84
-20
lines changed

3 files changed

+84
-20
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,6 @@ docs/.vitepress/cache
1313
/test/.tmp
1414
bin/gitit
1515
bin/gitit*
16+
/my-project
1617
/my-stack
1718
/stack-example

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,9 @@
6565
"preview:docs": "bun --bun vitepress preview docs",
6666
"typecheck": "bun --bun tsc --noEmit"
6767
},
68+
"dependencies": {
69+
"nanotar": "^0.2.0"
70+
},
6871
"devDependencies": {
6972
"@stacksjs/docs": "^0.70.23",
7073
"@stacksjs/eslint-config": "^4.10.2-beta.3",

src/gitit.ts

Lines changed: 80 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1+
import type { ParsedTarFileItem } from 'nanotar'
12
import type { DownloadTemplateOptions, DownloadTemplateResult, ExtractOptions as GitItExtractOptions, Hooks, InstallOptions, TemplateProvider } from './types'
23
import { spawn } from 'node:child_process'
34
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'
67
import process from 'node:process'
8+
import { gunzipSync } from 'node:zlib'
79
import defu from 'defu'
10+
import { parseTar } from 'nanotar'
811
import { providers } from './providers'
912
import { registryProvider } from './registry'
1013
import { cacheDirectory, debug, download, normalizeHeaders } from './utils'
@@ -58,34 +61,91 @@ async function installDependencies(options: InstallOptions): Promise<void> {
5861
}
5962

6063
/**
61-
* Extract a tarball using the native tar command
64+
* Extract a tarball using nanotar (cross-platform)
6265
*/
6366
async function extractTar(options: GitItExtractOptions): Promise<void> {
64-
const { file, cwd } = options
67+
const { file, cwd, onentry } = options
6568

6669
debug(`Extracting tarball ${file} to ${cwd}`)
6770

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)
7474

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)
79138
}
80139
else {
81-
reject(new Error(`tar -xzf exited with code ${code}`))
140+
debug(`Skipping unsupported entry type: ${entry.type} for ${entry.name}`)
82141
}
83-
})
142+
}
84143

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+
}
89149
}
90150

91151
/**

0 commit comments

Comments
 (0)