From 6c700ccb192e2aef894b2b4547db686fbae9f770 Mon Sep 17 00:00:00 2001 From: Duale Siad Date: Tue, 16 Sep 2025 15:11:26 +1000 Subject: [PATCH 1/7] feat: skeleton auth structure --- docs/content/docs/2.collections/3.sources.md | 106 ++++++- package.json | 1 + pnpm-lock.yaml | 283 +++++++++++++++++++ src/types/collection.ts | 10 +- src/types/git.ts | 15 + src/types/index.ts | 1 + 6 files changed, 396 insertions(+), 20 deletions(-) create mode 100644 src/types/git.ts diff --git a/docs/content/docs/2.collections/3.sources.md b/docs/content/docs/2.collections/3.sources.md index 20114ef01..2a7e1e4ae 100644 --- a/docs/content/docs/2.collections/3.sources.md +++ b/docs/content/docs/2.collections/3.sources.md @@ -80,7 +80,7 @@ source: { ### `repository` -External source representing a remote git repository URL (e.g., ). +External source representing a remote git repository URL (e.g., ). Only supports downloading from Github and Bitbucket by default, unless `cloneRepository` is set. When defining an external source you must also define the `include` option. `include` pattern is essential for the module to know which files to use for the collection. @@ -101,27 +101,103 @@ export default defineContentConfig({ }) ``` -### `authToken` +### `cloneRepository` +This option allows for shallow-cloning any Git repository (`git clone --depth=1`), with an optional Git reference. Enabling this to `true` will clone the repository at build-time, and work as normal. -Authentication token for private repositories (e.g., GitHub personal access token). +```js +export default defineContentConfig({ + collections: { + docs: defineCollection({ + type: 'page', + source: { + repository: 'https://github.com/nuxt/content', + include: 'docs/content/**', + cloneRepository: true + }, + }) + } +}) +``` + +#### `ref` +Declares the Git branch or tag the content source downloads from. +```js +export default defineContentConfig({ + collections: { + docs: defineCollection({ + type: 'page', + source: { + repository: 'https://github.com/nuxt/content', + include: 'docs/content/**', + cloneRepository: true, + ref: { + branch: "main" + // tag: "v4.0.0" + // commit: "4e5ade" + } + }, + }) + } +}) +``` + +### `authentication` +Manages authentication for private repositories. Can either provide an object for basic authentication (user/password), or an authentication token. ::warning{icon="i-lucide-shield-alert"} Never commit authentication tokens or credentials directly in your code. Use environment variables or other secure methods to provide these values at runtime. :: -### `authBasic` +#### `username` & `password` +A basic authentication configuration for private repositories (e.g. Bitbucket username and password). +```js +export default defineContentConfig({ + collections: { + docs: defineCollection({ + type: 'page', + source: { + repository: 'https://bitbucket.org/user/repo', + authBasic: { + username: 'username', + password: 'password', + } + }, + }) + } +}) +``` -Basic authentication for private repositories (e.g., Bitbucket username and password). +#### `authToken` +Authentication token for private repositories (e.g., Github Personal Access Token). Recommended for Github [Personal Access Tokens](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens), and Bitbucket [API Tokens](https://support.atlassian.com/bitbucket-cloud/docs/using-api-tokens/). +```js +export default defineContentConfig({ + collections: { + docs: defineCollection({ + type: 'page', + source: { + repository: 'https://github.com/user/repo', + authToken: { + // Note: providing a username is *optional* for Github. + username: "octocat", + authToken: "ghp_TOKENHERE" + } + }, + }) + } +}) +``` +If your provider does not need a username, you can simply declare `authToken` as a string. -```ts -defineCollection({ - type: 'page', - source: { - repository: 'https://bitbucket.org/username/repo', - authBasic: { - username: 'username', - password: 'password', - }, - }, +```js +export default defineContentConfig({ + collections: { + docs: defineCollection({ + type: 'page', + source: { + repository: 'https://github.com/user/repo', + authToken: "ghp_TOKENHERE" + }, + }) + } }) ``` diff --git a/package.json b/package.json index 8e11e16b2..295568b8b 100644 --- a/package.json +++ b/package.json @@ -72,6 +72,7 @@ "defu": "^6.1.4", "destr": "^2.0.5", "git-url-parse": "^16.1.0", + "isomorphic-git": "^1.33.1", "jiti": "^2.5.1", "json-schema-to-typescript": "^15.0.4", "knitwork": "^1.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3c1f6ae94..0ad90c252 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -59,6 +59,9 @@ importers: git-url-parse: specifier: ^16.1.0 version: 16.1.0 + isomorphic-git: + specifier: ^1.33.1 + version: 1.33.1 jiti: specifier: ^2.5.1 version: 2.5.1 @@ -3387,6 +3390,9 @@ packages: resolution: {integrity: sha512-3pYeLyDZ6nJew9QeBhS4Nly02269Dkdk32+zdbbKmL6n4ZuaGorwwA+xx12xgOciA8BF1w9x+dlH7oUkFTW91w==} engines: {node: '>=20.18.0'} + async-lock@1.4.1: + resolution: {integrity: sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==} + async-retry@1.3.3: resolution: {integrity: sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==} @@ -3403,6 +3409,10 @@ packages: peerDependencies: postcss: ^8.1.0 + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + b4a@1.7.0: resolution: {integrity: sha512-KtsH1alSKomfNi/yDAFaD8PPFfi0LxJCEbPuzogcXrMF+yH40Z1ykTDo2vyxuQfN1FLjv0LFM7CadLHEPrVifw==} peerDependencies: @@ -3561,6 +3571,18 @@ packages: resolution: {integrity: sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==} engines: {node: '>= 10'} + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bind@1.0.8: + resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} @@ -3646,6 +3668,9 @@ packages: citty@0.1.6: resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==} + clean-git-ref@2.0.1: + resolution: {integrity: sha512-bLSptAy2P0s6hU4PzuIMKmMJJSE6gLXGH1cntDu7bWJUksvuM+7ReOK61mozULErYvP6a15rnYl0zFDef+pyPw==} + clean-regexp@1.0.0: resolution: {integrity: sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==} engines: {node: '>=4'} @@ -4035,6 +4060,10 @@ packages: resolution: {integrity: sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==} engines: {node: '>=18'} + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + define-lazy-prop@2.0.0: resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==} engines: {node: '>=8'} @@ -4093,6 +4122,9 @@ packages: dfa@1.2.0: resolution: {integrity: sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==} + diff3@0.0.3: + resolution: {integrity: sha512-iSq8ngPOt0K53A6eVr4d5Kn6GNrM2nQZtC740pzIriHtn4pOQ2lyzEXQMBeVcWERN0ye7fhBsk9PbLLQOnUx/g==} + diff@8.0.2: resolution: {integrity: sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg==} engines: {node: '>=0.3.1'} @@ -4132,6 +4164,10 @@ packages: resolution: {integrity: sha512-Sf2LSQP+bOlhKWWyhFsn0UsfdK/kCWRv1iuA2gXAwt3dyNabr6QSj00I2V10pidqz69soatm9ZwZvpQMTIOd5Q==} engines: {node: '>=12'} + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + duplexer@0.1.2: resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} @@ -4249,9 +4285,21 @@ packages: errx@0.1.0: resolution: {integrity: sha512-fZmsRiDNv07K6s2KkKFTiD2aIvECa7++PKyD5NC32tpRw46qZA3sOz+aM+/V9V0GDHxVTKLziveV4JhzBHDp9Q==} + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + esbuild@0.25.4: resolution: {integrity: sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==} engines: {node: '>=18'} @@ -4554,6 +4602,10 @@ packages: fontkit@2.0.4: resolution: {integrity: sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g==} + for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} + foreground-child@3.3.1: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} @@ -4622,9 +4674,17 @@ packages: resolution: {integrity: sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==} engines: {node: '>=18'} + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + get-port-please@3.2.0: resolution: {integrity: sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A==} + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + get-stream@8.0.1: resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} engines: {node: '>=16'} @@ -4710,6 +4770,10 @@ packages: resolution: {integrity: sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA==} engines: {node: '>=18'} + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} @@ -4736,6 +4800,17 @@ packages: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + has-unicode@2.0.1: resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==} @@ -4974,6 +5049,10 @@ packages: resolution: {integrity: sha512-f4RqJKBUe5rQkJ2eJEJBXSticB3hGbN9j0yxxMQFqIW89Jp9WYFtzfTcRlstDKVUTRzSOTLKRfO9vIztenwtxA==} engines: {node: '>=18.20'} + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + is-core-module@2.16.1: resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} engines: {node: '>= 0.4'} @@ -5059,6 +5138,10 @@ packages: resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==} engines: {node: '>=18'} + is-typed-array@1.1.15: + resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} + engines: {node: '>= 0.4'} + is-unicode-supported@1.3.0: resolution: {integrity: sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==} engines: {node: '>=12'} @@ -5089,6 +5172,9 @@ packages: isarray@1.0.0: resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -5096,6 +5182,11 @@ packages: resolution: {integrity: sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==} engines: {node: '>=16'} + isomorphic-git@1.33.1: + resolution: {integrity: sha512-Fy5rPAncURJoqL9R+5nJXLl5rQH6YpcjJd7kdCoRJPhrBiLVkLm9b+esRqYQQlT1hKVtKtALbfNtpHjWWJgk6g==} + engines: {node: '>=14.17'} + hasBin: true + issue-parser@7.0.1: resolution: {integrity: sha512-3YZcUUR2Wt1WsapF+S/WiA2WmlW0cWAoPccMqne7AxEBhCdFeTPjfv/Axb8V2gyCgY3nRw+ksZ3xSUX+R47iAg==} engines: {node: ^18.17 || >=20.6.1} @@ -5387,6 +5478,10 @@ packages: marky@1.3.0: resolution: {integrity: sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==} + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + mdast-util-find-and-replace@3.0.2: resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==} @@ -5597,6 +5692,9 @@ packages: minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + minimisted@2.0.1: + resolution: {integrity: sha512-1oPjfuLQa2caorJUM8HV8lGgWCc0qqAO1MNv/k05G4qslmsndV/5WdNZrqCiyqiz3wohia2Ij2B7w2Dr7/IyrA==} + minipass-collect@1.0.2: resolution: {integrity: sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==} engines: {node: '>= 8'} @@ -6001,6 +6099,9 @@ packages: pako@0.2.9: resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==} + pako@1.0.11: + resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -6117,6 +6218,10 @@ packages: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} + pify@4.0.1: + resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} + engines: {node: '>=6'} + pkg-types@1.3.1: resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} @@ -6132,6 +6237,10 @@ packages: resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} engines: {node: '>=4'} + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} + postcss-calc@10.1.1: resolution: {integrity: sha512-NYEsLHh8DgG/PRH2+G9BTuUdtf9ViS+vdoQ0YA5OQdGsfN4ztiwtDWNtBl9EKeqNMFnIu8IKZ0cLxEQ5r5KVMw==} engines: {node: ^18.12 || ^20.9 || >=22.0} @@ -6686,9 +6795,18 @@ packages: set-blocking@2.0.0: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + sha.js@2.4.12: + resolution: {integrity: sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==} + engines: {node: '>= 0.10'} + hasBin: true + sharp@0.32.6: resolution: {integrity: sha512-KyLTWwgcR9Oe4d9HwCwNM2l7+J0dUQwn/yf7S0EnTtb0eVS4RxO0eUSvxPtzT4F3SY+C4K6fqdv/DO27sJ/v/w==} engines: {node: '>=14.15.0'} @@ -7051,6 +7169,10 @@ packages: resolution: {integrity: sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==} engines: {node: '>=14.0.0'} + to-buffer@1.2.1: + resolution: {integrity: sha512-tB82LpAIWjhLYbqjx3X4zEeHN6M8CiuOEy2JY8SEQVdYRe3CCHOFaqrBW1doLDrfpWhplcW7BL+bO3/6S3pcDQ==} + engines: {node: '>= 0.4'} + to-gatsby-remark-plugin@0.1.0: resolution: {integrity: sha512-blmhJ/gIrytWnWLgPSRCkhCPeki6UBK2daa3k9mGahN7GjwHu8KrS7F70MvwlsG7IE794JLgwAdCbi4hU4faFQ==} @@ -7126,6 +7248,10 @@ packages: type-level-regexp@0.1.17: resolution: {integrity: sha512-wTk4DH3cxwk196uGLK/E9pE45aLfeKJacKmcEgEOA/q5dnPGNxXt0cfYdFxb57L+sEpf1oJH4Dnx/pnRcku9jg==} + typed-array-buffer@1.0.3: + resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} + engines: {node: '>= 0.4'} + typedarray@0.0.6: resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} @@ -7666,6 +7792,10 @@ packages: resolution: {integrity: sha512-f+Gy33Oa5Z14XY9679Zze+7VFhbsQfBFXodnU2x589l4kxGM9L5Y8zETTmcMR5pWOPQyRv4Z0lNax6xCO0NSlA==} engines: {node: '>=18'} + which-typed-array@1.1.19: + resolution: {integrity: sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==} + engines: {node: '>= 0.4'} + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -11224,6 +11354,8 @@ snapshots: '@babel/parser': 7.28.4 ast-kit: 2.1.2 + async-lock@1.4.1: {} + async-retry@1.3.3: dependencies: retry: 0.13.1 @@ -11242,6 +11374,10 @@ snapshots: postcss: 8.5.6 postcss-value-parser: 4.2.0 + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.1.0 + b4a@1.7.0: {} bail@2.0.2: {} @@ -11421,6 +11557,23 @@ snapshots: - bluebird optional: true + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bind@1.0.8: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + callsites@3.1.0: {} camelize@1.0.1: {} @@ -11504,6 +11657,8 @@ snapshots: dependencies: consola: 3.4.2 + clean-git-ref@2.0.1: {} + clean-regexp@1.0.0: dependencies: escape-string-regexp: 1.0.5 @@ -11876,6 +12031,12 @@ snapshots: bundle-name: 4.1.0 default-browser-id: 5.0.0 + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + define-lazy-prop@2.0.0: {} define-lazy-prop@3.0.0: {} @@ -11915,6 +12076,8 @@ snapshots: dfa@1.2.0: {} + diff3@0.0.3: {} + diff@8.0.2: {} docus@4.1.3(80d8a823f7cd370f8e6e15b6c0de15ca): @@ -12033,6 +12196,12 @@ snapshots: dotenv@17.2.2: {} + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + duplexer@0.1.2: {} eastasianwidth@0.2.0: {} @@ -12134,8 +12303,16 @@ snapshots: errx@0.1.0: {} + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + es-module-lexer@1.7.0: {} + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + esbuild@0.25.4: optionalDependencies: '@esbuild/aix-ppc64': 0.25.4 @@ -12549,6 +12726,10 @@ snapshots: unicode-properties: 1.4.1 unicode-trie: 2.0.0 + for-each@0.3.5: + dependencies: + is-callable: 1.2.7 + foreground-child@3.3.1: dependencies: cross-spawn: 7.0.6 @@ -12602,8 +12783,26 @@ snapshots: get-east-asian-width@1.4.0: {} + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + get-port-please@3.2.0: {} + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + get-stream@8.0.1: {} get-stream@9.0.1: @@ -12718,6 +12917,8 @@ snapshots: slash: 5.1.0 unicorn-magic: 0.3.0 + gopd@1.2.0: {} + graceful-fs@4.2.11: {} graphemer@1.4.0: {} @@ -12755,6 +12956,16 @@ snapshots: has-flag@4.0.0: {} + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + has-unicode@2.0.1: optional: true @@ -13120,6 +13331,8 @@ snapshots: dependencies: builtin-modules: 5.0.0 + is-callable@1.2.7: {} + is-core-module@2.16.1: dependencies: hasown: 2.0.2 @@ -13178,6 +13391,10 @@ snapshots: is-stream@4.0.1: {} + is-typed-array@1.1.15: + dependencies: + which-typed-array: 1.1.19 + is-unicode-supported@1.3.0: {} is-unicode-supported@2.1.0: {} @@ -13200,10 +13417,27 @@ snapshots: isarray@1.0.0: {} + isarray@2.0.5: {} + isexe@2.0.0: {} isexe@3.1.1: {} + isomorphic-git@1.33.1: + dependencies: + async-lock: 1.4.1 + clean-git-ref: 2.0.1 + crc-32: 1.2.2 + diff3: 0.0.3 + ignore: 5.3.2 + minimisted: 2.0.1 + pako: 1.0.11 + path-browserify: 1.0.1 + pify: 4.0.1 + readable-stream: 3.6.2 + sha.js: 2.4.12 + simple-get: 4.0.1 + issue-parser@7.0.1: dependencies: lodash.capitalize: 4.2.1 @@ -13507,6 +13741,8 @@ snapshots: marky@1.3.0: {} + math-intrinsics@1.1.0: {} + mdast-util-find-and-replace@3.0.2: dependencies: '@types/mdast': 4.0.4 @@ -13897,6 +14133,10 @@ snapshots: minimist@1.2.8: {} + minimisted@2.0.1: + dependencies: + minimist: 1.2.8 + minipass-collect@1.0.2: dependencies: minipass: 3.3.6 @@ -14672,6 +14912,8 @@ snapshots: pako@0.2.9: {} + pako@1.0.11: {} + parent-module@1.0.1: dependencies: callsites: 3.1.0 @@ -14779,6 +15021,8 @@ snapshots: picomatch@4.0.3: {} + pify@4.0.1: {} + pkg-types@1.3.1: dependencies: confbox: 0.1.8 @@ -14795,6 +15039,8 @@ snapshots: pluralize@8.0.0: {} + possible-typed-array-names@1.1.0: {} + postcss-calc@10.1.1(postcss@8.5.6): dependencies: postcss: 8.5.6 @@ -15508,8 +15754,23 @@ snapshots: set-blocking@2.0.0: optional: true + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + setprototypeof@1.2.0: {} + sha.js@2.4.12: + dependencies: + inherits: 2.0.4 + safe-buffer: 5.2.1 + to-buffer: 1.2.1 + sharp@0.32.6: dependencies: color: 4.2.3 @@ -15958,6 +16219,12 @@ snapshots: tinyspy@4.0.3: {} + to-buffer@1.2.1: + dependencies: + isarray: 2.0.5 + safe-buffer: 5.2.1 + typed-array-buffer: 1.0.3 + to-gatsby-remark-plugin@0.1.0: dependencies: to-vfile: 6.1.0 @@ -16011,6 +16278,12 @@ snapshots: type-level-regexp@0.1.17: {} + typed-array-buffer@1.0.3: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-typed-array: 1.1.15 + typedarray@0.0.6: {} typescript@5.9.2: {} @@ -16674,6 +16947,16 @@ snapshots: wheel-gestures@2.2.48: {} + which-typed-array@1.1.19: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + for-each: 0.3.5 + get-proto: 1.0.1 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + which@2.0.2: dependencies: isexe: 2.0.0 diff --git a/src/types/collection.ts b/src/types/collection.ts index a47e735a7..d9444e0fb 100644 --- a/src/types/collection.ts +++ b/src/types/collection.ts @@ -1,5 +1,6 @@ import type { Draft07 } from '../types/schema' import type { MarkdownRoot } from './content' +import type { GitBasicAuth, GitRefType, GitTokenAuth } from './git' import type { ContentStandardSchemaV1 } from './schema' export interface PageCollections {} @@ -12,11 +13,10 @@ export type CollectionSource = { prefix?: string exclude?: string[] repository?: string - authToken?: string - authBasic?: { - username: string - password: string - } + authBasic?: GitBasicAuth + authToken?: GitTokenAuth | GitTokenAuth['authToken'] + cloneRepository?: boolean + ref?: GitRefType cwd?: string } diff --git a/src/types/git.ts b/src/types/git.ts new file mode 100644 index 000000000..81ef60899 --- /dev/null +++ b/src/types/git.ts @@ -0,0 +1,15 @@ +export type GitRefType = { + branch?: string + commit?: string + tag?: string +} + +export type GitBasicAuth = { + username?: string + password: string +} + +export type GitTokenAuth = { + username?: string + authToken: string +} diff --git a/src/types/index.ts b/src/types/index.ts index 4b81f7a02..fa34160e5 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -9,3 +9,4 @@ export type * from './tree' export type * from './database' export type * from './preview' export type * from './schema' +export type * from './git' From a339a65f73ba571e3d341f9758f8726d9962b99c Mon Sep 17 00:00:00 2001 From: Duale Siad Date: Tue, 16 Sep 2025 15:17:47 +1000 Subject: [PATCH 2/7] docs: refactor headings --- docs/content/docs/2.collections/3.sources.md | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/docs/content/docs/2.collections/3.sources.md b/docs/content/docs/2.collections/3.sources.md index 2a7e1e4ae..589777dbf 100644 --- a/docs/content/docs/2.collections/3.sources.md +++ b/docs/content/docs/2.collections/3.sources.md @@ -119,7 +119,7 @@ export default defineContentConfig({ }) ``` -#### `ref` +### `ref` Declares the Git branch or tag the content source downloads from. ```js export default defineContentConfig({ @@ -141,15 +141,14 @@ export default defineContentConfig({ }) ``` -### `authentication` -Manages authentication for private repositories. Can either provide an object for basic authentication (user/password), or an authentication token. + +### `authBasic` +A basic authentication configuration for private repositories (e.g. Bitbucket username and password). ::warning{icon="i-lucide-shield-alert"} -Never commit authentication tokens or credentials directly in your code. Use environment variables or other secure methods to provide these values at runtime. +Never commit Git provider credentials directly in your code. Use environment variables or other secure methods to provide these values at runtime. :: -#### `username` & `password` -A basic authentication configuration for private repositories (e.g. Bitbucket username and password). ```js export default defineContentConfig({ collections: { @@ -167,8 +166,12 @@ export default defineContentConfig({ }) ``` -#### `authToken` +### `authToken` Authentication token for private repositories (e.g., Github Personal Access Token). Recommended for Github [Personal Access Tokens](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens), and Bitbucket [API Tokens](https://support.atlassian.com/bitbucket-cloud/docs/using-api-tokens/). + +::warning{icon="i-lucide-shield-alert"} +Never commit authentication tokens directly in your code. Use environment variables or other secure methods to provide these values at runtime. +:: ```js export default defineContentConfig({ collections: { From bdc9c77149183f2afef96526fb10f4a88174c782 Mon Sep 17 00:00:00 2001 From: Duale Siad Date: Tue, 16 Sep 2025 15:51:28 +1000 Subject: [PATCH 3/7] feat: get git hash from remote repo --- src/types/git.ts | 1 - src/utils/git.ts | 30 ++++++++++++++++++++++++++++++ test/unit/git/git.test.ts | 34 ++++++++++++++++++++++++++++++++++ 3 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 test/unit/git/git.test.ts diff --git a/src/types/git.ts b/src/types/git.ts index 81ef60899..bd9e0d1bd 100644 --- a/src/types/git.ts +++ b/src/types/git.ts @@ -1,6 +1,5 @@ export type GitRefType = { branch?: string - commit?: string tag?: string } diff --git a/src/utils/git.ts b/src/utils/git.ts index bbc1738ac..733ae93fc 100644 --- a/src/utils/git.ts +++ b/src/utils/git.ts @@ -6,6 +6,10 @@ import { join } from 'pathe' import { extract } from 'tar' import { readGitConfig } from 'pkg-types' import gitUrlParse from 'git-url-parse' +import git from 'isomorphic-git' +import gitHttp from 'isomorphic-git/http/node' + +import type { GitRefType } from '../types' export interface GitInfo { // Repository name @@ -133,6 +137,32 @@ export async function getLocalGitInfo(rootDir: string): Promise { + try { + const remote = await git.getRemoteInfo({ http: gitHttp, url }) + if (ref) { + if (ref.branch) { + const headRef = remote.refs.heads![ref.branch] + return headRef + } + + if (ref.tag) { + const tagsRef = remote.refs.tags![ref.tag] + return tagsRef + } + } + else { + // default to the HEAD ref provided by the server + const head = remote.HEAD!.replace('refs/heads/', '') + const headRef = remote.refs.heads![head] + return headRef + } + } + catch { + // ignore error + } +} + export function getGitEnv(): GitInfo { // https://github.com/unjs/std-env/issues/59 const envInfo = { diff --git a/test/unit/git/git.test.ts b/test/unit/git/git.test.ts new file mode 100644 index 000000000..bcffff13a --- /dev/null +++ b/test/unit/git/git.test.ts @@ -0,0 +1,34 @@ +import { expect, describe, test } from 'vitest' +import { getGitRemoteHash } from '../../../src/utils/git' + +describe('getGitRemoteHash', () => { + test('get remote hash with defaults', async () => { + const url = 'https://github.com/nuxt/content' + const hash = await getGitRemoteHash(url) + + expect(hash).not.toBeUndefined() + }) + + test('get remote hash with branch', async () => { + const url = 'https://github.com/nuxt/content' + const ref = { branch: 'v1' } + const hash = await getGitRemoteHash(url, ref) + + expect(hash).not.toBeUndefined() + }) + + test('get remote hash with git tag', async () => { + const url = 'https://github.com/nuxt/content' + const ref = { tag: 'v3.7.0' } + const hash = await getGitRemoteHash(url, ref) + + expect(hash).not.toBeUndefined() + }) + + test('do not get remote hash from nonexistent repo', async () => { + const url = 'https://github.com/fake/repo' + const hash = await getGitRemoteHash(url) + + expect(hash).toBeUndefined() + }) +}) From 8d4de634663c37ddfd8b1e77317113945af02cd4 Mon Sep 17 00:00:00 2001 From: Duale Siad Date: Tue, 16 Sep 2025 15:52:34 +1000 Subject: [PATCH 4/7] refactor: rename git hash test file --- test/unit/git/git.test.ts | 34 ---------------------------------- 1 file changed, 34 deletions(-) delete mode 100644 test/unit/git/git.test.ts diff --git a/test/unit/git/git.test.ts b/test/unit/git/git.test.ts deleted file mode 100644 index bcffff13a..000000000 --- a/test/unit/git/git.test.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { expect, describe, test } from 'vitest' -import { getGitRemoteHash } from '../../../src/utils/git' - -describe('getGitRemoteHash', () => { - test('get remote hash with defaults', async () => { - const url = 'https://github.com/nuxt/content' - const hash = await getGitRemoteHash(url) - - expect(hash).not.toBeUndefined() - }) - - test('get remote hash with branch', async () => { - const url = 'https://github.com/nuxt/content' - const ref = { branch: 'v1' } - const hash = await getGitRemoteHash(url, ref) - - expect(hash).not.toBeUndefined() - }) - - test('get remote hash with git tag', async () => { - const url = 'https://github.com/nuxt/content' - const ref = { tag: 'v3.7.0' } - const hash = await getGitRemoteHash(url, ref) - - expect(hash).not.toBeUndefined() - }) - - test('do not get remote hash from nonexistent repo', async () => { - const url = 'https://github.com/fake/repo' - const hash = await getGitRemoteHash(url) - - expect(hash).toBeUndefined() - }) -}) From ac96a30407977a9f58688a9ae92058ccbc85deda Mon Sep 17 00:00:00 2001 From: Duale Siad Date: Tue, 16 Sep 2025 15:54:22 +1000 Subject: [PATCH 5/7] refactor: rename git hash test file --- test/unit/git/hash.test.ts | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 test/unit/git/hash.test.ts diff --git a/test/unit/git/hash.test.ts b/test/unit/git/hash.test.ts new file mode 100644 index 000000000..bcffff13a --- /dev/null +++ b/test/unit/git/hash.test.ts @@ -0,0 +1,34 @@ +import { expect, describe, test } from 'vitest' +import { getGitRemoteHash } from '../../../src/utils/git' + +describe('getGitRemoteHash', () => { + test('get remote hash with defaults', async () => { + const url = 'https://github.com/nuxt/content' + const hash = await getGitRemoteHash(url) + + expect(hash).not.toBeUndefined() + }) + + test('get remote hash with branch', async () => { + const url = 'https://github.com/nuxt/content' + const ref = { branch: 'v1' } + const hash = await getGitRemoteHash(url, ref) + + expect(hash).not.toBeUndefined() + }) + + test('get remote hash with git tag', async () => { + const url = 'https://github.com/nuxt/content' + const ref = { tag: 'v3.7.0' } + const hash = await getGitRemoteHash(url, ref) + + expect(hash).not.toBeUndefined() + }) + + test('do not get remote hash from nonexistent repo', async () => { + const url = 'https://github.com/fake/repo' + const hash = await getGitRemoteHash(url) + + expect(hash).toBeUndefined() + }) +}) From 43f69538bd87be8ca3688da0b95d0101da1378b3 Mon Sep 17 00:00:00 2001 From: Duale Siad Date: Tue, 16 Sep 2025 17:56:10 +1000 Subject: [PATCH 6/7] feat: shallow clone git repo --- src/types/collection.ts | 2 +- src/types/git.ts | 4 +-- src/utils/collection.ts | 6 ++++- src/utils/git.ts | 58 ++++++++++++++++++++++++++++++++++++++--- src/utils/source.ts | 27 +++++++++++++++++-- 5 files changed, 88 insertions(+), 9 deletions(-) diff --git a/src/types/collection.ts b/src/types/collection.ts index d9444e0fb..1ae741d81 100644 --- a/src/types/collection.ts +++ b/src/types/collection.ts @@ -14,7 +14,7 @@ export type CollectionSource = { exclude?: string[] repository?: string authBasic?: GitBasicAuth - authToken?: GitTokenAuth | GitTokenAuth['authToken'] + authToken?: GitTokenAuth | GitTokenAuth['token'] cloneRepository?: boolean ref?: GitRefType cwd?: string diff --git a/src/types/git.ts b/src/types/git.ts index bd9e0d1bd..1a85d65f4 100644 --- a/src/types/git.ts +++ b/src/types/git.ts @@ -5,10 +5,10 @@ export type GitRefType = { export type GitBasicAuth = { username?: string - password: string + password?: string } export type GitTokenAuth = { username?: string - authToken: string + token?: string } diff --git a/src/utils/collection.ts b/src/utils/collection.ts index 231d055b2..1a7780c81 100644 --- a/src/utils/collection.ts +++ b/src/utils/collection.ts @@ -2,7 +2,7 @@ import { hash } from 'ohash' import type { Collection, ResolvedCollection, CollectionSource, DefinedCollection, ResolvedCollectionSource, CustomCollectionSource, ResolvedCustomCollectionSource } from '../types/collection' import { getOrderedSchemaKeys, describeProperty, getCollectionFieldsTypes } from '../runtime/internal/schema' import type { Draft07, ParsedContentFile } from '../types' -import { defineLocalSource, defineGitHubSource, defineBitbucketSource } from './source' +import { defineLocalSource, defineGitHubSource, defineBitbucketSource, defineGitSource } from './source' import { emptyStandardSchema, mergeStandardSchema, metaStandardSchema, pageStandardSchema, infoStandardSchema, detectSchemaVendor, replaceComponentSchemas } from './schema' import { logger } from './dev' import nuxtContentContext from './context' @@ -102,6 +102,10 @@ function resolveSource(source: string | CollectionSource | CollectionSource[] | } if (source.repository) { + if (source.cloneRepository === true) { + return defineGitSource(source) + } + if (source.repository.startsWith('https://bitbucket.org/')) { return defineBitbucketSource(source) } diff --git a/src/utils/git.ts b/src/utils/git.ts index 733ae93fc..600d5fa5b 100644 --- a/src/utils/git.ts +++ b/src/utils/git.ts @@ -1,4 +1,5 @@ -import { createWriteStream } from 'node:fs' +import fs from 'node:fs' + import { mkdir, readFile, rm, writeFile } from 'node:fs/promises' import { pipeline } from 'node:stream' import { promisify } from 'node:util' @@ -9,7 +10,7 @@ import gitUrlParse from 'git-url-parse' import git from 'isomorphic-git' import gitHttp from 'isomorphic-git/http/node' -import type { GitRefType } from '../types' +import type { GitBasicAuth, GitRefType, GitTokenAuth } from '../types' export interface GitInfo { // Repository name @@ -42,7 +43,7 @@ export async function downloadRepository(url: string, cwd: string, { headers }: try { const response = await fetch(url, { headers }) - const stream = createWriteStream(tarFile) + const stream = fs.createWriteStream(tarFile) await promisify(pipeline)(response.body as unknown as ReadableStream[], stream) await extract({ @@ -66,6 +67,57 @@ export async function downloadRepository(url: string, cwd: string, { headers }: } } +export async function downloadGitRepository(url: string, cwd: string, auth?: GitBasicAuth | GitTokenAuth | string, ref?: GitRefType) { + const cacheFile = join(cwd, '.content.cache.json') + const cache = await readFile(cacheFile, 'utf8').then(d => JSON.parse(d)).catch((): null => null) + const hash = await getGitRemoteHash(url, ref) + + if (cache) { + // Directory exists, skip download + const hash = await getGitRemoteHash(url, ref) + if (hash === cache.hash) { + await writeFile(cacheFile, JSON.stringify({ + ...cache, + updatedAt: new Date().toISOString(), + }, null, 2)) + return + } + } + + await mkdir(cwd, { recursive: true }) + + let formattedRef: string | undefined + if (ref) { + if (ref.branch) formattedRef = `refs/heads/${ref.branch}` + if (ref.tag) formattedRef = `refs/tags/${ref.tag}` + } + + const authUrl = new URL(url) + if (typeof (auth) === 'string') { + authUrl.password = auth + } + if (typeof (auth) === 'object') { + if (auth.username) authUrl.username = auth.username + + if ('token' in auth) { + authUrl.password = auth.token! + } + + if ('password' in auth) { + authUrl.password = auth.password! + } + } + + await git.clone({ fs, http: gitHttp, dir: cwd, url: authUrl.toString(), depth: 1, singleBranch: true, ref: formattedRef }) + + await writeFile(cacheFile, JSON.stringify({ + url: url, + hash: hash, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }, null, 2)) +} + export function parseGitHubUrl(url: string) { const regex = /https:\/\/github\.com\/([^/]+)\/([^/]+)(?:\/tree\/([^/]+))?(?:\/(.+))?/ const match = url.match(regex) diff --git a/src/utils/source.ts b/src/utils/source.ts index 41c4ec01b..5a694f970 100644 --- a/src/utils/source.ts +++ b/src/utils/source.ts @@ -3,8 +3,9 @@ import { join, normalize } from 'pathe' import { withLeadingSlash, withoutTrailingSlash } from 'ufo' import { glob } from 'tinyglobby' import type { CollectionSource, ResolvedCollectionSource } from '../types/collection' -import { downloadRepository, parseBitBucketUrl, parseGitHubUrl } from './git' +import { downloadGitRepository, downloadRepository, parseBitBucketUrl, parseGitHubUrl } from './git' import { logger } from './dev' +import gitUrlParse from 'git-url-parse' export function getExcludedSourcePaths(source: CollectionSource) { return [ @@ -55,9 +56,12 @@ export function defineGitHubSource(source: CollectionSource): ResolvedCollection resolvedSource.cwd = join(rootDir, '.data', 'content', `github-${org}-${repo}-${branch}`) let headers: Record = {} - if (resolvedSource.authToken) { + if (typeof resolvedSource.authToken === 'string') { headers = { Authorization: `Bearer ${resolvedSource.authToken}` } } + if (typeof resolvedSource.authToken === 'object') { + headers = { Authorization: `Bearer ${resolvedSource.authToken.token}` } + } const url = headers.Authorization ? `https://api.github.com/repos/${org}/${repo}/tarball/${branch}` @@ -70,6 +74,25 @@ export function defineGitHubSource(source: CollectionSource): ResolvedCollection return resolvedSource } +export function defineGitSource(source: CollectionSource): ResolvedCollectionSource { + const resolvedSource = defineLocalSource(source) + resolvedSource.prepare = async ({ rootDir }) => { + const repository = source?.repository && gitUrlParse(source.repository!) + if (repository) { + const { source: gitSource, name } = repository + resolvedSource.cwd = join(rootDir, '.data', 'content', `${gitSource}-${name}-clone`) + + if (resolvedSource.authBasic) { + await downloadGitRepository(source.repository!, resolvedSource.cwd!, resolvedSource.authBasic, source.ref) + } + else { + await downloadGitRepository(source.repository!, resolvedSource.cwd!, resolvedSource.authToken, source.ref) + } + } + } + return resolvedSource +} + export function defineBitbucketSource( source: CollectionSource, ): ResolvedCollectionSource { From f597e2e72fe04c912d47b70a6d23dc610f01424a Mon Sep 17 00:00:00 2001 From: Duale Siad Date: Tue, 16 Sep 2025 18:11:23 +1000 Subject: [PATCH 7/7] docs: remove commit from docs --- docs/content/docs/2.collections/3.sources.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/content/docs/2.collections/3.sources.md b/docs/content/docs/2.collections/3.sources.md index 589777dbf..24f729447 100644 --- a/docs/content/docs/2.collections/3.sources.md +++ b/docs/content/docs/2.collections/3.sources.md @@ -133,7 +133,6 @@ export default defineContentConfig({ ref: { branch: "main" // tag: "v4.0.0" - // commit: "4e5ade" } }, })