@webflow/webflow-cli 1.21.0 → 1.22.0-next.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cloud-scaffolds/__tests__/fetcher.test.ts +249 -0
- package/dist/cloud-scaffolds/__tests__/registry.test.ts +134 -0
- package/dist/cloud-scaffolds/fetcher.ts +205 -0
- package/dist/cloud-scaffolds/index.ts +19 -0
- package/dist/cloud-scaffolds/registry.ts +132 -0
- package/dist/cloud-scaffolds/types.ts +68 -0
- package/dist/index.js +98 -82
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/cloud-scaffolds/astro/README.md +0 -76
- package/dist/cloud-scaffolds/astro/astro.config.mjs +0 -27
- package/dist/cloud-scaffolds/astro/gitignore +0 -31
- package/dist/cloud-scaffolds/astro/package.json +0 -26
- package/dist/cloud-scaffolds/astro/public/favicon.svg +0 -9
- package/dist/cloud-scaffolds/astro/src/assets/astro.svg +0 -1
- package/dist/cloud-scaffolds/astro/src/assets/background.svg +0 -1
- package/dist/cloud-scaffolds/astro/src/components/Welcome.astro +0 -210
- package/dist/cloud-scaffolds/astro/src/env.d.ts +0 -12
- package/dist/cloud-scaffolds/astro/src/layouts/Layout.astro +0 -32
- package/dist/cloud-scaffolds/astro/src/pages/index.astro +0 -73
- package/dist/cloud-scaffolds/astro/tsconfig.json +0 -17
- package/dist/cloud-scaffolds/astro/webflow.json +0 -13
- package/dist/cloud-scaffolds/astro/worker-configuration.d.ts +0 -4
- package/dist/cloud-scaffolds/astro/wrangler.json +0 -14
- package/dist/cloud-scaffolds/nextjs/README.md +0 -47
- package/dist/cloud-scaffolds/nextjs/cloudflare-env.d.ts +0 -5
- package/dist/cloud-scaffolds/nextjs/eslint.config.mjs +0 -22
- package/dist/cloud-scaffolds/nextjs/gitignore +0 -47
- package/dist/cloud-scaffolds/nextjs/next.config.ts +0 -10
- package/dist/cloud-scaffolds/nextjs/open-next.config.ts +0 -9
- package/dist/cloud-scaffolds/nextjs/package.json +0 -34
- package/dist/cloud-scaffolds/nextjs/postcss.config.mjs +0 -5
- package/dist/cloud-scaffolds/nextjs/public/file.svg +0 -1
- package/dist/cloud-scaffolds/nextjs/public/globe.svg +0 -1
- package/dist/cloud-scaffolds/nextjs/public/next.svg +0 -1
- package/dist/cloud-scaffolds/nextjs/public/window.svg +0 -1
- package/dist/cloud-scaffolds/nextjs/src/app/favicon.ico +0 -0
- package/dist/cloud-scaffolds/nextjs/src/app/globals.css +0 -31
- package/dist/cloud-scaffolds/nextjs/src/app/layout.tsx +0 -44
- package/dist/cloud-scaffolds/nextjs/src/app/page.css +0 -45
- package/dist/cloud-scaffolds/nextjs/src/app/page.tsx +0 -30
- package/dist/cloud-scaffolds/nextjs/src/webflow.d.ts +0 -14
- package/dist/cloud-scaffolds/nextjs/tsconfig.json +0 -30
- package/dist/cloud-scaffolds/nextjs/webflow.json +0 -13
- package/dist/cloud-scaffolds/nextjs/wrangler.json +0 -14
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import os from "os";
|
|
4
|
+
import * as tar from "tar";
|
|
5
|
+
import {
|
|
6
|
+
fetchScaffoldContent,
|
|
7
|
+
getScaffoldContent,
|
|
8
|
+
getScaffoldFromLocal,
|
|
9
|
+
} from "../fetcher";
|
|
10
|
+
import * as registry from "../registry";
|
|
11
|
+
import { ScaffoldError } from "../types";
|
|
12
|
+
|
|
13
|
+
jest.mock("../registry");
|
|
14
|
+
jest.mock("../../../utils/logger", () => ({
|
|
15
|
+
__esModule: true,
|
|
16
|
+
default: {
|
|
17
|
+
spinner: jest.fn(() => ({
|
|
18
|
+
succeed: jest.fn(),
|
|
19
|
+
fail: jest.fn(),
|
|
20
|
+
warn: jest.fn(),
|
|
21
|
+
info: jest.fn(),
|
|
22
|
+
})),
|
|
23
|
+
},
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
const mockGetFrameworkDefinition =
|
|
27
|
+
registry.getFrameworkDefinition as jest.MockedFunction<
|
|
28
|
+
typeof registry.getFrameworkDefinition
|
|
29
|
+
>;
|
|
30
|
+
const mockToMinimalScaffoldId =
|
|
31
|
+
registry.toMinimalScaffoldId as jest.MockedFunction<
|
|
32
|
+
typeof registry.toMinimalScaffoldId
|
|
33
|
+
>;
|
|
34
|
+
|
|
35
|
+
describe("scaffold fetcher", () => {
|
|
36
|
+
let tempDir: string;
|
|
37
|
+
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "fetcher-test-"));
|
|
40
|
+
jest.clearAllMocks();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
afterEach(() => {
|
|
44
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe("getScaffoldFromLocal", () => {
|
|
48
|
+
it("returns ScaffoldContent from local directory", async () => {
|
|
49
|
+
const scaffoldDir = path.join(tempDir, "scaffold");
|
|
50
|
+
fs.mkdirSync(scaffoldDir, { recursive: true });
|
|
51
|
+
fs.writeFileSync(path.join(scaffoldDir, "package.json"), "{}");
|
|
52
|
+
fs.mkdirSync(path.join(scaffoldDir, "src"), { recursive: true });
|
|
53
|
+
fs.writeFileSync(path.join(scaffoldDir, "src", "index.ts"), "// test");
|
|
54
|
+
|
|
55
|
+
mockGetFrameworkDefinition.mockReturnValue({
|
|
56
|
+
id: "test-minimal",
|
|
57
|
+
name: "Test",
|
|
58
|
+
source: {
|
|
59
|
+
type: "local",
|
|
60
|
+
path: path.resolve(scaffoldDir),
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const result = await getScaffoldFromLocal("test-minimal");
|
|
65
|
+
|
|
66
|
+
expect(result.scaffoldId).toBe("test-minimal");
|
|
67
|
+
expect(result.extractionRoot).toBe(scaffoldDir);
|
|
68
|
+
expect(result.files).toHaveLength(2);
|
|
69
|
+
expect(result.files).toContain(path.join(scaffoldDir, "package.json"));
|
|
70
|
+
expect(result.files).toContain(path.join(scaffoldDir, "src", "index.ts"));
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("throws ScaffoldError when source is not local", async () => {
|
|
74
|
+
mockGetFrameworkDefinition.mockReturnValue({
|
|
75
|
+
id: "astro-minimal",
|
|
76
|
+
name: "Astro",
|
|
77
|
+
source: {
|
|
78
|
+
type: "github",
|
|
79
|
+
repo: "Webflow-Examples/hello-world-astro",
|
|
80
|
+
ref: "main",
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
await expect(getScaffoldFromLocal("astro-minimal")).rejects.toThrow(
|
|
85
|
+
ScaffoldError
|
|
86
|
+
);
|
|
87
|
+
await expect(getScaffoldFromLocal("astro-minimal")).rejects.toThrow(
|
|
88
|
+
"does not have a local source"
|
|
89
|
+
);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("throws ScaffoldError when directory does not exist", async () => {
|
|
93
|
+
const missingPath = path.resolve(path.join(tempDir, "nonexistent"));
|
|
94
|
+
|
|
95
|
+
mockGetFrameworkDefinition.mockReturnValue({
|
|
96
|
+
id: "test-minimal",
|
|
97
|
+
name: "Test",
|
|
98
|
+
source: { type: "local", path: missingPath },
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
await expect(getScaffoldFromLocal("test-minimal")).rejects.toThrow(
|
|
102
|
+
ScaffoldError
|
|
103
|
+
);
|
|
104
|
+
await expect(getScaffoldFromLocal("test-minimal")).rejects.toThrow(
|
|
105
|
+
"Scaffold directory not found"
|
|
106
|
+
);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe("fetchScaffoldContent", () => {
|
|
111
|
+
it("dispatches to getScaffoldFromLocal for local source", async () => {
|
|
112
|
+
const scaffoldDir = path.join(tempDir, "scaffold");
|
|
113
|
+
fs.mkdirSync(scaffoldDir, { recursive: true });
|
|
114
|
+
fs.writeFileSync(path.join(scaffoldDir, "package.json"), "{}");
|
|
115
|
+
|
|
116
|
+
mockGetFrameworkDefinition.mockReturnValue({
|
|
117
|
+
id: "test-minimal",
|
|
118
|
+
name: "Test",
|
|
119
|
+
source: { type: "local", path: path.resolve(scaffoldDir) },
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const result = await fetchScaffoldContent("test-minimal");
|
|
123
|
+
|
|
124
|
+
expect(result.scaffoldId).toBe("test-minimal");
|
|
125
|
+
expect(result.files).toHaveLength(1);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("fetches from GitHub and returns ScaffoldContent", async () => {
|
|
129
|
+
// Create a minimal tarball matching GitHub structure: owner-repo-sha/
|
|
130
|
+
const topLevelDir = "Webflow-Examples-hello-world-astro-abc123";
|
|
131
|
+
const tarballDir = path.join(tempDir, "tarball-src");
|
|
132
|
+
fs.mkdirSync(path.join(tarballDir, topLevelDir), { recursive: true });
|
|
133
|
+
fs.writeFileSync(
|
|
134
|
+
path.join(tarballDir, topLevelDir, "package.json"),
|
|
135
|
+
'{"name":"test"}'
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
const tarballPath = path.join(tempDir, "archive.tar.gz");
|
|
139
|
+
await tar.create(
|
|
140
|
+
{
|
|
141
|
+
gzip: true,
|
|
142
|
+
file: tarballPath,
|
|
143
|
+
cwd: tarballDir,
|
|
144
|
+
},
|
|
145
|
+
[topLevelDir]
|
|
146
|
+
);
|
|
147
|
+
const tarballBuffer = fs.readFileSync(tarballPath);
|
|
148
|
+
|
|
149
|
+
const originalFetch = globalThis.fetch;
|
|
150
|
+
const arrayBuffer = tarballBuffer.buffer.slice(
|
|
151
|
+
tarballBuffer.byteOffset,
|
|
152
|
+
tarballBuffer.byteOffset + tarballBuffer.byteLength
|
|
153
|
+
);
|
|
154
|
+
globalThis.fetch = jest.fn().mockResolvedValue({
|
|
155
|
+
ok: true,
|
|
156
|
+
arrayBuffer: () => Promise.resolve(arrayBuffer),
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
mockGetFrameworkDefinition.mockReturnValue({
|
|
160
|
+
id: "astro-minimal",
|
|
161
|
+
name: "Astro",
|
|
162
|
+
source: {
|
|
163
|
+
type: "github",
|
|
164
|
+
repo: "Webflow-Examples/hello-world-astro",
|
|
165
|
+
ref: "main",
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
const result = await fetchScaffoldContent("astro-minimal");
|
|
171
|
+
|
|
172
|
+
expect(result.scaffoldId).toBe("astro-minimal");
|
|
173
|
+
expect(result.files).toHaveLength(1);
|
|
174
|
+
expect(result.files[0]).toContain("package.json");
|
|
175
|
+
expect(result.extractionRoot).toContain(topLevelDir);
|
|
176
|
+
} finally {
|
|
177
|
+
globalThis.fetch = originalFetch;
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("throws ScaffoldError when GitHub fetch fails", async () => {
|
|
182
|
+
const originalFetch = globalThis.fetch;
|
|
183
|
+
globalThis.fetch = jest.fn().mockResolvedValue({
|
|
184
|
+
ok: false,
|
|
185
|
+
status: 404,
|
|
186
|
+
statusText: "Not Found",
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
mockGetFrameworkDefinition.mockReturnValue({
|
|
190
|
+
id: "astro-minimal",
|
|
191
|
+
name: "Astro",
|
|
192
|
+
source: {
|
|
193
|
+
type: "github",
|
|
194
|
+
repo: "Webflow-Examples/hello-world-astro",
|
|
195
|
+
ref: "main",
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
await expect(fetchScaffoldContent("astro-minimal")).rejects.toThrow(
|
|
201
|
+
ScaffoldError
|
|
202
|
+
);
|
|
203
|
+
await expect(fetchScaffoldContent("astro-minimal")).rejects.toThrow(
|
|
204
|
+
"Failed to fetch scaffold from GitHub"
|
|
205
|
+
);
|
|
206
|
+
} finally {
|
|
207
|
+
globalThis.fetch = originalFetch;
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
describe("getScaffoldContent", () => {
|
|
213
|
+
it("resolves framework name to minimal ID and fetches", async () => {
|
|
214
|
+
const scaffoldDir = path.join(tempDir, "scaffold");
|
|
215
|
+
fs.mkdirSync(scaffoldDir, { recursive: true });
|
|
216
|
+
fs.writeFileSync(path.join(scaffoldDir, "package.json"), "{}");
|
|
217
|
+
|
|
218
|
+
mockToMinimalScaffoldId.mockReturnValue("astro-minimal");
|
|
219
|
+
mockGetFrameworkDefinition.mockReturnValue({
|
|
220
|
+
id: "astro-minimal",
|
|
221
|
+
name: "Astro",
|
|
222
|
+
source: { type: "local", path: path.resolve(scaffoldDir) },
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
const result = await getScaffoldContent("astro");
|
|
226
|
+
|
|
227
|
+
expect(mockToMinimalScaffoldId).toHaveBeenCalledWith("astro");
|
|
228
|
+
expect(result.scaffoldId).toBe("astro-minimal");
|
|
229
|
+
expect(result.files).toHaveLength(1);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("passes through full scaffold ID unchanged", async () => {
|
|
233
|
+
const scaffoldDir = path.join(tempDir, "scaffold");
|
|
234
|
+
fs.mkdirSync(scaffoldDir, { recursive: true });
|
|
235
|
+
fs.writeFileSync(path.join(scaffoldDir, "package.json"), "{}");
|
|
236
|
+
|
|
237
|
+
mockGetFrameworkDefinition.mockReturnValue({
|
|
238
|
+
id: "astro-minimal",
|
|
239
|
+
name: "Astro",
|
|
240
|
+
source: { type: "local", path: path.resolve(scaffoldDir) },
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
const result = await getScaffoldContent("astro-minimal");
|
|
244
|
+
|
|
245
|
+
expect(mockToMinimalScaffoldId).not.toHaveBeenCalled();
|
|
246
|
+
expect(result.scaffoldId).toBe("astro-minimal");
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
});
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import {
|
|
2
|
+
FRAMEWORK_REGISTRY,
|
|
3
|
+
getAvailableFrameworkNames,
|
|
4
|
+
getFrameworkDefinition,
|
|
5
|
+
listSupportedScaffoldIds,
|
|
6
|
+
toMinimalScaffoldId,
|
|
7
|
+
} from "../registry";
|
|
8
|
+
import { ScaffoldError } from "../types";
|
|
9
|
+
|
|
10
|
+
describe("scaffold registry", () => {
|
|
11
|
+
describe("getFrameworkDefinition", () => {
|
|
12
|
+
it("returns definition for astro-minimal", () => {
|
|
13
|
+
const def = getFrameworkDefinition("astro-minimal");
|
|
14
|
+
expect(def.id).toBe("astro-minimal");
|
|
15
|
+
expect(def.name).toBe("Astro");
|
|
16
|
+
expect(def.source.type).toBe("github");
|
|
17
|
+
if (def.source.type === "github") {
|
|
18
|
+
expect(def.source.repo).toBe("Webflow-Examples/hello-world-astro");
|
|
19
|
+
expect(def.source.ref).toBe("v1");
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("returns definition for nextjs-minimal", () => {
|
|
24
|
+
const def = getFrameworkDefinition("nextjs-minimal");
|
|
25
|
+
expect(def.id).toBe("nextjs-minimal");
|
|
26
|
+
expect(def.name).toBe("Next.js");
|
|
27
|
+
expect(def.source.type).toBe("github");
|
|
28
|
+
if (def.source.type === "github") {
|
|
29
|
+
expect(def.source.repo).toBe("Webflow-Examples/hello-world-nextjs");
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("returns definition for astro (site-attached)", () => {
|
|
34
|
+
const def = getFrameworkDefinition("astro");
|
|
35
|
+
expect(def.id).toBe("astro");
|
|
36
|
+
expect(def.name).toBe("Astro (site-attached)");
|
|
37
|
+
expect(def.source.type).toBe("github");
|
|
38
|
+
if (def.source.type === "github") {
|
|
39
|
+
expect(def.source.repo).toBe(
|
|
40
|
+
"Webflow-Examples/hello-world-astro-devlink"
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("returns definition for nextjs (site-attached)", () => {
|
|
46
|
+
const def = getFrameworkDefinition("nextjs");
|
|
47
|
+
expect(def.id).toBe("nextjs");
|
|
48
|
+
expect(def.name).toBe("Next.js (site-attached)");
|
|
49
|
+
expect(def.source.type).toBe("github");
|
|
50
|
+
if (def.source.type === "github") {
|
|
51
|
+
expect(def.source.repo).toBe(
|
|
52
|
+
"Webflow-Examples/hello-world-nextjs-devlink"
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("throws ScaffoldError for unsupported scaffold ID", () => {
|
|
58
|
+
expect(() => getFrameworkDefinition("remix-minimal")).toThrow(
|
|
59
|
+
ScaffoldError
|
|
60
|
+
);
|
|
61
|
+
expect(() => getFrameworkDefinition("remix-minimal")).toThrow(
|
|
62
|
+
'Unsupported framework: "remix-minimal"'
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("includes supported IDs in ScaffoldError", () => {
|
|
67
|
+
try {
|
|
68
|
+
getFrameworkDefinition("unknown");
|
|
69
|
+
fail("Expected ScaffoldError");
|
|
70
|
+
} catch (err) {
|
|
71
|
+
expect(err).toBeInstanceOf(ScaffoldError);
|
|
72
|
+
expect((err as ScaffoldError).scaffoldId).toBe("unknown");
|
|
73
|
+
expect((err as ScaffoldError).supportedIds).toContain("astro-minimal");
|
|
74
|
+
expect((err as ScaffoldError).supportedIds).toContain("nextjs-minimal");
|
|
75
|
+
expect((err as ScaffoldError).supportedIds).toContain("astro");
|
|
76
|
+
expect((err as ScaffoldError).supportedIds).toContain("nextjs");
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe("listSupportedScaffoldIds", () => {
|
|
82
|
+
it("returns all registry keys", () => {
|
|
83
|
+
const ids = listSupportedScaffoldIds();
|
|
84
|
+
expect(ids).toEqual(
|
|
85
|
+
expect.arrayContaining([
|
|
86
|
+
"astro-minimal",
|
|
87
|
+
"nextjs-minimal",
|
|
88
|
+
"astro",
|
|
89
|
+
"nextjs",
|
|
90
|
+
])
|
|
91
|
+
);
|
|
92
|
+
expect(ids).toHaveLength(Object.keys(FRAMEWORK_REGISTRY).length);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe("getAvailableFrameworkNames", () => {
|
|
97
|
+
it("returns framework names without -minimal suffix", () => {
|
|
98
|
+
const names = getAvailableFrameworkNames();
|
|
99
|
+
expect(names).toEqual(expect.arrayContaining(["astro", "nextjs"]));
|
|
100
|
+
expect(names).toHaveLength(2);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe("toMinimalScaffoldId", () => {
|
|
105
|
+
it("maps astro to astro-minimal", () => {
|
|
106
|
+
expect(toMinimalScaffoldId("astro")).toBe("astro-minimal");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("maps nextjs to nextjs-minimal", () => {
|
|
110
|
+
expect(toMinimalScaffoldId("nextjs")).toBe("nextjs-minimal");
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("passes through astro-minimal unchanged", () => {
|
|
114
|
+
expect(toMinimalScaffoldId("astro-minimal")).toBe("astro-minimal");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("throws ScaffoldError for unsupported framework", () => {
|
|
118
|
+
expect(() => toMinimalScaffoldId("remix")).toThrow(ScaffoldError);
|
|
119
|
+
expect(() => toMinimalScaffoldId("remix")).toThrow(
|
|
120
|
+
'No minimal scaffold for "remix"'
|
|
121
|
+
);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("includes supported IDs in ScaffoldError", () => {
|
|
125
|
+
try {
|
|
126
|
+
toMinimalScaffoldId("vue");
|
|
127
|
+
fail("Expected ScaffoldError");
|
|
128
|
+
} catch (err) {
|
|
129
|
+
expect(err).toBeInstanceOf(ScaffoldError);
|
|
130
|
+
expect((err as ScaffoldError).supportedIds).toBeDefined();
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
});
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import fsExtra from "fs-extra";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import * as os from "os";
|
|
5
|
+
import * as tar from "tar";
|
|
6
|
+
import { getFrameworkDefinition, toMinimalScaffoldId } from "./registry";
|
|
7
|
+
import type { ScaffoldContent } from "./types";
|
|
8
|
+
import { ScaffoldError } from "./types";
|
|
9
|
+
import logger from "../../utils/logger";
|
|
10
|
+
|
|
11
|
+
const GITHUB_TARBALL_BASE = "https://github.com";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Get scaffold content for a framework.
|
|
15
|
+
* Resolves the scaffold ID (e.g. "astro" -> "astro-minimal") and fetches.
|
|
16
|
+
*
|
|
17
|
+
* @param frameworkOrScaffoldId - "astro", "nextjs", or full "astro-minimal", "nextjs-minimal"
|
|
18
|
+
* @returns ScaffoldContent ready for applyScaffold
|
|
19
|
+
*/
|
|
20
|
+
export async function getScaffoldContent(
|
|
21
|
+
frameworkOrScaffoldId: string
|
|
22
|
+
): Promise<ScaffoldContent> {
|
|
23
|
+
const scaffoldId = frameworkOrScaffoldId.endsWith("-minimal")
|
|
24
|
+
? frameworkOrScaffoldId
|
|
25
|
+
: toMinimalScaffoldId(frameworkOrScaffoldId);
|
|
26
|
+
return fetchScaffoldContent(scaffoldId);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Get scaffold content from the configured source (github or local).
|
|
31
|
+
* Dispatches to the appropriate fetcher based on source.type.
|
|
32
|
+
*/
|
|
33
|
+
export async function fetchScaffoldContent(
|
|
34
|
+
scaffoldId: string
|
|
35
|
+
): Promise<ScaffoldContent> {
|
|
36
|
+
const def = getFrameworkDefinition(scaffoldId);
|
|
37
|
+
|
|
38
|
+
if (def.source.type === "github") {
|
|
39
|
+
return fetchScaffoldFromGitHub(scaffoldId);
|
|
40
|
+
}
|
|
41
|
+
return getScaffoldFromLocal(scaffoldId);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Get scaffold content from local filesystem.
|
|
46
|
+
*/
|
|
47
|
+
export async function getScaffoldFromLocal(
|
|
48
|
+
scaffoldId: string
|
|
49
|
+
): Promise<ScaffoldContent> {
|
|
50
|
+
const def = getFrameworkDefinition(scaffoldId);
|
|
51
|
+
|
|
52
|
+
if (def.source.type !== "local") {
|
|
53
|
+
throw new ScaffoldError(
|
|
54
|
+
`Scaffold "${scaffoldId}" does not have a local source`,
|
|
55
|
+
scaffoldId
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// path is relative to dist/ (e.g. "cloud-scaffolds/astro-minimal") or absolute
|
|
60
|
+
const scaffoldPath = path.isAbsolute(def.source.path)
|
|
61
|
+
? def.source.path
|
|
62
|
+
: path.join(__dirname, def.source.path);
|
|
63
|
+
if (!fs.existsSync(scaffoldPath)) {
|
|
64
|
+
throw new ScaffoldError(
|
|
65
|
+
`Scaffold directory not found: ${scaffoldPath}`,
|
|
66
|
+
scaffoldId
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const files = await listFilesRecursive(scaffoldPath);
|
|
71
|
+
return { scaffoldId, files, extractionRoot: scaffoldPath };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Download and extract a scaffold from GitHub.
|
|
76
|
+
* Uses the tarball API; no git dependency.
|
|
77
|
+
*/
|
|
78
|
+
async function fetchScaffoldFromGitHub(
|
|
79
|
+
scaffoldId: string
|
|
80
|
+
): Promise<ScaffoldContent> {
|
|
81
|
+
const def = getFrameworkDefinition(scaffoldId);
|
|
82
|
+
|
|
83
|
+
if (def.source.type !== "github") {
|
|
84
|
+
throw new ScaffoldError(
|
|
85
|
+
`Scaffold "${scaffoldId}" does not have a GitHub source`,
|
|
86
|
+
scaffoldId
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const ref = def.source.ref ?? "main";
|
|
91
|
+
const tarballUrl = `${GITHUB_TARBALL_BASE}/${def.source.repo}/archive/${ref}.tar.gz`;
|
|
92
|
+
|
|
93
|
+
const extractDir = await fs.promises.mkdtemp(
|
|
94
|
+
path.join(os.tmpdir(), `webflow-scaffold-${scaffoldId}-`)
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const downloadSpinner = logger.spinner(
|
|
98
|
+
`Downloading scaffold (${def.source.repo}@${ref})`
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
let response: Response;
|
|
103
|
+
try {
|
|
104
|
+
response = await fetch(tarballUrl, {
|
|
105
|
+
redirect: "follow",
|
|
106
|
+
headers: { "User-Agent": "webflow-cli" },
|
|
107
|
+
});
|
|
108
|
+
} catch (fetchErr) {
|
|
109
|
+
downloadSpinner.fail("Download failed");
|
|
110
|
+
throw new ScaffoldError(
|
|
111
|
+
`Failed to fetch scaffold from GitHub: ${
|
|
112
|
+
fetchErr instanceof Error ? fetchErr.message : String(fetchErr)
|
|
113
|
+
}. Repo: ${def.source.repo}, ref: ${ref}`,
|
|
114
|
+
scaffoldId
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (!response.ok) {
|
|
119
|
+
downloadSpinner.fail(
|
|
120
|
+
`GitHub returned ${response.status} ${response.statusText}`
|
|
121
|
+
);
|
|
122
|
+
throw new ScaffoldError(
|
|
123
|
+
`Failed to fetch scaffold from GitHub: ${response.status} ${response.statusText}. ` +
|
|
124
|
+
`Repo: ${def.source.repo}, ref: ${ref}`,
|
|
125
|
+
scaffoldId
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
130
|
+
downloadSpinner.succeed("Downloaded scaffold");
|
|
131
|
+
|
|
132
|
+
const unpackSpinner = logger.spinner("Unpacking scaffold");
|
|
133
|
+
try {
|
|
134
|
+
await extractTarball(buffer, extractDir);
|
|
135
|
+
unpackSpinner.succeed("Scaffold ready");
|
|
136
|
+
} catch (unpackErr) {
|
|
137
|
+
unpackSpinner.fail("Unpack failed");
|
|
138
|
+
throw unpackErr;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const entries = await fs.promises.readdir(extractDir);
|
|
142
|
+
const topLevelDir = entries[0];
|
|
143
|
+
if (!topLevelDir || entries.length !== 1) {
|
|
144
|
+
throw new ScaffoldError(
|
|
145
|
+
`Unexpected tarball structure for ${def.source.repo}`,
|
|
146
|
+
scaffoldId
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const extractionRoot = path.join(extractDir, topLevelDir);
|
|
151
|
+
const files = await listFilesRecursive(extractionRoot);
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
scaffoldId,
|
|
155
|
+
files,
|
|
156
|
+
extractionRoot,
|
|
157
|
+
};
|
|
158
|
+
} catch (err) {
|
|
159
|
+
try {
|
|
160
|
+
fsExtra.removeSync(extractDir);
|
|
161
|
+
} catch {
|
|
162
|
+
// Ignore cleanup errors
|
|
163
|
+
}
|
|
164
|
+
if (err instanceof ScaffoldError) throw err;
|
|
165
|
+
throw new ScaffoldError(
|
|
166
|
+
`Failed to fetch scaffold "${scaffoldId}": ${
|
|
167
|
+
err instanceof Error ? err.message : String(err)
|
|
168
|
+
}`,
|
|
169
|
+
scaffoldId
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function extractTarball(buffer: Buffer, destDir: string): Promise<void> {
|
|
175
|
+
const tmpFile = path.join(
|
|
176
|
+
os.tmpdir(),
|
|
177
|
+
`webflow-scaffold-${Date.now()}.tar.gz`
|
|
178
|
+
);
|
|
179
|
+
await fs.promises.writeFile(
|
|
180
|
+
tmpFile,
|
|
181
|
+
new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength)
|
|
182
|
+
);
|
|
183
|
+
try {
|
|
184
|
+
await tar.extract({ file: tmpFile, cwd: destDir });
|
|
185
|
+
} finally {
|
|
186
|
+
await fs.promises.unlink(tmpFile).catch(() => {
|
|
187
|
+
// Ignore cleanup errors
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function listFilesRecursive(dirPath: string): Promise<string[]> {
|
|
193
|
+
const result: string[] = [];
|
|
194
|
+
const entries = await fs.promises.readdir(dirPath, { withFileTypes: true });
|
|
195
|
+
|
|
196
|
+
for (const entry of entries) {
|
|
197
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
198
|
+
if (entry.isDirectory()) {
|
|
199
|
+
result.push(...(await listFilesRecursive(fullPath)));
|
|
200
|
+
} else {
|
|
201
|
+
result.push(fullPath);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return result;
|
|
205
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export {
|
|
2
|
+
getScaffoldContent,
|
|
3
|
+
fetchScaffoldContent,
|
|
4
|
+
getScaffoldFromLocal,
|
|
5
|
+
} from "./fetcher";
|
|
6
|
+
export {
|
|
7
|
+
FRAMEWORK_REGISTRY,
|
|
8
|
+
getFrameworkDefinition,
|
|
9
|
+
listSupportedScaffoldIds,
|
|
10
|
+
listExistingScaffoldIds,
|
|
11
|
+
getAvailableFrameworkNames,
|
|
12
|
+
toMinimalScaffoldId,
|
|
13
|
+
} from "./registry";
|
|
14
|
+
export type {
|
|
15
|
+
FrameworkDefinition,
|
|
16
|
+
ScaffoldContent,
|
|
17
|
+
ScaffoldSource,
|
|
18
|
+
} from "./types";
|
|
19
|
+
export { ScaffoldError } from "./types";
|