tempest-react-sdk 0.49.0 → 0.51.0
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/audio/sfx-pool.cjs +1 -1
- package/dist/audio/sfx-pool.cjs.map +1 -1
- package/dist/audio/sfx-pool.js +36 -36
- package/dist/audio/sfx-pool.js.map +1 -1
- package/dist/components/BarList/BarList.cjs +1 -1
- package/dist/components/BarList/BarList.cjs.map +1 -1
- package/dist/components/BarList/BarList.js +6 -1
- package/dist/components/BarList/BarList.js.map +1 -1
- package/dist/components/BarList/bar-list-model.cjs +1 -1
- package/dist/components/BarList/bar-list-model.cjs.map +1 -1
- package/dist/components/BarList/bar-list-model.js +8 -8
- package/dist/components/BarList/bar-list-model.js.map +1 -1
- package/dist/components/DataTable/DataTable.cjs +1 -1
- package/dist/components/DataTable/DataTable.cjs.map +1 -1
- package/dist/components/DataTable/DataTable.js +112 -110
- package/dist/components/DataTable/DataTable.js.map +1 -1
- package/dist/components/DataTable/use-dev-warnings.cjs +1 -1
- package/dist/components/DataTable/use-dev-warnings.cjs.map +1 -1
- package/dist/components/DataTable/use-dev-warnings.js +5 -3
- package/dist/components/DataTable/use-dev-warnings.js.map +1 -1
- package/dist/components/Markdown/markdown-parse.cjs +5 -5
- package/dist/components/Markdown/markdown-parse.cjs.map +1 -1
- package/dist/components/Markdown/markdown-parse.js +66 -63
- package/dist/components/Markdown/markdown-parse.js.map +1 -1
- package/dist/components/Sidebar/Sidebar.cjs +1 -1
- package/dist/components/Sidebar/Sidebar.cjs.map +1 -1
- package/dist/components/Sidebar/Sidebar.js +25 -45
- package/dist/components/Sidebar/Sidebar.js.map +1 -1
- package/dist/components/Sidebar/Sidebar.module.cjs.map +1 -1
- package/dist/components/Sidebar/Sidebar.module.js.map +1 -1
- package/dist/components/Sidebar/SidebarEntry.cjs +2 -0
- package/dist/components/Sidebar/SidebarEntry.cjs.map +1 -0
- package/dist/components/Sidebar/SidebarEntry.js +40 -0
- package/dist/components/Sidebar/SidebarEntry.js.map +1 -0
- package/dist/http/api-client.cjs +1 -1
- package/dist/http/api-client.cjs.map +1 -1
- package/dist/http/api-client.js +84 -71
- package/dist/http/api-client.js.map +1 -1
- package/dist/http/resumable-upload.cjs +1 -1
- package/dist/http/resumable-upload.cjs.map +1 -1
- package/dist/http/resumable-upload.js +112 -104
- package/dist/http/resumable-upload.js.map +1 -1
- package/dist/http/timeout.cjs +2 -0
- package/dist/http/timeout.cjs.map +1 -0
- package/dist/http/timeout.js +22 -0
- package/dist/http/timeout.js.map +1 -0
- package/dist/icons/material-symbols.cjs.map +1 -1
- package/dist/icons/material-symbols.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/tempest-react-sdk.d.ts +104 -18
- package/dist/utils/csv.cjs.map +1 -1
- package/dist/utils/csv.js.map +1 -1
- package/dist/vite/tempest-pwa-dev-sw.cjs +1 -1
- package/dist/vite/tempest-pwa-dev-sw.cjs.map +1 -1
- package/dist/vite/tempest-pwa-dev-sw.js +49 -41
- package/dist/vite/tempest-pwa-dev-sw.js.map +1 -1
- package/package.json +1 -1
|
@@ -566,7 +566,7 @@ export declare interface ApiClient {
|
|
|
566
566
|
put<T>(path: string, options?: RequestOptions): Promise<T>;
|
|
567
567
|
patch<T>(path: string, options?: RequestOptions): Promise<T>;
|
|
568
568
|
delete<T>(path: string, options?: RequestOptions): Promise<T>;
|
|
569
|
-
upload<T>(path: string, formData: FormData, method?: "POST" | "PUT" | "PATCH"): Promise<T>;
|
|
569
|
+
upload<T>(path: string, formData: FormData, method?: "POST" | "PUT" | "PATCH", options?: Omit<RequestOptions, "body" | "method">): Promise<T>;
|
|
570
570
|
}
|
|
571
571
|
|
|
572
572
|
export declare interface ApiClientConfig {
|
|
@@ -652,6 +652,35 @@ export declare interface ApiClientConfig {
|
|
|
652
652
|
*/
|
|
653
653
|
retry?: boolean | RetryOptions;
|
|
654
654
|
/** Whether to send cookies on cross-origin requests (default: false). */
|
|
655
|
+
/**
|
|
656
|
+
* Milliseconds before a request is abandoned. Default `15_000`. `null` turns
|
|
657
|
+
* it off.
|
|
658
|
+
*
|
|
659
|
+
* There was no timeout at all before, and the failure it leaves open is not
|
|
660
|
+
* an error: a TCP connection that dies without a FIN never answers, so the
|
|
661
|
+
* browser can hold the request for minutes or forever. In an offline-first
|
|
662
|
+
* SDK that is the wrong place to have no floor — the eternal spinner lands
|
|
663
|
+
* exactly on the bad network this package exists to survive.
|
|
664
|
+
*
|
|
665
|
+
* A timeout surfaces as an {@link ApiError} with `status: 0`, the same shape
|
|
666
|
+
* the client already uses for "never reached the server", so the built-in
|
|
667
|
+
* retry policy replays it without a special case.
|
|
668
|
+
*/
|
|
669
|
+
timeout?: number | null;
|
|
670
|
+
/**
|
|
671
|
+
* Milliseconds before a `FormData` request is abandoned. Default `300_000`.
|
|
672
|
+
* `null` turns it off.
|
|
673
|
+
*
|
|
674
|
+
* A binary upload is not a slow request, it is a different kind of request. A
|
|
675
|
+
* single timeout forces a choice between one short enough to protect a normal
|
|
676
|
+
* call and one long enough to finish a file, and 15 seconds cuts an upload
|
|
677
|
+
* mid-body — which the server then has to interpret as a truncated payload.
|
|
678
|
+
*
|
|
679
|
+
* Detected from the body being `FormData`, the same test that already decides
|
|
680
|
+
* the `Content-Type`. Override per request with `options.timeout` when a
|
|
681
|
+
* particular call does not fit either default.
|
|
682
|
+
*/
|
|
683
|
+
uploadTimeout?: number | null;
|
|
655
684
|
withCredentials?: boolean;
|
|
656
685
|
/** Default headers merged into every request. */
|
|
657
686
|
headers?: Record<string, string>;
|
|
@@ -1838,13 +1867,22 @@ export declare interface BuildApiUrlOptions {
|
|
|
1838
1867
|
* negative width does not exist) and reports 0%, but its number is still shown —
|
|
1839
1868
|
* hiding the row would be worse than showing an odd one.
|
|
1840
1869
|
*
|
|
1841
|
-
* @param
|
|
1842
|
-
* @param sort - Ordering to apply. `"none"` keeps the caller's order.
|
|
1843
|
-
* @param max - Keep at most this many rows.
|
|
1844
|
-
* @param otherLabel - Aggregate what `max` cut into one row with this label.
|
|
1870
|
+
* @param options - The rows and how to reduce them.
|
|
1845
1871
|
* @returns The rows to draw, in order.
|
|
1846
1872
|
*/
|
|
1847
|
-
export declare function buildBarListRows(
|
|
1873
|
+
export declare function buildBarListRows(options: BuildBarListRowsOptions): BarListRow[];
|
|
1874
|
+
|
|
1875
|
+
/** What {@link buildBarListRows} needs to reduce a list into rows. */
|
|
1876
|
+
export declare interface BuildBarListRowsOptions {
|
|
1877
|
+
/** The rows as given. */
|
|
1878
|
+
items: readonly BarListItem[];
|
|
1879
|
+
/** Ordering to apply. `"none"` keeps the caller's order. Default `"desc"`. */
|
|
1880
|
+
sort?: BarListSort;
|
|
1881
|
+
/** Keep at most this many rows. */
|
|
1882
|
+
max?: number;
|
|
1883
|
+
/** Aggregate what `max` cut into one row with this label. */
|
|
1884
|
+
otherLabel?: string;
|
|
1885
|
+
}
|
|
1848
1886
|
|
|
1849
1887
|
/**
|
|
1850
1888
|
* Builds an `intent://` URL that re-opens the current page inside Chrome on
|
|
@@ -3975,6 +4013,12 @@ export declare interface CredentialsContainerLike {
|
|
|
3975
4013
|
}): Promise<Credential | null>;
|
|
3976
4014
|
}
|
|
3977
4015
|
|
|
4016
|
+
/**
|
|
4017
|
+
* @tempest-limits param-count — `downloadCsv(rows, columns, "usuarios.csv")` reads
|
|
4018
|
+
* in the order the sentence does, and its two trailing parameters have defaults,
|
|
4019
|
+
* so the common call passes three. Wrapping them in an object would break every
|
|
4020
|
+
* caller of a published export for no gain in what the call site says.
|
|
4021
|
+
*/
|
|
3978
4022
|
/** One column of the exported file. */
|
|
3979
4023
|
export declare interface CsvColumn<T> {
|
|
3980
4024
|
/** Property of the row this column reads from. Doubles as the column key. */
|
|
@@ -4200,17 +4244,6 @@ export declare interface DataTableBaseProps<T> extends HTMLAttributes<HTMLDivEle
|
|
|
4200
4244
|
onCellChange?: (change: DataTableCellChange<T>) => void | Promise<void>;
|
|
4201
4245
|
/** Override the PT-BR copy of the editing affordances. */
|
|
4202
4246
|
editLabels?: Partial<DataTableEditLabels>;
|
|
4203
|
-
/**
|
|
4204
|
-
* Searching is the caller's job: typing reports through `onSearchChange` and
|
|
4205
|
-
* the rows are left as they arrived.
|
|
4206
|
-
*
|
|
4207
|
-
* Implied by `totalItems`. Filtering the current page would hide the rows
|
|
4208
|
-
* that do not match *on this page* and show nothing for a term that only
|
|
4209
|
-
* matches on page three — an empty table that looks like "no results".
|
|
4210
|
-
*/
|
|
4211
|
-
manualSearch?: boolean;
|
|
4212
|
-
/** Called with the current search term (debouncing, if any, is the caller's). */
|
|
4213
|
-
onSearchChange?: (term: string) => void;
|
|
4214
4247
|
/**
|
|
4215
4248
|
* A fetch is in flight.
|
|
4216
4249
|
*
|
|
@@ -4354,7 +4387,42 @@ export declare type DataTablePagingProps = {
|
|
|
4354
4387
|
* The table's props: the shared half, plus one valid paging shape and one valid
|
|
4355
4388
|
* sorting shape.
|
|
4356
4389
|
*/
|
|
4357
|
-
export declare type DataTableProps<T> = DataTableBaseProps<T> & DataTablePagingProps & DataTableSortProps<T
|
|
4390
|
+
export declare type DataTableProps<T> = DataTableBaseProps<T> & DataTablePagingProps & DataTableSortProps<T> & DataTableSearchProps;
|
|
4391
|
+
|
|
4392
|
+
/**
|
|
4393
|
+
* Searching: delegated, and therefore reported, or neither.
|
|
4394
|
+
*
|
|
4395
|
+
* `manualSearch` without `onSearchChange` renders a search box that filters
|
|
4396
|
+
* nothing and tells nobody — the same shape of lie as a header arrow that turns
|
|
4397
|
+
* without sorting.
|
|
4398
|
+
*
|
|
4399
|
+
* Independent of the paging axis on purpose, and that leaves one gap this type
|
|
4400
|
+
* does not close: `totalItems` *implies* `manualSearch`, so a server-mode table
|
|
4401
|
+
* with `searchable` and no `onSearchChange` falls into the same hole without ever
|
|
4402
|
+
* writing `manualSearch`. Closing it means the search axis has to read the paging
|
|
4403
|
+
* axis, which crosses two three-member unions into nine and turns every mismatch
|
|
4404
|
+
* into a wall of candidate shapes. That case is a dev warning instead — the one
|
|
4405
|
+
* spot where runtime really is the cheaper check, and `use-dev-warnings.ts` says
|
|
4406
|
+
* so at the call site.
|
|
4407
|
+
*/
|
|
4408
|
+
export declare type DataTableSearchProps = {
|
|
4409
|
+
/** The table filters the rows it has. */
|
|
4410
|
+
manualSearch?: false;
|
|
4411
|
+
/** Called with the current search term (debouncing, if any, is the caller's). */
|
|
4412
|
+
onSearchChange?: (term: string) => void;
|
|
4413
|
+
} | {
|
|
4414
|
+
/**
|
|
4415
|
+
* Searching is the caller's job: typing reports through `onSearchChange`
|
|
4416
|
+
* and the rows are left as they arrived.
|
|
4417
|
+
*
|
|
4418
|
+
* Implied by `totalItems`. Filtering the current page would hide the rows
|
|
4419
|
+
* that do not match *on this page* and show nothing for a term that only
|
|
4420
|
+
* matches on page three — an empty table that looks like "no results".
|
|
4421
|
+
*/
|
|
4422
|
+
manualSearch: true;
|
|
4423
|
+
/** Where the typing goes. Required, since nothing else acts on it. */
|
|
4424
|
+
onSearchChange: (term: string) => void;
|
|
4425
|
+
};
|
|
4358
4426
|
|
|
4359
4427
|
export declare interface DataTableSort<T> {
|
|
4360
4428
|
key: keyof T;
|
|
@@ -9659,6 +9727,24 @@ export declare interface ReorderPayload {
|
|
|
9659
9727
|
export declare interface RequestOptions extends Omit<RequestInit, "body"> {
|
|
9660
9728
|
body?: unknown;
|
|
9661
9729
|
params?: Record<string, string | number | boolean | undefined | null>;
|
|
9730
|
+
/**
|
|
9731
|
+
* Abort this request.
|
|
9732
|
+
*
|
|
9733
|
+
* Inherited from `RequestInit` and forwarded to `fetch`, so it has always
|
|
9734
|
+
* worked — but nothing said so, which made it a capability nobody could find.
|
|
9735
|
+
* Declared here for that reason alone. Pass the `signal` react-query hands to
|
|
9736
|
+
* a `queryFn` and the request is cancelled on unmount and on refetch.
|
|
9737
|
+
*/
|
|
9738
|
+
signal?: AbortSignal | null;
|
|
9739
|
+
/**
|
|
9740
|
+
* Override the client's timeout for this request, in milliseconds. `null`
|
|
9741
|
+
* disables it, which is the escape hatch for a stream or a long poll.
|
|
9742
|
+
*
|
|
9743
|
+
* Composed with `signal`, not replacing it: whichever fires first wins, and
|
|
9744
|
+
* the two are told apart — a timeout surfaces as an {@link ApiError} with
|
|
9745
|
+
* `status: 0`, while an abort you asked for propagates as an abort.
|
|
9746
|
+
*/
|
|
9747
|
+
timeout?: number | null;
|
|
9662
9748
|
}
|
|
9663
9749
|
|
|
9664
9750
|
/**
|
package/dist/utils/csv.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"csv.cjs","names":[],"sources":["../../src/utils/csv.ts"],"sourcesContent":["// CSV that survives contact with real data. Every app writes this by hand and\n// every hand-written version gets the same two things wrong: a value containing\n// the delimiter splits the row, and a value containing a quote breaks the quoting\n// it was supposed to be protected by. Both are one-liners in RFC 4180 and neither\n// is obvious until a customer's name has a comma in it.\n\nimport { shareOrDownloadBlob } from \"../share/share-or-download\";\n\n/**\n * Byte order mark, as the character `String.prototype` sees it.\n *\n * Excel on a pt-BR install reads a BOM-less UTF-8 file as Latin-1 and turns\n * every accented name into mojibake. It is three bytes to avoid a support\n * ticket, so it is on by default.\n */\nconst BOM = \"\\uFEFF\";\n\n/** One column of the exported file. */\nexport interface CsvColumn<T> {\n /** Property of the row this column reads from. Doubles as the column key. */\n key: keyof T;\n /** Column heading, written to the first line. */\n header: string;\n /**\n * Value for the file. Defaults to `String(row[key])`, with nullish becoming an\n * empty field.\n *\n * A `DataTableColumn` renders cells to `ReactNode`, which cannot be written to\n * a text file — a badge or a link would serialize as `[object Object]`. Give\n * the column this accessor and the export says what the badge said.\n */\n csv?: (row: T) => string | number | boolean | null | undefined;\n}\n\n/** Options for {@link toCsv}. */\nexport interface CsvOptions {\n /**\n * Field separator. Default `\",\"`.\n *\n * Use `\";\"` for Excel on a locale whose decimal separator is the comma —\n * which is every pt-BR install — otherwise it opens the file in one column.\n */\n delimiter?: \",\" | \";\";\n /** Prefix the output with a UTF-8 BOM. Default `true`. */\n bom?: boolean;\n}\n\n/**\n * Quote one field per RFC 4180.\n *\n * A field is quoted when it contains the delimiter, a double quote, or a line\n * break; inside a quoted field, each double quote is doubled. Fields that need\n * none of this are written bare, which keeps the common file readable.\n *\n * @param value - The already-stringified field.\n * @param delimiter - The separator in use, which decides part of the quoting.\n * @returns The field, quoted if it has to be.\n */\nfunction escapeField(value: string, delimiter: string): string {\n const mustQuote =\n value.includes(delimiter) ||\n value.includes('\"') ||\n value.includes(\"\\n\") ||\n value.includes(\"\\r\");\n if (!mustQuote) return value;\n return `\"${value.replaceAll('\"', '\"\"')}\"`;\n}\n\n/**\n * Stringify one cell, keeping the difference between \"no value\" and \"zero\".\n *\n * `null` and `undefined` become an empty field; `0` and `false` are values\n * somebody chose and are written out. Getting this backwards is how an export\n * ends up under-reporting every row that legitimately holds a zero.\n *\n * @param value - The raw cell value.\n * @returns The text to write.\n */\nfunction cellText(value: unknown): string {\n if (value === null || value === undefined) return \"\";\n if (value instanceof Date) return value.toISOString();\n return String(value);\n}\n\n/**\n * Serialize rows to CSV text, RFC 4180 style.\n *\n * Rows are separated by `\\r\\n` — the RFC's terminator and the one Excel is least\n * surprised by. A row is emitted for the header even when `rows` is empty, so the\n * person who opens the file sees which columns they asked for instead of a blank\n * document.\n *\n * @example\n * const csv = toCsv(users, [\n * { key: \"name\", header: \"Nome\" },\n * { key: \"email\", header: \"E-mail\" },\n * { key: \"plan\", header: \"Plano\", csv: (user) => user.plan.label },\n * ]);\n *\n * @param rows - The rows to export.\n * @param columns - Columns, in the order they should appear.\n * @param options - Delimiter and BOM.\n * @returns The complete file contents.\n */\nexport function toCsv<T>(\n rows: readonly T[],\n columns: readonly CsvColumn<T>[],\n options: CsvOptions = {},\n): string {\n const { delimiter = \",\", bom = true } = options;\n\n const lines = [columns.map((column) => escapeField(column.header, delimiter)).join(delimiter)];\n\n for (const row of rows) {\n const cells = columns.map((column) => {\n const raw = column.csv\n ? column.csv(row)\n : (row as Record<string, unknown>)[column.key as string];\n return escapeField(cellText(raw), delimiter);\n });\n lines.push(cells.join(delimiter));\n }\n\n return `${bom ? BOM : \"\"}${lines.join(\"\\r\\n\")}`;\n}\n\n/**\n * Build a CSV and hand it to the user.\n *\n * Goes through {@link shareOrDownloadBlob}, so on a phone it opens the native\n * share sheet and everywhere else it downloads — the same path every other\n * generated artifact in the SDK takes, instead of a fourth hand-rolled `<a\n * download>`.\n *\n * @example\n * await downloadCsv(users, COLUMNS, \"usuarios.csv\");\n *\n * @param rows - The rows to export.\n * @param columns - Columns, in the order they should appear.\n * @param fileName - File name offered to the user. Default `\"export.csv\"`.\n * @param options - Delimiter and BOM, forwarded to {@link toCsv}.\n * @returns A promise that resolves once the share or download completes.\n */\nexport async function downloadCsv<T>(\n rows: readonly T[],\n columns: readonly CsvColumn<T>[],\n fileName = \"export.csv\",\n options: CsvOptions = {},\n): Promise<void> {\n const blob = new Blob([toCsv(rows, columns, options)], {\n type: \"text/csv;charset=utf-8\",\n });\n await shareOrDownloadBlob(blob, fileName);\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"csv.cjs","names":[],"sources":["../../src/utils/csv.ts"],"sourcesContent":["/**\n * @tempest-limits param-count — `downloadCsv(rows, columns, \"usuarios.csv\")` reads\n * in the order the sentence does, and its two trailing parameters have defaults,\n * so the common call passes three. Wrapping them in an object would break every\n * caller of a published export for no gain in what the call site says.\n */\n// CSV that survives contact with real data. Every app writes this by hand and\n// every hand-written version gets the same two things wrong: a value containing\n// the delimiter splits the row, and a value containing a quote breaks the quoting\n// it was supposed to be protected by. Both are one-liners in RFC 4180 and neither\n// is obvious until a customer's name has a comma in it.\n\nimport { shareOrDownloadBlob } from \"../share/share-or-download\";\n\n/**\n * Byte order mark, as the character `String.prototype` sees it.\n *\n * Excel on a pt-BR install reads a BOM-less UTF-8 file as Latin-1 and turns\n * every accented name into mojibake. It is three bytes to avoid a support\n * ticket, so it is on by default.\n */\nconst BOM = \"\\uFEFF\";\n\n/** One column of the exported file. */\nexport interface CsvColumn<T> {\n /** Property of the row this column reads from. Doubles as the column key. */\n key: keyof T;\n /** Column heading, written to the first line. */\n header: string;\n /**\n * Value for the file. Defaults to `String(row[key])`, with nullish becoming an\n * empty field.\n *\n * A `DataTableColumn` renders cells to `ReactNode`, which cannot be written to\n * a text file — a badge or a link would serialize as `[object Object]`. Give\n * the column this accessor and the export says what the badge said.\n */\n csv?: (row: T) => string | number | boolean | null | undefined;\n}\n\n/** Options for {@link toCsv}. */\nexport interface CsvOptions {\n /**\n * Field separator. Default `\",\"`.\n *\n * Use `\";\"` for Excel on a locale whose decimal separator is the comma —\n * which is every pt-BR install — otherwise it opens the file in one column.\n */\n delimiter?: \",\" | \";\";\n /** Prefix the output with a UTF-8 BOM. Default `true`. */\n bom?: boolean;\n}\n\n/**\n * Quote one field per RFC 4180.\n *\n * A field is quoted when it contains the delimiter, a double quote, or a line\n * break; inside a quoted field, each double quote is doubled. Fields that need\n * none of this are written bare, which keeps the common file readable.\n *\n * @param value - The already-stringified field.\n * @param delimiter - The separator in use, which decides part of the quoting.\n * @returns The field, quoted if it has to be.\n */\nfunction escapeField(value: string, delimiter: string): string {\n const mustQuote =\n value.includes(delimiter) ||\n value.includes('\"') ||\n value.includes(\"\\n\") ||\n value.includes(\"\\r\");\n if (!mustQuote) return value;\n return `\"${value.replaceAll('\"', '\"\"')}\"`;\n}\n\n/**\n * Stringify one cell, keeping the difference between \"no value\" and \"zero\".\n *\n * `null` and `undefined` become an empty field; `0` and `false` are values\n * somebody chose and are written out. Getting this backwards is how an export\n * ends up under-reporting every row that legitimately holds a zero.\n *\n * @param value - The raw cell value.\n * @returns The text to write.\n */\nfunction cellText(value: unknown): string {\n if (value === null || value === undefined) return \"\";\n if (value instanceof Date) return value.toISOString();\n return String(value);\n}\n\n/**\n * Serialize rows to CSV text, RFC 4180 style.\n *\n * Rows are separated by `\\r\\n` — the RFC's terminator and the one Excel is least\n * surprised by. A row is emitted for the header even when `rows` is empty, so the\n * person who opens the file sees which columns they asked for instead of a blank\n * document.\n *\n * @example\n * const csv = toCsv(users, [\n * { key: \"name\", header: \"Nome\" },\n * { key: \"email\", header: \"E-mail\" },\n * { key: \"plan\", header: \"Plano\", csv: (user) => user.plan.label },\n * ]);\n *\n * @param rows - The rows to export.\n * @param columns - Columns, in the order they should appear.\n * @param options - Delimiter and BOM.\n * @returns The complete file contents.\n */\nexport function toCsv<T>(\n rows: readonly T[],\n columns: readonly CsvColumn<T>[],\n options: CsvOptions = {},\n): string {\n const { delimiter = \",\", bom = true } = options;\n\n const lines = [columns.map((column) => escapeField(column.header, delimiter)).join(delimiter)];\n\n for (const row of rows) {\n const cells = columns.map((column) => {\n const raw = column.csv\n ? column.csv(row)\n : (row as Record<string, unknown>)[column.key as string];\n return escapeField(cellText(raw), delimiter);\n });\n lines.push(cells.join(delimiter));\n }\n\n return `${bom ? BOM : \"\"}${lines.join(\"\\r\\n\")}`;\n}\n\n/**\n * Build a CSV and hand it to the user.\n *\n * Goes through {@link shareOrDownloadBlob}, so on a phone it opens the native\n * share sheet and everywhere else it downloads — the same path every other\n * generated artifact in the SDK takes, instead of a fourth hand-rolled `<a\n * download>`.\n *\n * @example\n * await downloadCsv(users, COLUMNS, \"usuarios.csv\");\n *\n * @param rows - The rows to export.\n * @param columns - Columns, in the order they should appear.\n * @param fileName - File name offered to the user. Default `\"export.csv\"`.\n * @param options - Delimiter and BOM, forwarded to {@link toCsv}.\n * @returns A promise that resolves once the share or download completes.\n */\nexport async function downloadCsv<T>(\n rows: readonly T[],\n columns: readonly CsvColumn<T>[],\n fileName = \"export.csv\",\n options: CsvOptions = {},\n): Promise<void> {\n const blob = new Blob([toCsv(rows, columns, options)], {\n type: \"text/csv;charset=utf-8\",\n });\n await shareOrDownloadBlob(blob, fileName);\n}\n"],"mappings":"kDAqBA,IAAM,EAAM,IA2CZ,SAAS,EAAY,EAAe,EAA2B,CAO3D,OALI,EAAM,SAAS,CAAS,GACxB,EAAM,SAAS,GAAG,GAClB,EAAM,SAAS;CAAI,GACnB,EAAM,SAAS,IAAI,EAEhB,IAAI,EAAM,WAAW,IAAK,IAAI,EAAE,GADhB,CAE3B,CAYA,SAAS,EAAS,EAAwB,CAGtC,OAFI,GAAU,KAAoC,GAC9C,aAAiB,KAAa,EAAM,YAAY,EAC7C,OAAO,CAAK,CACvB,CAsBA,SAAgB,EACZ,EACA,EACA,EAAsB,CAAC,EACjB,CACN,GAAM,CAAE,YAAY,IAAK,MAAM,IAAS,EAElC,EAAQ,CAAC,EAAQ,IAAK,GAAW,EAAY,EAAO,OAAQ,CAAS,CAAC,CAAC,CAAC,KAAK,CAAS,CAAC,EAE7F,IAAK,IAAM,KAAO,EAAM,CACpB,IAAM,EAAQ,EAAQ,IAAK,GAIhB,EAAY,EAHP,EAAO,IACb,EAAO,IAAI,CAAG,EACb,EAAgC,EAAO,IACf,EAAG,CAAS,CAC9C,EACD,EAAM,KAAK,EAAM,KAAK,CAAS,CAAC,CACpC,CAEA,MAAO,GAAG,EAAM,EAAM,KAAK,EAAM,KAAK;CAAM,GAChD,CAmBA,eAAsB,EAClB,EACA,EACA,EAAW,aACX,EAAsB,CAAC,EACV,CACb,IAAM,EAAO,IAAI,KAAK,CAAC,EAAM,EAAM,EAAS,CAAO,CAAC,EAAG,CACnD,KAAM,wBACV,CAAC,EACD,MAAM,EAAA,oBAAoB,EAAM,CAAQ,CAC5C"}
|
package/dist/utils/csv.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"csv.js","names":[],"sources":["../../src/utils/csv.ts"],"sourcesContent":["// CSV that survives contact with real data. Every app writes this by hand and\n// every hand-written version gets the same two things wrong: a value containing\n// the delimiter splits the row, and a value containing a quote breaks the quoting\n// it was supposed to be protected by. Both are one-liners in RFC 4180 and neither\n// is obvious until a customer's name has a comma in it.\n\nimport { shareOrDownloadBlob } from \"../share/share-or-download\";\n\n/**\n * Byte order mark, as the character `String.prototype` sees it.\n *\n * Excel on a pt-BR install reads a BOM-less UTF-8 file as Latin-1 and turns\n * every accented name into mojibake. It is three bytes to avoid a support\n * ticket, so it is on by default.\n */\nconst BOM = \"\\uFEFF\";\n\n/** One column of the exported file. */\nexport interface CsvColumn<T> {\n /** Property of the row this column reads from. Doubles as the column key. */\n key: keyof T;\n /** Column heading, written to the first line. */\n header: string;\n /**\n * Value for the file. Defaults to `String(row[key])`, with nullish becoming an\n * empty field.\n *\n * A `DataTableColumn` renders cells to `ReactNode`, which cannot be written to\n * a text file — a badge or a link would serialize as `[object Object]`. Give\n * the column this accessor and the export says what the badge said.\n */\n csv?: (row: T) => string | number | boolean | null | undefined;\n}\n\n/** Options for {@link toCsv}. */\nexport interface CsvOptions {\n /**\n * Field separator. Default `\",\"`.\n *\n * Use `\";\"` for Excel on a locale whose decimal separator is the comma —\n * which is every pt-BR install — otherwise it opens the file in one column.\n */\n delimiter?: \",\" | \";\";\n /** Prefix the output with a UTF-8 BOM. Default `true`. */\n bom?: boolean;\n}\n\n/**\n * Quote one field per RFC 4180.\n *\n * A field is quoted when it contains the delimiter, a double quote, or a line\n * break; inside a quoted field, each double quote is doubled. Fields that need\n * none of this are written bare, which keeps the common file readable.\n *\n * @param value - The already-stringified field.\n * @param delimiter - The separator in use, which decides part of the quoting.\n * @returns The field, quoted if it has to be.\n */\nfunction escapeField(value: string, delimiter: string): string {\n const mustQuote =\n value.includes(delimiter) ||\n value.includes('\"') ||\n value.includes(\"\\n\") ||\n value.includes(\"\\r\");\n if (!mustQuote) return value;\n return `\"${value.replaceAll('\"', '\"\"')}\"`;\n}\n\n/**\n * Stringify one cell, keeping the difference between \"no value\" and \"zero\".\n *\n * `null` and `undefined` become an empty field; `0` and `false` are values\n * somebody chose and are written out. Getting this backwards is how an export\n * ends up under-reporting every row that legitimately holds a zero.\n *\n * @param value - The raw cell value.\n * @returns The text to write.\n */\nfunction cellText(value: unknown): string {\n if (value === null || value === undefined) return \"\";\n if (value instanceof Date) return value.toISOString();\n return String(value);\n}\n\n/**\n * Serialize rows to CSV text, RFC 4180 style.\n *\n * Rows are separated by `\\r\\n` — the RFC's terminator and the one Excel is least\n * surprised by. A row is emitted for the header even when `rows` is empty, so the\n * person who opens the file sees which columns they asked for instead of a blank\n * document.\n *\n * @example\n * const csv = toCsv(users, [\n * { key: \"name\", header: \"Nome\" },\n * { key: \"email\", header: \"E-mail\" },\n * { key: \"plan\", header: \"Plano\", csv: (user) => user.plan.label },\n * ]);\n *\n * @param rows - The rows to export.\n * @param columns - Columns, in the order they should appear.\n * @param options - Delimiter and BOM.\n * @returns The complete file contents.\n */\nexport function toCsv<T>(\n rows: readonly T[],\n columns: readonly CsvColumn<T>[],\n options: CsvOptions = {},\n): string {\n const { delimiter = \",\", bom = true } = options;\n\n const lines = [columns.map((column) => escapeField(column.header, delimiter)).join(delimiter)];\n\n for (const row of rows) {\n const cells = columns.map((column) => {\n const raw = column.csv\n ? column.csv(row)\n : (row as Record<string, unknown>)[column.key as string];\n return escapeField(cellText(raw), delimiter);\n });\n lines.push(cells.join(delimiter));\n }\n\n return `${bom ? BOM : \"\"}${lines.join(\"\\r\\n\")}`;\n}\n\n/**\n * Build a CSV and hand it to the user.\n *\n * Goes through {@link shareOrDownloadBlob}, so on a phone it opens the native\n * share sheet and everywhere else it downloads — the same path every other\n * generated artifact in the SDK takes, instead of a fourth hand-rolled `<a\n * download>`.\n *\n * @example\n * await downloadCsv(users, COLUMNS, \"usuarios.csv\");\n *\n * @param rows - The rows to export.\n * @param columns - Columns, in the order they should appear.\n * @param fileName - File name offered to the user. Default `\"export.csv\"`.\n * @param options - Delimiter and BOM, forwarded to {@link toCsv}.\n * @returns A promise that resolves once the share or download completes.\n */\nexport async function downloadCsv<T>(\n rows: readonly T[],\n columns: readonly CsvColumn<T>[],\n fileName = \"export.csv\",\n options: CsvOptions = {},\n): Promise<void> {\n const blob = new Blob([toCsv(rows, columns, options)], {\n type: \"text/csv;charset=utf-8\",\n });\n await shareOrDownloadBlob(blob, fileName);\n}\n"],"mappings":";;
|
|
1
|
+
{"version":3,"file":"csv.js","names":[],"sources":["../../src/utils/csv.ts"],"sourcesContent":["/**\n * @tempest-limits param-count — `downloadCsv(rows, columns, \"usuarios.csv\")` reads\n * in the order the sentence does, and its two trailing parameters have defaults,\n * so the common call passes three. Wrapping them in an object would break every\n * caller of a published export for no gain in what the call site says.\n */\n// CSV that survives contact with real data. Every app writes this by hand and\n// every hand-written version gets the same two things wrong: a value containing\n// the delimiter splits the row, and a value containing a quote breaks the quoting\n// it was supposed to be protected by. Both are one-liners in RFC 4180 and neither\n// is obvious until a customer's name has a comma in it.\n\nimport { shareOrDownloadBlob } from \"../share/share-or-download\";\n\n/**\n * Byte order mark, as the character `String.prototype` sees it.\n *\n * Excel on a pt-BR install reads a BOM-less UTF-8 file as Latin-1 and turns\n * every accented name into mojibake. It is three bytes to avoid a support\n * ticket, so it is on by default.\n */\nconst BOM = \"\\uFEFF\";\n\n/** One column of the exported file. */\nexport interface CsvColumn<T> {\n /** Property of the row this column reads from. Doubles as the column key. */\n key: keyof T;\n /** Column heading, written to the first line. */\n header: string;\n /**\n * Value for the file. Defaults to `String(row[key])`, with nullish becoming an\n * empty field.\n *\n * A `DataTableColumn` renders cells to `ReactNode`, which cannot be written to\n * a text file — a badge or a link would serialize as `[object Object]`. Give\n * the column this accessor and the export says what the badge said.\n */\n csv?: (row: T) => string | number | boolean | null | undefined;\n}\n\n/** Options for {@link toCsv}. */\nexport interface CsvOptions {\n /**\n * Field separator. Default `\",\"`.\n *\n * Use `\";\"` for Excel on a locale whose decimal separator is the comma —\n * which is every pt-BR install — otherwise it opens the file in one column.\n */\n delimiter?: \",\" | \";\";\n /** Prefix the output with a UTF-8 BOM. Default `true`. */\n bom?: boolean;\n}\n\n/**\n * Quote one field per RFC 4180.\n *\n * A field is quoted when it contains the delimiter, a double quote, or a line\n * break; inside a quoted field, each double quote is doubled. Fields that need\n * none of this are written bare, which keeps the common file readable.\n *\n * @param value - The already-stringified field.\n * @param delimiter - The separator in use, which decides part of the quoting.\n * @returns The field, quoted if it has to be.\n */\nfunction escapeField(value: string, delimiter: string): string {\n const mustQuote =\n value.includes(delimiter) ||\n value.includes('\"') ||\n value.includes(\"\\n\") ||\n value.includes(\"\\r\");\n if (!mustQuote) return value;\n return `\"${value.replaceAll('\"', '\"\"')}\"`;\n}\n\n/**\n * Stringify one cell, keeping the difference between \"no value\" and \"zero\".\n *\n * `null` and `undefined` become an empty field; `0` and `false` are values\n * somebody chose and are written out. Getting this backwards is how an export\n * ends up under-reporting every row that legitimately holds a zero.\n *\n * @param value - The raw cell value.\n * @returns The text to write.\n */\nfunction cellText(value: unknown): string {\n if (value === null || value === undefined) return \"\";\n if (value instanceof Date) return value.toISOString();\n return String(value);\n}\n\n/**\n * Serialize rows to CSV text, RFC 4180 style.\n *\n * Rows are separated by `\\r\\n` — the RFC's terminator and the one Excel is least\n * surprised by. A row is emitted for the header even when `rows` is empty, so the\n * person who opens the file sees which columns they asked for instead of a blank\n * document.\n *\n * @example\n * const csv = toCsv(users, [\n * { key: \"name\", header: \"Nome\" },\n * { key: \"email\", header: \"E-mail\" },\n * { key: \"plan\", header: \"Plano\", csv: (user) => user.plan.label },\n * ]);\n *\n * @param rows - The rows to export.\n * @param columns - Columns, in the order they should appear.\n * @param options - Delimiter and BOM.\n * @returns The complete file contents.\n */\nexport function toCsv<T>(\n rows: readonly T[],\n columns: readonly CsvColumn<T>[],\n options: CsvOptions = {},\n): string {\n const { delimiter = \",\", bom = true } = options;\n\n const lines = [columns.map((column) => escapeField(column.header, delimiter)).join(delimiter)];\n\n for (const row of rows) {\n const cells = columns.map((column) => {\n const raw = column.csv\n ? column.csv(row)\n : (row as Record<string, unknown>)[column.key as string];\n return escapeField(cellText(raw), delimiter);\n });\n lines.push(cells.join(delimiter));\n }\n\n return `${bom ? BOM : \"\"}${lines.join(\"\\r\\n\")}`;\n}\n\n/**\n * Build a CSV and hand it to the user.\n *\n * Goes through {@link shareOrDownloadBlob}, so on a phone it opens the native\n * share sheet and everywhere else it downloads — the same path every other\n * generated artifact in the SDK takes, instead of a fourth hand-rolled `<a\n * download>`.\n *\n * @example\n * await downloadCsv(users, COLUMNS, \"usuarios.csv\");\n *\n * @param rows - The rows to export.\n * @param columns - Columns, in the order they should appear.\n * @param fileName - File name offered to the user. Default `\"export.csv\"`.\n * @param options - Delimiter and BOM, forwarded to {@link toCsv}.\n * @returns A promise that resolves once the share or download completes.\n */\nexport async function downloadCsv<T>(\n rows: readonly T[],\n columns: readonly CsvColumn<T>[],\n fileName = \"export.csv\",\n options: CsvOptions = {},\n): Promise<void> {\n const blob = new Blob([toCsv(rows, columns, options)], {\n type: \"text/csv;charset=utf-8\",\n });\n await shareOrDownloadBlob(blob, fileName);\n}\n"],"mappings":";;AAqBA,IAAM,IAAM;AA2CZ,SAAS,EAAY,GAAe,GAA2B;CAO3D,OALI,EAAM,SAAS,CAAS,KACxB,EAAM,SAAS,IAAG,KAClB,EAAM,SAAS,IAAI,KACnB,EAAM,SAAS,IAAI,IAEhB,IAAI,EAAM,WAAW,MAAK,MAAI,EAAE,KADhB;AAE3B;AAYA,SAAS,EAAS,GAAwB;CAGtC,OAFI,KAAU,OAAoC,KAC9C,aAAiB,OAAa,EAAM,YAAY,IAC7C,OAAO,CAAK;AACvB;AAsBA,SAAgB,EACZ,GACA,GACA,IAAsB,CAAC,GACjB;CACN,IAAM,EAAE,eAAY,KAAK,SAAM,OAAS,GAElC,IAAQ,CAAC,EAAQ,KAAK,MAAW,EAAY,EAAO,QAAQ,CAAS,CAAC,CAAC,CAAC,KAAK,CAAS,CAAC;CAE7F,KAAK,IAAM,KAAO,GAAM;EACpB,IAAM,IAAQ,EAAQ,KAAK,MAIhB,EAAY,EAHP,EAAO,MACb,EAAO,IAAI,CAAG,IACb,EAAgC,EAAO,IACf,GAAG,CAAS,CAC9C;EACD,EAAM,KAAK,EAAM,KAAK,CAAS,CAAC;CACpC;CAEA,OAAO,GAAG,IAAM,IAAM,KAAK,EAAM,KAAK,MAAM;AAChD;AAmBA,eAAsB,EAClB,GACA,GACA,IAAW,cACX,IAAsB,CAAC,GACV;CACb,IAAM,IAAO,IAAI,KAAK,CAAC,EAAM,GAAM,GAAS,CAAO,CAAC,GAAG,EACnD,MAAM,yBACV,CAAC;CACD,MAAM,EAAoB,GAAM,CAAQ;AAC5C"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("./base-url.cjs");let t=require("node:path");function n(n={}){let{swSrc:
|
|
1
|
+
const e=require("./base-url.cjs");let t=require("node:path");function n(e){return async(t,n,r)=>{let i=(t.url??``).split(`?`)[0];if(e.matches(i,e.swUrl)){try{let t=await e.buildWorker();n.setHeader(`Content-Type`,`application/javascript`),n.setHeader(`Service-Worker-Allowed`,`/`),n.setHeader(`Cache-Control`,`no-cache`),n.end(t)}catch(e){n.statusCode=500,n.end(`// SW dev build failed:\n// ${String(e)}`)}return}if(e.matches(i,e.manifestUrl)){n.setHeader(`Content-Type`,`application/json`),n.setHeader(`Cache-Control`,`no-cache`),n.end(JSON.stringify({version:`dev`,urls:[]}));return}r()}}function r(r={}){let{swSrc:i=`src/sw.ts`,swUrl:a=`/sw.js`,manifestUrl:o=`/precache-manifest.json`,enabled:s=!0}=r,c=process.cwd(),l=`/`;function u(t,n){if(t===n)return!0;let r=e.basePrefix(l);return r!==`/`&&t===`${r}${n.replace(/^\//,``)}`}let d=null,f=null;async function p(){return d||(d=await(await import(`esbuild`)).context({entryPoints:[(0,t.resolve)(c,i)],bundle:!0,format:`iife`,platform:`browser`,target:`es2020`,write:!1,absWorkingDir:c,logLevel:`silent`}),d)}function m(){if(f)return f;let e=(async()=>{let e=(await(await p()).rebuild()).outputFiles?.[0]?.text;if(e===void 0)throw Error(`esbuild produced no service-worker output`);return e})();f=e;let t=()=>{f=null};return e.then(t,t),e}async function h(){let e=d;d=null,f=null,await e?.dispose()}return{name:`tempest-pwa-dev-sw`,apply:`serve`,async buildEnd(){await h()},configResolved(e){c=e.root??process.cwd(),l=e.base??`/`},configureServer(e){s&&(e.httpServer?.once(`close`,()=>void h()),e.middlewares.use(n({swUrl:a,manifestUrl:o,matches:u,buildWorker:m})))}}}exports.tempestPwaDevSw=r;
|
|
2
2
|
//# sourceMappingURL=tempest-pwa-dev-sw.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tempest-pwa-dev-sw.cjs","names":[],"sources":["../../src/vite/tempest-pwa-dev-sw.ts"],"sourcesContent":["import { resolve } from \"node:path\";\nimport type { Plugin } from \"vite\";\nimport { basePrefix } from \"./base-url\";\nimport type { TempestVitePlugin } from \"./tempest-pwa-manifest\";\n\n/** Options for {@link tempestPwaDevSw}. */\nexport interface TempestPwaDevSwOptions {\n /** Service-worker entry, relative to the project root. Default `src/sw.ts`. */\n swSrc?: string;\n /**\n * URL the worker is served at (must match `registerServiceWorker`).\n * Default `/sw.js`.\n *\n * Write it relative to the site root; requests are matched against both the\n * bare path and the path prefixed with the resolved Vite `base`, so a\n * project served from a subpath is handled without extra configuration.\n */\n swUrl?: string;\n /**\n * Dev URL of the precache manifest. Default `/precache-manifest.json`.\n * Matched the same way as {@link TempestPwaDevSwOptions.swUrl}.\n */\n manifestUrl?: string;\n /** Serve the worker in dev. Default `true`; set `false` to opt out. */\n enabled?: boolean;\n}\n\n/**\n * Dev-server plugin that makes the service worker available under `npm run dev`.\n *\n * The production worker is bundled at build time (`vite.sw.config.ts`), so in\n * dev there is no `/sw.js` to register. This plugin compiles `swSrc` on the fly\n * with esbuild — through one incremental context, not a cold build per request —\n * and serves it as a classic worker, plus an empty `precache-manifest.json` (there are no hashed build assets to precache in\n * dev — push and runtime caching still work). It closes the \"SW in dev\" gap\n * that otherwise only `vite-plugin-pwa`'s `devOptions` covered.\n *\n * @example\n * // vite.config.ts\n * import { createViteConfig, tempestPwaDevSw } from \"tempest-react-sdk/vite\";\n *\n * export default createViteConfig({ plugins: [tempestPwaDevSw()] });\n */\nexport function tempestPwaDevSw(options: TempestPwaDevSwOptions = {}): TempestVitePlugin {\n const {\n swSrc = \"src/sw.ts\",\n swUrl = \"/sw.js\",\n manifestUrl = \"/precache-manifest.json\",\n enabled = true,\n } = options;\n\n let root = process.cwd();\n let base = \"/\";\n\n /**\n * Whether a request path addresses `target`.\n *\n * Both the bare path and the base-prefixed one are accepted. Which of the\n * two arrives depends on where this middleware lands relative to Vite's own\n * base handling, and a project served from a subpath would otherwise never\n * match — the browser asks for `/app/sw.js` while the option says `/sw.js`.\n */\n function matches(url: string, target: string): boolean {\n if (url === target) return true;\n const prefix = basePrefix(base);\n if (prefix === \"/\") return false;\n return url === `${prefix}${target.replace(/^\\//, \"\")}`;\n }\n\n /**\n * The slice of esbuild's incremental API this plugin uses.\n *\n * Typed structurally rather than imported: `esbuild` is not a dependency of\n * this package. It is resolved at runtime from the app's own tree, where Vite\n * already brings it, so importing its types would add a build-time dependency\n * the runtime does not have.\n */\n interface BuildContext {\n rebuild(): Promise<{ outputFiles?: readonly { text: string }[] }>;\n dispose(): Promise<void>;\n }\n\n let context: BuildContext | null = null;\n let pending: Promise<string> | null = null;\n\n /**\n * The incremental build context, created on the first request that needs it.\n *\n * Lazy because a project that never registers a worker should not pay for an\n * esbuild child process, and because `apply: \"serve\"` still loads this module\n * in a build.\n *\n * @returns The context, reused across requests.\n */\n async function ensureContext(): Promise<BuildContext> {\n if (context) return context;\n const esbuild = (await import(\"esbuild\")) as unknown as {\n context(options: Record<string, unknown>): Promise<BuildContext>;\n };\n context = await esbuild.context({\n entryPoints: [resolve(root, swSrc)],\n bundle: true,\n format: \"iife\",\n platform: \"browser\",\n target: \"es2020\",\n write: false,\n absWorkingDir: root,\n logLevel: \"silent\",\n });\n return context;\n }\n\n /**\n * Bundle the worker, reusing the previous graph.\n *\n * `esbuild.build()` per request was a cold bundle every time, and the request\n * count is not one per session: `Cache-Control: no-cache` guarantees one per\n * page load, and Chrome re-fetches the worker script on every navigation and\n * on its own update checks. `rebuild()` reuses the graph and only redoes what\n * changed on disk.\n *\n * Single-flight, because those requests arrive in bursts and esbuild does not\n * promise anything about concurrent `rebuild()` calls on one context. Callers\n * that land while a bundle is in flight share its result, which is the right\n * answer anyway — they asked for the same file at the same moment.\n *\n * @returns The bundled worker source.\n */\n function buildWorker(): Promise<string> {\n if (pending) return pending;\n\n const run = (async (): Promise<string> => {\n const ctx = await ensureContext();\n const result = await ctx.rebuild();\n const text = result.outputFiles?.[0]?.text;\n if (text === undefined) throw new Error(\"esbuild produced no service-worker output\");\n return text;\n })();\n\n pending = run;\n const clear = (): void => {\n pending = null;\n };\n run.then(clear, clear);\n return run;\n }\n\n /**\n * Tear the context down, releasing esbuild's child process.\n *\n * Idempotent, because it is wired to both the dev server closing and the\n * plugin's `buildEnd`. Without it, restarting the dev server leaks one esbuild\n * process per restart.\n */\n async function disposeContext(): Promise<void> {\n const current = context;\n context = null;\n pending = null;\n await current?.dispose();\n }\n\n const plugin: Plugin = {\n name: \"tempest-pwa-dev-sw\",\n apply: \"serve\",\n async buildEnd() {\n await disposeContext();\n },\n configResolved(config) {\n root = config.root ?? process.cwd();\n base = config.base ?? \"/\";\n },\n configureServer(server) {\n if (!enabled) return;\n\n server.httpServer?.once(\"close\", () => void disposeContext());\n\n server.middlewares.use(async (req, res, next) => {\n const url = (req.url ?? \"\").split(\"?\")[0];\n\n if (matches(url, swUrl)) {\n try {\n const source = await buildWorker();\n res.setHeader(\"Content-Type\", \"application/javascript\");\n res.setHeader(\"Service-Worker-Allowed\", \"/\");\n res.setHeader(\"Cache-Control\", \"no-cache\");\n res.end(source);\n } catch (error) {\n res.statusCode = 500;\n res.end(`// SW dev build failed:\\n// ${String(error)}`);\n }\n return;\n }\n\n if (matches(url, manifestUrl)) {\n res.setHeader(\"Content-Type\", \"application/json\");\n res.setHeader(\"Cache-Control\", \"no-cache\");\n res.end(JSON.stringify({ version: \"dev\", urls: [] }));\n return;\n }\n\n next();\n });\n },\n };\n\n return plugin as TempestVitePlugin;\n}\n"],"mappings":"6DA2CA,SAAgB,EAAgB,EAAkC,CAAC,EAAsB,CACrF,GAAM,CACF,QAAQ,YACR,QAAQ,SACR,cAAc,0BACd,UAAU,IACV,EAEA,EAAO,QAAQ,IAAI,EACnB,EAAO,IAUX,SAAS,EAAQ,EAAa,EAAyB,CACnD,GAAI,IAAQ,EAAQ,MAAO,GAC3B,IAAM,EAAS,EAAA,WAAW,CAAI,EAE9B,OADI,IAAW,KACR,IAAQ,GAAG,IAAS,EAAO,QAAQ,MAAO,EAAE,GACvD,CAeA,IAAI,EAA+B,KAC/B,EAAkC,KAWtC,eAAe,GAAuC,CAelD,OAdI,IAIJ,EAAU,MAAM,MAHO,OAAO,WAAA,CAGN,QAAQ,CAC5B,YAAa,EAAA,EAAC,EAAA,QAAA,CAAQ,EAAM,CAAK,CAAC,EAClC,OAAQ,GACR,OAAQ,OACR,SAAU,UACV,OAAQ,SACR,MAAO,GACP,cAAe,EACf,SAAU,QACd,CAAC,EACM,EACX,CAkBA,SAAS,GAA+B,CACpC,GAAI,EAAS,OAAO,EAEpB,IAAM,GAAO,SAA6B,CAGtC,IAAM,GAAO,MADQ,MADH,EAAc,EAAA,CACP,QAAQ,EAAA,CACb,cAAc,EAAE,EAAE,KACtC,GAAI,IAAS,IAAA,GAAW,MAAU,MAAM,2CAA2C,EACnF,OAAO,CACX,EAAA,CAAG,EAEH,EAAU,EACV,IAAM,MAAoB,CACtB,EAAU,IACd,EAEA,OADA,EAAI,KAAK,EAAO,CAAK,EACd,CACX,CASA,eAAe,GAAgC,CAC3C,IAAM,EAAU,EAChB,EAAU,KACV,EAAU,KACV,MAAM,GAAS,QAAQ,CAC3B,CA8CA,MAAO,CA3CH,KAAM,qBACN,MAAO,QACP,MAAM,UAAW,CACb,MAAM,EAAe,CACzB,EACA,eAAe,EAAQ,CACnB,EAAO,EAAO,MAAQ,QAAQ,IAAI,EAClC,EAAO,EAAO,MAAQ,GAC1B,EACA,gBAAgB,EAAQ,CACf,IAEL,EAAO,YAAY,KAAK,YAAe,KAAK,EAAe,CAAC,EAE5D,EAAO,YAAY,IAAI,MAAO,EAAK,EAAK,IAAS,CAC7C,IAAM,GAAO,EAAI,KAAO,GAAA,CAAI,MAAM,GAAG,CAAC,CAAC,GAEvC,GAAI,EAAQ,EAAK,CAAK,EAAG,CACrB,GAAI,CACA,IAAM,EAAS,MAAM,EAAY,EACjC,EAAI,UAAU,eAAgB,wBAAwB,EACtD,EAAI,UAAU,yBAA0B,GAAG,EAC3C,EAAI,UAAU,gBAAiB,UAAU,EACzC,EAAI,IAAI,CAAM,CAClB,OAAS,EAAO,CACZ,EAAI,WAAa,IACjB,EAAI,IAAI,+BAA+B,OAAO,CAAK,GAAG,CAC1D,CACA,MACJ,CAEA,GAAI,EAAQ,EAAK,CAAW,EAAG,CAC3B,EAAI,UAAU,eAAgB,kBAAkB,EAChD,EAAI,UAAU,gBAAiB,UAAU,EACzC,EAAI,IAAI,KAAK,UAAU,CAAE,QAAS,MAAO,KAAM,CAAC,CAAE,CAAC,CAAC,EACpD,MACJ,CAEA,EAAK,CACT,CAAC,EACL,CAGG,CACX"}
|
|
1
|
+
{"version":3,"file":"tempest-pwa-dev-sw.cjs","names":[],"sources":["../../src/vite/tempest-pwa-dev-sw.ts"],"sourcesContent":["import { resolve } from \"node:path\";\nimport type { Plugin } from \"vite\";\nimport { basePrefix } from \"./base-url\";\nimport type { TempestVitePlugin } from \"./tempest-pwa-manifest\";\n\n/** Options for {@link tempestPwaDevSw}. */\nexport interface TempestPwaDevSwOptions {\n /** Service-worker entry, relative to the project root. Default `src/sw.ts`. */\n swSrc?: string;\n /**\n * URL the worker is served at (must match `registerServiceWorker`).\n * Default `/sw.js`.\n *\n * Write it relative to the site root; requests are matched against both the\n * bare path and the path prefixed with the resolved Vite `base`, so a\n * project served from a subpath is handled without extra configuration.\n */\n swUrl?: string;\n /**\n * Dev URL of the precache manifest. Default `/precache-manifest.json`.\n * Matched the same way as {@link TempestPwaDevSwOptions.swUrl}.\n */\n manifestUrl?: string;\n /** Serve the worker in dev. Default `true`; set `false` to opt out. */\n enabled?: boolean;\n}\n\n/**\n * The dev middleware: serve the compiled worker, and a placeholder manifest.\n *\n * Extracted from the plugin so the request handling reads on its own — the\n * plugin body is then only wiring. The manifest is deliberately empty in dev:\n * precaching a set of URLs Vite rewrites on every change would serve stale\n * modules, and the worker's runtime routes still work without it.\n *\n * @param deps - The two URLs, the path matcher and the incremental build.\n * @returns A connect-style middleware.\n */\nfunction createDevSwMiddleware(deps: {\n swUrl: string;\n manifestUrl: string;\n matches: (url: string, target: string) => boolean;\n buildWorker: () => Promise<string>;\n}): (\n req: { url?: string },\n res: {\n statusCode?: number;\n setHeader: (name: string, value: string) => void;\n end: (body: string) => void;\n },\n next: () => void,\n) => Promise<void> {\n return async (req, res, next) => {\n const url = (req.url ?? \"\").split(\"?\")[0];\n\n if (deps.matches(url, deps.swUrl)) {\n try {\n const source = await deps.buildWorker();\n res.setHeader(\"Content-Type\", \"application/javascript\");\n res.setHeader(\"Service-Worker-Allowed\", \"/\");\n res.setHeader(\"Cache-Control\", \"no-cache\");\n res.end(source);\n } catch (error) {\n res.statusCode = 500;\n res.end(`// SW dev build failed:\\n// ${String(error)}`);\n }\n return;\n }\n\n if (deps.matches(url, deps.manifestUrl)) {\n res.setHeader(\"Content-Type\", \"application/json\");\n res.setHeader(\"Cache-Control\", \"no-cache\");\n res.end(JSON.stringify({ version: \"dev\", urls: [] }));\n return;\n }\n\n next();\n };\n}\n\n/**\n * Dev-server plugin that makes the service worker available under `npm run dev`.\n *\n * The production worker is bundled at build time (`vite.sw.config.ts`), so in\n * dev there is no `/sw.js` to register. This plugin compiles `swSrc` on the fly\n * with esbuild — through one incremental context, not a cold build per request —\n * and serves it as a classic worker, plus an empty `precache-manifest.json` (there are no hashed build assets to precache in\n * dev — push and runtime caching still work). It closes the \"SW in dev\" gap\n * that otherwise only `vite-plugin-pwa`'s `devOptions` covered.\n *\n * @example\n * // vite.config.ts\n * import { createViteConfig, tempestPwaDevSw } from \"tempest-react-sdk/vite\";\n *\n * export default createViteConfig({ plugins: [tempestPwaDevSw()] });\n */\nexport function tempestPwaDevSw(options: TempestPwaDevSwOptions = {}): TempestVitePlugin {\n const {\n swSrc = \"src/sw.ts\",\n swUrl = \"/sw.js\",\n manifestUrl = \"/precache-manifest.json\",\n enabled = true,\n } = options;\n\n let root = process.cwd();\n let base = \"/\";\n\n /**\n * Whether a request path addresses `target`.\n *\n * Both the bare path and the base-prefixed one are accepted. Which of the\n * two arrives depends on where this middleware lands relative to Vite's own\n * base handling, and a project served from a subpath would otherwise never\n * match — the browser asks for `/app/sw.js` while the option says `/sw.js`.\n */\n function matches(url: string, target: string): boolean {\n if (url === target) return true;\n const prefix = basePrefix(base);\n if (prefix === \"/\") return false;\n return url === `${prefix}${target.replace(/^\\//, \"\")}`;\n }\n\n /**\n * The slice of esbuild's incremental API this plugin uses.\n *\n * Typed structurally rather than imported: `esbuild` is not a dependency of\n * this package. It is resolved at runtime from the app's own tree, where Vite\n * already brings it, so importing its types would add a build-time dependency\n * the runtime does not have.\n */\n interface BuildContext {\n rebuild(): Promise<{ outputFiles?: readonly { text: string }[] }>;\n dispose(): Promise<void>;\n }\n\n let context: BuildContext | null = null;\n let pending: Promise<string> | null = null;\n\n /**\n * The incremental build context, created on the first request that needs it.\n *\n * Lazy because a project that never registers a worker should not pay for an\n * esbuild child process, and because `apply: \"serve\"` still loads this module\n * in a build.\n *\n * @returns The context, reused across requests.\n */\n async function ensureContext(): Promise<BuildContext> {\n if (context) return context;\n const esbuild = (await import(\"esbuild\")) as unknown as {\n context(options: Record<string, unknown>): Promise<BuildContext>;\n };\n context = await esbuild.context({\n entryPoints: [resolve(root, swSrc)],\n bundle: true,\n format: \"iife\",\n platform: \"browser\",\n target: \"es2020\",\n write: false,\n absWorkingDir: root,\n logLevel: \"silent\",\n });\n return context;\n }\n\n /**\n * Bundle the worker, reusing the previous graph.\n *\n * `esbuild.build()` per request was a cold bundle every time, and the request\n * count is not one per session: `Cache-Control: no-cache` guarantees one per\n * page load, and Chrome re-fetches the worker script on every navigation and\n * on its own update checks. `rebuild()` reuses the graph and only redoes what\n * changed on disk.\n *\n * Single-flight, because those requests arrive in bursts and esbuild does not\n * promise anything about concurrent `rebuild()` calls on one context. Callers\n * that land while a bundle is in flight share its result, which is the right\n * answer anyway — they asked for the same file at the same moment.\n *\n * @returns The bundled worker source.\n */\n function buildWorker(): Promise<string> {\n if (pending) return pending;\n\n const run = (async (): Promise<string> => {\n const ctx = await ensureContext();\n const result = await ctx.rebuild();\n const text = result.outputFiles?.[0]?.text;\n if (text === undefined) throw new Error(\"esbuild produced no service-worker output\");\n return text;\n })();\n\n pending = run;\n const clear = (): void => {\n pending = null;\n };\n run.then(clear, clear);\n return run;\n }\n\n /**\n * Tear the context down, releasing esbuild's child process.\n *\n * Idempotent, because it is wired to both the dev server closing and the\n * plugin's `buildEnd`. Without it, restarting the dev server leaks one esbuild\n * process per restart.\n */\n async function disposeContext(): Promise<void> {\n const current = context;\n context = null;\n pending = null;\n await current?.dispose();\n }\n\n const plugin: Plugin = {\n name: \"tempest-pwa-dev-sw\",\n apply: \"serve\",\n async buildEnd() {\n await disposeContext();\n },\n configResolved(config) {\n root = config.root ?? process.cwd();\n base = config.base ?? \"/\";\n },\n configureServer(server) {\n if (!enabled) return;\n\n server.httpServer?.once(\"close\", () => void disposeContext());\n\n server.middlewares.use(\n createDevSwMiddleware({ swUrl, manifestUrl, matches, buildWorker }),\n );\n },\n };\n\n return plugin as TempestVitePlugin;\n}\n"],"mappings":"6DAsCA,SAAS,EAAsB,EAaZ,CACf,OAAO,MAAO,EAAK,EAAK,IAAS,CAC7B,IAAM,GAAO,EAAI,KAAO,GAAA,CAAI,MAAM,GAAG,CAAC,CAAC,GAEvC,GAAI,EAAK,QAAQ,EAAK,EAAK,KAAK,EAAG,CAC/B,GAAI,CACA,IAAM,EAAS,MAAM,EAAK,YAAY,EACtC,EAAI,UAAU,eAAgB,wBAAwB,EACtD,EAAI,UAAU,yBAA0B,GAAG,EAC3C,EAAI,UAAU,gBAAiB,UAAU,EACzC,EAAI,IAAI,CAAM,CAClB,OAAS,EAAO,CACZ,EAAI,WAAa,IACjB,EAAI,IAAI,+BAA+B,OAAO,CAAK,GAAG,CAC1D,CACA,MACJ,CAEA,GAAI,EAAK,QAAQ,EAAK,EAAK,WAAW,EAAG,CACrC,EAAI,UAAU,eAAgB,kBAAkB,EAChD,EAAI,UAAU,gBAAiB,UAAU,EACzC,EAAI,IAAI,KAAK,UAAU,CAAE,QAAS,MAAO,KAAM,CAAC,CAAE,CAAC,CAAC,EACpD,MACJ,CAEA,EAAK,CACT,CACJ,CAkBA,SAAgB,EAAgB,EAAkC,CAAC,EAAsB,CACrF,GAAM,CACF,QAAQ,YACR,QAAQ,SACR,cAAc,0BACd,UAAU,IACV,EAEA,EAAO,QAAQ,IAAI,EACnB,EAAO,IAUX,SAAS,EAAQ,EAAa,EAAyB,CACnD,GAAI,IAAQ,EAAQ,MAAO,GAC3B,IAAM,EAAS,EAAA,WAAW,CAAI,EAE9B,OADI,IAAW,KACR,IAAQ,GAAG,IAAS,EAAO,QAAQ,MAAO,EAAE,GACvD,CAeA,IAAI,EAA+B,KAC/B,EAAkC,KAWtC,eAAe,GAAuC,CAelD,OAdI,IAIJ,EAAU,MAAM,MAHO,OAAO,WAAA,CAGN,QAAQ,CAC5B,YAAa,EAAA,EAAC,EAAA,QAAA,CAAQ,EAAM,CAAK,CAAC,EAClC,OAAQ,GACR,OAAQ,OACR,SAAU,UACV,OAAQ,SACR,MAAO,GACP,cAAe,EACf,SAAU,QACd,CAAC,EACM,EACX,CAkBA,SAAS,GAA+B,CACpC,GAAI,EAAS,OAAO,EAEpB,IAAM,GAAO,SAA6B,CAGtC,IAAM,GAAO,MADQ,MADH,EAAc,EAAA,CACP,QAAQ,EAAA,CACb,cAAc,EAAE,EAAE,KACtC,GAAI,IAAS,IAAA,GAAW,MAAU,MAAM,2CAA2C,EACnF,OAAO,CACX,EAAA,CAAG,EAEH,EAAU,EACV,IAAM,MAAoB,CACtB,EAAU,IACd,EAEA,OADA,EAAI,KAAK,EAAO,CAAK,EACd,CACX,CASA,eAAe,GAAgC,CAC3C,IAAM,EAAU,EAChB,EAAU,KACV,EAAU,KACV,MAAM,GAAS,QAAQ,CAC3B,CAuBA,MAAO,CApBH,KAAM,qBACN,MAAO,QACP,MAAM,UAAW,CACb,MAAM,EAAe,CACzB,EACA,eAAe,EAAQ,CACnB,EAAO,EAAO,MAAQ,QAAQ,IAAI,EAClC,EAAO,EAAO,MAAQ,GAC1B,EACA,gBAAgB,EAAQ,CACf,IAEL,EAAO,YAAY,KAAK,YAAe,KAAK,EAAe,CAAC,EAE5D,EAAO,YAAY,IACf,EAAsB,CAAE,QAAO,cAAa,UAAS,aAAY,CAAC,CACtE,EACJ,CAGG,CACX"}
|
|
@@ -1,77 +1,85 @@
|
|
|
1
1
|
import { basePrefix as e } from "./base-url.js";
|
|
2
2
|
import { resolve as t } from "node:path";
|
|
3
3
|
//#region src/vite/tempest-pwa-dev-sw.ts
|
|
4
|
-
function n(
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
function n(e) {
|
|
5
|
+
return async (t, n, r) => {
|
|
6
|
+
let i = (t.url ?? "").split("?")[0];
|
|
7
|
+
if (e.matches(i, e.swUrl)) {
|
|
8
|
+
try {
|
|
9
|
+
let t = await e.buildWorker();
|
|
10
|
+
n.setHeader("Content-Type", "application/javascript"), n.setHeader("Service-Worker-Allowed", "/"), n.setHeader("Cache-Control", "no-cache"), n.end(t);
|
|
11
|
+
} catch (e) {
|
|
12
|
+
n.statusCode = 500, n.end(`// SW dev build failed:\n// ${String(e)}`);
|
|
13
|
+
}
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
if (e.matches(i, e.manifestUrl)) {
|
|
17
|
+
n.setHeader("Content-Type", "application/json"), n.setHeader("Cache-Control", "no-cache"), n.end(JSON.stringify({
|
|
18
|
+
version: "dev",
|
|
19
|
+
urls: []
|
|
20
|
+
}));
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
r();
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function r(r = {}) {
|
|
27
|
+
let { swSrc: i = "src/sw.ts", swUrl: a = "/sw.js", manifestUrl: o = "/precache-manifest.json", enabled: s = !0 } = r, c = process.cwd(), l = "/";
|
|
28
|
+
function u(t, n) {
|
|
7
29
|
if (t === n) return !0;
|
|
8
|
-
let r = e(
|
|
30
|
+
let r = e(l);
|
|
9
31
|
return r !== "/" && t === `${r}${n.replace(/^\//, "")}`;
|
|
10
32
|
}
|
|
11
|
-
let
|
|
12
|
-
async function
|
|
13
|
-
return
|
|
14
|
-
entryPoints: [t(
|
|
33
|
+
let d = null, f = null;
|
|
34
|
+
async function p() {
|
|
35
|
+
return d || (d = await (await import("esbuild")).context({
|
|
36
|
+
entryPoints: [t(c, i)],
|
|
15
37
|
bundle: !0,
|
|
16
38
|
format: "iife",
|
|
17
39
|
platform: "browser",
|
|
18
40
|
target: "es2020",
|
|
19
41
|
write: !1,
|
|
20
|
-
absWorkingDir:
|
|
42
|
+
absWorkingDir: c,
|
|
21
43
|
logLevel: "silent"
|
|
22
|
-
}),
|
|
44
|
+
}), d);
|
|
23
45
|
}
|
|
24
|
-
function
|
|
25
|
-
if (
|
|
46
|
+
function m() {
|
|
47
|
+
if (f) return f;
|
|
26
48
|
let e = (async () => {
|
|
27
|
-
let e = (await (await
|
|
49
|
+
let e = (await (await p()).rebuild()).outputFiles?.[0]?.text;
|
|
28
50
|
if (e === void 0) throw Error("esbuild produced no service-worker output");
|
|
29
51
|
return e;
|
|
30
52
|
})();
|
|
31
|
-
|
|
53
|
+
f = e;
|
|
32
54
|
let t = () => {
|
|
33
|
-
|
|
55
|
+
f = null;
|
|
34
56
|
};
|
|
35
57
|
return e.then(t, t), e;
|
|
36
58
|
}
|
|
37
|
-
async function
|
|
38
|
-
let e =
|
|
39
|
-
|
|
59
|
+
async function h() {
|
|
60
|
+
let e = d;
|
|
61
|
+
d = null, f = null, await e?.dispose();
|
|
40
62
|
}
|
|
41
63
|
return {
|
|
42
64
|
name: "tempest-pwa-dev-sw",
|
|
43
65
|
apply: "serve",
|
|
44
66
|
async buildEnd() {
|
|
45
|
-
await
|
|
67
|
+
await h();
|
|
46
68
|
},
|
|
47
69
|
configResolved(e) {
|
|
48
|
-
|
|
70
|
+
c = e.root ?? process.cwd(), l = e.base ?? "/";
|
|
49
71
|
},
|
|
50
72
|
configureServer(e) {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
} catch (e) {
|
|
58
|
-
t.statusCode = 500, t.end(`// SW dev build failed:\n// ${String(e)}`);
|
|
59
|
-
}
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
if (l(r, a)) {
|
|
63
|
-
t.setHeader("Content-Type", "application/json"), t.setHeader("Cache-Control", "no-cache"), t.end(JSON.stringify({
|
|
64
|
-
version: "dev",
|
|
65
|
-
urls: []
|
|
66
|
-
}));
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
n();
|
|
70
|
-
}));
|
|
73
|
+
s && (e.httpServer?.once("close", () => void h()), e.middlewares.use(n({
|
|
74
|
+
swUrl: a,
|
|
75
|
+
manifestUrl: o,
|
|
76
|
+
matches: u,
|
|
77
|
+
buildWorker: m
|
|
78
|
+
})));
|
|
71
79
|
}
|
|
72
80
|
};
|
|
73
81
|
}
|
|
74
82
|
//#endregion
|
|
75
|
-
export {
|
|
83
|
+
export { r as tempestPwaDevSw };
|
|
76
84
|
|
|
77
85
|
//# sourceMappingURL=tempest-pwa-dev-sw.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tempest-pwa-dev-sw.js","names":[],"sources":["../../src/vite/tempest-pwa-dev-sw.ts"],"sourcesContent":["import { resolve } from \"node:path\";\nimport type { Plugin } from \"vite\";\nimport { basePrefix } from \"./base-url\";\nimport type { TempestVitePlugin } from \"./tempest-pwa-manifest\";\n\n/** Options for {@link tempestPwaDevSw}. */\nexport interface TempestPwaDevSwOptions {\n /** Service-worker entry, relative to the project root. Default `src/sw.ts`. */\n swSrc?: string;\n /**\n * URL the worker is served at (must match `registerServiceWorker`).\n * Default `/sw.js`.\n *\n * Write it relative to the site root; requests are matched against both the\n * bare path and the path prefixed with the resolved Vite `base`, so a\n * project served from a subpath is handled without extra configuration.\n */\n swUrl?: string;\n /**\n * Dev URL of the precache manifest. Default `/precache-manifest.json`.\n * Matched the same way as {@link TempestPwaDevSwOptions.swUrl}.\n */\n manifestUrl?: string;\n /** Serve the worker in dev. Default `true`; set `false` to opt out. */\n enabled?: boolean;\n}\n\n/**\n * Dev-server plugin that makes the service worker available under `npm run dev`.\n *\n * The production worker is bundled at build time (`vite.sw.config.ts`), so in\n * dev there is no `/sw.js` to register. This plugin compiles `swSrc` on the fly\n * with esbuild — through one incremental context, not a cold build per request —\n * and serves it as a classic worker, plus an empty `precache-manifest.json` (there are no hashed build assets to precache in\n * dev — push and runtime caching still work). It closes the \"SW in dev\" gap\n * that otherwise only `vite-plugin-pwa`'s `devOptions` covered.\n *\n * @example\n * // vite.config.ts\n * import { createViteConfig, tempestPwaDevSw } from \"tempest-react-sdk/vite\";\n *\n * export default createViteConfig({ plugins: [tempestPwaDevSw()] });\n */\nexport function tempestPwaDevSw(options: TempestPwaDevSwOptions = {}): TempestVitePlugin {\n const {\n swSrc = \"src/sw.ts\",\n swUrl = \"/sw.js\",\n manifestUrl = \"/precache-manifest.json\",\n enabled = true,\n } = options;\n\n let root = process.cwd();\n let base = \"/\";\n\n /**\n * Whether a request path addresses `target`.\n *\n * Both the bare path and the base-prefixed one are accepted. Which of the\n * two arrives depends on where this middleware lands relative to Vite's own\n * base handling, and a project served from a subpath would otherwise never\n * match — the browser asks for `/app/sw.js` while the option says `/sw.js`.\n */\n function matches(url: string, target: string): boolean {\n if (url === target) return true;\n const prefix = basePrefix(base);\n if (prefix === \"/\") return false;\n return url === `${prefix}${target.replace(/^\\//, \"\")}`;\n }\n\n /**\n * The slice of esbuild's incremental API this plugin uses.\n *\n * Typed structurally rather than imported: `esbuild` is not a dependency of\n * this package. It is resolved at runtime from the app's own tree, where Vite\n * already brings it, so importing its types would add a build-time dependency\n * the runtime does not have.\n */\n interface BuildContext {\n rebuild(): Promise<{ outputFiles?: readonly { text: string }[] }>;\n dispose(): Promise<void>;\n }\n\n let context: BuildContext | null = null;\n let pending: Promise<string> | null = null;\n\n /**\n * The incremental build context, created on the first request that needs it.\n *\n * Lazy because a project that never registers a worker should not pay for an\n * esbuild child process, and because `apply: \"serve\"` still loads this module\n * in a build.\n *\n * @returns The context, reused across requests.\n */\n async function ensureContext(): Promise<BuildContext> {\n if (context) return context;\n const esbuild = (await import(\"esbuild\")) as unknown as {\n context(options: Record<string, unknown>): Promise<BuildContext>;\n };\n context = await esbuild.context({\n entryPoints: [resolve(root, swSrc)],\n bundle: true,\n format: \"iife\",\n platform: \"browser\",\n target: \"es2020\",\n write: false,\n absWorkingDir: root,\n logLevel: \"silent\",\n });\n return context;\n }\n\n /**\n * Bundle the worker, reusing the previous graph.\n *\n * `esbuild.build()` per request was a cold bundle every time, and the request\n * count is not one per session: `Cache-Control: no-cache` guarantees one per\n * page load, and Chrome re-fetches the worker script on every navigation and\n * on its own update checks. `rebuild()` reuses the graph and only redoes what\n * changed on disk.\n *\n * Single-flight, because those requests arrive in bursts and esbuild does not\n * promise anything about concurrent `rebuild()` calls on one context. Callers\n * that land while a bundle is in flight share its result, which is the right\n * answer anyway — they asked for the same file at the same moment.\n *\n * @returns The bundled worker source.\n */\n function buildWorker(): Promise<string> {\n if (pending) return pending;\n\n const run = (async (): Promise<string> => {\n const ctx = await ensureContext();\n const result = await ctx.rebuild();\n const text = result.outputFiles?.[0]?.text;\n if (text === undefined) throw new Error(\"esbuild produced no service-worker output\");\n return text;\n })();\n\n pending = run;\n const clear = (): void => {\n pending = null;\n };\n run.then(clear, clear);\n return run;\n }\n\n /**\n * Tear the context down, releasing esbuild's child process.\n *\n * Idempotent, because it is wired to both the dev server closing and the\n * plugin's `buildEnd`. Without it, restarting the dev server leaks one esbuild\n * process per restart.\n */\n async function disposeContext(): Promise<void> {\n const current = context;\n context = null;\n pending = null;\n await current?.dispose();\n }\n\n const plugin: Plugin = {\n name: \"tempest-pwa-dev-sw\",\n apply: \"serve\",\n async buildEnd() {\n await disposeContext();\n },\n configResolved(config) {\n root = config.root ?? process.cwd();\n base = config.base ?? \"/\";\n },\n configureServer(server) {\n if (!enabled) return;\n\n server.httpServer?.once(\"close\", () => void disposeContext());\n\n server.middlewares.use(async (req, res, next) => {\n const url = (req.url ?? \"\").split(\"?\")[0];\n\n if (matches(url, swUrl)) {\n try {\n const source = await buildWorker();\n res.setHeader(\"Content-Type\", \"application/javascript\");\n res.setHeader(\"Service-Worker-Allowed\", \"/\");\n res.setHeader(\"Cache-Control\", \"no-cache\");\n res.end(source);\n } catch (error) {\n res.statusCode = 500;\n res.end(`// SW dev build failed:\\n// ${String(error)}`);\n }\n return;\n }\n\n if (matches(url, manifestUrl)) {\n res.setHeader(\"Content-Type\", \"application/json\");\n res.setHeader(\"Cache-Control\", \"no-cache\");\n res.end(JSON.stringify({ version: \"dev\", urls: [] }));\n return;\n }\n\n next();\n });\n },\n };\n\n return plugin as TempestVitePlugin;\n}\n"],"mappings":";;;AA2CA,SAAgB,EAAgB,IAAkC,CAAC,GAAsB;CACrF,IAAM,EACF,WAAQ,aACR,WAAQ,UACR,iBAAc,2BACd,aAAU,OACV,GAEA,IAAO,QAAQ,IAAI,GACnB,IAAO;CAUX,SAAS,EAAQ,GAAa,GAAyB;EACnD,IAAI,MAAQ,GAAQ,OAAO;EAC3B,IAAM,IAAS,EAAW,CAAI;EAE9B,OADI,MAAW,OACR,MAAQ,GAAG,IAAS,EAAO,QAAQ,OAAO,EAAE;CACvD;CAeA,IAAI,IAA+B,MAC/B,IAAkC;CAWtC,eAAe,IAAuC;EAelD,OAdI,MAIJ,IAAU,OAAM,MAHO,OAAO,WAAA,CAGN,QAAQ;GAC5B,aAAa,CAAC,EAAQ,GAAM,CAAK,CAAC;GAClC,QAAQ;GACR,QAAQ;GACR,UAAU;GACV,QAAQ;GACR,OAAO;GACP,eAAe;GACf,UAAU;EACd,CAAC,GACM;CACX;CAkBA,SAAS,IAA+B;EACpC,IAAI,GAAS,OAAO;EAEpB,IAAM,KAAO,YAA6B;GAGtC,IAAM,KAAO,OADQ,MADH,EAAc,EAAA,CACP,QAAQ,EAAA,CACb,cAAc,EAAE,EAAE;GACtC,IAAI,MAAS,KAAA,GAAW,MAAU,MAAM,2CAA2C;GACnF,OAAO;EACX,EAAA,CAAG;EAEH,IAAU;EACV,IAAM,UAAoB;GACtB,IAAU;EACd;EAEA,OADA,EAAI,KAAK,GAAO,CAAK,GACd;CACX;CASA,eAAe,IAAgC;EAC3C,IAAM,IAAU;EAGhB,AAFA,IAAU,MACV,IAAU,MACV,MAAM,GAAS,QAAQ;CAC3B;CA8CA,OAAO;EA3CH,MAAM;EACN,OAAO;EACP,MAAM,WAAW;GACb,MAAM,EAAe;EACzB;EACA,eAAe,GAAQ;GAEnB,AADA,IAAO,EAAO,QAAQ,QAAQ,IAAI,GAClC,IAAO,EAAO,QAAQ;EAC1B;EACA,gBAAgB,GAAQ;GACf,MAEL,EAAO,YAAY,KAAK,eAAe,KAAK,EAAe,CAAC,GAE5D,EAAO,YAAY,IAAI,OAAO,GAAK,GAAK,MAAS;IAC7C,IAAM,KAAO,EAAI,OAAO,GAAA,CAAI,MAAM,GAAG,CAAC,CAAC;IAEvC,IAAI,EAAQ,GAAK,CAAK,GAAG;KACrB,IAAI;MACA,IAAM,IAAS,MAAM,EAAY;MAIjC,AAHA,EAAI,UAAU,gBAAgB,wBAAwB,GACtD,EAAI,UAAU,0BAA0B,GAAG,GAC3C,EAAI,UAAU,iBAAiB,UAAU,GACzC,EAAI,IAAI,CAAM;KAClB,SAAS,GAAO;MAEZ,AADA,EAAI,aAAa,KACjB,EAAI,IAAI,+BAA+B,OAAO,CAAK,GAAG;KAC1D;KACA;IACJ;IAEA,IAAI,EAAQ,GAAK,CAAW,GAAG;KAG3B,AAFA,EAAI,UAAU,gBAAgB,kBAAkB,GAChD,EAAI,UAAU,iBAAiB,UAAU,GACzC,EAAI,IAAI,KAAK,UAAU;MAAE,SAAS;MAAO,MAAM,CAAC;KAAE,CAAC,CAAC;KACpD;IACJ;IAEA,EAAK;GACT,CAAC;EACL;CAGG;AACX"}
|
|
1
|
+
{"version":3,"file":"tempest-pwa-dev-sw.js","names":[],"sources":["../../src/vite/tempest-pwa-dev-sw.ts"],"sourcesContent":["import { resolve } from \"node:path\";\nimport type { Plugin } from \"vite\";\nimport { basePrefix } from \"./base-url\";\nimport type { TempestVitePlugin } from \"./tempest-pwa-manifest\";\n\n/** Options for {@link tempestPwaDevSw}. */\nexport interface TempestPwaDevSwOptions {\n /** Service-worker entry, relative to the project root. Default `src/sw.ts`. */\n swSrc?: string;\n /**\n * URL the worker is served at (must match `registerServiceWorker`).\n * Default `/sw.js`.\n *\n * Write it relative to the site root; requests are matched against both the\n * bare path and the path prefixed with the resolved Vite `base`, so a\n * project served from a subpath is handled without extra configuration.\n */\n swUrl?: string;\n /**\n * Dev URL of the precache manifest. Default `/precache-manifest.json`.\n * Matched the same way as {@link TempestPwaDevSwOptions.swUrl}.\n */\n manifestUrl?: string;\n /** Serve the worker in dev. Default `true`; set `false` to opt out. */\n enabled?: boolean;\n}\n\n/**\n * The dev middleware: serve the compiled worker, and a placeholder manifest.\n *\n * Extracted from the plugin so the request handling reads on its own — the\n * plugin body is then only wiring. The manifest is deliberately empty in dev:\n * precaching a set of URLs Vite rewrites on every change would serve stale\n * modules, and the worker's runtime routes still work without it.\n *\n * @param deps - The two URLs, the path matcher and the incremental build.\n * @returns A connect-style middleware.\n */\nfunction createDevSwMiddleware(deps: {\n swUrl: string;\n manifestUrl: string;\n matches: (url: string, target: string) => boolean;\n buildWorker: () => Promise<string>;\n}): (\n req: { url?: string },\n res: {\n statusCode?: number;\n setHeader: (name: string, value: string) => void;\n end: (body: string) => void;\n },\n next: () => void,\n) => Promise<void> {\n return async (req, res, next) => {\n const url = (req.url ?? \"\").split(\"?\")[0];\n\n if (deps.matches(url, deps.swUrl)) {\n try {\n const source = await deps.buildWorker();\n res.setHeader(\"Content-Type\", \"application/javascript\");\n res.setHeader(\"Service-Worker-Allowed\", \"/\");\n res.setHeader(\"Cache-Control\", \"no-cache\");\n res.end(source);\n } catch (error) {\n res.statusCode = 500;\n res.end(`// SW dev build failed:\\n// ${String(error)}`);\n }\n return;\n }\n\n if (deps.matches(url, deps.manifestUrl)) {\n res.setHeader(\"Content-Type\", \"application/json\");\n res.setHeader(\"Cache-Control\", \"no-cache\");\n res.end(JSON.stringify({ version: \"dev\", urls: [] }));\n return;\n }\n\n next();\n };\n}\n\n/**\n * Dev-server plugin that makes the service worker available under `npm run dev`.\n *\n * The production worker is bundled at build time (`vite.sw.config.ts`), so in\n * dev there is no `/sw.js` to register. This plugin compiles `swSrc` on the fly\n * with esbuild — through one incremental context, not a cold build per request —\n * and serves it as a classic worker, plus an empty `precache-manifest.json` (there are no hashed build assets to precache in\n * dev — push and runtime caching still work). It closes the \"SW in dev\" gap\n * that otherwise only `vite-plugin-pwa`'s `devOptions` covered.\n *\n * @example\n * // vite.config.ts\n * import { createViteConfig, tempestPwaDevSw } from \"tempest-react-sdk/vite\";\n *\n * export default createViteConfig({ plugins: [tempestPwaDevSw()] });\n */\nexport function tempestPwaDevSw(options: TempestPwaDevSwOptions = {}): TempestVitePlugin {\n const {\n swSrc = \"src/sw.ts\",\n swUrl = \"/sw.js\",\n manifestUrl = \"/precache-manifest.json\",\n enabled = true,\n } = options;\n\n let root = process.cwd();\n let base = \"/\";\n\n /**\n * Whether a request path addresses `target`.\n *\n * Both the bare path and the base-prefixed one are accepted. Which of the\n * two arrives depends on where this middleware lands relative to Vite's own\n * base handling, and a project served from a subpath would otherwise never\n * match — the browser asks for `/app/sw.js` while the option says `/sw.js`.\n */\n function matches(url: string, target: string): boolean {\n if (url === target) return true;\n const prefix = basePrefix(base);\n if (prefix === \"/\") return false;\n return url === `${prefix}${target.replace(/^\\//, \"\")}`;\n }\n\n /**\n * The slice of esbuild's incremental API this plugin uses.\n *\n * Typed structurally rather than imported: `esbuild` is not a dependency of\n * this package. It is resolved at runtime from the app's own tree, where Vite\n * already brings it, so importing its types would add a build-time dependency\n * the runtime does not have.\n */\n interface BuildContext {\n rebuild(): Promise<{ outputFiles?: readonly { text: string }[] }>;\n dispose(): Promise<void>;\n }\n\n let context: BuildContext | null = null;\n let pending: Promise<string> | null = null;\n\n /**\n * The incremental build context, created on the first request that needs it.\n *\n * Lazy because a project that never registers a worker should not pay for an\n * esbuild child process, and because `apply: \"serve\"` still loads this module\n * in a build.\n *\n * @returns The context, reused across requests.\n */\n async function ensureContext(): Promise<BuildContext> {\n if (context) return context;\n const esbuild = (await import(\"esbuild\")) as unknown as {\n context(options: Record<string, unknown>): Promise<BuildContext>;\n };\n context = await esbuild.context({\n entryPoints: [resolve(root, swSrc)],\n bundle: true,\n format: \"iife\",\n platform: \"browser\",\n target: \"es2020\",\n write: false,\n absWorkingDir: root,\n logLevel: \"silent\",\n });\n return context;\n }\n\n /**\n * Bundle the worker, reusing the previous graph.\n *\n * `esbuild.build()` per request was a cold bundle every time, and the request\n * count is not one per session: `Cache-Control: no-cache` guarantees one per\n * page load, and Chrome re-fetches the worker script on every navigation and\n * on its own update checks. `rebuild()` reuses the graph and only redoes what\n * changed on disk.\n *\n * Single-flight, because those requests arrive in bursts and esbuild does not\n * promise anything about concurrent `rebuild()` calls on one context. Callers\n * that land while a bundle is in flight share its result, which is the right\n * answer anyway — they asked for the same file at the same moment.\n *\n * @returns The bundled worker source.\n */\n function buildWorker(): Promise<string> {\n if (pending) return pending;\n\n const run = (async (): Promise<string> => {\n const ctx = await ensureContext();\n const result = await ctx.rebuild();\n const text = result.outputFiles?.[0]?.text;\n if (text === undefined) throw new Error(\"esbuild produced no service-worker output\");\n return text;\n })();\n\n pending = run;\n const clear = (): void => {\n pending = null;\n };\n run.then(clear, clear);\n return run;\n }\n\n /**\n * Tear the context down, releasing esbuild's child process.\n *\n * Idempotent, because it is wired to both the dev server closing and the\n * plugin's `buildEnd`. Without it, restarting the dev server leaks one esbuild\n * process per restart.\n */\n async function disposeContext(): Promise<void> {\n const current = context;\n context = null;\n pending = null;\n await current?.dispose();\n }\n\n const plugin: Plugin = {\n name: \"tempest-pwa-dev-sw\",\n apply: \"serve\",\n async buildEnd() {\n await disposeContext();\n },\n configResolved(config) {\n root = config.root ?? process.cwd();\n base = config.base ?? \"/\";\n },\n configureServer(server) {\n if (!enabled) return;\n\n server.httpServer?.once(\"close\", () => void disposeContext());\n\n server.middlewares.use(\n createDevSwMiddleware({ swUrl, manifestUrl, matches, buildWorker }),\n );\n },\n };\n\n return plugin as TempestVitePlugin;\n}\n"],"mappings":";;;AAsCA,SAAS,EAAsB,GAaZ;CACf,OAAO,OAAO,GAAK,GAAK,MAAS;EAC7B,IAAM,KAAO,EAAI,OAAO,GAAA,CAAI,MAAM,GAAG,CAAC,CAAC;EAEvC,IAAI,EAAK,QAAQ,GAAK,EAAK,KAAK,GAAG;GAC/B,IAAI;IACA,IAAM,IAAS,MAAM,EAAK,YAAY;IAItC,AAHA,EAAI,UAAU,gBAAgB,wBAAwB,GACtD,EAAI,UAAU,0BAA0B,GAAG,GAC3C,EAAI,UAAU,iBAAiB,UAAU,GACzC,EAAI,IAAI,CAAM;GAClB,SAAS,GAAO;IAEZ,AADA,EAAI,aAAa,KACjB,EAAI,IAAI,+BAA+B,OAAO,CAAK,GAAG;GAC1D;GACA;EACJ;EAEA,IAAI,EAAK,QAAQ,GAAK,EAAK,WAAW,GAAG;GAGrC,AAFA,EAAI,UAAU,gBAAgB,kBAAkB,GAChD,EAAI,UAAU,iBAAiB,UAAU,GACzC,EAAI,IAAI,KAAK,UAAU;IAAE,SAAS;IAAO,MAAM,CAAC;GAAE,CAAC,CAAC;GACpD;EACJ;EAEA,EAAK;CACT;AACJ;AAkBA,SAAgB,EAAgB,IAAkC,CAAC,GAAsB;CACrF,IAAM,EACF,WAAQ,aACR,WAAQ,UACR,iBAAc,2BACd,aAAU,OACV,GAEA,IAAO,QAAQ,IAAI,GACnB,IAAO;CAUX,SAAS,EAAQ,GAAa,GAAyB;EACnD,IAAI,MAAQ,GAAQ,OAAO;EAC3B,IAAM,IAAS,EAAW,CAAI;EAE9B,OADI,MAAW,OACR,MAAQ,GAAG,IAAS,EAAO,QAAQ,OAAO,EAAE;CACvD;CAeA,IAAI,IAA+B,MAC/B,IAAkC;CAWtC,eAAe,IAAuC;EAelD,OAdI,MAIJ,IAAU,OAAM,MAHO,OAAO,WAAA,CAGN,QAAQ;GAC5B,aAAa,CAAC,EAAQ,GAAM,CAAK,CAAC;GAClC,QAAQ;GACR,QAAQ;GACR,UAAU;GACV,QAAQ;GACR,OAAO;GACP,eAAe;GACf,UAAU;EACd,CAAC,GACM;CACX;CAkBA,SAAS,IAA+B;EACpC,IAAI,GAAS,OAAO;EAEpB,IAAM,KAAO,YAA6B;GAGtC,IAAM,KAAO,OADQ,MADH,EAAc,EAAA,CACP,QAAQ,EAAA,CACb,cAAc,EAAE,EAAE;GACtC,IAAI,MAAS,KAAA,GAAW,MAAU,MAAM,2CAA2C;GACnF,OAAO;EACX,EAAA,CAAG;EAEH,IAAU;EACV,IAAM,UAAoB;GACtB,IAAU;EACd;EAEA,OADA,EAAI,KAAK,GAAO,CAAK,GACd;CACX;CASA,eAAe,IAAgC;EAC3C,IAAM,IAAU;EAGhB,AAFA,IAAU,MACV,IAAU,MACV,MAAM,GAAS,QAAQ;CAC3B;CAuBA,OAAO;EApBH,MAAM;EACN,OAAO;EACP,MAAM,WAAW;GACb,MAAM,EAAe;EACzB;EACA,eAAe,GAAQ;GAEnB,AADA,IAAO,EAAO,QAAQ,QAAQ,IAAI,GAClC,IAAO,EAAO,QAAQ;EAC1B;EACA,gBAAgB,GAAQ;GACf,MAEL,EAAO,YAAY,KAAK,eAAe,KAAK,EAAe,CAAC,GAE5D,EAAO,YAAY,IACf,EAAsB;IAAE;IAAO;IAAa;IAAS;GAAY,CAAC,CACtE;EACJ;CAGG;AACX"}
|