tempest-react-sdk 0.81.0 → 0.83.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/README.md +9 -10
- package/bin/create-tempest-app.e2e.test.mjs +31 -0
- package/bin/create-tempest-app.mjs +11 -1
- package/bin/lib/doctor/doctor.e2e.test.mjs +29 -0
- package/bin/lib/doctor/vitest.mjs +120 -0
- package/bin/lib/doctor/vitest.test.mjs +139 -0
- package/bin/lib/fs/contain.mjs +7 -0
- package/bin/lib/openapi/escape.mjs +68 -4
- package/bin/lib/openapi/generate.hostile.test.mjs +228 -0
- package/bin/lib/openapi/generate.mjs +119 -15
- package/bin/lib/openapi/pattern-risk.mjs +134 -0
- package/bin/lib/openapi/pattern-risk.test.mjs +45 -0
- package/bin/tempest-gen.test.mjs +100 -0
- package/bin/tempest.mjs +24 -8
- package/dist/access/use-can.cjs +1 -1
- package/dist/access/use-can.cjs.map +1 -1
- package/dist/access/use-can.js +25 -28
- package/dist/access/use-can.js.map +1 -1
- package/dist/auth/create-tempest-auth.cjs +1 -1
- package/dist/auth/create-tempest-auth.cjs.map +1 -1
- package/dist/auth/create-tempest-auth.js +189 -143
- package/dist/auth/create-tempest-auth.js.map +1 -1
- package/dist/auth/session-identity.cjs +2 -0
- package/dist/auth/session-identity.cjs.map +1 -0
- package/dist/auth/session-identity.js +12 -0
- package/dist/auth/session-identity.js.map +1 -0
- package/dist/br/boleto.cjs.map +1 -1
- package/dist/br/boleto.js.map +1 -1
- package/dist/components/AuthForm/AuthLinks.cjs +1 -1
- package/dist/components/AuthForm/AuthLinks.cjs.map +1 -1
- package/dist/components/AuthForm/AuthLinks.js +18 -15
- package/dist/components/AuthForm/AuthLinks.js.map +1 -1
- package/dist/components/AuthPage/AuthPage.cjs +1 -1
- package/dist/components/AuthPage/AuthPage.cjs.map +1 -1
- package/dist/components/AuthPage/AuthPage.js +58 -52
- package/dist/components/AuthPage/AuthPage.js.map +1 -1
- package/dist/components/Breadcrumbs/Breadcrumbs.cjs +1 -1
- package/dist/components/Breadcrumbs/Breadcrumbs.cjs.map +1 -1
- package/dist/components/Breadcrumbs/Breadcrumbs.js +22 -21
- package/dist/components/Breadcrumbs/Breadcrumbs.js.map +1 -1
- package/dist/components/Button/Button.cjs +1 -1
- package/dist/components/Button/Button.cjs.map +1 -1
- package/dist/components/Button/Button.js +41 -40
- package/dist/components/Button/Button.js.map +1 -1
- package/dist/components/Chat/ChatAttachments.cjs +1 -1
- package/dist/components/Chat/ChatAttachments.cjs.map +1 -1
- package/dist/components/Chat/ChatAttachments.js +38 -38
- package/dist/components/Form/Form.cjs +1 -1
- package/dist/components/Form/Form.cjs.map +1 -1
- package/dist/components/Form/Form.js +30 -32
- package/dist/components/Form/Form.js.map +1 -1
- package/dist/components/Layout/Layout.cjs +1 -1
- package/dist/components/Layout/Layout.cjs.map +1 -1
- package/dist/components/Layout/Layout.js +25 -24
- package/dist/components/Layout/Layout.js.map +1 -1
- package/dist/components/Layout/gap.cjs +2 -0
- package/dist/components/Layout/gap.cjs.map +1 -0
- package/dist/components/Layout/gap.js +15 -0
- package/dist/components/Layout/gap.js.map +1 -0
- package/dist/components/NavigationMenu/NavigationMenu.cjs +1 -1
- package/dist/components/NavigationMenu/NavigationMenu.cjs.map +1 -1
- package/dist/components/NavigationMenu/NavigationMenu.js +71 -70
- package/dist/components/NavigationMenu/NavigationMenu.js.map +1 -1
- package/dist/components/Sidebar/Sidebar.cjs.map +1 -1
- package/dist/components/Sidebar/Sidebar.js.map +1 -1
- package/dist/components/Sidebar/SidebarEntry.cjs +1 -1
- package/dist/components/Sidebar/SidebarEntry.cjs.map +1 -1
- package/dist/components/Sidebar/SidebarEntry.js +30 -29
- package/dist/components/Sidebar/SidebarEntry.js.map +1 -1
- package/dist/data/create-data-provider.cjs +1 -1
- package/dist/data/create-data-provider.cjs.map +1 -1
- package/dist/data/create-data-provider.js +1 -2
- package/dist/data/create-data-provider.js.map +1 -1
- package/dist/forms/validate-form.cjs +1 -1
- package/dist/forms/validate-form.cjs.map +1 -1
- package/dist/forms/validate-form.js +6 -1
- package/dist/forms/validate-form.js.map +1 -1
- package/dist/forms/zod-resolver.cjs +1 -1
- package/dist/forms/zod-resolver.cjs.map +1 -1
- package/dist/forms/zod-resolver.js +8 -3
- package/dist/forms/zod-resolver.js.map +1 -1
- package/dist/http/api-client.cjs +1 -1
- package/dist/http/api-client.cjs.map +1 -1
- package/dist/http/api-client.js +90 -72
- package/dist/http/api-client.js.map +1 -1
- package/dist/http/credential-scope.cjs +1 -1
- package/dist/http/credential-scope.cjs.map +1 -1
- package/dist/http/credential-scope.js +33 -9
- package/dist/http/credential-scope.js.map +1 -1
- package/dist/http/presigned-post.cjs.map +1 -1
- package/dist/http/presigned-post.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 +167 -112
- package/dist/http/resumable-upload.js.map +1 -1
- package/dist/http/upload-with-progress.cjs +1 -1
- package/dist/http/upload-with-progress.cjs.map +1 -1
- package/dist/http/upload-with-progress.js +100 -44
- package/dist/http/upload-with-progress.js.map +1 -1
- package/dist/oauth/GoogleSignIn.cjs +1 -1
- package/dist/oauth/GoogleSignIn.cjs.map +1 -1
- package/dist/oauth/GoogleSignIn.js +5 -4
- package/dist/oauth/GoogleSignIn.js.map +1 -1
- package/dist/offline/create-offline-sync.cjs +1 -1
- package/dist/offline/create-offline-sync.cjs.map +1 -1
- package/dist/offline/create-offline-sync.js +87 -69
- package/dist/offline/create-offline-sync.js.map +1 -1
- package/dist/query/offline-persistence.cjs +1 -1
- package/dist/query/offline-persistence.cjs.map +1 -1
- package/dist/query/offline-persistence.js +67 -44
- package/dist/query/offline-persistence.js.map +1 -1
- package/dist/sse/create-event-stream.cjs +1 -1
- package/dist/sse/create-event-stream.cjs.map +1 -1
- package/dist/sse/create-event-stream.js +48 -41
- package/dist/sse/create-event-stream.js.map +1 -1
- package/dist/sse/ticket-event-stream.cjs +1 -1
- package/dist/sse/ticket-event-stream.cjs.map +1 -1
- package/dist/sse/ticket-event-stream.js +48 -19
- package/dist/sse/ticket-event-stream.js.map +1 -1
- package/dist/sse/use-event-stream.cjs +1 -1
- package/dist/sse/use-event-stream.cjs.map +1 -1
- package/dist/sse/use-event-stream.js +19 -14
- package/dist/sse/use-event-stream.js.map +1 -1
- package/dist/sw/background-sync.cjs +1 -1
- package/dist/sw/background-sync.cjs.map +1 -1
- package/dist/sw/background-sync.js +10 -8
- package/dist/sw/background-sync.js.map +1 -1
- package/dist/sw/cache.cjs +1 -1
- package/dist/sw/cache.cjs.map +1 -1
- package/dist/sw/cache.js +111 -55
- package/dist/sw/cache.js.map +1 -1
- package/dist/sw/create-push-handler.cjs +1 -1
- package/dist/sw/create-push-handler.cjs.map +1 -1
- package/dist/sw/create-push-handler.js +20 -10
- package/dist/sw/create-push-handler.js.map +1 -1
- package/dist/sw.d.ts +65 -14
- package/dist/tabular/compact.cjs +1 -1
- package/dist/tabular/compact.cjs.map +1 -1
- package/dist/tabular/compact.js +35 -16
- package/dist/tabular/compact.js.map +1 -1
- package/dist/tabular/manifest.cjs +1 -1
- package/dist/tabular/manifest.cjs.map +1 -1
- package/dist/tabular/manifest.js +33 -15
- package/dist/tabular/manifest.js.map +1 -1
- package/dist/tabular.d.ts +11 -1
- package/dist/tempest-react-sdk.cjs +1 -1
- package/dist/tempest-react-sdk.d.ts +543 -70
- package/dist/tempest-react-sdk.js +224 -224
- package/dist/theme/initial-theme.cjs +4 -4
- package/dist/theme/initial-theme.cjs.map +1 -1
- package/dist/theme/initial-theme.js +8 -5
- package/dist/theme/initial-theme.js.map +1 -1
- package/dist/utils/compressed-storage.cjs +1 -1
- package/dist/utils/compressed-storage.cjs.map +1 -1
- package/dist/utils/compressed-storage.js +13 -7
- package/dist/utils/compressed-storage.js.map +1 -1
- package/dist/utils/objects.cjs +1 -1
- package/dist/utils/objects.cjs.map +1 -1
- package/dist/utils/objects.js +13 -7
- package/dist/utils/objects.js.map +1 -1
- package/dist/utils/safe-url.cjs +1 -1
- package/dist/utils/safe-url.cjs.map +1 -1
- package/dist/utils/safe-url.js +4 -1
- package/dist/utils/safe-url.js.map +1 -1
- package/dist/vite/base-url.cjs +1 -1
- package/dist/vite/base-url.cjs.map +1 -1
- package/dist/vite/base-url.js +5 -1
- package/dist/vite/base-url.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 +96 -31
- package/dist/vite/tempest-pwa-dev-sw.js.map +1 -1
- package/dist/vite/tempest-pwa-manifest.cjs +2 -2
- package/dist/vite/tempest-pwa-manifest.cjs.map +1 -1
- package/dist/vite/tempest-pwa-manifest.js +32 -16
- package/dist/vite/tempest-pwa-manifest.js.map +1 -1
- package/dist/vite.cjs +1 -1
- package/dist/vite.d.ts +63 -12
- package/dist/vite.js +4 -4
- package/dist/webrtc/peer-mesh.cjs +1 -1
- package/dist/webrtc/peer-mesh.cjs.map +1 -1
- package/dist/webrtc/peer-mesh.js +7 -1
- package/dist/webrtc/peer-mesh.js.map +1 -1
- package/dist/ws/create-web-socket.cjs +1 -1
- package/dist/ws/create-web-socket.cjs.map +1 -1
- package/dist/ws/create-web-socket.js +77 -61
- package/dist/ws/create-web-socket.js.map +1 -1
- package/dist/ws/use-web-socket.cjs +1 -1
- package/dist/ws/use-web-socket.cjs.map +1 -1
- package/dist/ws/use-web-socket.js +4 -3
- package/dist/ws/use-web-socket.js.map +1 -1
- package/package.json +1 -1
- package/template-pwa/src/pwa.tsx +42 -0
- package/template-pwa/src/sw.ts +5 -4
package/README.md
CHANGED
|
@@ -159,7 +159,7 @@ Via `package.json`:
|
|
|
159
159
|
|
|
160
160
|
Requires React `>=18` and Node `>=22.12` to build.
|
|
161
161
|
|
|
162
|
-
**A bundler is assumed — outside one, load the shipped `.css` loader.** Every component module imports its own stylesheet (see [CSS import](#css-import)), and `.css` is not a format Node can load — `node -e "import('tempest-react-sdk')"` fails with `ERR_UNKNOWN_FILE_EXTENSION`. Vite, webpack, Rspack, Parcel and esbuild all resolve that import, and the SDK is client-side only, so no real app consumer is affected. For everything that is not a bundler — a script, a Jest suite, reading the surface by introspection — the package ships the loader: `node --import tempest-react-sdk/node-css-loader your-script.mjs` resolves every stylesheet to an empty module, for `import` and `require` both. A **Jest** suite can use it or the usual `moduleNameMapper: { "\\.css$": "<rootDir>/test/css-stub.js" }`. Vitest needs
|
|
162
|
+
**A bundler is assumed — outside one, load the shipped `.css` loader.** Every component module imports its own stylesheet (see [CSS import](#css-import)), and `.css` is not a format Node can load — `node -e "import('tempest-react-sdk')"` fails with `ERR_UNKNOWN_FILE_EXTENSION`. Vite, webpack, Rspack, Parcel and esbuild all resolve that import, and the SDK is client-side only, so no real app consumer is affected. For everything that is not a bundler — a script, a Jest suite, reading the surface by introspection — the package ships the loader: `node --import tempest-react-sdk/node-css-loader your-script.mjs` resolves every stylesheet to an empty module, for `import` and `require` both. A **Jest** suite can use it or the usual `moduleNameMapper: { "\\.css$": "<rootDir>/test/css-stub.js" }`. **Vitest** needs `tempestVitest()` from `tempest-react-sdk/vite`, because it hands every ESM package from `node_modules` to Node: `createViteConfig` already wires it, but a separate `vitest.config.ts` (or a plain `vite.config.ts`) must add it to `plugins`, or the first import of the SDK fails with `Unknown file extension ".css"` — see [Testing with Vitest](https://mauriciobenjamin700.github.io/tempest-react-sdk/en/vite-config/#testing-with-vitest).
|
|
163
163
|
|
|
164
164
|
### Peer & bundled dependencies
|
|
165
165
|
|
|
@@ -276,7 +276,7 @@ Every module is re-exported from the package root — `import { Button, useDebou
|
|
|
276
276
|
| `router` _(dep: `react-router`)_ | `defineRoutes`, `AppRouter`, `RouteGuard`, + re-exports of React Router's whole client-side surface (`Link`, `NavLink`, `Outlet`, `useNavigate`, `useParams`, `useSearchParams`, `matchPath`, `generatePath`, `createBrowserRouter`, `useLoaderData`, …; its `Form` as `RouterForm`), types: `TempestRouteObject`, `RouterKind`, `AppRouterProps`, `RouteGuardProps` |
|
|
277
277
|
| `store` _(dep: `zustand`)_ | `createStore`, `createSelectors`, types: `CreateStoreOptions`, `CreateStorePersistOptions`, `WithSelectors` |
|
|
278
278
|
| `app` | `AppProviders` (composes `ErrorBoundary` → `QueryProvider` → `ThemeProvider` → `I18nProvider`), type: `AppProvidersProps` |
|
|
279
|
-
| `vite` _(subpath `tempest-react-sdk/vite`)_ | `createViteConfig`, `tempestPwaManifest` (emits `precache-manifest.json` for offline precache), `tempestPwaIcons` (generates the PNG icon set from one SVG via `sharp`), `tempestPwaDevSw` (serves the SW under `npm run dev`), `tempestCsp` (writes a Content-Security-Policy whose `connect-src` is derived from the app's `VITE_*` URLs, enforced in dev too), `tempestPwaIcons({ appleSplash })` (Apple splash screens), types: `CreateViteConfigOptions`, `ProxyEntry`, `TempestViteConfig`, `TempestPwaManifestOptions`, `TempestPwaIconsOptions`, `TempestPwaDevSwOptions`, `TempestCspOptions`, `TempestCspPolicy`, `TempestCspSource`, `AppleSplashSpec`, `TempestVitePlugin`
|
|
279
|
+
| `vite` _(subpath `tempest-react-sdk/vite`)_ | `createViteConfig`, `tempestPwaManifest` (emits `precache-manifest.json` for offline precache), `tempestPwaIcons` (generates the PNG icon set from one SVG via `sharp`), `tempestPwaDevSw` (serves the SW under `npm run dev` and bundles it into `vite build`), `tempestCsp` (writes a Content-Security-Policy whose `connect-src` is derived from the app's `VITE_*` URLs, enforced in dev too), `tempestPwaIcons({ appleSplash })` (Apple splash screens), types: `CreateViteConfigOptions`, `ProxyEntry`, `TempestViteConfig`, `TempestPwaManifestOptions`, `TempestPwaIconsOptions`, `TempestPwaDevSwOptions`, `TempestCspOptions`, `TempestCspPolicy`, `TempestCspSource`, `AppleSplashSpec`, `TempestVitePlugin` |
|
|
280
280
|
| `forms` _(peer: `zod`, `react-hook-form`)_ | `validateForm`, `zodResolver`, `useZodForm`, `validateCPF`, `validateCNPJ`, `formatCEP`, `formatCNPJ`, `unmask`, `CPFInput`, `CNPJInput`, `PhoneInput`, `CEPInput`, `MoneyInput`, `useViaCEP` |
|
|
281
281
|
| `sse` | `createEventStream`, `useEventStream` (`schema` valida cada frame — zod/valibot/arktype via Standard Schema — e `onParseError` para frame que não é JSON) |
|
|
282
282
|
| `ws` | `createWebSocket` (handshake timeout, silence watchdog, jittered backoff, `navigator.onLine` suspend, close-code classification, `opened` promise), `useWebSocket`, `schema` + `onValidationError` (frame fora do contrato não é entregue; o `pong` do heartbeat continua saindo), `onParseError` (frame que não é JSON: descarta em vez de entregar `string` como `T`), `isRejectionCloseCode`, `HEARTBEAT_CLOSE_CODE` |
|
|
@@ -312,7 +312,7 @@ Every module is re-exported from the package root — `import { Button, useDebou
|
|
|
312
312
|
| `router` _(dep: `react-router`)_ | `defineRoutes`, `AppRouter`, `RouteGuard`, + re-exports of React Router's whole client-side surface (`Link`, `NavLink`, `Outlet`, `useNavigate`, `useParams`, `useSearchParams`, `matchPath`, `generatePath`, `createBrowserRouter`, `useLoaderData`, …; its `Form` as `RouterForm`), types: `TempestRouteObject`, `RouterKind`, `AppRouterProps`, `RouteGuardProps` |
|
|
313
313
|
| `store` _(dep: `zustand`)_ | `createStore`, `createSelectors`, types: `CreateStoreOptions`, `CreateStorePersistOptions`, `WithSelectors` |
|
|
314
314
|
| `app` | `AppProviders` (composes `ErrorBoundary` → `QueryProvider` → `ThemeProvider` → `I18nProvider`), type: `AppProvidersProps` |
|
|
315
|
-
| `vite` _(subpath `tempest-react-sdk/vite`)_ | `createViteConfig`, `tempestPwaManifest` (emits `precache-manifest.json` for offline precache), `tempestPwaIcons` (generates the PNG icon set from one SVG via `sharp`), `tempestPwaDevSw` (serves the SW under `npm run dev`), `tempestPwaIcons({ appleSplash })` (Apple splash screens), types: `CreateViteConfigOptions`, `ProxyEntry`, `TempestViteConfig`, `TempestPwaManifestOptions`, `TempestPwaIconsOptions`, `TempestPwaDevSwOptions`, `AppleSplashSpec`, `TempestVitePlugin`
|
|
315
|
+
| `vite` _(subpath `tempest-react-sdk/vite`)_ | `createViteConfig`, `tempestPwaManifest` (emits `precache-manifest.json` for offline precache), `tempestPwaIcons` (generates the PNG icon set from one SVG via `sharp`), `tempestPwaDevSw` (serves the SW under `npm run dev` and bundles it into `vite build`), `tempestPwaIcons({ appleSplash })` (Apple splash screens), types: `CreateViteConfigOptions`, `ProxyEntry`, `TempestViteConfig`, `TempestPwaManifestOptions`, `TempestPwaIconsOptions`, `TempestPwaDevSwOptions`, `AppleSplashSpec`, `TempestVitePlugin` |
|
|
316
316
|
| `forms` _(peer: `zod`, `react-hook-form`)_ | `validateForm`, `zodResolver`, `useZodForm`, `validateCPF`, `validateCNPJ`, `formatCEP`, `formatCNPJ`, `unmask`, `CPFInput`, `CNPJInput`, `PhoneInput`, `CEPInput`, `MoneyInput`, `useViaCEP` |
|
|
317
317
|
| `sse` | `createEventStream`, `useEventStream` (`schema` valida cada frame — zod/valibot/arktype via Standard Schema — e `onParseError` para frame que não é JSON) |
|
|
318
318
|
| `ws` | `createWebSocket` (handshake timeout, silence watchdog, jittered backoff, `navigator.onLine` suspend, close-code classification, `opened` promise), `useWebSocket`, `schema` + `onValidationError` (frame fora do contrato não é entregue; o `pong` do heartbeat continua saindo), `onParseError` (frame que não é JSON: descarta em vez de entregar `string` como `T`), `isRejectionCloseCode`, `HEARTBEAT_CLOSE_CODE` |
|
|
@@ -343,7 +343,7 @@ Every module is re-exported from the package root — `import { Button, useDebou
|
|
|
343
343
|
|
|
344
344
|
Full per-module docs are published as a bilingual MkDocs site on GitHub Pages — **[Português (BR)](https://mauriciobenjamin700.github.io/tempest-react-sdk/)** / **[English (US)](https://mauriciobenjamin700.github.io/tempest-react-sdk/en/)** (one page per module + draw.io diagrams in [`docs/diagrams/`](./docs/diagrams)). The source markdown lives in [`docs/`](./docs) (PT-BR base files + `.en.md` translations).
|
|
345
345
|
|
|
346
|
-
> **Local preview:** `pip install -r docs/requirements.txt && mkdocs serve` (the published site is built and
|
|
346
|
+
> **Local preview:** `pip install -r docs/requirements.txt && mkdocs serve` (the published site is built and pushed to `gh-pages` by `make docs-deploy`, from the maintainer's machine).
|
|
347
347
|
|
|
348
348
|
A demo app exercising every module lives in [`examples/gallery`](./examples/gallery) — `cd examples/gallery && npm install && npm run dev`.
|
|
349
349
|
|
|
@@ -1321,7 +1321,7 @@ installNotificationClickHandler({
|
|
|
1321
1321
|
});
|
|
1322
1322
|
```
|
|
1323
1323
|
|
|
1324
|
-
The SDK does **not** ship the worker file
|
|
1324
|
+
The SDK does **not** ship the worker file — you write `src/sw.ts`. `tempestPwaDevSw()` from `tempest-react-sdk/vite` bundles it to `dist/sw.js` in `vite build` (and serves it in dev), and `tempestPwaManifest()` fails the build when the manifest would ship without a worker. See [Vite Config › PWA](https://mauriciobenjamin700.github.io/tempest-react-sdk/en/vite-config/#pwa-the-service-worker-in-the-production-build).
|
|
1325
1325
|
|
|
1326
1326
|
### Audio playback recipe
|
|
1327
1327
|
|
|
@@ -1995,15 +1995,14 @@ Every gallery section maps 1-to-1 with a docs page — see [`docs/gallery.md`](.
|
|
|
1995
1995
|
|
|
1996
1996
|
## Release
|
|
1997
1997
|
|
|
1998
|
-
|
|
1998
|
+
Two commands, with the release PR's review in between:
|
|
1999
1999
|
|
|
2000
2000
|
```bash
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
npm run release # build + changeset publish
|
|
2001
|
+
make release TAG=X.Y.Z # opens the release PR (no tag, no publish)
|
|
2002
|
+
make release-tag TAG=X.Y.Z # after the merge: build + smokes + npm publish + tag + GitHub Release
|
|
2004
2003
|
```
|
|
2005
2004
|
|
|
2006
|
-
|
|
2005
|
+
There is no GitHub Actions in this repository: `make release-tag` runs the ref guard, builds and smokes the merged commit, publishes the smoked tarball with the maintainer's npm credential (no provenance — that needs a CI runner's OIDC), pushes the tag and creates the GitHub Release. A bare `npm publish` is refused (`make publish` exits 1). Pipeline and the repository owner's settings checklist: [Release pipeline](https://mauriciobenjamin700.github.io/tempest-react-sdk/en/release/).
|
|
2007
2006
|
|
|
2008
2007
|
Tags follow `vMAJOR.MINOR.PATCH`. CSS tokens (`--tempest-*`) are treated as public API — renames bump the minor (or major, on removal).
|
|
2009
2008
|
|
|
@@ -49,6 +49,37 @@ function scaffoldHere() {
|
|
|
49
49
|
return { status: result.status, stdout: result.stdout + result.stderr };
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
describe("create-tempest-app new-folder mode refuses a destination that is a symlink (#533)", () => {
|
|
53
|
+
it("my-app -> ../outside is refused and nothing lands at the link target", () => {
|
|
54
|
+
symlinkSync("../outside", join(proj, "my-app"));
|
|
55
|
+
|
|
56
|
+
const result = spawnSync(process.execPath, [CLI, "my-app"], {
|
|
57
|
+
cwd: proj,
|
|
58
|
+
encoding: "utf8",
|
|
59
|
+
env: { ...process.env, NO_COLOR: "1" },
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
expect(result.status).toBe(1);
|
|
63
|
+
expect(result.stderr).toMatch(/"my-app" is a symbolic link/);
|
|
64
|
+
expect(readdirSync(outside)).toEqual([]);
|
|
65
|
+
expect(lstatSync(join(proj, "my-app")).isSymbolicLink()).toBe(true);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("a plain new folder name is still scaffolded", () => {
|
|
69
|
+
const result = spawnSync(process.execPath, [CLI, "my-app"], {
|
|
70
|
+
cwd: proj,
|
|
71
|
+
encoding: "utf8",
|
|
72
|
+
env: { ...process.env, NO_COLOR: "1" },
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
expect(result.status).toBe(0);
|
|
76
|
+
expect(existsSync(join(proj, "my-app", "src", "main.tsx"))).toBe(true);
|
|
77
|
+
expect(JSON.parse(readFileSync(join(proj, "my-app", "package.json"), "utf8")).name).toBe(
|
|
78
|
+
"my-app",
|
|
79
|
+
);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
52
83
|
describe("create-tempest-app merge mode never writes through a symlink (#436)", () => {
|
|
53
84
|
it("a symlinked src/ pointing outside the project is skipped, not written into", () => {
|
|
54
85
|
writeFileSync(join(outside, "main.tsx"), "OUTSIDE-USER-FILE");
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
// package.json has the Tempest scripts/deps merged in (your name/version are
|
|
11
11
|
// preserved). A symlink already in the destination is never written through:
|
|
12
12
|
// it is skipped and reported, so a link to a folder outside the project cannot
|
|
13
|
-
// redirect the template there.
|
|
13
|
+
// redirect the template there. A named destination that is itself a symlink is
|
|
14
|
+
// refused for the same reason. The `--pwa` flag overlays the PWA template
|
|
14
15
|
// (manifest, service worker, push-subscribe wiring) on top of the base.
|
|
15
16
|
import { cp, lstat, mkdir, readdir, readFile, realpath, writeFile } from "node:fs/promises";
|
|
16
17
|
import { existsSync } from "node:fs";
|
|
@@ -239,6 +240,15 @@ async function main() {
|
|
|
239
240
|
process.exit(1);
|
|
240
241
|
}
|
|
241
242
|
destDir = resolve(process.cwd(), arg);
|
|
243
|
+
if (await isSymlink(destDir)) {
|
|
244
|
+
console.error(
|
|
245
|
+
`${c.red}✗ "${arg}" is a symbolic link; the project would be created where it points.${c.reset}`,
|
|
246
|
+
);
|
|
247
|
+
console.error(
|
|
248
|
+
`${c.dim} Create the project at the real path, or remove the link.${c.reset}`,
|
|
249
|
+
);
|
|
250
|
+
process.exit(1);
|
|
251
|
+
}
|
|
242
252
|
if (!(await isEmptyDir(destDir))) {
|
|
243
253
|
console.error(`${c.red}✗ Directory "${arg}" exists and is not empty.${c.reset}`);
|
|
244
254
|
console.error(
|
|
@@ -98,6 +98,11 @@ describe("tempest doctor — a third-party project that does not use the SDK", (
|
|
|
98
98
|
expect(out).not.toContain('add import "tempest-react-sdk/styles.css"');
|
|
99
99
|
});
|
|
100
100
|
|
|
101
|
+
it("does not judge the Vitest config of a project without the SDK", () => {
|
|
102
|
+
installed("vitest", "4.1.12");
|
|
103
|
+
expect(doctor().out).not.toContain("inline the SDK");
|
|
104
|
+
});
|
|
105
|
+
|
|
101
106
|
it("does not demand a src/main.tsx it has no reason to expect", () => {
|
|
102
107
|
const { out } = doctor();
|
|
103
108
|
expect(out).not.toContain("app entry");
|
|
@@ -336,6 +341,30 @@ describe("tempest doctor — a project that does use the SDK", () => {
|
|
|
336
341
|
expect(doctor().out).toContain("leaflet missing");
|
|
337
342
|
});
|
|
338
343
|
|
|
344
|
+
it("warns when Vitest would load the SDK without the plugin", () => {
|
|
345
|
+
installed("vitest", "4.1.12");
|
|
346
|
+
const { out, code } = doctor();
|
|
347
|
+
expect(out).toContain("vite.config.ts does not inline the SDK for Vitest");
|
|
348
|
+
expect(out).toContain("tempestVitest()");
|
|
349
|
+
expect(code).toBe(0);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it("accepts a vitest.config that adds tempestVitest", () => {
|
|
353
|
+
installed("vitest", "4.1.12");
|
|
354
|
+
write(
|
|
355
|
+
"vitest.config.ts",
|
|
356
|
+
'import { tempestVitest } from "tempest-react-sdk/vite";\n' +
|
|
357
|
+
"export default defineConfig({ plugins: [tempestVitest()] });",
|
|
358
|
+
);
|
|
359
|
+
const { out } = doctor();
|
|
360
|
+
expect(out).toContain("vitest.config.ts lets Vitest load the SDK");
|
|
361
|
+
expect(out).not.toContain("does not inline the SDK");
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it("says nothing about Vitest in a project that does not install it", () => {
|
|
365
|
+
expect(doctor().out).not.toContain("Vitest");
|
|
366
|
+
});
|
|
367
|
+
|
|
339
368
|
it("warns when the SDK is installed but undeclared", () => {
|
|
340
369
|
write("package.json", {
|
|
341
370
|
name: "sdk-app",
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Vitest config check for `tempest doctor`.
|
|
3
|
+
*
|
|
4
|
+
* Every SDK component module imports its own stylesheet, and Vitest hands an ESM
|
|
5
|
+
* package from `node_modules` to plain Node, which has no loader for `.css`. A
|
|
6
|
+
* consumer suite that imports the SDK therefore dies before running with
|
|
7
|
+
* `Unknown file extension ".css"` unless the config inlines the SDK — which is what
|
|
8
|
+
* `tempestVitest()` does, and what `createViteConfig` includes. Measured in #548
|
|
9
|
+
* with the 0.81.0 tarball on Vitest 4.1.12: an own `vitest.config.ts`, a plain
|
|
10
|
+
* `vite.config.ts` and no config at all fail; only the plugin passes.
|
|
11
|
+
*
|
|
12
|
+
* The check is a text heuristic over the config file Vitest would load, not an
|
|
13
|
+
* evaluation of it: running the user's config would execute their code and pull
|
|
14
|
+
* their plugins in, which a health check has no business doing. The markers are the
|
|
15
|
+
* plugin and helper names, an explicit `inline: true` (which inlines everything,
|
|
16
|
+
* the SDK included), and an inline list naming the package — the three shapes that
|
|
17
|
+
* make the suite pass.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Config file names Vitest looks for, in the order it prefers them.
|
|
22
|
+
*
|
|
23
|
+
* Ported from Vitest 4.1.12's `configFiles` (`CONFIG_NAMES` × `CONFIG_EXTENSIONS`
|
|
24
|
+
* in `dist/chunks/constants.*.js`, not exported): a `vitest.config.*` wins over a
|
|
25
|
+
* `vite.config.*`, and within each the extensions are tried in this order. The
|
|
26
|
+
* test reads the installed Vitest and fails when the list drifts.
|
|
27
|
+
*/
|
|
28
|
+
export const VITEST_CONFIG_FILES = [
|
|
29
|
+
"vitest.config.ts",
|
|
30
|
+
"vitest.config.mts",
|
|
31
|
+
"vitest.config.cts",
|
|
32
|
+
"vitest.config.js",
|
|
33
|
+
"vitest.config.mjs",
|
|
34
|
+
"vitest.config.cjs",
|
|
35
|
+
"vite.config.ts",
|
|
36
|
+
"vite.config.mts",
|
|
37
|
+
"vite.config.cts",
|
|
38
|
+
"vite.config.js",
|
|
39
|
+
"vite.config.mjs",
|
|
40
|
+
"vite.config.cjs",
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
/** Text that, once comments are gone, means the config already inlines the SDK. */
|
|
44
|
+
const INLINES_SDK = [
|
|
45
|
+
/\btempestVitest\b/,
|
|
46
|
+
/\bcreateViteConfig\b/,
|
|
47
|
+
/\binline\s*:\s*true\b/,
|
|
48
|
+
/\binline\s*:\s*\[[^\]]*["'`]tempest-react-sdk["'`]/,
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
/** An import of the project's own `vite.config`, as `mergeConfig` setups do. */
|
|
52
|
+
const IMPORTS_VITE_CONFIG = /from\s*["']\.\/vite\.config(?:\.[cm]?[jt]s)?["']/;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Drop JavaScript comments so a marker that only appears in prose does not count.
|
|
56
|
+
*
|
|
57
|
+
* A line comment is only removed when `//` is not preceded by `:`, which keeps
|
|
58
|
+
* `https://` inside a string intact; a missed edge case here costs one false "ok",
|
|
59
|
+
* never a crash.
|
|
60
|
+
*
|
|
61
|
+
* @param {string} text - Source text of a config file.
|
|
62
|
+
* @returns {string} The text without block and line comments.
|
|
63
|
+
*/
|
|
64
|
+
function stripComments(text) {
|
|
65
|
+
return text.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:])\/\/.*$/gm, "$1");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Whether a config file's text makes Vitest inline the SDK.
|
|
70
|
+
*
|
|
71
|
+
* @param {string} text - Source text of the config.
|
|
72
|
+
* @returns {boolean} Whether one of the passing shapes is present.
|
|
73
|
+
*/
|
|
74
|
+
function inlinesSdk(text) {
|
|
75
|
+
const code = stripComments(text);
|
|
76
|
+
return INLINES_SDK.some((marker) => marker.test(code));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Audit whether the project's Vitest setup can import the SDK.
|
|
81
|
+
*
|
|
82
|
+
* Vitest loads the first `vitest.config.*`, else the first `vite.config.*`, else
|
|
83
|
+
* runs with defaults; this mirrors that choice and judges only the file it would
|
|
84
|
+
* load. A `vitest.config.*` that imports `./vite.config` (the `mergeConfig`
|
|
85
|
+
* pattern) passes when that `vite.config.*` does, so a project using
|
|
86
|
+
* `createViteConfig` there is not flagged.
|
|
87
|
+
*
|
|
88
|
+
* @param {object} params
|
|
89
|
+
* @param {boolean} params.vitestInstalled - Whether `vitest` resolves in the project.
|
|
90
|
+
* @param {Record<string, string>} params.configs - Text of each config file present,
|
|
91
|
+
* keyed by its name from {@link VITEST_CONFIG_FILES}.
|
|
92
|
+
* @returns {Array<[status: string, label: string, detail?: string]>} Check rows;
|
|
93
|
+
* empty when the project does not use Vitest.
|
|
94
|
+
*/
|
|
95
|
+
export function checkVitest({ vitestInstalled, configs }) {
|
|
96
|
+
if (!vitestInstalled) return [];
|
|
97
|
+
|
|
98
|
+
const loaded = VITEST_CONFIG_FILES.find((name) => name in configs);
|
|
99
|
+
const fix =
|
|
100
|
+
"add `plugins: [tempestVitest()]` from tempest-react-sdk/vite, or build the config " +
|
|
101
|
+
"with createViteConfig — otherwise any test importing the SDK fails with " +
|
|
102
|
+
'`Unknown file extension ".css"`';
|
|
103
|
+
|
|
104
|
+
if (!loaded) {
|
|
105
|
+
return [["warn", "Vitest runs with no config", fix]];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const text = configs[loaded];
|
|
109
|
+
const viaViteConfig =
|
|
110
|
+
loaded.startsWith("vitest.") &&
|
|
111
|
+
IMPORTS_VITE_CONFIG.test(stripComments(text)) &&
|
|
112
|
+
VITEST_CONFIG_FILES.some(
|
|
113
|
+
(name) => name.startsWith("vite.") && name in configs && inlinesSdk(configs[name]),
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
if (inlinesSdk(text) || viaViteConfig) {
|
|
117
|
+
return [["ok", `${loaded} lets Vitest load the SDK`]];
|
|
118
|
+
}
|
|
119
|
+
return [["warn", `${loaded} does not inline the SDK for Vitest`, fix]];
|
|
120
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
|
|
6
|
+
import { VITEST_CONFIG_FILES, checkVitest } from "./vitest.mjs";
|
|
7
|
+
|
|
8
|
+
const VITEST_CHUNKS = join(
|
|
9
|
+
dirname(fileURLToPath(import.meta.url)),
|
|
10
|
+
"..",
|
|
11
|
+
"..",
|
|
12
|
+
"..",
|
|
13
|
+
"node_modules",
|
|
14
|
+
"vitest",
|
|
15
|
+
"dist",
|
|
16
|
+
"chunks",
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
/** Statuses of the returned rows, in order. */
|
|
20
|
+
const statuses = (rows) => rows.map(([status]) => status);
|
|
21
|
+
|
|
22
|
+
/** Run the check for a project with Vitest and the given config files. */
|
|
23
|
+
const check = (configs) => checkVitest({ vitestInstalled: true, configs });
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The config file list the installed Vitest really searches, read from its source.
|
|
27
|
+
*
|
|
28
|
+
* @returns {string[]} `CONFIG_NAMES` × `CONFIG_EXTENSIONS`, in Vitest's order.
|
|
29
|
+
*/
|
|
30
|
+
function installedVitestConfigFiles() {
|
|
31
|
+
const file = readdirSync(VITEST_CHUNKS).find((name) => /^constants\..*\.js$/.test(name));
|
|
32
|
+
const text = readFileSync(join(VITEST_CHUNKS, file), "utf8");
|
|
33
|
+
const array = (name) =>
|
|
34
|
+
JSON.parse(
|
|
35
|
+
new RegExp(`const ${name} = (\\[[^\\]]*\\])`)
|
|
36
|
+
.exec(text)[1]
|
|
37
|
+
.replace(/\s+/g, "")
|
|
38
|
+
.replace(/,]/, "]"),
|
|
39
|
+
);
|
|
40
|
+
return array("CONFIG_NAMES").flatMap((name) =>
|
|
41
|
+
array("CONFIG_EXTENSIONS").map((ext) => name + ext),
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
describe("checkVitest — which file it judges", () => {
|
|
46
|
+
it("searches the same files, in the same order, as the installed Vitest", () => {
|
|
47
|
+
expect(VITEST_CONFIG_FILES).toEqual(installedVitestConfigFiles());
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("judges vitest.config over vite.config, the one Vitest loads", () => {
|
|
51
|
+
const rows = check({
|
|
52
|
+
"vite.config.ts": "export default createViteConfig();",
|
|
53
|
+
"vitest.config.ts": "export default defineConfig({ test: {} });",
|
|
54
|
+
});
|
|
55
|
+
expect(statuses(rows)).toEqual(["warn"]);
|
|
56
|
+
expect(rows[0][1]).toContain("vitest.config.ts");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("falls back to vite.config when there is no vitest.config", () => {
|
|
60
|
+
const rows = check({ "vite.config.mjs": "export default defineConfig({});" });
|
|
61
|
+
expect(rows[0][1]).toContain("vite.config.mjs");
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe("checkVitest — setups that fail the suite", () => {
|
|
66
|
+
it("says nothing when the project does not use Vitest", () => {
|
|
67
|
+
expect(checkVitest({ vitestInstalled: false, configs: {} })).toEqual([]);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("warns when Vitest would run with no config at all", () => {
|
|
71
|
+
const rows = check({});
|
|
72
|
+
expect(statuses(rows)).toEqual(["warn"]);
|
|
73
|
+
expect(rows[0][2]).toContain("tempestVitest()");
|
|
74
|
+
expect(rows[0][2]).toContain('Unknown file extension ".css"');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("warns on a plain vitest.config without the plugin", () => {
|
|
78
|
+
const rows = check({
|
|
79
|
+
"vitest.config.ts":
|
|
80
|
+
'import { defineConfig } from "vitest/config";\n' +
|
|
81
|
+
'export default defineConfig({ test: { environment: "jsdom" } });',
|
|
82
|
+
});
|
|
83
|
+
expect(statuses(rows)).toEqual(["warn"]);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("does not count the plugin named only in a comment", () => {
|
|
87
|
+
const rows = check({
|
|
88
|
+
"vitest.config.ts":
|
|
89
|
+
"// TODO: add tempestVitest()\n/* createViteConfig later */\n" +
|
|
90
|
+
"export default defineConfig({});",
|
|
91
|
+
});
|
|
92
|
+
expect(statuses(rows)).toEqual(["warn"]);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("does not trust an import of a vite.config that lacks the plugin too", () => {
|
|
96
|
+
const rows = check({
|
|
97
|
+
"vite.config.ts": "export default defineConfig({});",
|
|
98
|
+
"vitest.config.ts":
|
|
99
|
+
'import viteConfig from "./vite.config";\n' +
|
|
100
|
+
"export default mergeConfig(viteConfig, defineConfig({ test: {} }));",
|
|
101
|
+
});
|
|
102
|
+
expect(statuses(rows)).toEqual(["warn"]);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe("checkVitest — setups that pass", () => {
|
|
107
|
+
it.each([
|
|
108
|
+
["tempestVitest()", "export default defineConfig({ plugins: [tempestVitest()] });"],
|
|
109
|
+
["createViteConfig", "export default createViteConfig({ overrides: {} });"],
|
|
110
|
+
[
|
|
111
|
+
"inline: true",
|
|
112
|
+
"export default defineConfig({ test: { server: { deps: { inline: true } } } });",
|
|
113
|
+
],
|
|
114
|
+
[
|
|
115
|
+
"an inline list naming the SDK",
|
|
116
|
+
'export default defineConfig({ test: { server: { deps: { inline: ["x", "tempest-react-sdk"] } } } });',
|
|
117
|
+
],
|
|
118
|
+
])("accepts %s", (_, text) => {
|
|
119
|
+
expect(statuses(check({ "vitest.config.ts": text }))).toEqual(["ok"]);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("accepts a vitest.config merged over a vite.config that uses createViteConfig", () => {
|
|
123
|
+
const rows = check({
|
|
124
|
+
"vite.config.ts": "export default createViteConfig();",
|
|
125
|
+
"vitest.config.ts":
|
|
126
|
+
'import viteConfig from "./vite.config.ts";\n' +
|
|
127
|
+
"export default mergeConfig(viteConfig, defineConfig({ test: {} }));",
|
|
128
|
+
});
|
|
129
|
+
expect(statuses(rows)).toEqual(["ok"]);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("keeps a URL inside a string from hiding a marker after it", () => {
|
|
133
|
+
const rows = check({
|
|
134
|
+
"vitest.config.ts":
|
|
135
|
+
'const docs = "https://example.com"; export default defineConfig({ plugins: [tempestVitest()] });',
|
|
136
|
+
});
|
|
137
|
+
expect(statuses(rows)).toEqual(["ok"]);
|
|
138
|
+
});
|
|
139
|
+
});
|
package/bin/lib/fs/contain.mjs
CHANGED
|
@@ -28,6 +28,13 @@ export function lstatOrNull(path) {
|
|
|
28
28
|
* not skipped over: its `realpath` throws and the path counts as outside, since
|
|
29
29
|
* `mkdir` or `writeFile` would create whatever the link names.
|
|
30
30
|
*
|
|
31
|
+
* The answer is about the tree as it is now; nothing makes it and the write that
|
|
32
|
+
* follows one atomic step. A process swapping a folder for a symlink between the
|
|
33
|
+
* two can still redirect the write, and Node has no `openat`/`O_NOFOLLOW` walk
|
|
34
|
+
* to close that. Such a process is already writing inside the user's project, so
|
|
35
|
+
* it could edit the generated files or `package.json` directly: the window is an
|
|
36
|
+
* accepted risk, the same one `create-tempest-app` documents (#533).
|
|
37
|
+
*
|
|
31
38
|
* @param {string} root - Project root.
|
|
32
39
|
* @param {string} path - Absolute path, existing or about to be created.
|
|
33
40
|
* @returns {boolean}
|
|
@@ -16,7 +16,11 @@
|
|
|
16
16
|
* {@link assertNoCollision}, never silently merged;
|
|
17
17
|
* - string position → `JSON.stringify` at the call site;
|
|
18
18
|
* - doc comment → {@link commentText};
|
|
19
|
-
* - URL template literal → {@link urlTemplate}
|
|
19
|
+
* - URL template literal → {@link urlTemplate};
|
|
20
|
+
* - the developer's terminal → {@link terminalText}.
|
|
21
|
+
*
|
|
22
|
+
* One more check covers the text that becomes a URL rather than being escaped
|
|
23
|
+
* into one: {@link assertSafePath}.
|
|
20
24
|
*/
|
|
21
25
|
|
|
22
26
|
const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
|
|
@@ -150,19 +154,79 @@ export function assertNoCollision(entries, what) {
|
|
|
150
154
|
}
|
|
151
155
|
}
|
|
152
156
|
|
|
157
|
+
/**
|
|
158
|
+
* C0 and C1 control characters except tab and line feed: everything a terminal
|
|
159
|
+
* may read as the start of an escape sequence (`ESC`, the 8-bit `CSI` and
|
|
160
|
+
* `OSC`), plus `CR` and backspace, which rewrite what is already on the line.
|
|
161
|
+
*/
|
|
162
|
+
const TERMINAL_CONTROLS = /[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/g;
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Make spec text safe to print to the developer's terminal.
|
|
166
|
+
*
|
|
167
|
+
* `tempest gen api` echoes the spec's tag names, and its errors quote spec
|
|
168
|
+
* names. `JSON.stringify` escapes the C0 range but leaves the C1 range raw, so
|
|
169
|
+
* neither path was safe on its own: a tag carrying `ESC ] 0 ; ... BEL` retitled
|
|
170
|
+
* the terminal window, and an OSC 8 sequence printed a clickable link whose
|
|
171
|
+
* target the developer never saw (#533). Every control character a terminal
|
|
172
|
+
* acts on is dropped; tab and line feed are kept so a multi-line error still
|
|
173
|
+
* reads as one.
|
|
174
|
+
*
|
|
175
|
+
* @param {unknown} text - Text that may come from the spec.
|
|
176
|
+
* @returns {string} The text without terminal control characters.
|
|
177
|
+
*/
|
|
178
|
+
export function terminalText(text) {
|
|
179
|
+
return String(text).replace(TERMINAL_CONTROLS, "");
|
|
180
|
+
}
|
|
181
|
+
|
|
153
182
|
/**
|
|
154
183
|
* Make spec text safe inside a `/** ... *\/` block comment.
|
|
155
184
|
*
|
|
156
185
|
* Line terminators collapse to a space, so the text cannot start a new line of
|
|
157
186
|
* the comment, and every `*\/` is broken as `*\\/` so it cannot close it.
|
|
187
|
+
* Terminal control characters are dropped too ({@link terminalText}): they are
|
|
188
|
+
* not code, but printing the generated file with `cat` would replay them.
|
|
158
189
|
*
|
|
159
190
|
* @param {unknown} text - The spec text (tag, summary, path).
|
|
160
191
|
* @returns {string} The neutralized single-line text.
|
|
161
192
|
*/
|
|
162
193
|
export function commentText(text) {
|
|
163
|
-
return String(text)
|
|
164
|
-
|
|
165
|
-
|
|
194
|
+
return terminalText(String(text).replace(/[\r\n\u2028\u2029]+/g, " ")).replace(/\*\//g, "*\\/");
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Throw when an OpenAPI path key would not stay a path under the client's
|
|
199
|
+
* `baseURL`.
|
|
200
|
+
*
|
|
201
|
+
* The key becomes the URL the generated method requests, and the client passes
|
|
202
|
+
* an absolute URL through untouched: a key `"https://other.example/x"` made the
|
|
203
|
+
* method call another origin \u2014 the Bearer is withheld there, but the body and,
|
|
204
|
+
* with `credentials: "include"`, the cookies were not. A static `.` or `..`
|
|
205
|
+
* segment (`"/../x"`) climbs out of the `baseURL`'s path prefix once the URL is
|
|
206
|
+
* normalized. OpenAPI requires a path key to start with `/` and to be relative
|
|
207
|
+
* to the server URL, so neither is a valid spec; both are refused before any
|
|
208
|
+
* file is written instead of becoming a call the developer never reads. `\` is
|
|
209
|
+
* a separator and `%2e` a dot here because the WHATWG URL parser normalizes them
|
|
210
|
+
* that way.
|
|
211
|
+
*
|
|
212
|
+
* @param {unknown} path - The key from `paths`.
|
|
213
|
+
* @throws {Error} When the key does not start with `/` or has a dot segment.
|
|
214
|
+
*/
|
|
215
|
+
export function assertSafePath(path) {
|
|
216
|
+
if (typeof path !== "string" || !path.startsWith("/")) {
|
|
217
|
+
throw new Error(
|
|
218
|
+
`OpenAPI path ${JSON.stringify(path)} does not start with "/". A path key is relative to the server URL; an absolute URL here would send the request to another origin.`,
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
const dotSegment = path
|
|
222
|
+
.replace(/[?#][\s\S]*$/, "")
|
|
223
|
+
.split(/[/\\]/)
|
|
224
|
+
.some((segment) => /^(?:\.|%2e){1,2}$/i.test(segment));
|
|
225
|
+
if (dotSegment) {
|
|
226
|
+
throw new Error(
|
|
227
|
+
`OpenAPI path ${JSON.stringify(path)} has a "." or ".." segment, which leaves the baseURL's path once the URL is normalized. Rename the path in the spec.`,
|
|
228
|
+
);
|
|
229
|
+
}
|
|
166
230
|
}
|
|
167
231
|
|
|
168
232
|
/**
|