Skip to content

Commit b595a47

Browse files
committed
feat: add cloneProject + stackblitz-clone
1 parent 2a37540 commit b595a47

File tree

11 files changed

+377
-22
lines changed

11 files changed

+377
-22
lines changed

README.md

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,17 @@ Download: https://stackblitz.zip/edit/nuxt-starter-k7spa3r4
3636
## CLI
3737

3838
```bash
39-
# download a project
39+
# download a project as a zip file
4040
npx stackblitz-zip https://stackblitz.com/edit/nuxt-starter-k7spa3r4
4141

42-
# specify output path
42+
# specify output path for zip
4343
npx stackblitz-zip https://stackblitz.com/edit/nuxt-starter-k7spa3r4 my-project.zip
44+
45+
# clone project to a directory
46+
npx stackblitz-clone https://stackblitz.com/edit/nuxt-starter-k7spa3r4
47+
48+
# clone to a specific directory
49+
npx stackblitz-clone https://stackblitz.com/edit/nuxt-starter-k7spa3r4 ./my-project
4450
```
4551

4652
## Programmatic Usage
@@ -54,9 +60,9 @@ npm install stackblitz-zip
5460
### Node.js
5561

5662
```typescript
57-
import { downloadToFile, parseUrl } from 'stackblitz-zip'
63+
import { cloneProject, downloadToFile, parseUrl } from 'stackblitz-zip'
5864

59-
// download from a URL
65+
// download from a URL as a zip file
6066
const projectId = parseUrl('https://stackblitz.com/edit/nuxt-starter-k7spa3r4')
6167
await downloadToFile({ projectId, outputPath: './output.zip' })
6268

@@ -65,6 +71,12 @@ await downloadToFile({
6571
projectId: 'nuxt-starter-k7spa3r4',
6672
outputPath: './my-project.zip',
6773
})
74+
75+
// clone project to a directory
76+
await cloneProject({
77+
projectId: 'nuxt-starter-k7spa3r4',
78+
outputPath: './my-project',
79+
})
6880
```
6981

7082
### Web APIs
@@ -140,6 +152,20 @@ Downloads a StackBlitz project and returns the path to the created zip file.
140152
- `maxTotalSize` (number, optional): Maximum total project size in bytes. Defaults to `104857600` (100MB)
141153
- `verbose` (boolean, optional): Enable console logging. Defaults to `false`
142154

155+
### `cloneProject(options: CloneOptions): Promise<string>`
156+
157+
Clones a StackBlitz project by creating all files in a target directory.
158+
159+
**Options:**
160+
- `projectId` (string, required): The StackBlitz project ID
161+
- `outputPath` (string, optional): Path where the project directory should be created. Defaults to `<project-id>/`
162+
- `timeout` (number, optional): Timeout in milliseconds for loading the project. Defaults to `30000` (30 seconds)
163+
- `maxFileSize` (number, optional): Maximum size per file in bytes. Defaults to `10485760` (10MB)
164+
- `maxTotalSize` (number, optional): Maximum total project size in bytes. Defaults to `104857600` (100MB)
165+
- `verbose` (boolean, optional): Enable console logging. Defaults to `false`
166+
167+
**Returns:** Path to the created directory
168+
143169
### `downloadToResponse(options): Promise<Response>`
144170

145171
Downloads a StackBlitz project and returns it as a Web Response (universal).

app/index.html

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,10 @@ <h3>CLI</h3>
7979
</button>
8080
</div>
8181
<pre><code id="cli-code"><span class="comment"># download to my-project.zip</span>
82+
npx stackblitz-zip https://stackblitz.com/edit/nuxtblitz my-project.zip
8283

83-
npx stackblitz-zip https://stackblitz.com/edit/nuxtblitz my-project.zip</code></pre>
84+
<span class="comment"># or clone to a directory</span>
85+
npx stackblitz-clone https://stackblitz.com/edit/nuxtblitz my-project</code></pre>
8486
</div>
8587

8688
<div class="code-sample">
@@ -96,9 +98,11 @@ <h3>Programmatic</h3>
9698
<span>copy</span>
9799
</button>
98100
</div>
99-
<pre><code id="programmatic-code"><span class="keyword">import</span> { <span class="function">downloadProject</span> } <span class="keyword">from</span> <span class="string">'stackblitz-zip'</span>
101+
<pre><code id="programmatic-code"><span class="keyword">import</span> { <span class="function">downloadToFile</span>, <span class="function">cloneProject</span> } <span class="keyword">from</span> <span class="string">'stackblitz-zip'</span>
100102

101-
<span class="keyword">await</span> <span class="function">downloadProject</span>(<span class="string">'nuxtblitz'</span>, <span class="string">'output.zip'</span>)</code></pre>
103+
<span class="keyword">await</span> <span class="function">downloadToFile</span>({ <span class="string">projectId</span>: <span class="string">'nuxtblitz'</span>, <span class="string">outputPath</span>: <span class="string">'out.zip'</span> })
104+
105+
<span class="keyword">await</span> <span class="function">cloneProject</span>({ <span class="string">projectId</span>: <span class="string">'nuxtblitz'</span>, <span class="string">outputPath</span>: <span class="string">'./out'</span> })</code></pre>
102106
</div>
103107
</div>
104108
</div>

app/server.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ export default defineHandler(async (event) => {
66
if (pathname === '/')
77
return // render index.html
88

9+
// Check if this is a clone request (just redirect to regular download for now)
10+
if (pathname.startsWith('/clone/')) {
11+
const cleanPath = pathname.replace(/^\/clone\//, '')
12+
return Response.redirect(`https://stackblitz.zip/${cleanPath}`, 302)
13+
}
14+
915
// Convert stackblitz.zip URL to stackblitz.com URL
1016
const stackblitzUrl = `https://stackblitz.com/${pathname.replace(/^\/|\.zip$/g, '')}`
1117

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@
2020
}
2121
},
2222
"bin": {
23-
"stackblitz-zip": "./dist/cli.mjs"
23+
"stackblitz-zip": "./dist/cli/zip.mjs",
24+
"stackblitz-clone": "./dist/cli/clone.mjs"
2425
},
2526
"files": [
2627
"dist"
@@ -32,6 +33,7 @@
3233
"prepare": "simple-git-hooks",
3334
"prepack": "pnpm build",
3435
"prepublishOnly": "pnpm lint && pnpm test",
36+
"publish:alias": "node scripts/publish-alias.js",
3537
"release": "bumpp",
3638
"test": "pnpm test:unit && pnpm test:types",
3739
"test:unit": "vitest",

scripts/publish-alias.js

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
#!/usr/bin/env node
2+
3+
/**
4+
* Publish the package as 'stackblitz-clone' alias
5+
* Run this after publishing 'stackblitz-zip'
6+
*/
7+
8+
import { execSync } from 'node:child_process'
9+
import { readFile, writeFile } from 'node:fs/promises'
10+
import { resolve } from 'node:path'
11+
import process from 'node:process'
12+
13+
const packageJsonPath = resolve(process.cwd(), 'package.json')
14+
15+
async function main() {
16+
console.log('📦 Publishing as stackblitz-clone...\n')
17+
18+
// Read current package.json
19+
const originalContent = await readFile(packageJsonPath, 'utf-8')
20+
const pkg = JSON.parse(originalContent)
21+
const originalName = pkg.name
22+
23+
if (originalName !== 'stackblitz-zip') {
24+
console.error('❌ Error: Expected package name to be "stackblitz-zip"')
25+
process.exit(1)
26+
}
27+
28+
try {
29+
// Update package name
30+
pkg.name = 'stackblitz-clone'
31+
pkg.description = 'Clone StackBlitz projects to local directories'
32+
await writeFile(packageJsonPath, `${JSON.stringify(pkg, null, 2)}\n`)
33+
console.log('✓ Updated package.json name to "stackblitz-clone"')
34+
35+
// Publish
36+
console.log('\n📤 Publishing to npm...')
37+
execSync('pnpm publish --no-git-checks', { stdio: 'inherit' })
38+
console.log('\n✓ Published stackblitz-clone successfully!')
39+
}
40+
catch (error) {
41+
console.error('\n❌ Error during publishing:', error)
42+
}
43+
finally {
44+
// Restore original package.json
45+
await writeFile(packageJsonPath, originalContent)
46+
console.log('✓ Restored package.json to "stackblitz-zip"')
47+
}
48+
}
49+
50+
main().catch(console.error)

src/cli.ts renamed to src/cli/clone.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,25 @@
11
#!/usr/bin/env node
22

33
import process from 'node:process'
4-
import { downloadToFile, parseUrl } from './download'
4+
import { cloneProject, parseUrl } from '../download'
55

66
async function main() {
77
const args = process.argv.slice(2)
88

99
if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
1010
console.log(`
11-
StackBlitz Project Downloader
11+
StackBlitz Project Clone Tool
1212
1313
Usage:
14-
stackblitz-zip <url> [output-path]
14+
stackblitz-clone <url> [output-path]
1515
1616
Arguments:
1717
url StackBlitz project URL (e.g., https://stackblitz.com/edit/nuxt-starter-k7spa3r4)
18-
output-path Optional path for the output zip file (defaults to <project-id>.zip)
18+
output-path Optional path for the output directory (defaults to <project-id>/)
1919
2020
Examples:
21-
stackblitz-zip https://stackblitz.com/edit/nuxt-starter-k7spa3r4
22-
stackblitz-zip https://stackblitz.com/edit/nuxt-starter-k7spa3r4 my-project.zip
21+
stackblitz-clone https://stackblitz.com/edit/nuxt-starter-k7spa3r4
22+
stackblitz-clone https://stackblitz.com/edit/nuxt-starter-k7spa3r4 ./my-project
2323
`)
2424
process.exit(0)
2525
}
@@ -29,7 +29,7 @@ Examples:
2929

3030
try {
3131
const projectId = parseUrl(url)
32-
await downloadToFile({ projectId, outputPath, verbose: true })
32+
await cloneProject({ projectId, outputPath, verbose: true })
3333
}
3434
catch (error) {
3535
console.error('❌ Error:', error instanceof Error ? error.message : error)

src/cli/zip.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
#!/usr/bin/env node
2+
3+
import process from 'node:process'
4+
import { cloneProject, downloadToFile, parseUrl } from '../download'
5+
6+
async function main() {
7+
const args = process.argv.slice(2)
8+
9+
if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
10+
console.log(`
11+
StackBlitz Project Downloader
12+
13+
Usage:
14+
stackblitz-zip <url> [output-path] Download as zip file
15+
stackblitz-zip clone <url> [output-path] Clone to directory
16+
17+
Arguments:
18+
url StackBlitz project URL (e.g., https://stackblitz.com/edit/nuxt-starter-k7spa3r4)
19+
output-path Optional path for the output (defaults to <project-id>.zip or <project-id>/)
20+
21+
Examples:
22+
stackblitz-zip https://stackblitz.com/edit/nuxt-starter-k7spa3r4
23+
stackblitz-zip https://stackblitz.com/edit/nuxt-starter-k7spa3r4 my-project.zip
24+
stackblitz-zip clone https://stackblitz.com/edit/nuxt-starter-k7spa3r4
25+
stackblitz-zip clone https://stackblitz.com/edit/nuxt-starter-k7spa3r4 ./my-project
26+
`)
27+
process.exit(0)
28+
}
29+
30+
const isClone = args[0] === 'clone'
31+
const url = isClone ? args[1]! : args[0]!
32+
const outputPath = isClone ? args[2] : args[1]
33+
34+
try {
35+
const projectId = parseUrl(url)
36+
37+
if (isClone) {
38+
await cloneProject({ projectId, outputPath, verbose: true })
39+
}
40+
else {
41+
await downloadToFile({ projectId, outputPath, verbose: true })
42+
}
43+
}
44+
catch (error) {
45+
console.error('❌ Error:', error instanceof Error ? error.message : error)
46+
process.exit(1)
47+
}
48+
}
49+
50+
main()

0 commit comments

Comments
 (0)