tempest-react-sdk 0.81.0 → 0.82.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 +5 -6
- 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 +137 -0
- package/bin/lib/openapi/generate.mjs +78 -13
- 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 +192 -149
- 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 +90 -84
- 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 +8 -8
- 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 +54 -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 +36 -30
- 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 +44 -13
- 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 +457 -68
- 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/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
|
|
|
@@ -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: tags the merge commit and pushes it
|
|
2004
2003
|
```
|
|
2005
2004
|
|
|
2006
|
-
|
|
2005
|
+
The tag push runs `.github/workflows/release-npm.yml`, which publishes through npm Trusted Publishing (OIDC, with provenance) — no npm token anywhere, and no local publish (`make publish` refuses). 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
|
/**
|
|
@@ -672,3 +672,140 @@ describe("generate — names that collide with what the generator itself declare
|
|
|
672
672
|
);
|
|
673
673
|
});
|
|
674
674
|
});
|
|
675
|
+
|
|
676
|
+
describe("generate — spec text that would leave its place in the output (#533)", () => {
|
|
677
|
+
const ESC_TAG = "items\u001b]0;PWNED\u0007\u009b31m\r";
|
|
678
|
+
const POST_BODY = {
|
|
679
|
+
"/a": { post: { tags: ["t"], requestBody: JSON_BODY("Body"), responses: OK } },
|
|
680
|
+
};
|
|
681
|
+
|
|
682
|
+
it("a path key that is an absolute URL is refused, so no method calls another origin", () => {
|
|
683
|
+
expect(() =>
|
|
684
|
+
generate(spec({ "https://other.example/x": { get: { tags: ["t"], responses: OK } } })),
|
|
685
|
+
).toThrow(/path "https:\/\/other\.example\/x" does not start with "\/"/);
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
it.each(["/../x", "/a/./b", "/a/..", "/%2e%2E/x", "/a\\..\\x", "/./x?y=1"])(
|
|
689
|
+
"a path key with a dot segment (%s) is refused, so it cannot climb out of the baseURL path",
|
|
690
|
+
(path) => {
|
|
691
|
+
expect(() =>
|
|
692
|
+
generate(spec({ [path]: { get: { tags: ["t"], responses: OK } } })),
|
|
693
|
+
).toThrow(/has a "\." or "\.\." segment/);
|
|
694
|
+
},
|
|
695
|
+
);
|
|
696
|
+
|
|
697
|
+
it("dots inside a segment, dots in the query and a doubled slash still generate", () => {
|
|
698
|
+
const { files } = generate(
|
|
699
|
+
spec({
|
|
700
|
+
"/v1.2/..cache/file.json": { get: { tags: ["t"], responses: OK } },
|
|
701
|
+
"//double/slash": { get: { tags: ["t"], responses: OK } },
|
|
702
|
+
"/search?q=..": { get: { tags: ["t"], responses: OK } },
|
|
703
|
+
}),
|
|
704
|
+
);
|
|
705
|
+
expect(files["t/service.ts"]).toContain('"/v1.2/..cache/file.json"');
|
|
706
|
+
expect(files["t/service.ts"]).toContain('"/search?q=.."');
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
it("a property named __proto__ is refused instead of silently vanishing from the Zod shape", () => {
|
|
710
|
+
const Body = JSON.parse(
|
|
711
|
+
'{"type":"object","required":["__proto__"],"properties":{"__proto__":{"type":"string"}}}',
|
|
712
|
+
);
|
|
713
|
+
expect(() => generate(spec(POST_BODY, { Body }))).toThrow(
|
|
714
|
+
/schema "Body" at \(root\): a property named "__proto__" cannot be validated/,
|
|
715
|
+
);
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
it("a default value with a nested __proto__ key is refused", () => {
|
|
719
|
+
const Body = JSON.parse(
|
|
720
|
+
'{"type":"object","properties":{"meta":{"type":"object","default":{"a":{"__proto__":{"admin":true}}}}}}',
|
|
721
|
+
);
|
|
722
|
+
expect(() => generate(spec(POST_BODY, { Body }))).toThrow(
|
|
723
|
+
/schema "Body" at properties\.meta: the default value has a "__proto__" key/,
|
|
724
|
+
);
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
it("properties called constructor or prototype still generate and are validated", () => {
|
|
728
|
+
const { files } = generate(
|
|
729
|
+
spec(POST_BODY, {
|
|
730
|
+
Body: {
|
|
731
|
+
type: "object",
|
|
732
|
+
required: ["constructor"],
|
|
733
|
+
properties: {
|
|
734
|
+
constructor: { type: "string" },
|
|
735
|
+
prototype: { type: "string" },
|
|
736
|
+
},
|
|
737
|
+
},
|
|
738
|
+
}),
|
|
739
|
+
);
|
|
740
|
+
const schemas = evaluate(files["t/schemas.ts"], vm.createContext({}), {});
|
|
741
|
+
expect(schemas.BodySchema.safeParse({ constructor: 1 }).success).toBe(false);
|
|
742
|
+
expect(schemas.BodySchema.safeParse({ constructor: "x" }).success).toBe(true);
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
it("a nested-quantifier pattern still generates and comes back as a warning naming the schema", () => {
|
|
746
|
+
const { files, warnings } = generate(
|
|
747
|
+
spec(POST_BODY, {
|
|
748
|
+
Body: {
|
|
749
|
+
type: "object",
|
|
750
|
+
properties: {
|
|
751
|
+
slug: { type: "string", pattern: "^([a-z0-9]+-)*[a-z0-9]+$" },
|
|
752
|
+
evil: { type: "string", pattern: "^(a+)+$" },
|
|
753
|
+
},
|
|
754
|
+
},
|
|
755
|
+
}),
|
|
756
|
+
);
|
|
757
|
+
expect(files["t/schemas.ts"]).toContain('new RegExp("^(a+)+$")');
|
|
758
|
+
expect(warnings).toHaveLength(1);
|
|
759
|
+
expect(warnings[0]).toMatch(/schema "Body" at properties\.evil: pattern "\^\(a\+\)\+\$"/);
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
it("terminal control characters in a tag do not reach the generated doc comment", () => {
|
|
763
|
+
const { files, tags } = generate(
|
|
764
|
+
spec({ "/a": { get: { tags: [ESC_TAG], responses: OK } } }),
|
|
765
|
+
);
|
|
766
|
+
const service = Object.entries(files).find(([f]) => f.endsWith("/service.ts"))[1];
|
|
767
|
+
expect(tags).toEqual([ESC_TAG]);
|
|
768
|
+
expect(service).not.toMatch(/[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/);
|
|
769
|
+
expect(service).toContain('the "items]0;PWNED31m " routes');
|
|
770
|
+
});
|
|
771
|
+
|
|
772
|
+
it("the folder and class reported for each group are the ones written", () => {
|
|
773
|
+
const { files, services } = generate(
|
|
774
|
+
spec({ "/a": { get: { tags: ["Items API"], responses: OK } } }),
|
|
775
|
+
);
|
|
776
|
+
expect(services).toEqual([{ slug: "items-api", className: "ItemsApiService" }]);
|
|
777
|
+
expect(files["items-api/service.ts"]).toContain("export class ItemsApiService");
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
it("the output for a spec with control characters and a flagged pattern typechecks", () => {
|
|
781
|
+
const { files } = generate(
|
|
782
|
+
spec(
|
|
783
|
+
{
|
|
784
|
+
"/a/{id}": {
|
|
785
|
+
post: {
|
|
786
|
+
tags: ["t"],
|
|
787
|
+
summary: "x\u001b[2J\u009d8;;https://evil/\u009c",
|
|
788
|
+
parameters: [
|
|
789
|
+
{
|
|
790
|
+
name: "id",
|
|
791
|
+
in: "path",
|
|
792
|
+
required: true,
|
|
793
|
+
schema: { type: "string" },
|
|
794
|
+
},
|
|
795
|
+
],
|
|
796
|
+
requestBody: JSON_BODY("Body"),
|
|
797
|
+
responses: OK,
|
|
798
|
+
},
|
|
799
|
+
},
|
|
800
|
+
},
|
|
801
|
+
{
|
|
802
|
+
Body: {
|
|
803
|
+
type: "object",
|
|
804
|
+
properties: { evil: { type: "string", pattern: "(\\w+\\s?)*" } },
|
|
805
|
+
},
|
|
806
|
+
},
|
|
807
|
+
),
|
|
808
|
+
);
|
|
809
|
+
expect(typecheck(files, "t")).toEqual([]);
|
|
810
|
+
});
|
|
811
|
+
});
|