@netlify/spark-ui 1.29.0 → 1.31.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/README.md +4 -3
  2. package/dist/chunks/browser.Ca2rTNTu.js +769 -0
  3. package/dist/components/articleCopyActions.d.ts +9 -0
  4. package/dist/components/articleCopyActions.test.d.ts +1 -0
  5. package/dist/components/index.d.ts +2 -2
  6. package/dist/components/preact/DropZone/DropZone.d.ts +84 -15
  7. package/dist/components/preact/DropZone/DropZone.js +117 -85
  8. package/dist/components/preact/DropZone/drop-core/authedDropClient.d.ts +80 -0
  9. package/dist/components/preact/DropZone/drop-core/authedDropClient.js +99 -0
  10. package/dist/components/preact/DropZone/drop-core/buildClient.d.ts +98 -0
  11. package/dist/components/preact/DropZone/drop-core/buildClient.js +92 -0
  12. package/dist/components/preact/DropZone/drop-core/client.d.ts +4 -0
  13. package/dist/components/preact/DropZone/drop-core/client.js +2 -1
  14. package/dist/components/preact/DropZone/drop-core/deploy.d.ts +31 -0
  15. package/dist/components/preact/DropZone/drop-core/deploy.js +36 -18
  16. package/dist/components/preact/DropZone/drop-core/detectBuild.d.ts +26 -0
  17. package/dist/components/preact/DropZone/drop-core/detectBuild.js +80 -53
  18. package/dist/components/preact/DropZone/drop-core/errors.d.ts +38 -0
  19. package/dist/components/preact/DropZone/drop-core/errors.js +50 -5
  20. package/dist/components/preact/DropZone/drop-core/fileTypes.d.ts +17 -0
  21. package/dist/components/preact/DropZone/drop-core/fileTypes.js +11 -0
  22. package/dist/components/preact/DropZone/drop-core/index.d.ts +4 -0
  23. package/dist/components/preact/DropZone/drop-core/index.js +32 -15
  24. package/dist/components/preact/DropZone/drop-core/readFiles.js +1 -1
  25. package/dist/components/preact/DropZone/drop-core/types.d.ts +6 -1
  26. package/dist/components/preact/DropZone/drop-core/zip.d.ts +46 -0
  27. package/dist/components/preact/DropZone/drop-core/zip.js +43 -0
  28. package/dist/components/preact/DropZone/index.d.ts +3 -2
  29. package/dist/components/preact/DropZone/index.js +27 -9
  30. package/dist/components/preact/DropZone/useDropDeploy.d.ts +36 -5
  31. package/dist/components/preact/DropZone/useDropDeploy.js +140 -65
  32. package/dist/components/preact/index.d.ts +2 -1
  33. package/dist/components/preact/index.js +17 -12
  34. package/package.json +5 -2
  35. package/packages/components/ArticleCopyActions.astro +122 -0
  36. package/packages/components/articleCopyActions.test.ts +143 -0
  37. package/packages/components/articleCopyActions.ts +41 -0
  38. package/packages/components/index.ts +15 -0
  39. package/packages/components/preact/DropZone/DropZone.tsx +145 -24
  40. package/packages/components/preact/DropZone/drop-core/authedDropClient.ts +194 -0
  41. package/packages/components/preact/DropZone/drop-core/buildClient.ts +190 -0
  42. package/packages/components/preact/DropZone/drop-core/client.ts +1 -1
  43. package/packages/components/preact/DropZone/drop-core/deploy.ts +65 -0
  44. package/packages/components/preact/DropZone/drop-core/detectBuild.ts +125 -41
  45. package/packages/components/preact/DropZone/drop-core/errors.ts +84 -1
  46. package/packages/components/preact/DropZone/drop-core/fileTypes.ts +34 -0
  47. package/packages/components/preact/DropZone/drop-core/index.ts +4 -0
  48. package/packages/components/preact/DropZone/drop-core/readFiles.ts +1 -1
  49. package/packages/components/preact/DropZone/drop-core/types.ts +12 -1
  50. package/packages/components/preact/DropZone/drop-core/zip.ts +118 -0
  51. package/packages/components/preact/DropZone/index.tsx +24 -0
  52. package/packages/components/preact/DropZone/useDropDeploy.ts +219 -34
  53. package/packages/components/preact/index.ts +2 -1
  54. package/dist/chunks/browser.D58rdxmc.js +0 -444
@@ -12,6 +12,67 @@ export class UnsupportedDropError extends Error {
12
12
  }
13
13
  }
14
14
 
15
+ /**
16
+ * Thrown when an authenticated path (static or build) turns out to have no
17
+ * usable session — the short-lived upload token came back empty, or the
18
+ * session-cookie site create was refused, so nothing was created. For static
19
+ * drops the flow falls back to the anonymous drop + claim; builds have no
20
+ * anonymous equivalent, so the consumer's only recourse is to send the visitor
21
+ * through login/signup and have them drop again.
22
+ */
23
+ export class NotAuthenticatedError extends Error {
24
+ constructor(message = 'not authenticated') {
25
+ super(message);
26
+ this.name = 'NotAuthenticatedError';
27
+ }
28
+ }
29
+
30
+ /** `POST …/sites` failed, so there is nothing to build into. Carries the HTTP status. */
31
+ export class SiteCreateError extends Error {
32
+ status?: number;
33
+
34
+ constructor(status?: number) {
35
+ super(`site create failed${status ? `: ${status}` : ''}`);
36
+ this.name = 'SiteCreateError';
37
+ this.status = status;
38
+ }
39
+ }
40
+
41
+ /**
42
+ * The multipart `POST /sites/{id}/builds` upload itself failed — the build never
43
+ * started. Distinct from `BuildFailedError`, which is a build that ran and broke.
44
+ */
45
+ export class BuildCreateError extends Error {
46
+ status?: number;
47
+
48
+ constructor(status?: number) {
49
+ super(`build create failed${status ? `: ${status}` : ''}`);
50
+ this.name = 'BuildCreateError';
51
+ this.status = status;
52
+ }
53
+ }
54
+
55
+ /**
56
+ * The build ran and ended in `error`/`rejected`. `reason` is the API's
57
+ * `error_message` when there is one — it's the only part of a build failure that
58
+ * says anything actionable (a failing build command, a missing dependency), so
59
+ * it is surfaced to the user rather than swallowed.
60
+ */
61
+ export class BuildFailedError extends Error {
62
+ constructor(public reason?: string) {
63
+ super(reason ? `build failed: ${reason}` : 'build failed');
64
+ this.name = 'BuildFailedError';
65
+ }
66
+ }
67
+
68
+ /** The build hadn't settled before the poller gave up. The build itself may still finish. */
69
+ export class BuildTimeoutError extends Error {
70
+ constructor() {
71
+ super('Build did not finish in time');
72
+ this.name = 'BuildTimeoutError';
73
+ }
74
+ }
75
+
15
76
  /**
16
77
  * Turn a raw deploy failure into copy a non-technical user can act on.
17
78
  * The original message is still tracked via analytics — this is only for display.
@@ -23,6 +84,26 @@ export function humanizeDropError(err: unknown): string {
23
84
  return 'To deploy, drop a folder or a .zip file — or a single HTML page.';
24
85
  }
25
86
 
87
+ if (err instanceof NotAuthenticatedError) {
88
+ return 'Please log in to Netlify to deploy a project that needs a build, then try again.';
89
+ }
90
+
91
+ if (err instanceof SiteCreateError) {
92
+ return "We couldn't create a project for this build. Please try again.";
93
+ }
94
+
95
+ // The build's own error message is the only actionable part of a build
96
+ // failure, so pass it straight through instead of a generic apology.
97
+ if (err instanceof BuildFailedError) {
98
+ return err.reason
99
+ ? `Your build failed: ${err.reason}`
100
+ : 'Your build failed. Check your build settings and try again.';
101
+ }
102
+
103
+ if (err instanceof BuildTimeoutError) {
104
+ return 'Your build is taking longer than expected. It may still finish — check your Netlify dashboard.';
105
+ }
106
+
26
107
  if (err.message === 'Deploy did not become ready in time') {
27
108
  return 'Your site is taking longer than expected to go live. Please try again in a moment.';
28
109
  }
@@ -31,7 +112,9 @@ export function humanizeDropError(err: unknown): string {
31
112
  return "Couldn't connect to Netlify. Check your internet connection and try again.";
32
113
  }
33
114
 
34
- // client.ts attaches `.status` to API failures — see apiError().
115
+ // client.ts attaches `.status` to API failures (see apiError()), as do the
116
+ // typed build errors above — so `BuildCreateError(413)` lands on the
117
+ // upload-too-large copy rather than a generic message.
35
118
  const status = 'status' in err && typeof err.status === 'number' ? err.status : undefined;
36
119
 
37
120
  switch (status) {
@@ -0,0 +1,34 @@
1
+ /** What we report when the browser gives us no MIME type at all. */
2
+ const UNKNOWN_TYPE = 'application/octet-stream';
3
+
4
+ /**
5
+ * Distinct types kept per event. Deduplication alone keeps this small for a real
6
+ * site (a handful of types across hundreds of files); the cap is a backstop so an
7
+ * unusual folder can't push an oversized property into the analytics payload.
8
+ */
9
+ const MAX_TYPES = 20;
10
+
11
+ /**
12
+ * Distinct MIME types across a drop or file-picker selection, sorted so the same
13
+ * selection always yields the same array.
14
+ *
15
+ * Read from the raw `File.type` at the drop boundary rather than from
16
+ * `ProcessedFile`s: the MIME type only exists on the original `File`, and reading
17
+ * can throw before any `ProcessedFile` does (an unsupported single file) — the
18
+ * very case the file type is most worth knowing about.
19
+ *
20
+ * Browsers leave `File.type` empty for anything they don't recognise — a dragged
21
+ * folder, an `.exe`, an extensionless file — so those collapse to
22
+ * `application/octet-stream` and the property never carries an empty string.
23
+ *
24
+ * Call this synchronously inside the drop handler: a `DataTransfer` detaches once
25
+ * the event turn ends.
26
+ */
27
+ export function getFileTypes(files: FileList | null | undefined): string[] {
28
+ if (!files?.length) return [];
29
+ const types = new Set<string>();
30
+ for (const file of Array.from(files)) {
31
+ types.add(file.type || UNKNOWN_TYPE);
32
+ }
33
+ return Array.from(types).sort().slice(0, MAX_TYPES);
34
+ }
@@ -1,7 +1,11 @@
1
1
  export * from './types';
2
2
  export * from './digest';
3
3
  export * from './readFiles';
4
+ export * from './fileTypes';
4
5
  export * from './detectBuild';
5
6
  export * from './client';
7
+ export * from './authedDropClient';
8
+ export * from './buildClient';
9
+ export * from './zip';
6
10
  export * from './deploy';
7
11
  export * from './errors';
@@ -123,7 +123,7 @@ async function readZip(zip: File): Promise<ProcessedFile[]> {
123
123
  // ArrayBuffer, so narrow it for toProcessedFile's ArrayBuffer parameter.
124
124
  const content = bytes.buffer.slice(
125
125
  bytes.byteOffset,
126
- bytes.byteOffset + bytes.byteLength,
126
+ bytes.byteOffset + bytes.byteLength
127
127
  ) as ArrayBuffer;
128
128
  out.push(toProcessedFile(path, content, path.split('/').pop()!));
129
129
  }
@@ -34,7 +34,18 @@ export interface DeployResult {
34
34
  url: string; // https://<subdomain>.netlify.app
35
35
  }
36
36
 
37
- export type DeployPhase = 'creating' | 'uploading' | 'processing' | 'ready';
37
+ /**
38
+ * Phases shared by both deploy paths. A static drop runs
39
+ * creating → uploading → processing → ready; a build drop runs
40
+ * zipping → creating → uploading → building → ready.
41
+ */
42
+ export type DeployPhase =
43
+ | 'zipping'
44
+ | 'creating'
45
+ | 'uploading'
46
+ | 'processing'
47
+ | 'building'
48
+ | 'ready';
38
49
 
39
50
  export interface DeployProgress {
40
51
  phase: DeployPhase;
@@ -0,0 +1,118 @@
1
+ import { inferBuildSettings, type InferredBuildSettings } from './detectBuild';
2
+ import type { ProcessedFile } from './types';
3
+
4
+ const NETLIFY_TOML_PATH = '/netlify.toml';
5
+
6
+ /** TOML basic-string escaping — enough for the two values we ever write. */
7
+ function escapeTomlString(value: string): string {
8
+ return value
9
+ .replace(/\\/g, '\\\\')
10
+ .replace(/"/g, '\\"')
11
+ .replace(/\n/g, '\\n')
12
+ .replace(/\r/g, '\\r')
13
+ .replace(/\t/g, '\\t');
14
+ }
15
+
16
+ /**
17
+ * Render inferred settings as a `[build]` table. Only `command` and `publish`
18
+ * are written: everything else (functions directory, plugins, node version)
19
+ * is zero-config on Netlify's side and guessing at it would override detection
20
+ * that is better informed than we are.
21
+ */
22
+ export function generateNetlifyToml({ command, publish }: InferredBuildSettings): string {
23
+ return [
24
+ '[build]',
25
+ ` command = "${escapeTomlString(command)}"`,
26
+ ` publish = "${escapeTomlString(publish)}"`,
27
+ '',
28
+ ].join('\n');
29
+ }
30
+
31
+ export interface ZipOptions {
32
+ /** Filename given to the produced `File`. Shows up as the build's source archive. */
33
+ name?: string;
34
+ /**
35
+ * Synthesise a `netlify.toml` `[build]` table from `inferBuildSettings` when the
36
+ * drop doesn't declare one (default `true`). Set `false` to upload the files
37
+ * exactly as dropped and let buildbot resolve every setting itself.
38
+ */
39
+ synthesizeNetlifyToml?: boolean;
40
+ }
41
+
42
+ export interface ZipResult {
43
+ /** The archive, ready to hand to `createBuildFromZip`. */
44
+ file: File;
45
+ /**
46
+ * The settings written into `netlify.toml`, or `null` when the archive is a
47
+ * faithful copy of the drop — either because it already declared a `[build]`
48
+ * table, or because nothing could be inferred with confidence.
49
+ */
50
+ buildSettings: InferredBuildSettings | null;
51
+ }
52
+
53
+ const encoder = new TextEncoder();
54
+ const decoder = new TextDecoder();
55
+
56
+ function toBytes(content: ArrayBuffer): Uint8Array {
57
+ return new Uint8Array(content);
58
+ }
59
+
60
+ /**
61
+ * Zip an already-read file set into the source archive that
62
+ * `POST /sites/{id}/builds` expects.
63
+ *
64
+ * The input is the same root-normalized `ProcessedFile[]` the static deploy path
65
+ * uses (see `readFiles`), so junk — dotfiles, `node_modules`, macOS metadata — is
66
+ * already gone and paths sit at the deploy root. Output directories (`dist/`,
67
+ * `build/`) are deliberately *not* stripped: buildbot regenerates them when
68
+ * there's a build command, and a drop whose `netlify.toml` publishes a committed
69
+ * directory without a command would break if we removed it.
70
+ *
71
+ * When the drop needs a build but declares no `[build]` table, an inferred one is
72
+ * written in (appended to an existing `netlify.toml`, or created if there is
73
+ * none) so buildbot doesn't publish the project's source. See
74
+ * `inferBuildSettings` for how conservative that inference is.
75
+ */
76
+ export async function zipFiles(
77
+ files: ProcessedFile[],
78
+ { name = 'source.zip', synthesizeNetlifyToml = true }: ZipOptions = {}
79
+ ): Promise<ZipResult> {
80
+ if (!files.length) throw new Error('No files to deploy');
81
+
82
+ const buildSettings = synthesizeNetlifyToml ? inferBuildSettings(files) : null;
83
+
84
+ const entries: Record<string, Uint8Array> = {};
85
+ for (const file of files) {
86
+ // fflate keys are paths relative to the archive root, so drop the leading
87
+ // slash `ProcessedFile.path` carries.
88
+ const path = file.path.replace(/^\//, '');
89
+ if (path) entries[path] = toBytes(file.content);
90
+ }
91
+
92
+ if (buildSettings) {
93
+ const existing = files.find(f => f.path === NETLIFY_TOML_PATH);
94
+ const table = generateNetlifyToml(buildSettings);
95
+ // Append rather than replace so the drop's own redirects, headers and
96
+ // environment survive alongside the synthesised build settings.
97
+ const contents = existing
98
+ ? `${decoder.decode(existing.content).replace(/\n*$/, '\n')}\n${table}`
99
+ : table;
100
+ entries['netlify.toml'] = encoder.encode(contents);
101
+ }
102
+
103
+ // fflate is loaded on demand — same as the unzip path in readFiles — so a
104
+ // consumer that never drops a build project never pays for it.
105
+ const { zip } = await import('fflate');
106
+ const bytes = await new Promise<Uint8Array>((resolve, reject) =>
107
+ zip(entries, { level: 6 }, (err, out) => (err ? reject(err) : resolve(out)))
108
+ );
109
+
110
+ // `bytes.buffer` is ArrayBufferLike; narrow it to the plain ArrayBuffer the
111
+ // File constructor takes, the same way readFiles does on the unzip side.
112
+ const archive = bytes.buffer.slice(
113
+ bytes.byteOffset,
114
+ bytes.byteOffset + bytes.byteLength
115
+ ) as ArrayBuffer;
116
+ const file = new File([archive], name, { type: 'application/zip' });
117
+ return { file, buildSettings };
118
+ }
@@ -1,19 +1,43 @@
1
1
  export { default as DropZone } from './DropZone';
2
2
  export {
3
3
  AnonymousDropClient,
4
+ AuthenticatedBuildClient,
5
+ AuthenticatedDropClient,
6
+ BuildCreateError,
7
+ BuildFailedError,
8
+ BuildTimeoutError,
9
+ deployBuild,
4
10
  deployFiles,
5
11
  detectBuild,
12
+ getFileTypes,
13
+ humanizeDropError,
14
+ inferBuildSettings,
15
+ NotAuthenticatedError,
6
16
  readDataTransfer,
7
17
  readFileList,
18
+ SiteCreateError,
19
+ UnsupportedDropError,
20
+ zipFiles,
8
21
  } from './drop-core';
9
22
  export type {
23
+ AuthenticatedBuildClientConfig,
24
+ AuthenticatedDropClientConfig,
25
+ BuildClient,
26
+ BuildDeployOptions,
27
+ BuildDeployResult,
10
28
  BuildDetection,
11
29
  BuildReason,
30
+ BuildResponse,
31
+ BuildSite,
12
32
  DropClient,
13
33
  DropResponse,
14
34
  Digest,
15
35
  DeployResult,
16
36
  DeployProgress,
17
37
  DeployPhase,
38
+ InferredBuildSettings,
18
39
  ProcessedFile,
40
+ ZipOptions,
41
+ ZipResult,
19
42
  } from './drop-core';
43
+ export type { DeployedBuild, DeployedSite, DeployError, DeployStatus } from './useDropDeploy';