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') + }) + }) +})