@netlify/spark-ui 1.31.0-alpha.2 → 1.31.0-alpha.4
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/components/index.d.ts +2 -2
- package/dist/components/preact/DropZone/DropZone.d.ts +28 -2
- package/dist/components/preact/DropZone/DropZone.js +58 -51
- package/dist/components/preact/DropZone/DropZone.test.d.ts +1 -0
- package/dist/components/preact/DropZone/index.d.ts +3 -3
- package/dist/components/preact/DropZone/index.js +25 -23
- package/dist/components/preact/DropZone/useDropDeploy.d.ts +11 -6
- package/dist/components/preact/DropZone/useDropDeploy.js +183 -129
- package/dist/components/preact/DropZone/useDropDeploy.test.d.ts +1 -0
- package/dist/components/preact/index.d.ts +2 -2
- package/dist/components/preact/index.js +25 -19
- package/dist/drop-core/analytics.d.ts +107 -0
- package/dist/drop-core/analytics.js +21 -0
- package/dist/drop-core/analytics.test.d.ts +1 -0
- package/dist/drop-core/authedApi.d.ts +55 -0
- package/dist/drop-core/authedApi.js +35 -0
- package/dist/drop-core/authedApi.test.d.ts +1 -0
- package/dist/drop-core/authedDropClient.d.ts +100 -0
- package/dist/drop-core/authedDropClient.js +109 -0
- package/dist/drop-core/authedDropClient.test.d.ts +1 -0
- package/dist/{components/preact/DropZone/drop-core → drop-core}/buildClient.d.ts +32 -26
- package/dist/drop-core/buildClient.js +84 -0
- package/dist/drop-core/buildClient.test.d.ts +1 -0
- package/dist/drop-core/buildStash.d.ts +37 -0
- package/dist/drop-core/buildStash.js +84 -0
- package/dist/drop-core/buildStash.test.d.ts +0 -0
- package/dist/{components/preact/DropZone/drop-core → drop-core}/deploy.d.ts +7 -0
- package/dist/drop-core/deploy.js +53 -0
- package/dist/drop-core/deploy.test.d.ts +1 -0
- package/dist/drop-core/detectBuild.test.d.ts +1 -0
- package/dist/drop-core/digest.test.d.ts +1 -0
- package/dist/{components/preact/DropZone/drop-core → drop-core}/errors.js +1 -1
- package/dist/drop-core/errors.test.d.ts +1 -0
- package/dist/drop-core/fileTypes.test.d.ts +1 -0
- package/dist/{components/preact/DropZone/drop-core → drop-core}/index.d.ts +3 -0
- package/dist/drop-core/index.js +51 -0
- package/dist/{components/preact/DropZone/drop-core → drop-core}/readFiles.js +2 -2
- package/dist/drop-core/readFiles.test.d.ts +1 -0
- package/dist/{components/preact/DropZone/drop-core → drop-core}/types.d.ts +10 -0
- package/dist/{components/preact/DropZone/drop-core → drop-core}/zip.js +1 -1
- package/dist/drop-core/zip.test.d.ts +1 -0
- package/package.json +4 -1
- package/packages/components/index.ts +19 -0
- package/packages/components/preact/DropZone/DropZone.test.ts +71 -0
- package/packages/components/preact/DropZone/DropZone.tsx +32 -2
- package/packages/components/preact/DropZone/index.tsx +14 -3
- package/packages/components/preact/DropZone/useDropDeploy.test.ts +376 -0
- package/packages/components/preact/DropZone/useDropDeploy.ts +305 -134
- package/packages/components/preact/index.ts +19 -2
- package/packages/drop-core/analytics.test.ts +17 -0
- package/packages/drop-core/analytics.ts +122 -0
- package/packages/drop-core/authedApi.test.ts +100 -0
- package/packages/drop-core/authedApi.ts +94 -0
- package/packages/drop-core/authedDropClient.test.ts +196 -0
- package/packages/drop-core/authedDropClient.ts +210 -0
- package/packages/drop-core/buildClient.test.ts +112 -0
- package/packages/{components/preact/DropZone/drop-core → drop-core}/buildClient.ts +55 -70
- package/packages/drop-core/buildStash.test.ts +85 -0
- package/packages/drop-core/buildStash.ts +185 -0
- package/packages/drop-core/deploy.test.ts +135 -0
- package/packages/{components/preact/DropZone/drop-core → drop-core}/deploy.ts +18 -4
- package/packages/drop-core/detectBuild.test.ts +139 -0
- package/packages/drop-core/digest.test.ts +24 -0
- package/packages/drop-core/errors.test.ts +69 -0
- package/packages/{components/preact/DropZone/drop-core → drop-core}/errors.ts +1 -1
- package/packages/drop-core/fileTypes.test.ts +37 -0
- package/packages/{components/preact/DropZone/drop-core → drop-core}/index.ts +3 -0
- package/packages/drop-core/readFiles.test.ts +125 -0
- package/packages/{components/preact/DropZone/drop-core → drop-core}/types.ts +12 -6
- package/packages/drop-core/zip.test.ts +92 -0
- package/dist/components/preact/DropZone/drop-core/authedDropClient.d.ts +0 -77
- package/dist/components/preact/DropZone/drop-core/authedDropClient.js +0 -97
- package/dist/components/preact/DropZone/drop-core/buildClient.js +0 -92
- package/dist/components/preact/DropZone/drop-core/deploy.js +0 -48
- package/dist/components/preact/DropZone/drop-core/index.js +0 -36
- package/packages/components/preact/DropZone/drop-core/authedDropClient.ts +0 -185
- /package/dist/{components/preact/DropZone/drop-core → drop-core}/client.d.ts +0 -0
- /package/dist/{components/preact/DropZone/drop-core → drop-core}/client.js +0 -0
- /package/dist/{components/preact/DropZone/drop-core → drop-core}/detectBuild.d.ts +0 -0
- /package/dist/{components/preact/DropZone/drop-core → drop-core}/detectBuild.js +0 -0
- /package/dist/{components/preact/DropZone/drop-core → drop-core}/digest.d.ts +0 -0
- /package/dist/{components/preact/DropZone/drop-core → drop-core}/digest.js +0 -0
- /package/dist/{components/preact/DropZone/drop-core → drop-core}/errors.d.ts +0 -0
- /package/dist/{components/preact/DropZone/drop-core → drop-core}/fileTypes.d.ts +0 -0
- /package/dist/{components/preact/DropZone/drop-core → drop-core}/fileTypes.js +0 -0
- /package/dist/{components/preact/DropZone/drop-core → drop-core}/readFiles.d.ts +0 -0
- /package/dist/{components/preact/DropZone/drop-core → drop-core}/types.js +0 -0
- /package/dist/{components/preact/DropZone/drop-core → drop-core}/zip.d.ts +0 -0
- /package/packages/{components/preact/DropZone/drop-core → drop-core}/client.ts +0 -0
- /package/packages/{components/preact/DropZone/drop-core → drop-core}/detectBuild.ts +0 -0
- /package/packages/{components/preact/DropZone/drop-core → drop-core}/digest.ts +0 -0
- /package/packages/{components/preact/DropZone/drop-core → drop-core}/fileTypes.ts +0 -0
- /package/packages/{components/preact/DropZone/drop-core → drop-core}/readFiles.ts +0 -0
- /package/packages/{components/preact/DropZone/drop-core → drop-core}/zip.ts +0 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { zip } from 'fflate';
|
|
3
|
+
import {
|
|
4
|
+
getSingleNonIndexHtmlFileName,
|
|
5
|
+
hasMultipleHtmlFilesWithoutIndex,
|
|
6
|
+
readFileList,
|
|
7
|
+
renameSingleNonIndexHtmlToIndex,
|
|
8
|
+
} from './readFiles';
|
|
9
|
+
import { UnsupportedDropError } from './errors';
|
|
10
|
+
import type { ProcessedFile } from './types';
|
|
11
|
+
|
|
12
|
+
// `readDataTransfer` is deliberately untested: it rides the webkit FileSystem
|
|
13
|
+
// entries API (webkitGetAsEntry / createReader), which no test DOM implements.
|
|
14
|
+
// Its FileList fallback and zip branch share the code paths covered below.
|
|
15
|
+
|
|
16
|
+
const encoder = new TextEncoder();
|
|
17
|
+
|
|
18
|
+
const processed = (path: string, name = path.split('/').pop()!): ProcessedFile => ({
|
|
19
|
+
path,
|
|
20
|
+
name,
|
|
21
|
+
size: 1,
|
|
22
|
+
sha: 'test-sha',
|
|
23
|
+
content: encoder.encode('x').buffer as ArrayBuffer,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
/** A File carrying the relative path a directory-picker <input> would report. */
|
|
27
|
+
const pickedFile = (relativePath: string, contents = 'x'): File => {
|
|
28
|
+
const f = new File([contents], relativePath.split('/').pop()!);
|
|
29
|
+
Object.defineProperty(f, 'webkitRelativePath', { value: relativePath });
|
|
30
|
+
return f;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const asFileList = (files: File[]): FileList =>
|
|
34
|
+
Object.assign(files, { item: (i: number) => files[i] }) as unknown as FileList;
|
|
35
|
+
|
|
36
|
+
const zipBytes = (entries: Record<string, string>): Promise<Uint8Array> =>
|
|
37
|
+
new Promise((resolve, reject) => {
|
|
38
|
+
const encoded: Record<string, Uint8Array> = {};
|
|
39
|
+
for (const [path, contents] of Object.entries(entries))
|
|
40
|
+
encoded[path] = encoder.encode(contents);
|
|
41
|
+
zip(encoded, (err, out) => (err ? reject(err) : resolve(out)));
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('readFileList', () => {
|
|
45
|
+
it('reads a folder selection into root-relative ProcessedFiles', async () => {
|
|
46
|
+
const files = await readFileList(
|
|
47
|
+
asFileList([pickedFile('site/index.html', '<h1>hi</h1>'), pickedFile('site/css/site.css')])
|
|
48
|
+
);
|
|
49
|
+
// The common root folder is stripped so config sits at the deploy root.
|
|
50
|
+
expect(files.map(f => f.path).sort()).toEqual(['/css/site.css', '/index.html']);
|
|
51
|
+
expect(files[0].sha).toMatch(/^[0-9a-f]{40}$/);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('does not strip a shared folder when a file sits beside it at the root', async () => {
|
|
55
|
+
const files = await readFileList(
|
|
56
|
+
asFileList([pickedFile('index.html'), pickedFile('assets/app.js')])
|
|
57
|
+
);
|
|
58
|
+
expect(files.map(f => f.path).sort()).toEqual(['/assets/app.js', '/index.html']);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('rejects a single loose file that is neither a zip nor an HTML page', async () => {
|
|
62
|
+
await expect(readFileList(asFileList([pickedFile('photo.png')]))).rejects.toBeInstanceOf(
|
|
63
|
+
UnsupportedDropError
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('allows a lone HTML page to stand alone', async () => {
|
|
68
|
+
const files = await readFileList(asFileList([pickedFile('page.html', '<p>solo</p>')]));
|
|
69
|
+
expect(files).toHaveLength(1);
|
|
70
|
+
expect(files[0].path).toBe('/page.html');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('expands a single dropped zip, skipping directories and junk entries', async () => {
|
|
74
|
+
const bytes = await zipBytes({
|
|
75
|
+
'my-site/index.html': '<h1>zipped</h1>',
|
|
76
|
+
'my-site/__MACOSX/._index.html': 'resource fork',
|
|
77
|
+
'my-site/.DS_Store': 'junk',
|
|
78
|
+
'my-site/node_modules/pkg/index.js': 'dep',
|
|
79
|
+
'my-site/css/site.css': 'body{}',
|
|
80
|
+
});
|
|
81
|
+
const files = await readFileList(
|
|
82
|
+
asFileList([new File([bytes.buffer.slice(0) as ArrayBuffer], 'my-site.zip')])
|
|
83
|
+
);
|
|
84
|
+
// Junk gone, common "my-site/" root stripped.
|
|
85
|
+
expect(files.map(f => f.path).sort()).toEqual(['/css/site.css', '/index.html']);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe('index.html helpers', () => {
|
|
90
|
+
it('names the lone non-index HTML file, and only that case', () => {
|
|
91
|
+
expect(getSingleNonIndexHtmlFileName([processed('/about.html'), processed('/a.css')])).toBe(
|
|
92
|
+
'about.html'
|
|
93
|
+
);
|
|
94
|
+
expect(
|
|
95
|
+
getSingleNonIndexHtmlFileName([processed('/index.html'), processed('/about.html')])
|
|
96
|
+
).toBeNull();
|
|
97
|
+
expect(getSingleNonIndexHtmlFileName([processed('/a.html'), processed('/b.html')])).toBeNull();
|
|
98
|
+
expect(getSingleNonIndexHtmlFileName([processed('/a.css')])).toBeNull();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('flags multiple HTML files with no index', () => {
|
|
102
|
+
expect(hasMultipleHtmlFilesWithoutIndex([processed('/a.html'), processed('/b.html')])).toBe(
|
|
103
|
+
true
|
|
104
|
+
);
|
|
105
|
+
expect(hasMultipleHtmlFilesWithoutIndex([processed('/index.html'), processed('/b.html')])).toBe(
|
|
106
|
+
false
|
|
107
|
+
);
|
|
108
|
+
expect(hasMultipleHtmlFilesWithoutIndex([processed('/a.html')])).toBe(false);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('renames the lone HTML page to index.html in place, keeping its directory', () => {
|
|
112
|
+
const input = [processed('/docs/about.html'), processed('/docs/site.css')];
|
|
113
|
+
const renamed = renameSingleNonIndexHtmlToIndex(input);
|
|
114
|
+
const page = renamed.find(f => f.name === 'index.html')!;
|
|
115
|
+
expect(page.path).toBe('/docs/index.html');
|
|
116
|
+
// Content and sha untouched — only the served path changes.
|
|
117
|
+
expect(page.sha).toBe(input[0].sha);
|
|
118
|
+
expect(page.content).toBe(input[0].content);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('leaves sets with an index, several pages, or no HTML unchanged', () => {
|
|
122
|
+
const withIndex = [processed('/index.html'), processed('/about.html')];
|
|
123
|
+
expect(renameSingleNonIndexHtmlToIndex(withIndex)).toBe(withIndex);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
@@ -40,15 +40,21 @@ export interface DeployResult {
|
|
|
40
40
|
* zipping → creating → uploading → building → ready.
|
|
41
41
|
*/
|
|
42
42
|
export type DeployPhase =
|
|
43
|
-
| '
|
|
44
|
-
| 'creating'
|
|
45
|
-
| 'uploading'
|
|
46
|
-
| 'processing'
|
|
47
|
-
| 'building'
|
|
48
|
-
| 'ready';
|
|
43
|
+
'zipping' | 'creating' | 'uploading' | 'processing' | 'building' | 'ready';
|
|
49
44
|
|
|
50
45
|
export interface DeployProgress {
|
|
51
46
|
phase: DeployPhase;
|
|
52
47
|
uploaded?: number;
|
|
53
48
|
total?: number;
|
|
54
49
|
}
|
|
50
|
+
|
|
51
|
+
/** The subset of a Netlify Site the drop flow needs to redirect and report. */
|
|
52
|
+
export interface BuildSite {
|
|
53
|
+
/** site id (UUID) */
|
|
54
|
+
id: string;
|
|
55
|
+
/** the *.netlify.app subdomain, also the app's URL segment */
|
|
56
|
+
name: string;
|
|
57
|
+
/** https://<name>.netlify.app */
|
|
58
|
+
ssl_url?: string;
|
|
59
|
+
url?: string;
|
|
60
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { unzip } from 'fflate';
|
|
3
|
+
import { generateNetlifyToml, zipFiles } from './zip';
|
|
4
|
+
import type { ProcessedFile } from './types';
|
|
5
|
+
|
|
6
|
+
const encoder = new TextEncoder();
|
|
7
|
+
const decoder = new TextDecoder();
|
|
8
|
+
|
|
9
|
+
const file = (path: string, contents = 'x'): ProcessedFile => {
|
|
10
|
+
const content = encoder.encode(contents).buffer as ArrayBuffer;
|
|
11
|
+
return { path, content, size: content.byteLength, sha: 'test-sha', name: path.split('/').pop()! };
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const buildableProject = () => [
|
|
15
|
+
file('/index.html', '<h1>hi</h1>'),
|
|
16
|
+
file(
|
|
17
|
+
'/package.json',
|
|
18
|
+
JSON.stringify({ scripts: { build: 'astro build' }, dependencies: { astro: '^4' } })
|
|
19
|
+
),
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
const unzipArchive = async (archive: File): Promise<Record<string, string>> => {
|
|
23
|
+
const bytes = new Uint8Array(await archive.arrayBuffer());
|
|
24
|
+
const entries = await new Promise<Record<string, Uint8Array>>((resolve, reject) =>
|
|
25
|
+
unzip(bytes, (err, data) => (err ? reject(err) : resolve(data)))
|
|
26
|
+
);
|
|
27
|
+
return Object.fromEntries(Object.entries(entries).map(([k, v]) => [k, decoder.decode(v)]));
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
describe('generateNetlifyToml', () => {
|
|
31
|
+
it('renders only command and publish — everything else stays zero-config', () => {
|
|
32
|
+
expect(
|
|
33
|
+
generateNetlifyToml({ command: 'npm run build', publish: 'dist', framework: 'astro' })
|
|
34
|
+
).toBe('[build]\n command = "npm run build"\n publish = "dist"\n');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('escapes TOML basic-string metacharacters', () => {
|
|
38
|
+
const toml = generateNetlifyToml({ command: 'echo "a\\b"', publish: 'dist', framework: 'x' });
|
|
39
|
+
expect(toml).toContain('command = "echo \\"a\\\\b\\""');
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe('zipFiles', () => {
|
|
44
|
+
it('throws on an empty file set', async () => {
|
|
45
|
+
await expect(zipFiles([])).rejects.toThrow('No files to deploy');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('zips files with archive-root-relative paths (no leading slash)', async () => {
|
|
49
|
+
const { file: archive, buildSettings } = await zipFiles([file('/index.html', '<h1>hi</h1>')]);
|
|
50
|
+
expect(archive.name).toBe('source.zip');
|
|
51
|
+
expect(archive.type).toBe('application/zip');
|
|
52
|
+
expect(buildSettings).toBeNull(); // nothing inferable for a static site
|
|
53
|
+
expect(await unzipArchive(archive)).toEqual({ 'index.html': '<h1>hi</h1>' });
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('synthesises a netlify.toml when settings are inferable', async () => {
|
|
57
|
+
const { file: archive, buildSettings } = await zipFiles(buildableProject());
|
|
58
|
+
expect(buildSettings).toEqual({
|
|
59
|
+
command: 'npm run build',
|
|
60
|
+
publish: 'dist',
|
|
61
|
+
framework: 'astro',
|
|
62
|
+
});
|
|
63
|
+
const entries = await unzipArchive(archive);
|
|
64
|
+
expect(entries['netlify.toml']).toContain('[build]');
|
|
65
|
+
expect(entries['netlify.toml']).toContain('publish = "dist"');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('appends to an existing netlify.toml so redirects and headers survive', async () => {
|
|
69
|
+
const existing = '[[redirects]]\n from = "/old"\n to = "/new"\n';
|
|
70
|
+
const { file: archive } = await zipFiles([
|
|
71
|
+
...buildableProject(),
|
|
72
|
+
file('/netlify.toml', existing),
|
|
73
|
+
]);
|
|
74
|
+
const toml = (await unzipArchive(archive))['netlify.toml'];
|
|
75
|
+
expect(toml).toContain('[[redirects]]');
|
|
76
|
+
expect(toml).toContain('[build]');
|
|
77
|
+
expect(toml.indexOf('[[redirects]]')).toBeLessThan(toml.indexOf('[build]'));
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('uploads the drop untouched when synthesizeNetlifyToml is off', async () => {
|
|
81
|
+
const { file: archive, buildSettings } = await zipFiles(buildableProject(), {
|
|
82
|
+
synthesizeNetlifyToml: false,
|
|
83
|
+
});
|
|
84
|
+
expect(buildSettings).toBeNull();
|
|
85
|
+
expect((await unzipArchive(archive))['netlify.toml']).toBeUndefined();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('honors a custom archive name', async () => {
|
|
89
|
+
const { file: archive } = await zipFiles([file('/index.html')], { name: 'drop.zip' });
|
|
90
|
+
expect(archive.name).toBe('drop.zip');
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
import { DropClient } from './client';
|
|
2
|
-
import { Digest, DropResponse } from './types';
|
|
3
|
-
export interface AuthenticatedDropClientConfig {
|
|
4
|
-
/**
|
|
5
|
-
* API base for every call. Requests go straight to `api.netlify.com`, whose
|
|
6
|
-
* `/api/*` CORS policy allows any origin, rather than through a consumer's
|
|
7
|
-
* access-control rewrite — that proxy allowlists paths per origin, and site
|
|
8
|
-
* and deploy creation are not on the list for netlify.com (they 400 with
|
|
9
|
-
* `x-debug: request origin not allowed to call endpoint`).
|
|
10
|
-
*/
|
|
11
|
-
apiBase?: string;
|
|
12
|
-
/**
|
|
13
|
-
* Mint a short-lived bearer token authorizing the calls. On netlify.com this
|
|
14
|
-
* is `GET /access-control/generate-access-control-token`, whose
|
|
15
|
-
* `accessControlToken` the API decrypts back into the user's access token.
|
|
16
|
-
* Resolving `null` means "no session" and surfaces as `NotAuthenticatedError`.
|
|
17
|
-
*/
|
|
18
|
-
getUploadToken: () => Promise<string | null>;
|
|
19
|
-
/** Create the site in this account. Omit to use the user's default account. */
|
|
20
|
-
accountSlug?: string;
|
|
21
|
-
/** How often to re-read the deploy while waiting. Default 1s. */
|
|
22
|
-
pollIntervalMs?: number;
|
|
23
|
-
/** How long to wait before giving up on the deploy. Default 10 minutes. */
|
|
24
|
-
timeoutMs?: number;
|
|
25
|
-
}
|
|
26
|
-
/**
|
|
27
|
-
* Authenticated drop client for logged-in visitors. Unlike `AnonymousDropClient`
|
|
28
|
-
* — which creates an account-less site the visitor must later claim — this
|
|
29
|
-
* deploys straight into the visitor's account via the sites/deploys API,
|
|
30
|
-
* mirroring what app.netlify.com does for a logged-in drag-and-drop deploy.
|
|
31
|
-
* There is no claim step, so the caller should hand off to the site's dashboard
|
|
32
|
-
* page rather than the claim page.
|
|
33
|
-
*
|
|
34
|
-
* Every request goes direct to `api.netlify.com` with the short-lived bearer
|
|
35
|
-
* from `getUploadToken()`. Routing the JSON calls through a consumer's
|
|
36
|
-
* access-control rewrite instead (cookie session, no token in the browser)
|
|
37
|
-
* would be the tighter design, but that proxy allowlists paths per origin and
|
|
38
|
-
* site/deploy creation isn't allowed from netlify.com — it 400s before
|
|
39
|
-
* reaching the API. Revisit if that allowlist gains these paths.
|
|
40
|
-
*
|
|
41
|
-
* Error-mapping invariant: only pre-mutation failures — a null upload token, or
|
|
42
|
-
* a 401/403 creating the site — throw `NotAuthenticatedError`, which is the
|
|
43
|
-
* signal the drop flow may safely retry anonymously. Once the site exists, an
|
|
44
|
-
* auth failure throws a plain status-carrying error instead: falling back at
|
|
45
|
-
* that point would strand an empty site in the account *and* deploy a duplicate
|
|
46
|
-
* anonymously.
|
|
47
|
-
*/
|
|
48
|
-
export declare class AuthenticatedDropClient implements DropClient {
|
|
49
|
-
private apiBase;
|
|
50
|
-
private getUploadToken;
|
|
51
|
-
private accountSlug?;
|
|
52
|
-
private pollIntervalMs;
|
|
53
|
-
private timeoutMs;
|
|
54
|
-
constructor({ apiBase, getUploadToken, accountSlug, pollIntervalMs, timeoutMs, }: AuthenticatedDropClientConfig);
|
|
55
|
-
/**
|
|
56
|
-
* The token here is the upload bearer, not a drop token — it authorizes every
|
|
57
|
-
* call this client makes. Minted once per deploy, before anything is created,
|
|
58
|
-
* so a missing session fails the whole attempt cleanly.
|
|
59
|
-
*/
|
|
60
|
-
getToken(): Promise<string>;
|
|
61
|
-
/**
|
|
62
|
-
* Two calls: create the site in the visitor's account, then the deploy on it.
|
|
63
|
-
* Mapped into the anonymous `DropResponse` shape so `deployFiles` runs
|
|
64
|
-
* unchanged over either client.
|
|
65
|
-
*/
|
|
66
|
-
createDeploy(files: Digest, token: string): Promise<DropResponse>;
|
|
67
|
-
uploadFile(deployId: string, path: string, content: ArrayBuffer, token: string): Promise<void>;
|
|
68
|
-
/**
|
|
69
|
-
* Poll until the deploy settles: resolve on `ready`, throw on
|
|
70
|
-
* `error`/`rejected`, throw on timeout. Reuses the bearer minted at the start
|
|
71
|
-
* of the deploy, so a deploy that outlives the token's TTL will start seeing
|
|
72
|
-
* 401s here — those are ignored like any other non-ok read and the loop runs
|
|
73
|
-
* to its timeout rather than failing fast. Acceptable while static deploys
|
|
74
|
-
* finish in seconds; re-mint per poll if that stops being true.
|
|
75
|
-
*/
|
|
76
|
-
waitUntilReady(deploy: DropResponse, token: string): Promise<void>;
|
|
77
|
-
}
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
var c = Object.defineProperty;
|
|
2
|
-
var h = (r, t, e) => t in r ? c(r, t, { enumerable: !0, configurable: !0, writable: !0, value: e }) : r[t] = e;
|
|
3
|
-
var n = (r, t, e) => h(r, typeof t != "symbol" ? t + "" : t, e);
|
|
4
|
-
import { apiError as l } from "./client.js";
|
|
5
|
-
import { NotAuthenticatedError as p, SiteCreateError as u } from "./errors.js";
|
|
6
|
-
const y = "https://api.netlify.com/api/v1", w = 1e3, f = 10 * 60 * 1e3, m = /* @__PURE__ */ new Set(["ready", "error", "rejected"]);
|
|
7
|
-
class g {
|
|
8
|
-
constructor({
|
|
9
|
-
apiBase: t = y,
|
|
10
|
-
getUploadToken: e,
|
|
11
|
-
accountSlug: i,
|
|
12
|
-
pollIntervalMs: o = w,
|
|
13
|
-
timeoutMs: s = f
|
|
14
|
-
}) {
|
|
15
|
-
n(this, "apiBase");
|
|
16
|
-
n(this, "getUploadToken");
|
|
17
|
-
n(this, "accountSlug");
|
|
18
|
-
n(this, "pollIntervalMs");
|
|
19
|
-
n(this, "timeoutMs");
|
|
20
|
-
this.apiBase = t, this.getUploadToken = e, this.accountSlug = i, this.pollIntervalMs = o, this.timeoutMs = s;
|
|
21
|
-
}
|
|
22
|
-
/**
|
|
23
|
-
* The token here is the upload bearer, not a drop token — it authorizes every
|
|
24
|
-
* call this client makes. Minted once per deploy, before anything is created,
|
|
25
|
-
* so a missing session fails the whole attempt cleanly.
|
|
26
|
-
*/
|
|
27
|
-
async getToken() {
|
|
28
|
-
const t = await this.getUploadToken();
|
|
29
|
-
if (!t) throw new p();
|
|
30
|
-
return t;
|
|
31
|
-
}
|
|
32
|
-
/**
|
|
33
|
-
* Two calls: create the site in the visitor's account, then the deploy on it.
|
|
34
|
-
* Mapped into the anonymous `DropResponse` shape so `deployFiles` runs
|
|
35
|
-
* unchanged over either client.
|
|
36
|
-
*/
|
|
37
|
-
async createDeploy(t, e) {
|
|
38
|
-
const i = this.accountSlug ? `/${this.accountSlug}/sites` : "/sites", o = await fetch(`${this.apiBase}${i}`, {
|
|
39
|
-
method: "POST",
|
|
40
|
-
headers: { "Content-Type": "application/json", Authorization: `Bearer ${e}` },
|
|
41
|
-
body: JSON.stringify({ created_via: "drop" })
|
|
42
|
-
});
|
|
43
|
-
if (o.status === 401 || o.status === 403) throw new p();
|
|
44
|
-
if (!o.ok) throw new u(o.status);
|
|
45
|
-
const s = await o.json(), a = await fetch(`${this.apiBase}/sites/${s.id}/deploys`, {
|
|
46
|
-
method: "POST",
|
|
47
|
-
headers: { "Content-Type": "application/json", Authorization: `Bearer ${e}` },
|
|
48
|
-
body: JSON.stringify({ files: t, deploy_source: "drop" })
|
|
49
|
-
});
|
|
50
|
-
if (!a.ok) throw l("deploy create", a.status);
|
|
51
|
-
const d = await a.json();
|
|
52
|
-
return {
|
|
53
|
-
id: s.id,
|
|
54
|
-
deploy_id: d.id,
|
|
55
|
-
subdomain: s.name,
|
|
56
|
-
required: d.required ?? []
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
async uploadFile(t, e, i, o) {
|
|
60
|
-
const s = await fetch(`${this.apiBase}/deploys/${t}/files${e}`, {
|
|
61
|
-
method: "PUT",
|
|
62
|
-
headers: { "Content-Type": "application/octet-stream", Authorization: `Bearer ${o}` },
|
|
63
|
-
body: i
|
|
64
|
-
});
|
|
65
|
-
if (!s.ok) throw l(`upload ${e}`, s.status);
|
|
66
|
-
}
|
|
67
|
-
/**
|
|
68
|
-
* Poll until the deploy settles: resolve on `ready`, throw on
|
|
69
|
-
* `error`/`rejected`, throw on timeout. Reuses the bearer minted at the start
|
|
70
|
-
* of the deploy, so a deploy that outlives the token's TTL will start seeing
|
|
71
|
-
* 401s here — those are ignored like any other non-ok read and the loop runs
|
|
72
|
-
* to its timeout rather than failing fast. Acceptable while static deploys
|
|
73
|
-
* finish in seconds; re-mint per poll if that stops being true.
|
|
74
|
-
*/
|
|
75
|
-
async waitUntilReady(t, e) {
|
|
76
|
-
const i = this.timeoutMs / this.pollIntervalMs;
|
|
77
|
-
for (let o = 0; o < i; o++) {
|
|
78
|
-
const s = await fetch(`${this.apiBase}/deploys/${t.deploy_id}`, {
|
|
79
|
-
headers: { Authorization: `Bearer ${e}` }
|
|
80
|
-
});
|
|
81
|
-
if (s.ok) {
|
|
82
|
-
const a = await s.json();
|
|
83
|
-
if (a.state && m.has(a.state)) {
|
|
84
|
-
if (a.state === "ready") return;
|
|
85
|
-
throw new Error(
|
|
86
|
-
a.error_message ? `deploy failed: ${a.error_message}` : "deploy failed"
|
|
87
|
-
);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
await new Promise((a) => setTimeout(a, this.pollIntervalMs));
|
|
91
|
-
}
|
|
92
|
-
throw new Error("Deploy did not become ready in time");
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
export {
|
|
96
|
-
g as AuthenticatedDropClient
|
|
97
|
-
};
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
var h = Object.defineProperty;
|
|
2
|
-
var d = (n, t, o) => t in n ? h(n, t, { enumerable: !0, configurable: !0, writable: !0, value: o }) : n[t] = o;
|
|
3
|
-
var r = (n, t, o) => d(n, typeof t != "symbol" ? t + "" : t, o);
|
|
4
|
-
import { NotAuthenticatedError as l, SiteCreateError as c, BuildCreateError as p, BuildFailedError as u, BuildTimeoutError as w } from "./errors.js";
|
|
5
|
-
const f = "https://api.netlify.com/api/v1", m = 1e3, y = 10 * 60 * 1e3, B = /* @__PURE__ */ new Set(["ready", "error", "rejected"]);
|
|
6
|
-
class g {
|
|
7
|
-
constructor({
|
|
8
|
-
apiBase: t = f,
|
|
9
|
-
getUploadToken: o,
|
|
10
|
-
accountSlug: e,
|
|
11
|
-
title: s = "Build from drop deployment",
|
|
12
|
-
pollIntervalMs: i = m,
|
|
13
|
-
timeoutMs: a = y
|
|
14
|
-
}) {
|
|
15
|
-
r(this, "apiBase");
|
|
16
|
-
r(this, "getUploadToken");
|
|
17
|
-
r(this, "accountSlug");
|
|
18
|
-
r(this, "title");
|
|
19
|
-
r(this, "pollIntervalMs");
|
|
20
|
-
r(this, "timeoutMs");
|
|
21
|
-
this.apiBase = t, this.getUploadToken = o, this.accountSlug = e, this.title = s, this.pollIntervalMs = i, this.timeoutMs = a;
|
|
22
|
-
}
|
|
23
|
-
/**
|
|
24
|
-
* Create the site the build deploys into. Authorized by the bearer token;
|
|
25
|
-
* without an account slug the API picks the user's default account.
|
|
26
|
-
*/
|
|
27
|
-
async createSite() {
|
|
28
|
-
const t = await this.getUploadToken();
|
|
29
|
-
if (!t) throw new l();
|
|
30
|
-
const o = this.accountSlug ? `/${this.accountSlug}/sites` : "/sites", e = await fetch(`${this.apiBase}${o}`, {
|
|
31
|
-
method: "POST",
|
|
32
|
-
headers: { "Content-Type": "application/json", Authorization: `Bearer ${t}` },
|
|
33
|
-
body: JSON.stringify({ created_via: "drop" })
|
|
34
|
-
});
|
|
35
|
-
if (e.status === 401 || e.status === 403) throw new l();
|
|
36
|
-
if (!e.ok) throw new c(e.status);
|
|
37
|
-
return e.json();
|
|
38
|
-
}
|
|
39
|
-
/**
|
|
40
|
-
* Upload the source archive and enqueue a build. Direct to `api.netlify.com`
|
|
41
|
-
* (see the note on `apiBase`), authorized by a freshly minted bearer token.
|
|
42
|
-
* `Content-Type` is left unset on purpose so the browser adds the multipart
|
|
43
|
-
* boundary itself.
|
|
44
|
-
*/
|
|
45
|
-
async createBuildFromZip(t, o) {
|
|
46
|
-
const e = await this.getUploadToken();
|
|
47
|
-
if (!e) throw new l();
|
|
48
|
-
const s = new FormData();
|
|
49
|
-
s.append("zip", o), s.append("title", this.title);
|
|
50
|
-
const i = await fetch(`${this.apiBase}/sites/${t}/builds`, {
|
|
51
|
-
method: "POST",
|
|
52
|
-
headers: { Authorization: `Bearer ${e}` },
|
|
53
|
-
body: s
|
|
54
|
-
});
|
|
55
|
-
if (i.status === 401 || i.status === 403) throw new l();
|
|
56
|
-
if (!i.ok) throw new p(i.status);
|
|
57
|
-
return i.json();
|
|
58
|
-
}
|
|
59
|
-
/**
|
|
60
|
-
* Poll the build's deploy until it settles, mirroring
|
|
61
|
-
* `AnonymousDropClient.waitUntilReady` — resolve on `ready`, throw on
|
|
62
|
-
* `error`/`rejected`, throw on timeout. A build that hasn't been given a
|
|
63
|
-
* deploy id yet has nothing pollable, so it resolves immediately and the
|
|
64
|
-
* caller falls back to the site page.
|
|
65
|
-
*
|
|
66
|
-
* Optional in the drop flow, and off by default: a build takes minutes, which
|
|
67
|
-
* can outlive the bearer token minted for it. Each poll re-mints so a long
|
|
68
|
-
* build keeps reading; a token that can't be minted any more ends the wait as
|
|
69
|
-
* a timeout rather than a false failure.
|
|
70
|
-
*/
|
|
71
|
-
async waitUntilBuildReady(t) {
|
|
72
|
-
if (!t.deploy_id) return;
|
|
73
|
-
const o = this.timeoutMs / this.pollIntervalMs;
|
|
74
|
-
for (let e = 0; e < o; e++) {
|
|
75
|
-
const s = await this.getUploadToken(), i = await fetch(`${this.apiBase}/deploys/${t.deploy_id}`, {
|
|
76
|
-
headers: s ? { Authorization: `Bearer ${s}` } : {}
|
|
77
|
-
});
|
|
78
|
-
if (i.ok) {
|
|
79
|
-
const a = await i.json();
|
|
80
|
-
if (a.state && B.has(a.state)) {
|
|
81
|
-
if (a.state === "ready") return;
|
|
82
|
-
throw new u(a.error_message);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
await new Promise((a) => setTimeout(a, this.pollIntervalMs));
|
|
86
|
-
}
|
|
87
|
-
throw new w();
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
export {
|
|
91
|
-
g as AuthenticatedBuildClient
|
|
92
|
-
};
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { getDigest as w } from "./digest.js";
|
|
2
|
-
import { zipFiles as y } from "./zip.js";
|
|
3
|
-
const f = 5;
|
|
4
|
-
async function m(i, l, n, t, p, a) {
|
|
5
|
-
const u = n.filter((d) => t.has(d.sha));
|
|
6
|
-
let e = 0, h = 0;
|
|
7
|
-
const c = async () => {
|
|
8
|
-
for (; e < u.length; ) {
|
|
9
|
-
const d = u[e++];
|
|
10
|
-
await i.uploadFile(l, d.path, d.content, p), a == null || a(++h);
|
|
11
|
-
}
|
|
12
|
-
};
|
|
13
|
-
await Promise.all(Array.from({ length: Math.min(f, u.length) }, c));
|
|
14
|
-
}
|
|
15
|
-
async function s(i, l, n = {}) {
|
|
16
|
-
if (!l.length) throw new Error("No files to deploy");
|
|
17
|
-
const { onProgress: t } = n;
|
|
18
|
-
t == null || t({ phase: "creating" });
|
|
19
|
-
const p = await i.getToken(), a = await i.createDeploy(w(l), p), u = a.required.length;
|
|
20
|
-
return t == null || t({ phase: "uploading", uploaded: 0, total: u }), await m(
|
|
21
|
-
i,
|
|
22
|
-
a.deploy_id,
|
|
23
|
-
l,
|
|
24
|
-
new Set(a.required),
|
|
25
|
-
p,
|
|
26
|
-
(e) => t == null ? void 0 : t({ phase: "uploading", uploaded: e, total: u })
|
|
27
|
-
), t == null || t({ phase: "processing" }), await i.waitUntilReady(a, p), t == null || t({ phase: "ready" }), { deploy: a, token: p, url: `https://${a.subdomain}.netlify.app` };
|
|
28
|
-
}
|
|
29
|
-
async function F(i, l, n = {}) {
|
|
30
|
-
if (!l.length) throw new Error("No files to deploy");
|
|
31
|
-
const { onProgress: t, waitForBuild: p = !1 } = n;
|
|
32
|
-
t == null || t({ phase: "zipping" });
|
|
33
|
-
const { file: a, buildSettings: u } = await y(l);
|
|
34
|
-
t == null || t({ phase: "creating" });
|
|
35
|
-
const e = await i.createSite();
|
|
36
|
-
t == null || t({ phase: "uploading" });
|
|
37
|
-
const h = await i.createBuildFromZip(e.id, a);
|
|
38
|
-
return t == null || t({ phase: "building" }), p && await i.waitUntilBuildReady(h), t == null || t({ phase: "ready" }), {
|
|
39
|
-
site: e,
|
|
40
|
-
build: h,
|
|
41
|
-
buildSettings: u,
|
|
42
|
-
url: e.ssl_url || e.url || `https://${e.name}.netlify.app`
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
export {
|
|
46
|
-
F as deployBuild,
|
|
47
|
-
s as deployFiles
|
|
48
|
-
};
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { getDigest as t } from "./digest.js";
|
|
2
|
-
import { getSingleNonIndexHtmlFileName as i, hasMultipleHtmlFilesWithoutIndex as l, readDataTransfer as n, readFileList as p, renameSingleNonIndexHtmlToIndex as d } from "./readFiles.js";
|
|
3
|
-
import { getFileTypes as a } from "./fileTypes.js";
|
|
4
|
-
import { detectBuild as x, inferBuildSettings as f } from "./detectBuild.js";
|
|
5
|
-
import { AnonymousDropClient as g, apiError as E } from "./client.js";
|
|
6
|
-
import { AuthenticatedDropClient as F } from "./authedDropClient.js";
|
|
7
|
-
import { AuthenticatedBuildClient as D } from "./buildClient.js";
|
|
8
|
-
import { generateNetlifyToml as C, zipFiles as N } from "./zip.js";
|
|
9
|
-
import { deployBuild as c, deployFiles as A } from "./deploy.js";
|
|
10
|
-
import { BuildCreateError as S, BuildFailedError as H, BuildTimeoutError as z, NotAuthenticatedError as L, SiteCreateError as M, UnsupportedDropError as U, humanizeDropError as W } from "./errors.js";
|
|
11
|
-
export {
|
|
12
|
-
g as AnonymousDropClient,
|
|
13
|
-
D as AuthenticatedBuildClient,
|
|
14
|
-
F as AuthenticatedDropClient,
|
|
15
|
-
S as BuildCreateError,
|
|
16
|
-
H as BuildFailedError,
|
|
17
|
-
z as BuildTimeoutError,
|
|
18
|
-
L as NotAuthenticatedError,
|
|
19
|
-
M as SiteCreateError,
|
|
20
|
-
U as UnsupportedDropError,
|
|
21
|
-
E as apiError,
|
|
22
|
-
c as deployBuild,
|
|
23
|
-
A as deployFiles,
|
|
24
|
-
x as detectBuild,
|
|
25
|
-
C as generateNetlifyToml,
|
|
26
|
-
t as getDigest,
|
|
27
|
-
a as getFileTypes,
|
|
28
|
-
i as getSingleNonIndexHtmlFileName,
|
|
29
|
-
l as hasMultipleHtmlFilesWithoutIndex,
|
|
30
|
-
W as humanizeDropError,
|
|
31
|
-
f as inferBuildSettings,
|
|
32
|
-
n as readDataTransfer,
|
|
33
|
-
p as readFileList,
|
|
34
|
-
d as renameSingleNonIndexHtmlToIndex,
|
|
35
|
-
N as zipFiles
|
|
36
|
-
};
|