create-visualbuild-app 0.1.0 → 1.0.0

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.
Files changed (44) hide show
  1. package/README.md +306 -123
  2. package/docs/user-guide.md +759 -0
  3. package/package.json +92 -85
  4. package/scripts/create-builder-app.mjs +513 -501
  5. package/scripts/vb-dev.mjs +41 -32
  6. package/scripts/vb-generate.mjs +332 -224
  7. package/templates/default/app/.vercelignore +6 -0
  8. package/templates/default/app/README.md +56 -21
  9. package/templates/default/app/visualbuild/components.json +3 -0
  10. package/templates/default/app/visualbuild/config.d.ts +48 -0
  11. package/templates/default/app/visualbuild.config.ts +38 -0
  12. package/templates/default/builder/README.md +37 -21
  13. package/visual-app-builder/server/parseReactPage.ts +776 -571
  14. package/visual-app-builder/shared/createReactComponentSource.d.mts +2 -0
  15. package/visual-app-builder/shared/createReactComponentSource.mjs +45 -0
  16. package/visual-app-builder/shared/generateReactSource.d.mts +57 -37
  17. package/visual-app-builder/shared/generateReactSource.mjs +608 -443
  18. package/visual-app-builder/shared/npmBuildCommand.d.mts +17 -0
  19. package/visual-app-builder/shared/npmBuildCommand.mjs +26 -0
  20. package/visual-app-builder/shared/syncCustomComponentSource.d.mts +13 -0
  21. package/visual-app-builder/shared/syncCustomComponentSource.mjs +345 -0
  22. package/visual-app-builder/shared/vercelDeployment.d.mts +31 -0
  23. package/visual-app-builder/shared/vercelDeployment.mjs +266 -0
  24. package/visual-app-builder/shared/visualbuildConfig.d.mts +144 -0
  25. package/visual-app-builder/shared/visualbuildConfig.mjs +578 -0
  26. package/visual-app-builder/src/App.tsx +1090 -874
  27. package/visual-app-builder/src/components/Canvas.tsx +1116 -1059
  28. package/visual-app-builder/src/components/CodePanel.tsx +1147 -812
  29. package/visual-app-builder/src/components/ComponentSidebar.tsx +365 -302
  30. package/visual-app-builder/src/components/DeploymentModal.tsx +295 -0
  31. package/visual-app-builder/src/components/PageSwitcher.tsx +133 -133
  32. package/visual-app-builder/src/components/ProjectExplorer.tsx +1054 -1054
  33. package/visual-app-builder/src/components/PropertiesPanel.tsx +792 -692
  34. package/visual-app-builder/src/components/Topbar.tsx +257 -128
  35. package/visual-app-builder/src/data/componentRegistry.tsx +613 -292
  36. package/visual-app-builder/src/data/tailwindClassCatalog.ts +95 -0
  37. package/visual-app-builder/src/index.css +383 -111
  38. package/visual-app-builder/src/stores/useAppStore.ts +2385 -1265
  39. package/visual-app-builder/src/theme.ts +71 -0
  40. package/visual-app-builder/src/types/index.ts +350 -261
  41. package/visual-app-builder/src/utils/codegen.ts +90 -66
  42. package/visual-app-builder/src/utils/deployment.ts +54 -0
  43. package/visual-app-builder/src/utils/projectPersistence.ts +218 -177
  44. package/visual-app-builder/vite.config.ts +1946 -1479
@@ -0,0 +1,17 @@
1
+ export interface NpmBuildCommand {
2
+ executable: string
3
+ args: string[]
4
+ }
5
+
6
+ export interface NpmBuildCommandEnvironment {
7
+ npm_execpath?: string
8
+ ComSpec?: string
9
+ COMSPEC?: string
10
+ [key: string]: string | undefined
11
+ }
12
+
13
+ export function resolveNpmBuildCommand(options?: {
14
+ platform?: NodeJS.Platform
15
+ env?: NpmBuildCommandEnvironment
16
+ nodeExecutable?: string
17
+ }): NpmBuildCommand
@@ -0,0 +1,26 @@
1
+ export function resolveNpmBuildCommand({
2
+ platform = process.platform,
3
+ env = process.env,
4
+ nodeExecutable = process.execPath,
5
+ } = {}) {
6
+ const npmCliPath = String(env.npm_execpath ?? '').trim()
7
+
8
+ if (npmCliPath) {
9
+ return {
10
+ executable: nodeExecutable,
11
+ args: [npmCliPath, 'run', 'build'],
12
+ }
13
+ }
14
+
15
+ if (platform === 'win32') {
16
+ return {
17
+ executable: env.ComSpec || env.COMSPEC || 'cmd.exe',
18
+ args: ['/d', '/s', '/c', 'npm.cmd run build'],
19
+ }
20
+ }
21
+
22
+ return {
23
+ executable: 'npm',
24
+ args: ['run', 'build'],
25
+ }
26
+ }
@@ -0,0 +1,13 @@
1
+ export interface CustomComponentRootSettings {
2
+ name: string
3
+ rootTag?: string
4
+ id?: unknown
5
+ className?: unknown
6
+ attributes?: unknown
7
+ children?: unknown[]
8
+ }
9
+
10
+ export function syncCustomComponentRoot(
11
+ source: string,
12
+ settings: CustomComponentRootSettings
13
+ ): string
@@ -0,0 +1,345 @@
1
+ import { generate } from '@babel/generator'
2
+ import { parse } from '@babel/parser'
3
+ import * as t from '@babel/types'
4
+
5
+ const allowedRootTags = new Set([
6
+ 'div',
7
+ 'section',
8
+ 'main',
9
+ 'article',
10
+ 'aside',
11
+ 'header',
12
+ 'footer',
13
+ 'nav',
14
+ 'form',
15
+ ])
16
+
17
+ export function syncCustomComponentRoot(source, settings) {
18
+ const ast = parse(source, {
19
+ sourceType: 'module',
20
+ plugins: ['jsx', 'typescript'],
21
+ })
22
+ const root = findReturnedJsxElement(ast.program)
23
+
24
+ if (!root) {
25
+ throw new Error(
26
+ `Registered component "${settings.name}" must return one JSX element`
27
+ )
28
+ }
29
+
30
+ const rootTag = allowedRootTags.has(String(settings.rootTag))
31
+ ? String(settings.rootTag)
32
+ : 'div'
33
+ const additionalAttributes =
34
+ settings.attributes &&
35
+ typeof settings.attributes === 'object' &&
36
+ !Array.isArray(settings.attributes)
37
+ ? settings.attributes
38
+ : {}
39
+ const serializedAttributes = Object.entries(additionalAttributes).filter(
40
+ ([name, value]) =>
41
+ isSafeJsxAttributeName(name) &&
42
+ !['children', 'key', 'ref', 'id', 'className'].includes(name) &&
43
+ isSerializableAttributeValue(value)
44
+ )
45
+ const id = settings.id ? String(settings.id) : undefined
46
+ const className = settings.className
47
+ ? String(settings.className)
48
+ : undefined
49
+
50
+ if (
51
+ !Array.isArray(settings.children) &&
52
+ rootMatchesSettings(
53
+ root,
54
+ rootTag,
55
+ id,
56
+ className,
57
+ serializedAttributes
58
+ )
59
+ ) {
60
+ return source
61
+ }
62
+
63
+ root.openingElement.name = t.jsxIdentifier(rootTag)
64
+
65
+ if (root.closingElement) {
66
+ root.closingElement.name = t.jsxIdentifier(rootTag)
67
+ }
68
+
69
+ const managedNames = new Set([
70
+ 'id',
71
+ 'className',
72
+ ...Object.keys(additionalAttributes),
73
+ ])
74
+ const preservedAttributes = root.openingElement.attributes.filter(
75
+ (attribute) =>
76
+ attribute.type === 'JSXSpreadAttribute' ||
77
+ !managedNames.has(attribute.name.name)
78
+ )
79
+ const nextAttributes = [
80
+ id ? createAttribute('id', id) : null,
81
+ className ? createAttribute('className', className) : null,
82
+ ...serializedAttributes
83
+ .map(([name, value]) => createAttribute(name, value)),
84
+ ].filter(Boolean)
85
+
86
+ root.openingElement.attributes = [
87
+ ...nextAttributes,
88
+ ...preservedAttributes,
89
+ ]
90
+
91
+ if (Array.isArray(settings.children)) {
92
+ const children = settings.children
93
+ .map((child) => createVisualChild(child))
94
+ .filter(Boolean)
95
+ root.children =
96
+ children.length > 0 ? withLineBreaks(children, 1) : []
97
+ }
98
+
99
+ return `${generate(ast, {
100
+ comments: true,
101
+ compact: false,
102
+ jsescOption: { minimal: true },
103
+ }).code}\n`
104
+ }
105
+
106
+ function createVisualChild(rawNode) {
107
+ if (!rawNode || typeof rawNode !== 'object') return null
108
+
109
+ if (rawNode.type === '__visualbuild_children_slot__') {
110
+ return t.jsxExpressionContainer(t.identifier('children'))
111
+ }
112
+
113
+ const tagName = String(rawNode.type ?? 'div')
114
+
115
+ if (!/^[A-Za-z_$][A-Za-z0-9_$.-]*$/.test(tagName)) {
116
+ return null
117
+ }
118
+
119
+ const props =
120
+ rawNode.props && typeof rawNode.props === 'object'
121
+ ? rawNode.props
122
+ : {}
123
+ const attributeEntries = Object.entries(props).filter(
124
+ ([name, value]) =>
125
+ ![
126
+ 'text',
127
+ 'wrapperTag',
128
+ 'componentTree',
129
+ 'attributes',
130
+ ].includes(name) &&
131
+ value !== undefined &&
132
+ value !== '' &&
133
+ isSafeJsxAttributeName(name) &&
134
+ isSerializableAttributeValue(value)
135
+ )
136
+ const additionalAttributes =
137
+ props.attributes &&
138
+ typeof props.attributes === 'object' &&
139
+ !Array.isArray(props.attributes)
140
+ ? Object.entries(props.attributes).filter(
141
+ ([name, value]) =>
142
+ isSafeJsxAttributeName(name) &&
143
+ isSerializableAttributeValue(value)
144
+ )
145
+ : []
146
+ const attributes = [...attributeEntries, ...additionalAttributes].map(
147
+ ([name, value]) => createAttribute(name, value)
148
+ )
149
+ const isVoid = ['br', 'hr', 'img', 'input', 'source', 'col'].includes(
150
+ tagName
151
+ )
152
+ const opening = t.jsxOpeningElement(
153
+ t.jsxIdentifier(tagName),
154
+ attributes,
155
+ isVoid
156
+ )
157
+
158
+ if (isVoid) {
159
+ return t.jsxElement(opening, null, [], true)
160
+ }
161
+
162
+ const nestedChildren = Array.isArray(rawNode.children)
163
+ ? rawNode.children.map(createVisualChild).filter(Boolean)
164
+ : []
165
+ const text = String(props.text ?? '')
166
+
167
+ if (nestedChildren.length === 0 && text) {
168
+ nestedChildren.push(t.jsxText(text))
169
+ }
170
+
171
+ return t.jsxElement(
172
+ opening,
173
+ t.jsxClosingElement(t.jsxIdentifier(tagName)),
174
+ nestedChildren.length > 0
175
+ ? withLineBreaks(nestedChildren, 2)
176
+ : [],
177
+ false
178
+ )
179
+ }
180
+
181
+ function withLineBreaks(children, depth) {
182
+ const childIndent = ' '.repeat(depth)
183
+ const closingIndent = ' '.repeat(Math.max(0, depth - 1))
184
+
185
+ return [
186
+ t.jsxText(`\n${childIndent}`),
187
+ ...children.flatMap((child, index) => [
188
+ child,
189
+ t.jsxText(
190
+ index === children.length - 1
191
+ ? `\n${closingIndent}`
192
+ : `\n${childIndent}`
193
+ ),
194
+ ]),
195
+ ]
196
+ }
197
+
198
+ function rootMatchesSettings(
199
+ root,
200
+ rootTag,
201
+ id,
202
+ className,
203
+ additionalAttributes
204
+ ) {
205
+ if (
206
+ root.openingElement.name.type !== 'JSXIdentifier' ||
207
+ root.openingElement.name.name !== rootTag
208
+ ) {
209
+ return false
210
+ }
211
+
212
+ const attributes = root.openingElement.attributes
213
+
214
+ return (
215
+ attributeMatches(attributes, 'id', id) &&
216
+ attributeMatches(attributes, 'className', className) &&
217
+ additionalAttributes.every(([name, value]) =>
218
+ attributeMatches(attributes, name, value)
219
+ )
220
+ )
221
+ }
222
+
223
+ function attributeMatches(attributes, name, expected) {
224
+ const attribute = attributes.find(
225
+ (candidate) =>
226
+ candidate.type === 'JSXAttribute' &&
227
+ candidate.name.type === 'JSXIdentifier' &&
228
+ candidate.name.name === name
229
+ )
230
+
231
+ if (expected === undefined) {
232
+ return attribute === undefined
233
+ }
234
+
235
+ if (!attribute) return false
236
+ return JSON.stringify(readAttributeValue(attribute)) === JSON.stringify(expected)
237
+ }
238
+
239
+ function readAttributeValue(attribute) {
240
+ if (!attribute.value) return true
241
+ if (attribute.value.type === 'StringLiteral') {
242
+ return attribute.value.value
243
+ }
244
+ if (attribute.value.type !== 'JSXExpressionContainer') {
245
+ return undefined
246
+ }
247
+
248
+ return readExpressionValue(attribute.value.expression)
249
+ }
250
+
251
+ function readExpressionValue(expression) {
252
+ if (
253
+ expression.type === 'StringLiteral' ||
254
+ expression.type === 'NumericLiteral' ||
255
+ expression.type === 'BooleanLiteral'
256
+ ) {
257
+ return expression.value
258
+ }
259
+ if (expression.type === 'ArrayExpression') {
260
+ return expression.elements.map((element) =>
261
+ element ? readExpressionValue(element) : null
262
+ )
263
+ }
264
+
265
+ return undefined
266
+ }
267
+
268
+ function findReturnedJsxElement(program) {
269
+ let result = null
270
+
271
+ const visit = (value) => {
272
+ if (result || !value || typeof value !== 'object') return
273
+
274
+ if (value.type === 'ReturnStatement') {
275
+ const argument = unwrapExpression(value.argument)
276
+ if (argument?.type === 'JSXElement') {
277
+ result = argument
278
+ return
279
+ }
280
+ }
281
+
282
+ if (
283
+ value.type === 'ArrowFunctionExpression' &&
284
+ unwrapExpression(value.body)?.type === 'JSXElement'
285
+ ) {
286
+ result = unwrapExpression(value.body)
287
+ return
288
+ }
289
+
290
+ for (const child of Object.values(value)) {
291
+ if (Array.isArray(child)) {
292
+ child.forEach(visit)
293
+ } else {
294
+ visit(child)
295
+ }
296
+ }
297
+ }
298
+
299
+ visit(program)
300
+ return result
301
+ }
302
+
303
+ function unwrapExpression(value) {
304
+ if (
305
+ value &&
306
+ [
307
+ 'ParenthesizedExpression',
308
+ 'TSAsExpression',
309
+ 'TSTypeAssertion',
310
+ 'TSSatisfiesExpression',
311
+ ].includes(value.type)
312
+ ) {
313
+ return unwrapExpression(value.expression)
314
+ }
315
+ return value
316
+ }
317
+
318
+ function createAttribute(name, value) {
319
+ return t.jsxAttribute(
320
+ t.jsxIdentifier(name),
321
+ t.jsxExpressionContainer(toExpression(value))
322
+ )
323
+ }
324
+
325
+ function toExpression(value) {
326
+ if (typeof value === 'number') return t.numericLiteral(value)
327
+ if (typeof value === 'boolean') return t.booleanLiteral(value)
328
+ if (Array.isArray(value)) {
329
+ return t.arrayExpression(value.map(toExpression))
330
+ }
331
+ return t.stringLiteral(String(value))
332
+ }
333
+
334
+ function isSafeJsxAttributeName(name) {
335
+ return /^[A-Za-z_:][A-Za-z0-9_.:-]*$/.test(name)
336
+ }
337
+
338
+ function isSerializableAttributeValue(value) {
339
+ return (
340
+ typeof value === 'string' ||
341
+ typeof value === 'number' ||
342
+ typeof value === 'boolean' ||
343
+ Array.isArray(value)
344
+ )
345
+ }
@@ -0,0 +1,31 @@
1
+ export interface StaticDeploymentFile {
2
+ file: string
3
+ data: Buffer
4
+ sha: string
5
+ size: number
6
+ }
7
+
8
+ export interface VercelDeploymentResult {
9
+ id: string
10
+ url: string
11
+ inspectorUrl?: string
12
+ readyState: 'READY'
13
+ fileCount: number
14
+ totalBytes: number
15
+ }
16
+
17
+ export function validateVercelProjectName(value: unknown): string
18
+ export function validateVercelTeamId(value: unknown): string | undefined
19
+ export function collectStaticDeploymentFiles(
20
+ outputRoot: string
21
+ ): Promise<StaticDeploymentFile[]>
22
+ export function deployStaticOutputToVercel(options: {
23
+ outputRoot: string
24
+ token: string
25
+ projectName: string
26
+ teamId?: string
27
+ fetchImpl?: typeof fetch
28
+ pollIntervalMs?: number
29
+ maxPolls?: number
30
+ wait?: (milliseconds: number) => Promise<unknown>
31
+ }): Promise<VercelDeploymentResult>
@@ -0,0 +1,266 @@
1
+ import { createHash } from 'node:crypto'
2
+ import fs from 'node:fs/promises'
3
+ import path from 'node:path'
4
+
5
+ const vercelApiBase = 'https://api.vercel.com'
6
+ const forbiddenDeploymentSegments = new Set([
7
+ '.git',
8
+ '.vercel',
9
+ 'node_modules',
10
+ 'visual-app-builder',
11
+ 'visual-builder',
12
+ 'visualbuild',
13
+ ])
14
+ const terminalDeploymentStates = new Set(['READY', 'ERROR', 'CANCELED'])
15
+
16
+ export function validateVercelProjectName(value) {
17
+ const projectName = String(value ?? '').trim().toLowerCase()
18
+
19
+ if (
20
+ !projectName ||
21
+ projectName.length > 100 ||
22
+ !/^[a-z0-9](?:[a-z0-9._-]*[a-z0-9])?$/.test(projectName) ||
23
+ projectName.includes('---')
24
+ ) {
25
+ throw new Error(
26
+ 'Vercel project name must use lowercase letters, numbers, dots, underscores, or hyphens'
27
+ )
28
+ }
29
+
30
+ return projectName
31
+ }
32
+
33
+ export function validateVercelTeamId(value) {
34
+ const teamId = String(value ?? '').trim()
35
+
36
+ if (!teamId) return undefined
37
+ if (!/^team_[A-Za-z0-9]+$/.test(teamId)) {
38
+ throw new Error('Vercel Team ID must start with "team_"')
39
+ }
40
+
41
+ return teamId
42
+ }
43
+
44
+ export async function collectStaticDeploymentFiles(outputRoot) {
45
+ const absoluteRoot = path.resolve(outputRoot)
46
+ const rootStats = await fs.stat(absoluteRoot).catch(() => null)
47
+
48
+ if (!rootStats?.isDirectory()) {
49
+ throw new Error('The app build did not create its deployment output folder')
50
+ }
51
+
52
+ const files = []
53
+
54
+ async function visit(directoryPath, relativeDirectory = '') {
55
+ const entries = await fs.readdir(directoryPath, { withFileTypes: true })
56
+
57
+ for (const entry of entries.sort((first, second) =>
58
+ first.name.localeCompare(second.name)
59
+ )) {
60
+ const relativePath = relativeDirectory
61
+ ? `${relativeDirectory}/${entry.name}`
62
+ : entry.name
63
+ const absolutePath = path.join(directoryPath, entry.name)
64
+
65
+ assertProductionPath(relativePath)
66
+
67
+ if (entry.isSymbolicLink()) {
68
+ throw new Error(
69
+ `Deployment output cannot contain symbolic links (${relativePath})`
70
+ )
71
+ }
72
+
73
+ if (entry.isDirectory()) {
74
+ await visit(absolutePath, relativePath)
75
+ continue
76
+ }
77
+
78
+ if (!entry.isFile()) continue
79
+
80
+ const data = await fs.readFile(absolutePath)
81
+ files.push({
82
+ file: relativePath,
83
+ data,
84
+ sha: createHash('sha1').update(data).digest('hex'),
85
+ size: data.byteLength,
86
+ })
87
+ }
88
+ }
89
+
90
+ await visit(absoluteRoot)
91
+
92
+ if (!files.some(({ file }) => file === 'index.html')) {
93
+ throw new Error('Deployment output must contain an index.html file')
94
+ }
95
+
96
+ if (!files.some(({ file }) => file === 'vercel.json')) {
97
+ const data = Buffer.from(
98
+ `${JSON.stringify(
99
+ {
100
+ version: 2,
101
+ routes: [
102
+ { handle: 'filesystem' },
103
+ { src: '/.*', dest: '/index.html' },
104
+ ],
105
+ },
106
+ null,
107
+ 2
108
+ )}\n`
109
+ )
110
+ files.push({
111
+ file: 'vercel.json',
112
+ data,
113
+ sha: createHash('sha1').update(data).digest('hex'),
114
+ size: data.byteLength,
115
+ })
116
+ }
117
+
118
+ return files.sort((first, second) => first.file.localeCompare(second.file))
119
+ }
120
+
121
+ export async function deployStaticOutputToVercel({
122
+ outputRoot,
123
+ token,
124
+ projectName,
125
+ teamId,
126
+ fetchImpl = fetch,
127
+ pollIntervalMs = 1500,
128
+ maxPolls = 200,
129
+ wait = (milliseconds) =>
130
+ new Promise((resolve) => setTimeout(resolve, milliseconds)),
131
+ }) {
132
+ const accessToken = String(token ?? '').trim()
133
+
134
+ if (!accessToken) {
135
+ throw new Error(
136
+ 'Set VERCEL_TOKEN in the visual-builder terminal, then restart the builder'
137
+ )
138
+ }
139
+
140
+ const name = validateVercelProjectName(projectName)
141
+ const scope = validateVercelTeamId(teamId)
142
+ const files = await collectStaticDeploymentFiles(outputRoot)
143
+ const query = scope ? `?teamId=${encodeURIComponent(scope)}` : ''
144
+ const authorization = `Bearer ${accessToken}`
145
+
146
+ for (const file of files) {
147
+ const response = await fetchImpl(`${vercelApiBase}/v2/files${query}`, {
148
+ method: 'POST',
149
+ headers: {
150
+ Authorization: authorization,
151
+ 'Content-Type': 'application/octet-stream',
152
+ 'Content-Length': String(file.size),
153
+ 'x-vercel-digest': file.sha,
154
+ },
155
+ body: file.data,
156
+ })
157
+
158
+ if (!response.ok) {
159
+ throw await createVercelApiError(response, `upload ${file.file}`)
160
+ }
161
+ }
162
+
163
+ const deploymentResponse = await fetchImpl(
164
+ `${vercelApiBase}/v13/deployments${query}`,
165
+ {
166
+ method: 'POST',
167
+ headers: {
168
+ Authorization: authorization,
169
+ 'Content-Type': 'application/json',
170
+ },
171
+ body: JSON.stringify({
172
+ name,
173
+ target: 'production',
174
+ files: files.map(({ file, sha, size }) => ({ file, sha, size })),
175
+ projectSettings: {
176
+ framework: null,
177
+ },
178
+ }),
179
+ }
180
+ )
181
+
182
+ if (!deploymentResponse.ok) {
183
+ throw await createVercelApiError(deploymentResponse, 'create deployment')
184
+ }
185
+
186
+ let deployment = await deploymentResponse.json()
187
+ const deploymentId = deployment.id ?? deployment.uid
188
+ const initialInspectorUrl = deployment.inspectorUrl
189
+
190
+ if (!deploymentId || !deployment.url) {
191
+ throw new Error('Vercel returned an incomplete deployment response')
192
+ }
193
+
194
+ for (let attempt = 0; attempt < maxPolls; attempt += 1) {
195
+ const state = String(
196
+ deployment.readyState ?? deployment.status ?? deployment.state ?? ''
197
+ ).toUpperCase()
198
+
199
+ if (terminalDeploymentStates.has(state)) {
200
+ if (state !== 'READY') {
201
+ throw new Error(
202
+ `Vercel deployment ${state.toLowerCase()}. Open the deployment inspector for build details.`
203
+ )
204
+ }
205
+
206
+ return {
207
+ id: deploymentId,
208
+ url: normalizeDeploymentUrl(deployment.url),
209
+ inspectorUrl: deployment.inspectorUrl ?? initialInspectorUrl,
210
+ readyState: state,
211
+ fileCount: files.length,
212
+ totalBytes: files.reduce((total, file) => total + file.size, 0),
213
+ }
214
+ }
215
+
216
+ await wait(pollIntervalMs)
217
+ const statusResponse = await fetchImpl(
218
+ `${vercelApiBase}/v13/deployments/${encodeURIComponent(deploymentId)}${query}`,
219
+ {
220
+ headers: { Authorization: authorization },
221
+ }
222
+ )
223
+
224
+ if (!statusResponse.ok) {
225
+ throw await createVercelApiError(statusResponse, 'read deployment status')
226
+ }
227
+ deployment = await statusResponse.json()
228
+ }
229
+
230
+ throw new Error(
231
+ 'Vercel is still building. Open the deployment dashboard to follow its status.'
232
+ )
233
+ }
234
+
235
+ function assertProductionPath(relativePath) {
236
+ const normalized = relativePath.replace(/\\/g, '/')
237
+ const segments = normalized.split('/')
238
+
239
+ if (
240
+ normalized.startsWith('/') ||
241
+ segments.some(
242
+ (segment) =>
243
+ segment === '..' || forbiddenDeploymentSegments.has(segment.toLowerCase())
244
+ ) ||
245
+ /^visualbuild\.config\.[cm]?[jt]s$/i.test(path.posix.basename(normalized))
246
+ ) {
247
+ throw new Error(
248
+ `Builder-only path cannot enter a production deployment (${relativePath})`
249
+ )
250
+ }
251
+ }
252
+
253
+ async function createVercelApiError(response, operation) {
254
+ const payload = await response.json().catch(() => null)
255
+ const message = payload?.error?.message ?? payload?.message
256
+
257
+ return new Error(
258
+ message
259
+ ? `Vercel could not ${operation}: ${message}`
260
+ : `Vercel could not ${operation} (HTTP ${response.status})`
261
+ )
262
+ }
263
+
264
+ function normalizeDeploymentUrl(value) {
265
+ return /^https?:\/\//i.test(value) ? value : `https://${value}`
266
+ }