@glash/cli 0.2.32 → 0.2.34
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/README.md +20 -0
- package/bin/glash.mjs +4 -0
- package/commands/deploy.mjs +266 -16
- package/lib/zip.mjs +22 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -68,6 +68,7 @@ Common Node and frontend targets:
|
|
|
68
68
|
|
|
69
69
|
| Framework or runtime | Notes |
|
|
70
70
|
| --- | --- |
|
|
71
|
+
| glashjs | Native glashDB framework support; the CLI detects `glashjs`/`glash.config.*` and packages build/start scripts when missing |
|
|
71
72
|
| Next.js | Built and started with the Next runtime when no custom start script is present |
|
|
72
73
|
| React | Works with Vite, Create React App, Gatsby, Remix, and custom React servers |
|
|
73
74
|
| Vue | Works with Vue apps and Nuxt projects |
|
|
@@ -93,6 +94,7 @@ Deployment checklist:
|
|
|
93
94
|
- Set required secrets with `glash env:add` or the dashboard before deploying.
|
|
94
95
|
- Make sure the app listens on the expected runtime port.
|
|
95
96
|
- For large `public/` folders, deploy with `--asset-mode offload` to upload public assets to glashDB object storage/CDN and keep the source bundle small.
|
|
97
|
+
- Source bundles over 90 MB automatically use chunked source upload, keeping each request below Cloudflare Free's 100 MB upload limit.
|
|
96
98
|
|
|
97
99
|
Large public folders:
|
|
98
100
|
|
|
@@ -107,6 +109,21 @@ glash deploy --asset-mode auto --asset-threshold 100mb
|
|
|
107
109
|
Every deploy prints a live monitor link. Open it to watch the black-screen deployment flow while the CLI is packaging, uploading, building, routing, and checking the live URL.
|
|
108
110
|
Public asset offload prepares assets in small batches and uses multipart uploads for large files, so image/video-heavy `public/` folders can upload without forcing the source bundle over the platform limit.
|
|
109
111
|
|
|
112
|
+
Large source bundles:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
# Default: direct upload below 90 MB, chunked upload above it
|
|
116
|
+
glash deploy
|
|
117
|
+
|
|
118
|
+
# Force chunked upload even for a smaller bundle
|
|
119
|
+
glash deploy --source-upload chunked
|
|
120
|
+
|
|
121
|
+
# Tune the auto-chunk threshold
|
|
122
|
+
glash deploy --source-threshold 75mb
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
For glashjs projects, `glash deploy` detects `glashjs` or `glash.config.mjs`, marks the project as `glashjs`, keeps deploy-safe `.glash` output, and adds missing `build` / `start` scripts inside the deployment bundle without editing your local `package.json`.
|
|
126
|
+
|
|
110
127
|
## Commands
|
|
111
128
|
|
|
112
129
|
| Command | Purpose |
|
|
@@ -123,6 +140,7 @@ Public asset offload prepares assets in small batches and uses multipart uploads
|
|
|
123
140
|
| `glash open` | Open the linked project's primary domain |
|
|
124
141
|
| `glash deploy [--prod] [--detach]` | Deploy the linked project; streams the pipeline until live |
|
|
125
142
|
| `glash deploy --asset-mode offload` | Upload `public/` assets to object storage/CDN before zipping source |
|
|
143
|
+
| `glash deploy --source-upload chunked` | Stage the source zip through multipart object storage to bypass Cloudflare's single-request cap |
|
|
126
144
|
| `glash deployments` | List recent deployments |
|
|
127
145
|
| `glash status <id>` | Show a deployment's pipeline + events |
|
|
128
146
|
| `glash logs <id>` | Alias of `status` |
|
|
@@ -181,6 +199,8 @@ The commands require PostgreSQL client tools locally: `pg_dump` for dumps,
|
|
|
181
199
|
| `GLASH_TOKEN` | — | Use this token instead of `~/.glash/credentials.json` (useful in CI) |
|
|
182
200
|
| `GLASH_ASSET_OFFLOAD_THRESHOLD` | `100mb` | Default threshold for `glash deploy --asset-mode auto` |
|
|
183
201
|
| `GLASH_ASSET_BATCH_SIZE` | `50` | Number of public assets prepared per request during `--asset-mode offload` |
|
|
202
|
+
| `GLASH_SOURCE_UPLOAD_MODE` | `auto` | Source upload mode: `auto`, `direct`, or `chunked` |
|
|
203
|
+
| `GLASH_SOURCE_UPLOAD_THRESHOLD` | `90mb` | Default threshold for automatic chunked source upload |
|
|
184
204
|
|
|
185
205
|
## Requirements
|
|
186
206
|
|
package/bin/glash.mjs
CHANGED
|
@@ -100,6 +100,10 @@ ${color.bold('FLAGS')}
|
|
|
100
100
|
--no-env Skip automatic env sync during deploy
|
|
101
101
|
--asset-mode <mode> Public asset handling: inline, auto, offload
|
|
102
102
|
--asset-threshold <size> Auto-offload threshold (default: 100mb)
|
|
103
|
+
--source-upload <mode> Source upload: auto, direct, chunked
|
|
104
|
+
--source-threshold <size> Auto-chunk source threshold (default: 90mb)
|
|
105
|
+
--source-concurrency <n> Parallel chunk uploads for large source bundles (default: 4)
|
|
106
|
+
--framework <name> Override framework detection (glashjs, next, vite, etc.)
|
|
103
107
|
--help, -h Show this help
|
|
104
108
|
--version, -v Show CLI version
|
|
105
109
|
|
package/commands/deploy.mjs
CHANGED
|
@@ -118,9 +118,10 @@ async function createNewProjectFromCurrentDirectory() {
|
|
|
118
118
|
const dirName = basename(process.cwd());
|
|
119
119
|
const name = (await prompt(`What's your project's name? [${dirName}] `)) || dirName;
|
|
120
120
|
const slug = name.toLowerCase().replace(/[^a-z0-9-]+/g, '-').replace(/^-+|-+$/g, '');
|
|
121
|
+
const profile = await detectProjectProfile(process.cwd());
|
|
121
122
|
|
|
122
123
|
info(color.dim(`Creating project "${name}"...`));
|
|
123
|
-
const p = await apiPost('/projects', { name, slug });
|
|
124
|
+
const p = await apiPost('/projects', { name, slug, framework: profile.framework });
|
|
124
125
|
await writeProjectLink({ projectId: p.id, slug: p.slug, name: p.name });
|
|
125
126
|
ok(`Created and linked to ${p.name} (${p.slug})`);
|
|
126
127
|
return p.id;
|
|
@@ -135,6 +136,7 @@ export async function deploy({ flags }) {
|
|
|
135
136
|
const gitBranch = flags.branch || currentBranch();
|
|
136
137
|
const target = flags.prod || flags.production ? 'production' : (flags.target || 'production');
|
|
137
138
|
const requestedAssetMode = assetMode(flags);
|
|
139
|
+
const projectProfile = remoteDeploy ? { framework: frameworkHint(flags), label: frameworkHint(flags) || 'auto' } : await detectProjectProfile(process.cwd(), flags);
|
|
138
140
|
const monitorStartedAt = Date.now();
|
|
139
141
|
const initialStatusUrl = deployMonitorUrl({ projectId, after: monitorStartedAt });
|
|
140
142
|
|
|
@@ -150,6 +152,8 @@ export async function deploy({ flags }) {
|
|
|
150
152
|
deploymentId: '',
|
|
151
153
|
target,
|
|
152
154
|
source: remoteDeploy ? 'remote git deploy' : 'local upload',
|
|
155
|
+
framework: projectProfile.framework,
|
|
156
|
+
sourceUpload: { mode: remoteDeploy ? 'remote' : 'pending', parts: 0 },
|
|
153
157
|
packagesUploaded: 0,
|
|
154
158
|
filesIncluded: 0,
|
|
155
159
|
deploymentSizeBytes: 0,
|
|
@@ -200,23 +204,46 @@ export async function deploy({ flags }) {
|
|
|
200
204
|
indicator.setPhase('packaging');
|
|
201
205
|
const zipPath = join(tmpdir(), `glash-deploy-${Date.now()}.zip`);
|
|
202
206
|
try {
|
|
203
|
-
const
|
|
207
|
+
const packageOverrides = await packageOverridesForProfile(process.cwd(), projectProfile);
|
|
208
|
+
const bundle = await zipDirectory(process.cwd(), zipPath, {
|
|
209
|
+
ignore: assetSummary.offloaded ? ['public/**'] : [],
|
|
210
|
+
includeGlashOutput: projectProfile.framework === 'glashjs',
|
|
211
|
+
overrides: packageOverrides,
|
|
212
|
+
});
|
|
204
213
|
const zipStat = await stat(zipPath);
|
|
205
214
|
report.filesIncluded = bundle.fileCount ?? 0;
|
|
206
215
|
report.deploymentSizeBytes = zipStat.size || bundle.bytes || 0;
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
216
|
+
const uploadMeta = {
|
|
217
|
+
commitHash: commitHash || 'local-upload',
|
|
218
|
+
gitBranch: gitBranch || '',
|
|
219
|
+
target,
|
|
220
|
+
framework: projectProfile.framework,
|
|
221
|
+
flags,
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
if (shouldUseChunkedSourceUpload(zipStat.size, flags)) {
|
|
225
|
+
indicator.setPhase('uploading');
|
|
226
|
+
const staged = await uploadSourceBundleStaged(projectId, zipPath, zipStat, uploadMeta, indicator);
|
|
227
|
+
dep = staged.deployment;
|
|
228
|
+
report.source = 'local upload (chunked)';
|
|
229
|
+
report.sourceUpload = { mode: 'chunked', parts: staged.partsUploaded, sha256: staged.sha256 };
|
|
230
|
+
report.packagesUploaded = staged.partsUploaded;
|
|
231
|
+
} else {
|
|
232
|
+
const formData = new FormData();
|
|
233
|
+
formData.append('projectId', projectId);
|
|
234
|
+
formData.append('file', await openAsBlob(zipPath, { type: 'application/zip' }), 'project.zip');
|
|
235
|
+
formData.append('commitHash', uploadMeta.commitHash);
|
|
236
|
+
formData.append('gitBranch', uploadMeta.gitBranch);
|
|
237
|
+
formData.append('target', uploadMeta.target);
|
|
238
|
+
if (uploadMeta.framework) formData.append('framework', uploadMeta.framework);
|
|
239
|
+
|
|
240
|
+
indicator.setPhase('glashing');
|
|
241
|
+
dep = await apiPost('/deployments/upload', formData);
|
|
242
|
+
report.sourceUpload = { mode: 'direct', parts: 1 };
|
|
243
|
+
report.packagesUploaded = 1;
|
|
244
|
+
}
|
|
217
245
|
report.deploymentId = dep?.id ?? '';
|
|
218
246
|
report.statusUrl = deployMonitorUrl({ deploymentId: dep?.id, projectId, after: monitorStartedAt });
|
|
219
|
-
report.packagesUploaded = 1;
|
|
220
247
|
await rm(zipPath, { force: true }).catch(() => {});
|
|
221
248
|
} catch (e) {
|
|
222
249
|
await rm(zipPath, { force: true }).catch(() => {});
|
|
@@ -227,7 +254,7 @@ export async function deploy({ flags }) {
|
|
|
227
254
|
} else {
|
|
228
255
|
indicator.setPhase('hatching');
|
|
229
256
|
try {
|
|
230
|
-
dep = await apiPost('/deployments', { projectId, commitHash, gitBranch, target });
|
|
257
|
+
dep = await apiPost('/deployments', { projectId, commitHash, gitBranch, target, framework: projectProfile.framework });
|
|
231
258
|
report.deploymentId = dep?.id ?? '';
|
|
232
259
|
report.statusUrl = deployMonitorUrl({ deploymentId: dep?.id, projectId, after: monitorStartedAt });
|
|
233
260
|
} catch (e) {
|
|
@@ -470,6 +497,119 @@ async function uploadMultipartPublicAsset(asset, local) {
|
|
|
470
497
|
}
|
|
471
498
|
}
|
|
472
499
|
|
|
500
|
+
async function uploadSourceBundleStaged(projectId, zipPath, zipStat, meta, indicator) {
|
|
501
|
+
const sizeBytes = zipStat.size;
|
|
502
|
+
const sha256 = await hashFile(zipPath);
|
|
503
|
+
let prepared;
|
|
504
|
+
try {
|
|
505
|
+
prepared = await apiPost('/deployments/source-upload/prepare', {
|
|
506
|
+
projectId,
|
|
507
|
+
fileName: 'project.zip',
|
|
508
|
+
sizeBytes,
|
|
509
|
+
sha256,
|
|
510
|
+
contentType: 'application/zip',
|
|
511
|
+
framework: meta.framework,
|
|
512
|
+
}, { timeoutMs: 120_000 });
|
|
513
|
+
} catch (e) {
|
|
514
|
+
throw sourceUploadFailure(e);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
if (!prepared?.uploadPartUrl || !prepared?.completeUrl || !prepared?.uploadId) {
|
|
518
|
+
throw new Error('Chunked source upload was not prepared by this GlashDB API.');
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const partSize = sourceUploadPartSize(prepared.partSize);
|
|
522
|
+
const blob = await openAsBlob(zipPath, { type: 'application/zip' });
|
|
523
|
+
const chunks = [];
|
|
524
|
+
for (let offset = 0, partNumber = 1; offset < sizeBytes; offset += partSize, partNumber += 1) {
|
|
525
|
+
chunks.push({
|
|
526
|
+
partNumber,
|
|
527
|
+
offset,
|
|
528
|
+
end: Math.min(offset + partSize, sizeBytes),
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const parts = [];
|
|
533
|
+
let uploadedBytes = 0;
|
|
534
|
+
const concurrency = sourceUploadConcurrency(meta.flags);
|
|
535
|
+
await mapWithConcurrency(chunks, concurrency, async ({ partNumber, offset, end }) => {
|
|
536
|
+
const partUrl = withQueryParam(prepared.uploadPartUrl, 'partNumber', String(partNumber));
|
|
537
|
+
const res = await fetchWithRetries(partUrl, {
|
|
538
|
+
method: 'PUT',
|
|
539
|
+
headers: { 'Content-Type': 'application/octet-stream' },
|
|
540
|
+
body: blob.slice(offset, end, 'application/octet-stream'),
|
|
541
|
+
}, { timeoutMs: uploadTimeoutMsFor(end - offset) });
|
|
542
|
+
if (!res.ok) {
|
|
543
|
+
const detail = await res.text().catch(() => '');
|
|
544
|
+
throw new Error(`Chunked source upload part ${partNumber} failed with HTTP ${res.status}${detail ? `: ${detail.slice(0, 200)}` : ''}`);
|
|
545
|
+
}
|
|
546
|
+
let data;
|
|
547
|
+
try { data = await res.json(); } catch { data = {}; }
|
|
548
|
+
const part = data?.part;
|
|
549
|
+
if (!part?.etag || !Number.isInteger(Number(part.partNumber))) {
|
|
550
|
+
throw new Error(`Chunked source upload part ${partNumber} did not return a multipart ETag`);
|
|
551
|
+
}
|
|
552
|
+
parts.push({ partNumber: Number(part.partNumber), etag: String(part.etag) });
|
|
553
|
+
uploadedBytes += (end - offset);
|
|
554
|
+
const percent = 28 + Math.round((uploadedBytes / sizeBytes) * 14);
|
|
555
|
+
indicator?.setProgress?.(percent, 'uploading');
|
|
556
|
+
});
|
|
557
|
+
parts.sort((a, b) => a.partNumber - b.partNumber);
|
|
558
|
+
|
|
559
|
+
const completed = await fetchWithRetries(prepared.completeUrl, {
|
|
560
|
+
method: 'POST',
|
|
561
|
+
headers: { 'Content-Type': 'application/json' },
|
|
562
|
+
body: JSON.stringify({ parts }),
|
|
563
|
+
}, { timeoutMs: 120_000 });
|
|
564
|
+
if (!completed.ok) {
|
|
565
|
+
const detail = await completed.text().catch(() => '');
|
|
566
|
+
throw new Error(`Completing chunked source upload failed with HTTP ${completed.status}${detail ? `: ${detail.slice(0, 200)}` : ''}`);
|
|
567
|
+
}
|
|
568
|
+
let completion;
|
|
569
|
+
try { completion = await completed.json(); } catch { completion = {}; }
|
|
570
|
+
if (!completion?.sourceUrl) {
|
|
571
|
+
throw new Error('Chunked source upload completed without a staged source URL.');
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
indicator?.setProgress?.(43, 'hatching');
|
|
575
|
+
const deployment = await apiPost('/deployments/source-upload/commit', {
|
|
576
|
+
projectId,
|
|
577
|
+
sourceUrl: completion.sourceUrl,
|
|
578
|
+
sha256,
|
|
579
|
+
sizeBytes: String(sizeBytes),
|
|
580
|
+
commitHash: meta.commitHash,
|
|
581
|
+
gitBranch: meta.gitBranch,
|
|
582
|
+
target: meta.target,
|
|
583
|
+
framework: meta.framework,
|
|
584
|
+
}, { timeoutMs: 120_000 });
|
|
585
|
+
|
|
586
|
+
return { deployment, partsUploaded: parts.length, sha256 };
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
async function mapWithConcurrency(items, concurrency, worker) {
|
|
590
|
+
const limit = Math.max(1, Math.min(Number(concurrency) || 1, items.length || 1));
|
|
591
|
+
let next = 0;
|
|
592
|
+
const runners = Array.from({ length: limit }, async () => {
|
|
593
|
+
while (next < items.length) {
|
|
594
|
+
const item = items[next];
|
|
595
|
+
next += 1;
|
|
596
|
+
await worker(item);
|
|
597
|
+
}
|
|
598
|
+
});
|
|
599
|
+
await Promise.all(runners);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function sourceUploadFailure(error) {
|
|
603
|
+
const message = error?.message || String(error);
|
|
604
|
+
if (error?.status === 404 && /source-upload|Cannot POST/i.test(message)) {
|
|
605
|
+
return new Error('This GlashDB edge does not support chunked source upload yet. Deploy the router/backend source-upload update, then retry.');
|
|
606
|
+
}
|
|
607
|
+
if (error?.status === 503 && /storage is not configured/i.test(message)) {
|
|
608
|
+
return new Error('Chunked source upload storage is not configured on this GlashDB API.');
|
|
609
|
+
}
|
|
610
|
+
return error instanceof Error ? error : new Error(message);
|
|
611
|
+
}
|
|
612
|
+
|
|
473
613
|
async function fetchWithRetries(url, init, { retries = 3, timeoutMs = 120_000 } = {}) {
|
|
474
614
|
let attempt = 0;
|
|
475
615
|
// eslint-disable-next-line no-constant-condition
|
|
@@ -513,7 +653,7 @@ function uploadTimeoutMsFor(bytes) {
|
|
|
513
653
|
function publicAssetPartSize(value) {
|
|
514
654
|
const parsed = Number(value);
|
|
515
655
|
if (Number.isFinite(parsed) && parsed >= 5 * 1024 * 1024) return Math.round(parsed);
|
|
516
|
-
return
|
|
656
|
+
return 80 * 1024 * 1024;
|
|
517
657
|
}
|
|
518
658
|
|
|
519
659
|
function assetOffloadFailure(error, assetSummary) {
|
|
@@ -542,6 +682,103 @@ function assetMode(flags) {
|
|
|
542
682
|
throw new Error(`Unsupported --asset-mode "${raw}". Use inline, auto, or offload.`);
|
|
543
683
|
}
|
|
544
684
|
|
|
685
|
+
function sourceUploadMode(flags) {
|
|
686
|
+
const raw = String(flags['source-upload'] ?? process.env.GLASH_SOURCE_UPLOAD_MODE ?? 'auto').toLowerCase();
|
|
687
|
+
if (['auto', 'direct', 'chunked'].includes(raw)) return raw;
|
|
688
|
+
throw new Error(`Unsupported --source-upload "${raw}". Use auto, direct, or chunked.`);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
function shouldUseChunkedSourceUpload(sizeBytes, flags) {
|
|
692
|
+
const mode = sourceUploadMode(flags);
|
|
693
|
+
if (mode === 'direct') return false;
|
|
694
|
+
if (mode === 'chunked') return true;
|
|
695
|
+
return Number(sizeBytes) >= sourceUploadThresholdBytes(flags);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function sourceUploadThresholdBytes(flags) {
|
|
699
|
+
return parseByteSize(flags['source-threshold'] ?? process.env.GLASH_SOURCE_UPLOAD_THRESHOLD ?? '90mb');
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
function sourceUploadPartSize(value) {
|
|
703
|
+
const parsed = Number(value);
|
|
704
|
+
if (Number.isFinite(parsed) && parsed >= 5 * 1024 * 1024) return Math.round(parsed);
|
|
705
|
+
return 80 * 1024 * 1024;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
function sourceUploadConcurrency(flags = {}) {
|
|
709
|
+
const raw = flags['source-concurrency'] ?? process.env.GLASH_SOURCE_UPLOAD_CONCURRENCY ?? '4';
|
|
710
|
+
const parsed = Number(raw);
|
|
711
|
+
if (!Number.isFinite(parsed) || parsed < 1) return 4;
|
|
712
|
+
return Math.max(1, Math.min(8, Math.floor(parsed)));
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
async function detectProjectProfile(cwd, flags = {}) {
|
|
716
|
+
const forced = frameworkHint(flags);
|
|
717
|
+
if (forced) return { framework: forced, label: forced };
|
|
718
|
+
const pkg = await readPackageJson(cwd);
|
|
719
|
+
const deps = { ...(pkg?.dependencies ?? {}), ...(pkg?.devDependencies ?? {}) };
|
|
720
|
+
const scripts = pkg?.scripts ?? {};
|
|
721
|
+
const hasNextConfig = await existsAny(cwd, ['next.config.js', 'next.config.mjs', 'next.config.cjs', 'next.config.ts']);
|
|
722
|
+
const hasGlashConfig = await existsAny(cwd, ['glash.config.mjs', 'glash.config.js', 'glash.config.cjs']);
|
|
723
|
+
const scriptText = Object.values(scripts).join('\n');
|
|
724
|
+
if (deps.next || hasNextConfig || /\bnext\s+(build|dev|start)\b/i.test(scriptText)) return { framework: 'next', label: 'Next.js' };
|
|
725
|
+
if (deps.glashjs || hasGlashConfig || /\bglash\s+(build|serve|dev)\b/i.test(scriptText)) return { framework: 'glashjs', label: 'glashjs' };
|
|
726
|
+
if (deps.nuxt || deps.nuxt3) return { framework: 'nuxt', label: 'Nuxt' };
|
|
727
|
+
if (deps.vite) return { framework: 'vite', label: 'Vite' };
|
|
728
|
+
if (deps['@remix-run/dev'] || deps.remix) return { framework: 'remix', label: 'Remix' };
|
|
729
|
+
if (deps.astro) return { framework: 'astro', label: 'Astro' };
|
|
730
|
+
if (deps.svelte || deps['@sveltejs/kit']) return { framework: 'sveltekit', label: 'SvelteKit' };
|
|
731
|
+
if (deps.express || deps.fastify || deps['@nestjs/core']) return { framework: 'node', label: 'Node.js' };
|
|
732
|
+
if (pkg) return { framework: 'node', label: 'Node.js' };
|
|
733
|
+
return { framework: 'static', label: 'Static' };
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
async function packageOverridesForProfile(cwd, profile) {
|
|
737
|
+
if (profile?.framework !== 'glashjs') return {};
|
|
738
|
+
const pkg = await readPackageJson(cwd);
|
|
739
|
+
if (!pkg) return {};
|
|
740
|
+
const scripts = { ...(pkg.scripts ?? {}) };
|
|
741
|
+
let changed = false;
|
|
742
|
+
if (!scripts.build) {
|
|
743
|
+
scripts.build = 'glash build';
|
|
744
|
+
changed = true;
|
|
745
|
+
}
|
|
746
|
+
if (!scripts.start) {
|
|
747
|
+
scripts.start = 'glash serve --port ${PORT:-3000}';
|
|
748
|
+
changed = true;
|
|
749
|
+
}
|
|
750
|
+
if (!changed) return {};
|
|
751
|
+
return {
|
|
752
|
+
'package.json': `${JSON.stringify({ ...pkg, scripts }, null, 2)}\n`,
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
function frameworkHint(flags = {}) {
|
|
757
|
+
const raw = String(flags.framework ?? flags.runtime ?? '').trim().toLowerCase();
|
|
758
|
+
if (!raw) return '';
|
|
759
|
+
const aliases = { nextjs: 'next', glash: 'glashjs', glashdb: 'glashjs', react: 'vite' };
|
|
760
|
+
const normalized = aliases[raw] ?? raw;
|
|
761
|
+
return /^[a-z0-9._-]{1,40}$/.test(normalized) ? normalized : '';
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
async function readPackageJson(cwd) {
|
|
765
|
+
try {
|
|
766
|
+
return JSON.parse(await readFile(join(cwd, 'package.json'), 'utf8'));
|
|
767
|
+
} catch {
|
|
768
|
+
return null;
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
async function existsAny(cwd, files) {
|
|
773
|
+
for (const file of files) {
|
|
774
|
+
try {
|
|
775
|
+
await access(join(cwd, file));
|
|
776
|
+
return true;
|
|
777
|
+
} catch {}
|
|
778
|
+
}
|
|
779
|
+
return false;
|
|
780
|
+
}
|
|
781
|
+
|
|
545
782
|
async function publicAssetFiles(cwd) {
|
|
546
783
|
const entries = await fg('public/**/*', {
|
|
547
784
|
cwd,
|
|
@@ -620,7 +857,7 @@ function parseByteSize(value) {
|
|
|
620
857
|
if (typeof value === 'number') return value;
|
|
621
858
|
const raw = String(value || '').trim().toLowerCase();
|
|
622
859
|
const match = raw.match(/^(\d+(?:\.\d+)?)\s*(b|kb|kib|mb|mib|gb|gib)?$/);
|
|
623
|
-
if (!match) throw new Error(`Invalid
|
|
860
|
+
if (!match) throw new Error(`Invalid size "${value}"`);
|
|
624
861
|
const amount = Number(match[1]);
|
|
625
862
|
const unit = match[2] || 'b';
|
|
626
863
|
const multiplier = unit === 'gb' || unit === 'gib'
|
|
@@ -805,6 +1042,7 @@ function printDeployReport(report) {
|
|
|
805
1042
|
['Deployment ID', report.deploymentId || '-'],
|
|
806
1043
|
['Target', report.target || 'production'],
|
|
807
1044
|
['Source', report.source || '-'],
|
|
1045
|
+
['Framework', report.framework || 'auto'],
|
|
808
1046
|
['Packages uploaded', formatPackagesUploaded(report)],
|
|
809
1047
|
['Public assets', formatAssetSummary(report.assetSummary)],
|
|
810
1048
|
['Deployment size', formatDeploymentSize(report)],
|
|
@@ -833,6 +1071,14 @@ function formatReportStatus(status) {
|
|
|
833
1071
|
|
|
834
1072
|
function formatPackagesUploaded(report) {
|
|
835
1073
|
const count = report.packagesUploaded ?? 0;
|
|
1074
|
+
if (report.sourceUpload?.mode === 'chunked') {
|
|
1075
|
+
const files = report.filesIncluded ? ` (${report.filesIncluded.toLocaleString()} files included)` : '';
|
|
1076
|
+
return `${count.toLocaleString()} source chunk${count === 1 ? '' : 's'}${files}`;
|
|
1077
|
+
}
|
|
1078
|
+
if (report.sourceUpload?.mode === 'direct') {
|
|
1079
|
+
const files = report.filesIncluded ? ` (${report.filesIncluded.toLocaleString()} files included)` : '';
|
|
1080
|
+
return `1 source bundle${files}`;
|
|
1081
|
+
}
|
|
836
1082
|
const label = count === 1 ? 'source bundle' : 'source bundles';
|
|
837
1083
|
const files = report.filesIncluded
|
|
838
1084
|
? ` (${report.filesIncluded.toLocaleString()} files included)`
|
|
@@ -949,6 +1195,9 @@ function createDeployIndicator() {
|
|
|
949
1195
|
if (!nextPhase) return;
|
|
950
1196
|
setProgress(progressFromPhase(nextPhase), nextPhase);
|
|
951
1197
|
},
|
|
1198
|
+
setProgress(nextProgress, nextPhase = phase) {
|
|
1199
|
+
setProgress(nextProgress, nextPhase);
|
|
1200
|
+
},
|
|
952
1201
|
finish(finalPhase = 'complete') {
|
|
953
1202
|
stopTimer();
|
|
954
1203
|
phase = finalPhase;
|
|
@@ -978,6 +1227,7 @@ function progressFromPhase(phase) {
|
|
|
978
1227
|
syncing: 8,
|
|
979
1228
|
assets: 15,
|
|
980
1229
|
packaging: 22,
|
|
1230
|
+
uploading: 28,
|
|
981
1231
|
glashing: 35,
|
|
982
1232
|
hatching: 45,
|
|
983
1233
|
waiting: 48,
|
package/lib/zip.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import { createWriteStream } from 'node:fs';
|
|
|
4
4
|
import { join, relative } from 'node:path';
|
|
5
5
|
import fs from 'fs-extra';
|
|
6
6
|
|
|
7
|
-
export async function zipDirectory(sourceDir, outPath, { ignore = [] } = {}) {
|
|
7
|
+
export async function zipDirectory(sourceDir, outPath, { ignore = [], includeGlashOutput = false, overrides = {} } = {}) {
|
|
8
8
|
// Default ignores
|
|
9
9
|
const defaultIgnore = [
|
|
10
10
|
'node_modules/**',
|
|
@@ -32,7 +32,7 @@ export async function zipDirectory(sourceDir, outPath, { ignore = [] } = {}) {
|
|
|
32
32
|
'build/**',
|
|
33
33
|
'**/build/**',
|
|
34
34
|
'out/**',
|
|
35
|
-
'**/out/**',
|
|
35
|
+
...(includeGlashOutput ? [] : ['**/out/**']),
|
|
36
36
|
'coverage/**',
|
|
37
37
|
'**/coverage/**',
|
|
38
38
|
'.turbo/**',
|
|
@@ -43,8 +43,19 @@ export async function zipDirectory(sourceDir, outPath, { ignore = [] } = {}) {
|
|
|
43
43
|
'**/.parcel-cache/**',
|
|
44
44
|
'.vite/**',
|
|
45
45
|
'**/.vite/**',
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
...(includeGlashOutput
|
|
47
|
+
? [
|
|
48
|
+
'.glash/project.json',
|
|
49
|
+
'**/.glash/project.json',
|
|
50
|
+
'.glash/credentials.json',
|
|
51
|
+
'**/.glash/credentials.json',
|
|
52
|
+
'.glash/cache/**',
|
|
53
|
+
'**/.glash/cache/**',
|
|
54
|
+
]
|
|
55
|
+
: [
|
|
56
|
+
'.glash/**',
|
|
57
|
+
'**/.glash/**',
|
|
58
|
+
]),
|
|
48
59
|
'*.log',
|
|
49
60
|
'**/*.log',
|
|
50
61
|
'npm-debug.log*',
|
|
@@ -63,20 +74,21 @@ export async function zipDirectory(sourceDir, outPath, { ignore = [] } = {}) {
|
|
|
63
74
|
'**/.env.development',
|
|
64
75
|
];
|
|
65
76
|
const allIgnore = [...new Set([...defaultIgnore, ...loadIgnorePatterns(sourceDir), ...ignore])];
|
|
66
|
-
const
|
|
77
|
+
const overridePaths = new Set(Object.keys(overrides));
|
|
78
|
+
const files = (await fg('**/*', {
|
|
67
79
|
cwd: sourceDir,
|
|
68
80
|
ignore: allIgnore,
|
|
69
81
|
dot: true,
|
|
70
82
|
onlyFiles: true,
|
|
71
83
|
followSymbolicLinks: false,
|
|
72
|
-
});
|
|
84
|
+
})).filter((file) => !overridePaths.has(file));
|
|
73
85
|
|
|
74
86
|
return new Promise((resolve, reject) => {
|
|
75
87
|
const zip = new yazl.ZipFile();
|
|
76
88
|
const output = createWriteStream(outPath);
|
|
77
89
|
let bytes = 0;
|
|
78
90
|
|
|
79
|
-
output.on('close', () => resolve({ bytes, fileCount: files.length, files }));
|
|
91
|
+
output.on('close', () => resolve({ bytes, fileCount: files.length + overridePaths.size, files: [...files, ...overridePaths] }));
|
|
80
92
|
output.on('error', reject);
|
|
81
93
|
zip.outputStream.on('data', (chunk) => { bytes += chunk.length; });
|
|
82
94
|
zip.outputStream.on('error', reject);
|
|
@@ -85,6 +97,9 @@ export async function zipDirectory(sourceDir, outPath, { ignore = [] } = {}) {
|
|
|
85
97
|
for (const file of files) {
|
|
86
98
|
zip.addFile(join(sourceDir, file), file);
|
|
87
99
|
}
|
|
100
|
+
for (const [file, content] of Object.entries(overrides)) {
|
|
101
|
+
zip.addBuffer(Buffer.isBuffer(content) ? content : Buffer.from(String(content)), file);
|
|
102
|
+
}
|
|
88
103
|
zip.end();
|
|
89
104
|
});
|
|
90
105
|
}
|
package/package.json
CHANGED