@netlify/spark-ui 1.25.0 → 1.26.0-alpha.2
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/assets/DropZone.css +1 -0
- package/dist/chunks/{AskNetlify.jaxhKBOK.js → AskNetlify.bx0blyGV.js} +124 -126
- package/dist/chunks/_commonjsHelpers.C6fGbg64.js +6 -0
- package/dist/chunks/browser.D58rdxmc.js +444 -0
- package/dist/components/index.d.ts +3 -0
- package/dist/components/preact/AskNetlify/AskNetlify.js +1 -1
- package/dist/components/preact/DropZone/DropZone.d.ts +19 -0
- package/dist/components/preact/DropZone/DropZone.js +207 -0
- package/dist/components/preact/DropZone/drop-core/client.d.ts +19 -0
- package/dist/components/preact/DropZone/drop-core/client.js +41 -0
- package/dist/components/preact/DropZone/drop-core/deploy.d.ts +6 -0
- package/dist/components/preact/DropZone/drop-core/deploy.js +30 -0
- package/dist/components/preact/DropZone/drop-core/digest.d.ts +3 -0
- package/dist/components/preact/DropZone/drop-core/digest.js +12 -0
- package/dist/components/preact/DropZone/drop-core/index.d.ts +5 -0
- package/dist/components/preact/DropZone/drop-core/index.js +16 -0
- package/dist/components/preact/DropZone/drop-core/readFiles.d.ts +15 -0
- package/dist/components/preact/DropZone/drop-core/readFiles.js +616 -0
- package/dist/components/preact/DropZone/drop-core/types.d.ts +30 -0
- package/dist/components/preact/DropZone/drop-core/types.js +1 -0
- package/dist/components/preact/SearchModal/components/SearchModal/SearchModal.js +1 -1
- package/dist/components/preact/index.js +1 -1
- package/package.json +4 -2
- package/packages/components/index.ts +16 -0
- package/packages/components/preact/DropZone/DropZone.css +340 -0
- package/packages/components/preact/DropZone/DropZone.tsx +295 -0
- package/packages/components/preact/DropZone/drop-core/client.ts +61 -0
- package/packages/components/preact/DropZone/drop-core/deploy.ts +55 -0
- package/packages/components/preact/DropZone/drop-core/digest.ts +11 -0
- package/packages/components/preact/DropZone/drop-core/index.ts +5 -0
- package/packages/components/preact/DropZone/drop-core/readFiles.ts +136 -0
- package/packages/components/preact/DropZone/drop-core/types.ts +35 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { getDigest, getShas } from './digest';
|
|
2
|
+
import type { DropClient } from './client';
|
|
3
|
+
import type { DeployProgress, DeployResult, ProcessedFile } from './types';
|
|
4
|
+
|
|
5
|
+
const MAX_CONCURRENT_UPLOADS = 5;
|
|
6
|
+
|
|
7
|
+
async function uploadRequired(
|
|
8
|
+
client: DropClient,
|
|
9
|
+
deployId: string,
|
|
10
|
+
files: ProcessedFile[],
|
|
11
|
+
required: Set<string>,
|
|
12
|
+
token: string,
|
|
13
|
+
onUploaded?: (uploaded: number) => void
|
|
14
|
+
) {
|
|
15
|
+
const queue = files.filter(f => required.has(f.sha));
|
|
16
|
+
let cursor = 0;
|
|
17
|
+
let done = 0;
|
|
18
|
+
const worker = async () => {
|
|
19
|
+
while (cursor < queue.length) {
|
|
20
|
+
const f = queue[cursor++];
|
|
21
|
+
await client.uploadFile(deployId, f.path, f.content, token);
|
|
22
|
+
onUploaded?.(++done);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
await Promise.all(Array.from({ length: Math.min(MAX_CONCURRENT_UPLOADS, queue.length) }, worker));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface DeployOptions {
|
|
29
|
+
onProgress?: (e: DeployProgress) => void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function deployFiles(
|
|
33
|
+
client: DropClient,
|
|
34
|
+
files: ProcessedFile[],
|
|
35
|
+
opts: DeployOptions = {}
|
|
36
|
+
): Promise<DeployResult> {
|
|
37
|
+
if (!files.length) throw new Error('No files to deploy');
|
|
38
|
+
const { onProgress } = opts;
|
|
39
|
+
|
|
40
|
+
onProgress?.({ phase: 'creating' });
|
|
41
|
+
const token = await client.getToken();
|
|
42
|
+
const deploy = await client.createDeploy(getDigest(files), token);
|
|
43
|
+
|
|
44
|
+
const total = deploy.required.length;
|
|
45
|
+
onProgress?.({ phase: 'uploading', uploaded: 0, total });
|
|
46
|
+
await uploadRequired(client, deploy.id, files, getShas(deploy.required), token, uploaded =>
|
|
47
|
+
onProgress?.({ phase: 'uploading', uploaded, total })
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
onProgress?.({ phase: 'processing' });
|
|
51
|
+
await client.waitUntilReady(deploy, token);
|
|
52
|
+
|
|
53
|
+
onProgress?.({ phase: 'ready' });
|
|
54
|
+
return { deploy, token, url: `https://${deploy.subdomain}.netlify.app` };
|
|
55
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Digest, ProcessedFile } from './types';
|
|
2
|
+
|
|
3
|
+
export function getDigest(files: ProcessedFile[]): Digest {
|
|
4
|
+
const digest: Digest = {};
|
|
5
|
+
for (const f of files) digest[f.path] = f.sha;
|
|
6
|
+
return digest;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function getShas(required: string[]): Set<string> {
|
|
10
|
+
return new Set(required);
|
|
11
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import Rusha from 'rusha';
|
|
2
|
+
import type { ProcessedFile } from './types';
|
|
3
|
+
|
|
4
|
+
/* eslint-disable @typescript-eslint/no-explicit-any -- webkit FileSystem entry APIs are untyped */
|
|
5
|
+
|
|
6
|
+
const rusha = new Rusha();
|
|
7
|
+
|
|
8
|
+
const IGNORED = (name: string) =>
|
|
9
|
+
name.startsWith('.') || name === '__MACOSX' || name === 'node_modules';
|
|
10
|
+
|
|
11
|
+
const slashPrefix = (p: string) => (p.startsWith('/') ? p : `/${p}`);
|
|
12
|
+
|
|
13
|
+
function toProcessedFile(path: string, content: ArrayBuffer, name: string): ProcessedFile {
|
|
14
|
+
return {
|
|
15
|
+
path: slashPrefix(path),
|
|
16
|
+
size: content.byteLength,
|
|
17
|
+
sha: rusha.digest(content),
|
|
18
|
+
content,
|
|
19
|
+
name,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// --- folder drop (webkit entries) -------------------------------------------
|
|
24
|
+
|
|
25
|
+
async function readFileEntry(entry: any): Promise<ProcessedFile> {
|
|
26
|
+
const file: File = await new Promise((res, rej) => entry.file(res, rej));
|
|
27
|
+
const content = await file.arrayBuffer();
|
|
28
|
+
return toProcessedFile(entry.fullPath, content, file.name);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function readDirEntry(entry: any): Promise<ProcessedFile[]> {
|
|
32
|
+
const reader = entry.createReader();
|
|
33
|
+
const out: ProcessedFile[] = [];
|
|
34
|
+
// readEntries yields in batches; loop until empty
|
|
35
|
+
for (;;) {
|
|
36
|
+
const batch: any[] = await new Promise((res, rej) => reader.readEntries(res, rej));
|
|
37
|
+
if (!batch.length) break;
|
|
38
|
+
for (const child of batch) {
|
|
39
|
+
if (IGNORED(child.name)) continue;
|
|
40
|
+
out.push(...(child.isDirectory ? await readDirEntry(child) : [await readFileEntry(child)]));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Read a DataTransfer from a drop event into ProcessedFiles.
|
|
48
|
+
*
|
|
49
|
+
* NOTE: `webkitGetAsEntry()` must be called synchronously inside the drop
|
|
50
|
+
* handler — the DataTransfer is detached once the event turn ends. The
|
|
51
|
+
* component captures entries first (see `readDroppedEntries`) then calls the
|
|
52
|
+
* async readers, so this function is safe to `await`.
|
|
53
|
+
*/
|
|
54
|
+
export async function readDataTransfer(dt: DataTransfer): Promise<ProcessedFile[]> {
|
|
55
|
+
// single zip?
|
|
56
|
+
const first = dt.files?.[0];
|
|
57
|
+
if (dt.files.length === 1 && first && /\.zip$/i.test(first.name)) {
|
|
58
|
+
return readZip(first);
|
|
59
|
+
}
|
|
60
|
+
const items = Array.from(dt.items ?? []);
|
|
61
|
+
const entries = items.map(i => i.webkitGetAsEntry?.()).filter(Boolean) as any[];
|
|
62
|
+
if (entries.length) {
|
|
63
|
+
const nested = await Promise.all(
|
|
64
|
+
entries.map(e =>
|
|
65
|
+
IGNORED(e.name) ? [] : e.isDirectory ? readDirEntry(e) : readFileEntry(e).then(f => [f])
|
|
66
|
+
)
|
|
67
|
+
);
|
|
68
|
+
return normalizeFiles(nested.flat());
|
|
69
|
+
}
|
|
70
|
+
// fallback: plain FileList (e.g. multi-file <input>)
|
|
71
|
+
return readFileList(dt.files);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// --- <input type=file> ------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
export async function readFileList(files: FileList): Promise<ProcessedFile[]> {
|
|
77
|
+
const list = Array.from(files);
|
|
78
|
+
if (list.length === 1 && /\.zip$/i.test(list[0].name)) {
|
|
79
|
+
return readZip(list[0]);
|
|
80
|
+
}
|
|
81
|
+
const out: ProcessedFile[] = [];
|
|
82
|
+
for (const file of list) {
|
|
83
|
+
const rel = (file as any).webkitRelativePath || file.name;
|
|
84
|
+
out.push(toProcessedFile(rel, await file.arrayBuffer(), file.name));
|
|
85
|
+
}
|
|
86
|
+
return normalizeFiles(out);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// --- zip --------------------------------------------------------------------
|
|
90
|
+
|
|
91
|
+
export async function readZip(zip: File): Promise<ProcessedFile[]> {
|
|
92
|
+
const { unzip } = await import('fflate');
|
|
93
|
+
const buf = new Uint8Array(await zip.arrayBuffer());
|
|
94
|
+
const entries = await new Promise<Record<string, Uint8Array>>((resolve, reject) =>
|
|
95
|
+
unzip(buf, (err, data) => (err ? reject(err) : resolve(data)))
|
|
96
|
+
);
|
|
97
|
+
const out: ProcessedFile[] = [];
|
|
98
|
+
for (const [path, bytes] of Object.entries(entries)) {
|
|
99
|
+
// fflate uses a trailing slash to signal directory entries; also skip the
|
|
100
|
+
// usual junk (macOS metadata, dotfiles at any depth).
|
|
101
|
+
if (path.endsWith('/') || /(^|\/)(__MACOSX\/|\.)/.test(path)) continue;
|
|
102
|
+
const content = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
|
103
|
+
out.push(toProcessedFile(path, content, path.split('/').pop()!));
|
|
104
|
+
}
|
|
105
|
+
return normalizeFiles(out);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// --- path normalization (strip the common root folder) ----------------------
|
|
109
|
+
|
|
110
|
+
export function normalizeFiles(files: ProcessedFile[]): ProcessedFile[] {
|
|
111
|
+
if (!files.length) return files;
|
|
112
|
+
const split = (p: string) => slashPrefix(p).split('/').slice(1); // drop leading ''
|
|
113
|
+
let common = split(files[0].path).slice(0, -1);
|
|
114
|
+
for (const f of files) {
|
|
115
|
+
const segs = split(f.path);
|
|
116
|
+
let i = 0;
|
|
117
|
+
while (i < common.length && i < segs.length - 1 && common[i] === segs[i]) i++;
|
|
118
|
+
common = common.slice(0, i);
|
|
119
|
+
}
|
|
120
|
+
if (!common.length) return files;
|
|
121
|
+
const prefix = '/' + common.join('/');
|
|
122
|
+
return files.map(f => ({ ...f, path: slashPrefix(f.path.slice(prefix.length)) }));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// --- index.html validation (UI can surface these as warnings) ---------------
|
|
126
|
+
|
|
127
|
+
export function getSingleNonIndexHtmlFileName(files: ProcessedFile[]): string | null {
|
|
128
|
+
const html = files.filter(f => /\.html?$/i.test(f.name));
|
|
129
|
+
if (html.some(f => /^index\.html?$/i.test(f.name))) return null;
|
|
130
|
+
return html.length === 1 ? html[0].name : null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function hasMultipleHtmlFilesWithoutIndex(files: ProcessedFile[]): boolean {
|
|
134
|
+
const html = files.filter(f => /\.html?$/i.test(f.name));
|
|
135
|
+
return html.length > 1 && !html.some(f => /^index\.html?$/i.test(f.name));
|
|
136
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export interface ProcessedFile {
|
|
2
|
+
/** POSIX path relative to the deploy root, leading slash, e.g. "/index.html" */
|
|
3
|
+
path: string;
|
|
4
|
+
size: number;
|
|
5
|
+
/** lowercase hex SHA-1 of the file content */
|
|
6
|
+
sha: string;
|
|
7
|
+
content: ArrayBuffer;
|
|
8
|
+
name: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type Digest = Record<string, string>; // { "/index.html": "<sha1>" }
|
|
12
|
+
|
|
13
|
+
export interface Deploy {
|
|
14
|
+
id: string;
|
|
15
|
+
/** the *.netlify.app subdomain assigned to the anonymous site */
|
|
16
|
+
subdomain: string;
|
|
17
|
+
/** SHAs the backend does NOT yet have — only these get uploaded */
|
|
18
|
+
required: string[];
|
|
19
|
+
state?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface DeployResult {
|
|
23
|
+
deploy: Deploy;
|
|
24
|
+
/** drop token — persist this to later claim the site into an account */
|
|
25
|
+
token: string;
|
|
26
|
+
url: string; // https://<subdomain>.netlify.app
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type DeployPhase = 'creating' | 'uploading' | 'processing' | 'ready';
|
|
30
|
+
|
|
31
|
+
export interface DeployProgress {
|
|
32
|
+
phase: DeployPhase;
|
|
33
|
+
uploaded?: number;
|
|
34
|
+
total?: number;
|
|
35
|
+
}
|