diff --git a/package.json b/package.json
index 4494c673d..f3cd70a9d 100644
--- a/package.json
+++ b/package.json
@@ -86,6 +86,7 @@
"micromatch": "^4.0.8",
"minimark": "^0.2.0",
"minimatch": "^10.0.3",
+ "modern-tar": "^0.6.1",
"nuxt-component-meta": "https://pkg.pr.new/nuxt-component-meta@e3eb2c4",
"nypm": "^0.6.2",
"ohash": "^2.0.11",
@@ -97,7 +98,6 @@
"slugify": "^1.6.6",
"socket.io-client": "^4.8.1",
"std-env": "^3.10.0",
- "tar": "^7.5.1",
"tinyglobby": "^0.2.15",
"ufo": "^1.6.1",
"unctx": "^2.4.1",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 552000e25..a7ad59c40 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -101,6 +101,9 @@ importers:
minimatch:
specifier: ^10.0.3
version: 10.0.3
+ modern-tar:
+ specifier: ^0.6.1
+ version: 0.6.1
nuxt-component-meta:
specifier: https://pkg.pr.new/nuxt-component-meta@e3eb2c4
version: https://pkg.pr.new/nuxt-component-meta@e3eb2c4(magicast@0.3.5)
@@ -137,9 +140,6 @@ importers:
std-env:
specifier: ^3.10.0
version: 3.10.0
- tar:
- specifier: ^7.5.1
- version: 7.5.1
tinyglobby:
specifier: ^0.2.15
version: 0.2.15
@@ -6506,6 +6506,10 @@ packages:
mocked-exports@0.1.1:
resolution: {integrity: sha512-aF7yRQr/Q0O2/4pIXm6PZ5G+jAd7QS4Yu8m+WEeEHGnbo+7mE36CbLSDQiXYV8bVL3NfmdeqPJct0tUlnjVSnA==}
+ modern-tar@0.6.1:
+ resolution: {integrity: sha512-PS5kTfMLBrIP7X7dj3x+N17T1Fc2InnFJAe/pGleM0hT9GhDFkltsLYxB1eQisg55bjnvgLsbTRFYp47AUhcbg==}
+ engines: {node: '>=18.0.0'}
+
motion-dom@12.23.12:
resolution: {integrity: sha512-RcR4fvMCTESQBD/uKQe49D5RUeDOokkGRmz4ceaJKDBgHYtZtntC/s2vLvY38gqGaytinij/yi3hMcWVcEF5Kw==}
@@ -15867,6 +15871,8 @@ snapshots:
mocked-exports@0.1.1: {}
+ modern-tar@0.6.1: {}
+
motion-dom@12.23.12:
dependencies:
motion-utils: 12.23.6
diff --git a/src/utils/git.ts b/src/utils/git.ts
index bbc1738ac..c9062e4a6 100644
--- a/src/utils/git.ts
+++ b/src/utils/git.ts
@@ -1,11 +1,11 @@
-import { createWriteStream } from 'node:fs'
+import { createReadStream, createWriteStream } from 'node:fs'
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises'
-import { pipeline } from 'node:stream'
-import { promisify } from 'node:util'
+import { pipeline } from 'node:stream/promises'
+import { createGunzip } from 'node:zlib'
import { join } from 'pathe'
-import { extract } from 'tar'
import { readGitConfig } from 'pkg-types'
import gitUrlParse from 'git-url-parse'
+import { unpackTar } from 'modern-tar/fs'
export interface GitInfo {
// Repository name
@@ -39,16 +39,15 @@ export async function downloadRepository(url: string, cwd: string, { headers }:
try {
const response = await fetch(url, { headers })
const stream = createWriteStream(tarFile)
- await promisify(pipeline)(response.body as unknown as ReadableStream[], stream)
-
- await extract({
- file: tarFile,
- cwd: cwd,
- onentry(entry) {
- // Remove root directory from zip contents to save files directly in cwd
- entry.path = entry.path.split('/').splice(1).join('/')
- },
- })
+ await pipeline(response.body as unknown as ReadableStream[], stream)
+
+ await pipeline(
+ createReadStream(tarFile),
+ createGunzip(),
+ unpackTar(cwd, {
+ strip: 1, // Remove root directory from zip contents to save files directly in cwd
+ }),
+ )
await writeFile(cacheFile, JSON.stringify({
url: url,
diff --git a/test/fixtures/remote-repository/app.vue b/test/fixtures/remote-repository/app.vue
new file mode 100644
index 000000000..09f935bbb
--- /dev/null
+++ b/test/fixtures/remote-repository/app.vue
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/test/fixtures/remote-repository/content.config.ts b/test/fixtures/remote-repository/content.config.ts
new file mode 100644
index 000000000..bd115053d
--- /dev/null
+++ b/test/fixtures/remote-repository/content.config.ts
@@ -0,0 +1,16 @@
+import { defineCollection, defineContentConfig } from '@nuxt/content'
+
+export default defineContentConfig({
+ collections: {
+ content: defineCollection({
+ type: 'page',
+ source: {
+ repository: 'https://github.com/nuxt/content',
+ include: 'docs/content/**',
+ exclude: [
+ '**/_dir.yml',
+ ],
+ },
+ }),
+ },
+})
diff --git a/test/fixtures/remote-repository/nuxt.config.ts b/test/fixtures/remote-repository/nuxt.config.ts
new file mode 100644
index 000000000..ed45d60f9
--- /dev/null
+++ b/test/fixtures/remote-repository/nuxt.config.ts
@@ -0,0 +1,9 @@
+import { defineNuxtConfig } from 'nuxt/config'
+
+export default defineNuxtConfig({
+ modules: [
+ '../../../src/module',
+ ],
+ devtools: { enabled: true },
+ compatibilityDate: '2025-09-03',
+})
diff --git a/test/fixtures/remote-repository/server/api/content/get.get.ts b/test/fixtures/remote-repository/server/api/content/get.get.ts
new file mode 100644
index 000000000..509352cd5
--- /dev/null
+++ b/test/fixtures/remote-repository/server/api/content/get.get.ts
@@ -0,0 +1,10 @@
+import { eventHandler, getQuery } from 'h3'
+import { queryCollection } from '@nuxt/content/server'
+
+export default eventHandler(async (event) => {
+ const path = String(getQuery(event).path || '/')
+
+ const content = await queryCollection(event, 'content' as never).path(path).first()
+
+ return content
+})
diff --git a/test/remote-repository.test.ts b/test/remote-repository.test.ts
new file mode 100644
index 000000000..9aa98d708
--- /dev/null
+++ b/test/remote-repository.test.ts
@@ -0,0 +1,40 @@
+import fs from 'node:fs/promises'
+import { createResolver } from '@nuxt/kit'
+import { setup, $fetch } from '@nuxt/test-utils'
+import { afterAll, describe, expect, test } from 'vitest'
+
+const resolver = createResolver(import.meta.url)
+
+async function cleanup() {
+ await fs.rm(resolver.resolve('./fixtures/remote-repository/node_modules'), { recursive: true, force: true })
+ await fs.rm(resolver.resolve('./fixtures/remote-repository/.nuxt'), { recursive: true, force: true })
+ await fs.rm(resolver.resolve('./fixtures/remote-repository/.data'), { recursive: true, force: true })
+ await fs.rm(resolver.resolve('./fixtures/remote-repository/content'), { recursive: true, force: true })
+}
+
+describe('remote-repository', async () => {
+ await cleanup()
+ afterAll(async () => {
+ await cleanup()
+ })
+
+ await setup({
+ rootDir: resolver.resolve('./fixtures/remote-repository'),
+ dev: true,
+ })
+
+ describe('Repository', () => {
+ test('is cloned', async () => {
+ const stat = await fs.stat(resolver.resolve('./fixtures/remote-repository/.data/content/github-nuxt-content-main'))
+ expect(stat?.isDirectory()).toBe(true)
+ })
+ })
+
+ describe('Content', () => {
+ test('is loaded', async () => {
+ const doc: Record = await $fetch('/api/content/get?path=/docs/content')
+ expect(doc).toBeDefined()
+ expect(doc.path).toBe('/docs/content')
+ })
+ })
+})