@glash/cli 0.2.32 → 0.2.33
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 +3 -0
- package/commands/deploy.mjs +231 -15
- 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,9 @@ ${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
|
+
--framework <name> Override framework detection (glashjs, next, vite, etc.)
|
|
103
106
|
--help, -h Show this help
|
|
104
107
|
--version, -v Show CLI version
|
|
105
108
|
|
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,45 @@ 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
|
+
};
|
|
222
|
+
|
|
223
|
+
if (shouldUseChunkedSourceUpload(zipStat.size, flags)) {
|
|
224
|
+
indicator.setPhase('uploading');
|
|
225
|
+
const staged = await uploadSourceBundleStaged(projectId, zipPath, zipStat, uploadMeta, indicator);
|
|
226
|
+
dep = staged.deployment;
|
|
227
|
+
report.source = 'local upload (chunked)';
|
|
228
|
+
report.sourceUpload = { mode: 'chunked', parts: staged.partsUploaded, sha256: staged.sha256 };
|
|
229
|
+
report.packagesUploaded = staged.partsUploaded;
|
|
230
|
+
} else {
|
|
231
|
+
const formData = new FormData();
|
|
232
|
+
formData.append('projectId', projectId);
|
|
233
|
+
formData.append('file', await openAsBlob(zipPath, { type: 'application/zip' }), 'project.zip');
|
|
234
|
+
formData.append('commitHash', uploadMeta.commitHash);
|
|
235
|
+
formData.append('gitBranch', uploadMeta.gitBranch);
|
|
236
|
+
formData.append('target', uploadMeta.target);
|
|
237
|
+
if (uploadMeta.framework) formData.append('framework', uploadMeta.framework);
|
|
238
|
+
|
|
239
|
+
indicator.setPhase('glashing');
|
|
240
|
+
dep = await apiPost('/deployments/upload', formData);
|
|
241
|
+
report.sourceUpload = { mode: 'direct', parts: 1 };
|
|
242
|
+
report.packagesUploaded = 1;
|
|
243
|
+
}
|
|
217
244
|
report.deploymentId = dep?.id ?? '';
|
|
218
245
|
report.statusUrl = deployMonitorUrl({ deploymentId: dep?.id, projectId, after: monitorStartedAt });
|
|
219
|
-
report.packagesUploaded = 1;
|
|
220
246
|
await rm(zipPath, { force: true }).catch(() => {});
|
|
221
247
|
} catch (e) {
|
|
222
248
|
await rm(zipPath, { force: true }).catch(() => {});
|
|
@@ -227,7 +253,7 @@ export async function deploy({ flags }) {
|
|
|
227
253
|
} else {
|
|
228
254
|
indicator.setPhase('hatching');
|
|
229
255
|
try {
|
|
230
|
-
dep = await apiPost('/deployments', { projectId, commitHash, gitBranch, target });
|
|
256
|
+
dep = await apiPost('/deployments', { projectId, commitHash, gitBranch, target, framework: projectProfile.framework });
|
|
231
257
|
report.deploymentId = dep?.id ?? '';
|
|
232
258
|
report.statusUrl = deployMonitorUrl({ deploymentId: dep?.id, projectId, after: monitorStartedAt });
|
|
233
259
|
} catch (e) {
|
|
@@ -470,6 +496,96 @@ async function uploadMultipartPublicAsset(asset, local) {
|
|
|
470
496
|
}
|
|
471
497
|
}
|
|
472
498
|
|
|
499
|
+
async function uploadSourceBundleStaged(projectId, zipPath, zipStat, meta, indicator) {
|
|
500
|
+
const sizeBytes = zipStat.size;
|
|
501
|
+
const sha256 = await hashFile(zipPath);
|
|
502
|
+
let prepared;
|
|
503
|
+
try {
|
|
504
|
+
prepared = await apiPost('/deployments/source-upload/prepare', {
|
|
505
|
+
projectId,
|
|
506
|
+
fileName: 'project.zip',
|
|
507
|
+
sizeBytes,
|
|
508
|
+
sha256,
|
|
509
|
+
contentType: 'application/zip',
|
|
510
|
+
framework: meta.framework,
|
|
511
|
+
}, { timeoutMs: 120_000 });
|
|
512
|
+
} catch (e) {
|
|
513
|
+
throw sourceUploadFailure(e);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
if (!prepared?.uploadPartUrl || !prepared?.completeUrl || !prepared?.uploadId) {
|
|
517
|
+
throw new Error('Chunked source upload was not prepared by this GlashDB API.');
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const partSize = sourceUploadPartSize(prepared.partSize);
|
|
521
|
+
const blob = await openAsBlob(zipPath, { type: 'application/zip' });
|
|
522
|
+
const parts = [];
|
|
523
|
+
let partNumber = 1;
|
|
524
|
+
for (let offset = 0; offset < sizeBytes; offset += partSize) {
|
|
525
|
+
const end = Math.min(offset + partSize, sizeBytes);
|
|
526
|
+
const partUrl = withQueryParam(prepared.uploadPartUrl, 'partNumber', String(partNumber));
|
|
527
|
+
const res = await fetchWithRetries(partUrl, {
|
|
528
|
+
method: 'PUT',
|
|
529
|
+
headers: { 'Content-Type': 'application/octet-stream' },
|
|
530
|
+
body: blob.slice(offset, end, 'application/octet-stream'),
|
|
531
|
+
}, { timeoutMs: uploadTimeoutMsFor(end - offset) });
|
|
532
|
+
if (!res.ok) {
|
|
533
|
+
const detail = await res.text().catch(() => '');
|
|
534
|
+
throw new Error(`Chunked source upload part ${partNumber} failed with HTTP ${res.status}${detail ? `: ${detail.slice(0, 200)}` : ''}`);
|
|
535
|
+
}
|
|
536
|
+
let data;
|
|
537
|
+
try { data = await res.json(); } catch { data = {}; }
|
|
538
|
+
const part = data?.part;
|
|
539
|
+
if (!part?.etag || !Number.isInteger(Number(part.partNumber))) {
|
|
540
|
+
throw new Error(`Chunked source upload part ${partNumber} did not return a multipart ETag`);
|
|
541
|
+
}
|
|
542
|
+
parts.push({ partNumber: Number(part.partNumber), etag: String(part.etag) });
|
|
543
|
+
const percent = 28 + Math.round((end / sizeBytes) * 14);
|
|
544
|
+
indicator?.setProgress?.(percent, 'uploading');
|
|
545
|
+
partNumber += 1;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const completed = await fetchWithRetries(prepared.completeUrl, {
|
|
549
|
+
method: 'POST',
|
|
550
|
+
headers: { 'Content-Type': 'application/json' },
|
|
551
|
+
body: JSON.stringify({ parts }),
|
|
552
|
+
}, { timeoutMs: 120_000 });
|
|
553
|
+
if (!completed.ok) {
|
|
554
|
+
const detail = await completed.text().catch(() => '');
|
|
555
|
+
throw new Error(`Completing chunked source upload failed with HTTP ${completed.status}${detail ? `: ${detail.slice(0, 200)}` : ''}`);
|
|
556
|
+
}
|
|
557
|
+
let completion;
|
|
558
|
+
try { completion = await completed.json(); } catch { completion = {}; }
|
|
559
|
+
if (!completion?.sourceUrl) {
|
|
560
|
+
throw new Error('Chunked source upload completed without a staged source URL.');
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
indicator?.setProgress?.(43, 'hatching');
|
|
564
|
+
const deployment = await apiPost('/deployments/source-upload/commit', {
|
|
565
|
+
projectId,
|
|
566
|
+
sourceUrl: completion.sourceUrl,
|
|
567
|
+
sha256,
|
|
568
|
+
sizeBytes: String(sizeBytes),
|
|
569
|
+
commitHash: meta.commitHash,
|
|
570
|
+
gitBranch: meta.gitBranch,
|
|
571
|
+
target: meta.target,
|
|
572
|
+
framework: meta.framework,
|
|
573
|
+
}, { timeoutMs: 120_000 });
|
|
574
|
+
|
|
575
|
+
return { deployment, partsUploaded: parts.length, sha256 };
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function sourceUploadFailure(error) {
|
|
579
|
+
const message = error?.message || String(error);
|
|
580
|
+
if (error?.status === 404 && /source-upload|Cannot POST/i.test(message)) {
|
|
581
|
+
return new Error('This GlashDB edge does not support chunked source upload yet. Deploy the router/backend source-upload update, then retry.');
|
|
582
|
+
}
|
|
583
|
+
if (error?.status === 503 && /storage is not configured/i.test(message)) {
|
|
584
|
+
return new Error('Chunked source upload storage is not configured on this GlashDB API.');
|
|
585
|
+
}
|
|
586
|
+
return error instanceof Error ? error : new Error(message);
|
|
587
|
+
}
|
|
588
|
+
|
|
473
589
|
async function fetchWithRetries(url, init, { retries = 3, timeoutMs = 120_000 } = {}) {
|
|
474
590
|
let attempt = 0;
|
|
475
591
|
// eslint-disable-next-line no-constant-condition
|
|
@@ -542,6 +658,93 @@ function assetMode(flags) {
|
|
|
542
658
|
throw new Error(`Unsupported --asset-mode "${raw}". Use inline, auto, or offload.`);
|
|
543
659
|
}
|
|
544
660
|
|
|
661
|
+
function sourceUploadMode(flags) {
|
|
662
|
+
const raw = String(flags['source-upload'] ?? process.env.GLASH_SOURCE_UPLOAD_MODE ?? 'auto').toLowerCase();
|
|
663
|
+
if (['auto', 'direct', 'chunked'].includes(raw)) return raw;
|
|
664
|
+
throw new Error(`Unsupported --source-upload "${raw}". Use auto, direct, or chunked.`);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function shouldUseChunkedSourceUpload(sizeBytes, flags) {
|
|
668
|
+
const mode = sourceUploadMode(flags);
|
|
669
|
+
if (mode === 'direct') return false;
|
|
670
|
+
if (mode === 'chunked') return true;
|
|
671
|
+
return Number(sizeBytes) >= sourceUploadThresholdBytes(flags);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function sourceUploadThresholdBytes(flags) {
|
|
675
|
+
return parseByteSize(flags['source-threshold'] ?? process.env.GLASH_SOURCE_UPLOAD_THRESHOLD ?? '90mb');
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function sourceUploadPartSize(value) {
|
|
679
|
+
const parsed = Number(value);
|
|
680
|
+
if (Number.isFinite(parsed) && parsed >= 5 * 1024 * 1024) return Math.round(parsed);
|
|
681
|
+
return 16 * 1024 * 1024;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
async function detectProjectProfile(cwd, flags = {}) {
|
|
685
|
+
const forced = frameworkHint(flags);
|
|
686
|
+
if (forced) return { framework: forced, label: forced };
|
|
687
|
+
const pkg = await readPackageJson(cwd);
|
|
688
|
+
const deps = { ...(pkg?.dependencies ?? {}), ...(pkg?.devDependencies ?? {}) };
|
|
689
|
+
const hasGlashConfig = await existsAny(cwd, ['glash.config.mjs', 'glash.config.js', 'glash.config.cjs']);
|
|
690
|
+
if (deps.glashjs || hasGlashConfig) return { framework: 'glashjs', label: 'glashjs' };
|
|
691
|
+
if (deps.next) return { framework: 'next', label: 'Next.js' };
|
|
692
|
+
if (deps.nuxt || deps.nuxt3) return { framework: 'nuxt', label: 'Nuxt' };
|
|
693
|
+
if (deps.vite) return { framework: 'vite', label: 'Vite' };
|
|
694
|
+
if (deps['@remix-run/dev'] || deps.remix) return { framework: 'remix', label: 'Remix' };
|
|
695
|
+
if (deps.astro) return { framework: 'astro', label: 'Astro' };
|
|
696
|
+
if (deps.svelte || deps['@sveltejs/kit']) return { framework: 'sveltekit', label: 'SvelteKit' };
|
|
697
|
+
if (deps.express || deps.fastify || deps['@nestjs/core']) return { framework: 'node', label: 'Node.js' };
|
|
698
|
+
if (pkg) return { framework: 'node', label: 'Node.js' };
|
|
699
|
+
return { framework: 'static', label: 'Static' };
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
async function packageOverridesForProfile(cwd, profile) {
|
|
703
|
+
if (profile?.framework !== 'glashjs') return {};
|
|
704
|
+
const pkg = await readPackageJson(cwd);
|
|
705
|
+
if (!pkg) return {};
|
|
706
|
+
const scripts = { ...(pkg.scripts ?? {}) };
|
|
707
|
+
let changed = false;
|
|
708
|
+
if (!scripts.build) {
|
|
709
|
+
scripts.build = 'glash build';
|
|
710
|
+
changed = true;
|
|
711
|
+
}
|
|
712
|
+
if (!scripts.start) {
|
|
713
|
+
scripts.start = 'glash serve --port ${PORT:-3000}';
|
|
714
|
+
changed = true;
|
|
715
|
+
}
|
|
716
|
+
if (!changed) return {};
|
|
717
|
+
return {
|
|
718
|
+
'package.json': `${JSON.stringify({ ...pkg, scripts }, null, 2)}\n`,
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function frameworkHint(flags = {}) {
|
|
723
|
+
const raw = String(flags.framework ?? flags.runtime ?? '').trim().toLowerCase();
|
|
724
|
+
if (!raw) return '';
|
|
725
|
+
const aliases = { nextjs: 'next', glash: 'glashjs', glashdb: 'glashjs', react: 'vite' };
|
|
726
|
+
const normalized = aliases[raw] ?? raw;
|
|
727
|
+
return /^[a-z0-9._-]{1,40}$/.test(normalized) ? normalized : '';
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
async function readPackageJson(cwd) {
|
|
731
|
+
try {
|
|
732
|
+
return JSON.parse(await readFile(join(cwd, 'package.json'), 'utf8'));
|
|
733
|
+
} catch {
|
|
734
|
+
return null;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
async function existsAny(cwd, files) {
|
|
739
|
+
for (const file of files) {
|
|
740
|
+
try {
|
|
741
|
+
await access(join(cwd, file));
|
|
742
|
+
return true;
|
|
743
|
+
} catch {}
|
|
744
|
+
}
|
|
745
|
+
return false;
|
|
746
|
+
}
|
|
747
|
+
|
|
545
748
|
async function publicAssetFiles(cwd) {
|
|
546
749
|
const entries = await fg('public/**/*', {
|
|
547
750
|
cwd,
|
|
@@ -620,7 +823,7 @@ function parseByteSize(value) {
|
|
|
620
823
|
if (typeof value === 'number') return value;
|
|
621
824
|
const raw = String(value || '').trim().toLowerCase();
|
|
622
825
|
const match = raw.match(/^(\d+(?:\.\d+)?)\s*(b|kb|kib|mb|mib|gb|gib)?$/);
|
|
623
|
-
if (!match) throw new Error(`Invalid
|
|
826
|
+
if (!match) throw new Error(`Invalid size "${value}"`);
|
|
624
827
|
const amount = Number(match[1]);
|
|
625
828
|
const unit = match[2] || 'b';
|
|
626
829
|
const multiplier = unit === 'gb' || unit === 'gib'
|
|
@@ -805,6 +1008,7 @@ function printDeployReport(report) {
|
|
|
805
1008
|
['Deployment ID', report.deploymentId || '-'],
|
|
806
1009
|
['Target', report.target || 'production'],
|
|
807
1010
|
['Source', report.source || '-'],
|
|
1011
|
+
['Framework', report.framework || 'auto'],
|
|
808
1012
|
['Packages uploaded', formatPackagesUploaded(report)],
|
|
809
1013
|
['Public assets', formatAssetSummary(report.assetSummary)],
|
|
810
1014
|
['Deployment size', formatDeploymentSize(report)],
|
|
@@ -833,6 +1037,14 @@ function formatReportStatus(status) {
|
|
|
833
1037
|
|
|
834
1038
|
function formatPackagesUploaded(report) {
|
|
835
1039
|
const count = report.packagesUploaded ?? 0;
|
|
1040
|
+
if (report.sourceUpload?.mode === 'chunked') {
|
|
1041
|
+
const files = report.filesIncluded ? ` (${report.filesIncluded.toLocaleString()} files included)` : '';
|
|
1042
|
+
return `${count.toLocaleString()} source chunk${count === 1 ? '' : 's'}${files}`;
|
|
1043
|
+
}
|
|
1044
|
+
if (report.sourceUpload?.mode === 'direct') {
|
|
1045
|
+
const files = report.filesIncluded ? ` (${report.filesIncluded.toLocaleString()} files included)` : '';
|
|
1046
|
+
return `1 source bundle${files}`;
|
|
1047
|
+
}
|
|
836
1048
|
const label = count === 1 ? 'source bundle' : 'source bundles';
|
|
837
1049
|
const files = report.filesIncluded
|
|
838
1050
|
? ` (${report.filesIncluded.toLocaleString()} files included)`
|
|
@@ -949,6 +1161,9 @@ function createDeployIndicator() {
|
|
|
949
1161
|
if (!nextPhase) return;
|
|
950
1162
|
setProgress(progressFromPhase(nextPhase), nextPhase);
|
|
951
1163
|
},
|
|
1164
|
+
setProgress(nextProgress, nextPhase = phase) {
|
|
1165
|
+
setProgress(nextProgress, nextPhase);
|
|
1166
|
+
},
|
|
952
1167
|
finish(finalPhase = 'complete') {
|
|
953
1168
|
stopTimer();
|
|
954
1169
|
phase = finalPhase;
|
|
@@ -978,6 +1193,7 @@ function progressFromPhase(phase) {
|
|
|
978
1193
|
syncing: 8,
|
|
979
1194
|
assets: 15,
|
|
980
1195
|
packaging: 22,
|
|
1196
|
+
uploading: 28,
|
|
981
1197
|
glashing: 35,
|
|
982
1198
|
hatching: 45,
|
|
983
1199
|
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