@netlify/spark-ui 1.31.0-alpha.5 → 1.31.0-alpha.7

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 (42) hide show
  1. package/README.md +3 -4
  2. package/dist/components/preact/DropZone/DropZone.d.ts +23 -97
  3. package/dist/components/preact/DropZone/useDropDeploy.js +110 -105
  4. package/dist/drop-core/analytics.d.ts +4 -15
  5. package/dist/drop-core/authedApi.d.ts +9 -22
  6. package/dist/drop-core/authedDropClient.d.ts +13 -55
  7. package/dist/drop-core/authedDropClient.js +5 -18
  8. package/dist/drop-core/buildClient.d.ts +19 -53
  9. package/dist/drop-core/buildClient.js +27 -40
  10. package/dist/drop-core/buildStash.d.ts +6 -20
  11. package/dist/drop-core/buildStash.js +36 -24
  12. package/dist/drop-core/client.d.ts +1 -1
  13. package/dist/drop-core/constants.d.ts +1 -21
  14. package/dist/drop-core/deploy.d.ts +5 -17
  15. package/dist/drop-core/detectBuild.d.ts +8 -26
  16. package/dist/drop-core/errors.d.ts +8 -27
  17. package/dist/drop-core/fileTypes.d.ts +4 -14
  18. package/dist/drop-core/readFiles.d.ts +5 -11
  19. package/dist/drop-core/types.d.ts +2 -4
  20. package/dist/drop-core/zip.d.ts +7 -30
  21. package/package.json +1 -1
  22. package/packages/components/preact/DropZone/DropZone.tsx +27 -121
  23. package/packages/components/preact/DropZone/useDropDeploy.test.ts +12 -0
  24. package/packages/components/preact/DropZone/useDropDeploy.ts +39 -60
  25. package/packages/drop-core/README.md +311 -39
  26. package/packages/drop-core/analytics.ts +4 -15
  27. package/packages/drop-core/authedApi.ts +9 -22
  28. package/packages/drop-core/authedDropClient.ts +13 -55
  29. package/packages/drop-core/buildClient.test.ts +19 -4
  30. package/packages/drop-core/buildClient.ts +21 -63
  31. package/packages/drop-core/buildStash.test.ts +19 -0
  32. package/packages/drop-core/buildStash.ts +38 -29
  33. package/packages/drop-core/client.ts +6 -10
  34. package/packages/drop-core/constants.ts +8 -22
  35. package/packages/drop-core/deploy.ts +6 -22
  36. package/packages/drop-core/detectBuild.ts +15 -50
  37. package/packages/drop-core/errors.test.ts +3 -1
  38. package/packages/drop-core/errors.ts +9 -30
  39. package/packages/drop-core/fileTypes.ts +5 -19
  40. package/packages/drop-core/readFiles.ts +5 -12
  41. package/packages/drop-core/types.ts +2 -4
  42. package/packages/drop-core/zip.ts +7 -30
@@ -8,23 +8,16 @@ export type BuildReason =
8
8
  | 'functions-directory';
9
9
 
10
10
  export interface BuildDetection {
11
- /** True when the dropped project needs a build step and can't be served as static files. */
11
+ /** The drop can't be served as static files. */
12
12
  buildRequired: boolean;
13
13
  /** The signal that decided it (first match wins), or null for a plain static drop. */
14
14
  reason: BuildReason | null;
15
15
  }
16
16
 
17
17
  /**
18
- * Dependencies whose presence implies a build even without an explicit `build`
19
- * script (e.g. a framework wired through its own CLI). Not exhaustive — the
20
- * `build` script check below catches the common case; this is the backstop.
21
- *
22
- * The value is the framework's default publish directory, or `null` where that
23
- * directory isn't a fixed string we can state with confidence — Nuxt, SvelteKit,
24
- * Remix and Angular all resolve it from an adapter or project name. `null` means
25
- * "a build is required, but don't guess a publish directory": see
26
- * `inferBuildSettings`, which only synthesises a `netlify.toml` when it knows
27
- * both halves of the answer.
18
+ * Frameworks that imply a build even without a `build` script, mapped to their
19
+ * default publish dir — `null` where it isn't a fixed string (Nuxt, SvelteKit,
20
+ * Remix, Angular resolve theirs), meaning "build required, don't guess".
28
21
  */
29
22
  const FRAMEWORK_DEPENDENCIES = new Map<string, string | null>([
30
23
  ['astro', 'dist'],
@@ -49,11 +42,7 @@ const FRAMEWORK_DEPENDENCIES = new Map<string, string | null>([
49
42
  ['@solidjs/start', null],
50
43
  ]);
51
44
 
52
- /**
53
- * A `netlify.toml` build config: a bare `[build]` table or any of its
54
- * sub-tables (`[build.environment]`, `[build.processing]`, …). Anchored to the
55
- * line start so a `[build]` mention inside a string/comment doesn't count.
56
- */
45
+ // A [build] table or sub-table, line-anchored so a mention in a comment doesn't count.
57
46
  const NETLIFY_TOML_BUILD_SECTION = /^\s*\[build(\.[\w-]+)?\]/m;
58
47
 
59
48
  /** Zero-config functions directories — their presence alone requires bundling. */
@@ -61,7 +50,7 @@ const FUNCTIONS_DIR_PREFIXES = ['/netlify/functions/', '/netlify/edge-functions/
61
50
 
62
51
  const decoder = new TextDecoder();
63
52
 
64
- /** Find a file at the deploy root by its normalized path (leading slash). */
53
+ /** Root paths are normalized with a leading slash. */
65
54
  function rootFile(files: ProcessedFile[], path: string): ProcessedFile | undefined {
66
55
  return files.find(f => f.path === path);
67
56
  }
@@ -90,23 +79,14 @@ function findFrameworkDependency(pkg: Record<string, unknown>): string | null {
90
79
  return null;
91
80
  }
92
81
 
93
- /** True if any dependency (prod or dev) is a known build-requiring framework. */
94
82
  function hasFrameworkDependency(pkg: Record<string, unknown>): boolean {
95
83
  return findFrameworkDependency(pkg) !== null;
96
84
  }
97
85
 
98
86
  /**
99
- * Decide whether a dropped project needs a build. Runs on the already-read,
100
- * root-normalized `ProcessedFile[]` (a dropped `.zip` is expanded and the common
101
- * root folder stripped upstream in `readFiles`), so build config sits at the
102
- * deploy root, e.g. `/netlify.toml`, `/package.json`.
103
- *
104
- * The anonymous drop API only performs static deploys, so a build-required
105
- * project can't go through it — the consumer redirects such drops to the
106
- * authenticated Drop page instead. This heuristic is deliberately lightweight
107
- * (no framework-detection dependency): it reads the two config files and looks
108
- * for the zero-config functions directories. A plain static site (HTML/CSS/JS,
109
- * no build config) returns `{ buildRequired: false }`.
87
+ * Does this drop need a build? Deliberately lightweight (two config files +
88
+ * the functions dirs; no framework-detection dependency). Input is
89
+ * root-normalized by `readFiles`, so config sits at `/netlify.toml` etc.
110
90
  */
111
91
  export function detectBuild(files: ProcessedFile[]): BuildDetection {
112
92
  const toml = rootFile(files, '/netlify.toml');
@@ -165,13 +145,8 @@ const LOCKFILE_PACKAGE_MANAGERS: ReadonlyArray<readonly [string, string]> = [
165
145
  ['/package-lock.json', 'npm'],
166
146
  ];
167
147
 
168
- /**
169
- * A bare `[build]` table header at the start of a line. Stricter than
170
- * `NETLIFY_TOML_BUILD_SECTION` on purpose: a config with only
171
- * `[build.environment]` carries no command or publish directory, so it still
172
- * needs a `[build]` table synthesised. (TOML permits defining a super-table
173
- * after its sub-tables, so appending one is valid.)
174
- */
148
+ // Bare [build] only — stricter than the detection regex, since a config with
149
+ // just [build.environment] still needs a [build] table synthesised.
175
150
  const NETLIFY_TOML_BUILD_TABLE = /^\s*\[build\]/m;
176
151
 
177
152
  function detectPackageManager(files: ProcessedFile[]): string {
@@ -182,20 +157,10 @@ function detectPackageManager(files: ProcessedFile[]): string {
182
157
  }
183
158
 
184
159
  /**
185
- * Infer the build command and publish directory for a build-required drop, so
186
- * `zipFiles` can write them into a `netlify.toml` that buildbot reads.
187
- *
188
- * Deliberately conservative: it returns settings **only** when both halves are
189
- * known — a `build` script (which gives the command) and a recognised framework
190
- * with a fixed default publish directory (which gives the output). Anything else
191
- * returns `null`, leaving the zip without a synthesised `[build]` table so
192
- * buildbot's own framework detection resolves the settings instead. That matters
193
- * because a `[build]` table with a command but no `publish` makes buildbot
194
- * publish the deploy root — shipping the project's *source* rather than its
195
- * built output, which is worse than not guessing at all.
196
- *
197
- * Returns `null` too when the drop already declares a `[build]` table: the
198
- * author's own config always wins.
160
+ * Build settings for the synthesised netlify.toml — only when BOTH halves are
161
+ * known (build script → command, known framework → publish). Guessing publish
162
+ * wrong ships source instead of output, so `null` beats a bad guess. An
163
+ * author-declared [build] table always wins (also `null`).
199
164
  */
200
165
  export function inferBuildSettings(files: ProcessedFile[]): InferredBuildSettings | null {
201
166
  const toml = rootFile(files, '/netlify.toml');
@@ -76,7 +76,9 @@ describe('humanizeDropError', () => {
76
76
  expect(humanizeDropError(new DeployFailedError('no index.html'))).toBe(
77
77
  'Your deploy failed: no index.html'
78
78
  );
79
- expect(humanizeDropError(new DeployFailedError())).toBe('Your deploy failed. Please try again.');
79
+ expect(humanizeDropError(new DeployFailedError())).toBe(
80
+ 'Your deploy failed. Please try again.'
81
+ );
80
82
  });
81
83
 
82
84
  it('reads a fetch TypeError as a connectivity problem', () => {
@@ -1,9 +1,6 @@
1
1
  /**
2
- * Thrown when the user drops or picks a single loose file that Drop can't turn
3
- * into a site on its own — anything that isn't a `.zip` (a site archive) or an
4
- * HTML page (served as index.html). A folder, multiple files, a zip, or a lone
5
- * HTML file are all fine; any other single file is rejected rather than
6
- * deployed as a one-asset site (matches react-ui).
2
+ * A single loose file that can't stand up a site — only a zip, folder,
3
+ * multiple files, or a lone HTML page may deploy (matches react-ui).
7
4
  */
8
5
  export class UnsupportedDropError extends Error {
9
6
  readonly code = 'unsupported-drop';
@@ -15,12 +12,8 @@ export class UnsupportedDropError extends Error {
15
12
  }
16
13
 
17
14
  /**
18
- * Thrown when an authenticated path (static or build) turns out to have no
19
- * usable session — the short-lived upload token came back empty, or the
20
- * session-cookie site create was refused, so nothing was created. For static
21
- * drops the flow falls back to the anonymous drop + claim; builds have no
22
- * anonymous equivalent, so the consumer's only recourse is to send the visitor
23
- * through login/signup and have them drop again.
15
+ * The pre-mutation "no session" signal: nothing was created yet, so static
16
+ * drops may fall back to the anonymous flow and build drops stash + hand off.
24
17
  */
25
18
  export class NotAuthenticatedError extends Error {
26
19
  readonly code = 'not-authenticated';
@@ -32,12 +25,8 @@ export class NotAuthenticatedError extends Error {
32
25
  }
33
26
 
34
27
  /**
35
- * Whether an error is the pre-mutation "no session" signal, however it
36
- * traveled. This package ships as both raw TypeScript (`./components`) and
37
- * compiled dist (`./preact`, `./drop-core`), so two copies of the class can
38
- * coexist in one app and defeat `instanceof` — the `code` brand is the
39
- * copy-proof fallback. Control flow that decides whether an anonymous retry
40
- * is safe must use this, never a bare `instanceof`.
28
+ * Copy-proof check for the "no session" error — `instanceof` misses class
29
+ * copies from the package's other entrypoint. Fallback gates must use this.
41
30
  */
42
31
  export function isNotAuthenticatedError(err: unknown): err is NotAuthenticatedError {
43
32
  if (err instanceof NotAuthenticatedError) return true;
@@ -71,12 +60,7 @@ export class BuildCreateError extends Error {
71
60
  }
72
61
  }
73
62
 
74
- /**
75
- * The build ran and ended in `error`/`rejected`. `reason` is the API's
76
- * `error_message` when there is one — it's the only part of a build failure that
77
- * says anything actionable (a failing build command, a missing dependency), so
78
- * it is surfaced to the user rather than swallowed.
79
- */
63
+ /** The build ran and broke. `reason` carries the API's `error_message` — the only actionable part. */
80
64
  export class BuildFailedError extends Error {
81
65
  readonly code = 'build-failed';
82
66
 
@@ -96,10 +80,7 @@ export class BuildTimeoutError extends Error {
96
80
  }
97
81
  }
98
82
 
99
- /**
100
- * A static deploy ran and ended in `error`/`rejected`. Like a build failure,
101
- * the API's `error_message` is the actionable part, so it travels on the error.
102
- */
83
+ /** A static deploy ran and ended in `error`/`rejected`; `reason` carries the API's `error_message`. */
103
84
  export class DeployFailedError extends Error {
104
85
  readonly code = 'deploy-failed';
105
86
 
@@ -152,9 +133,7 @@ export function humanizeDropError(err: unknown): string {
152
133
  }
153
134
  if (code === 'deploy-failed') {
154
135
  const reason = (err as DeployFailedError).reason;
155
- return reason
156
- ? `Your deploy failed: ${reason}`
157
- : 'Your deploy failed. Please try again.';
136
+ return reason ? `Your deploy failed: ${reason}` : 'Your deploy failed. Please try again.';
158
137
  }
159
138
 
160
139
  if (code === 'build-timeout') {
@@ -1,28 +1,14 @@
1
1
  /** What we report when the browser gives us no MIME type at all. */
2
2
  const UNKNOWN_TYPE = 'application/octet-stream';
3
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
- */
4
+ // Cap on distinct types per event — a backstop against oversized analytics payloads.
9
5
  const MAX_TYPES = 20;
10
6
 
11
7
  /**
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.
8
+ * Distinct, sorted MIME types of a selection. Read at the drop boundary — the
9
+ * type only exists on the original File, and reading can throw before any
10
+ * ProcessedFile exists. Call synchronously in the drop handler: a DataTransfer
11
+ * detaches after the event turn.
26
12
  */
27
13
  export function getFileTypes(files: FileList | null | undefined): string[] {
28
14
  if (!files?.length) return [];
@@ -53,15 +53,11 @@ async function readDirEntry(entry: any): Promise<ProcessedFile[]> {
53
53
  }
54
54
 
55
55
  /**
56
- * Read a DataTransfer from a drop event into ProcessedFiles.
57
- *
58
- * NOTE: `webkitGetAsEntry()` must be called synchronously inside the drop
59
- * handler — the DataTransfer is detached once the event turn ends. The
60
- * component captures entries first (see `readDroppedEntries`) then calls the
61
- * async readers, so this function is safe to `await`.
56
+ * Read a drop's DataTransfer into ProcessedFiles. `webkitGetAsEntry()` runs
57
+ * synchronously before the first await — the DataTransfer detaches once the
58
+ * event turn ends.
62
59
  */
63
60
  export async function readDataTransfer(dt: DataTransfer): Promise<ProcessedFile[]> {
64
- // single zip?
65
61
  const first = dt.files?.[0];
66
62
  if (dt.files.length === 1 && first && isZipName(first.name)) {
67
63
  return readZip(first);
@@ -164,11 +160,8 @@ export function hasMultipleHtmlFilesWithoutIndex(files: ProcessedFile[]): boolea
164
160
  }
165
161
 
166
162
  /**
167
- * When the set has no index.html but exactly one HTML file (a lone page
168
- * alongside its assets, or a single dropped file), rename that file to
169
- * index.html — keeping its directory — so it is served at the site root
170
- * instead of 404ing. Content and sha are untouched. Any other set (already has
171
- * an index, multiple HTML files, or no HTML) is returned unchanged.
163
+ * A lone non-index HTML page is renamed to index.html (same directory,
164
+ * content/sha untouched) so it serves instead of 404ing. Other sets unchanged.
172
165
  */
173
166
  export function renameSingleNonIndexHtmlToIndex(files: ProcessedFile[]): ProcessedFile[] {
174
167
  const name = getSingleNonIndexHtmlFileName(files);
@@ -11,10 +11,8 @@ export interface ProcessedFile {
11
11
  export type Digest = Record<string, string>; // { "/index.html": "<sha1>" }
12
12
 
13
13
  /**
14
- * Response body of `POST /api/v1/drop`. Not a canonical Deploy resource —
15
- * it's a mixed shape carrying both the anonymous site and the initial deploy
16
- * so the client has everything it needs to upload, poll, and hand off to the
17
- * user in one round trip.
14
+ * Response of `POST /api/v1/drop` — a mixed site+deploy shape, everything the
15
+ * client needs to upload, poll, and hand off in one round trip.
18
16
  */
19
17
  export interface DropResponse {
20
18
  /** site id (UUID) — surfaced to callers so they can build claim links */
@@ -13,12 +13,7 @@ function escapeTomlString(value: string): string {
13
13
  .replace(/\t/g, '\\t');
14
14
  }
15
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
- */
16
+ /** Renders a [build] table — command and publish only; everything else is zero-config server-side. */
22
17
  export function generateNetlifyToml({ command, publish }: InferredBuildSettings): string {
23
18
  return [
24
19
  '[build]',
@@ -31,22 +26,14 @@ export function generateNetlifyToml({ command, publish }: InferredBuildSettings)
31
26
  export interface ZipOptions {
32
27
  /** Filename given to the produced `File`. Shows up as the build's source archive. */
33
28
  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
- */
29
+ /** Synthesise a [build] table when the drop lacks one (default true); false uploads as-dropped. */
39
30
  synthesizeNetlifyToml?: boolean;
40
31
  }
41
32
 
42
33
  export interface ZipResult {
43
34
  /** The archive, ready to hand to `createBuildFromZip`. */
44
35
  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
- */
36
+ /** Settings written into netlify.toml, or null when the archive is a faithful copy. */
50
37
  buildSettings: InferredBuildSettings | null;
51
38
  }
52
39
 
@@ -58,20 +45,10 @@ function toBytes(content: ArrayBuffer): Uint8Array {
58
45
  }
59
46
 
60
47
  /**
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.
48
+ * Zip a read file set into the archive `POST /sites/{id}/builds` expects.
49
+ * Junk is already gone (readFiles); dist/build are deliberately kept — a drop
50
+ * publishing a committed directory without a command would break without them.
51
+ * A [build] table is synthesised when missing (appended, so redirects survive).
75
52
  */
76
53
  export async function zipFiles(
77
54
  files: ProcessedFile[],