netjes 0.0.2 → 0.0.4
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 +13 -11
- package/cli/agents.mjs +25 -2
- package/cli/commands.mjs +56 -6
- package/cli/help.mjs +7 -2
- package/cli/info.mjs +275 -33
- package/cli/routes.mjs +35 -8
- package/cli/scaffold.mjs +31 -4
- package/dist/actions.d.ts +7 -2
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +9 -3
- package/dist/actions.js.map +1 -1
- package/dist/client-entry.d.ts +1 -0
- package/dist/client-entry.d.ts.map +1 -1
- package/dist/client-entry.js +19 -2
- package/dist/client-entry.js.map +1 -1
- package/dist/deferred.d.ts +11 -0
- package/dist/deferred.d.ts.map +1 -1
- package/dist/deferred.js +15 -1
- package/dist/deferred.js.map +1 -1
- package/dist/document.d.ts.map +1 -1
- package/dist/document.js +10 -1
- package/dist/document.js.map +1 -1
- package/dist/errors.d.ts +7 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +11 -0
- package/dist/errors.js.map +1 -0
- package/dist/form.d.ts +5 -5
- package/dist/form.d.ts.map +1 -1
- package/dist/form.js +5 -2
- package/dist/form.js.map +1 -1
- package/dist/href.d.ts +31 -3
- package/dist/href.d.ts.map +1 -1
- package/dist/href.js +131 -10
- package/dist/href.js.map +1 -1
- package/dist/image.d.ts +3 -3
- package/dist/image.d.ts.map +1 -1
- package/dist/image.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/link.d.ts +15 -7
- package/dist/link.d.ts.map +1 -1
- package/dist/link.js +38 -7
- package/dist/link.js.map +1 -1
- package/dist/metadata.d.ts +8 -2
- package/dist/metadata.d.ts.map +1 -1
- package/dist/metadata.js +14 -5
- package/dist/metadata.js.map +1 -1
- package/dist/modules.d.ts +15 -1
- package/dist/modules.d.ts.map +1 -1
- package/dist/modules.js.map +1 -1
- package/dist/mounted.d.ts +55 -0
- package/dist/mounted.d.ts.map +1 -0
- package/dist/mounted.js +110 -0
- package/dist/mounted.js.map +1 -0
- package/dist/navigation-focus.d.ts +38 -0
- package/dist/navigation-focus.d.ts.map +1 -0
- package/dist/navigation-focus.js +71 -0
- package/dist/navigation-focus.js.map +1 -0
- package/dist/payload.d.ts +8 -0
- package/dist/payload.d.ts.map +1 -1
- package/dist/payload.js +28 -0
- package/dist/payload.js.map +1 -1
- package/dist/query.d.ts +5 -2
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +11 -8
- package/dist/query.js.map +1 -1
- package/dist/render-document.d.ts +74 -0
- package/dist/render-document.d.ts.map +1 -0
- package/dist/render-document.js +393 -0
- package/dist/render-document.js.map +1 -0
- package/dist/route-announcer.d.ts +18 -0
- package/dist/route-announcer.d.ts.map +1 -0
- package/dist/route-announcer.js +58 -0
- package/dist/route-announcer.js.map +1 -0
- package/dist/route-tree.d.ts +6 -2
- package/dist/route-tree.d.ts.map +1 -1
- package/dist/route-tree.js +17 -7
- package/dist/route-tree.js.map +1 -1
- package/dist/router.d.ts +28 -5
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +71 -26
- package/dist/router.js.map +1 -1
- package/dist/script.d.ts +28 -5
- package/dist/script.d.ts.map +1 -1
- package/dist/script.js +192 -21
- package/dist/script.js.map +1 -1
- package/dist/server-render.d.ts.map +1 -1
- package/dist/server-render.js +4 -1
- package/dist/server-render.js.map +1 -1
- package/dist/testing-vitest.d.ts +8 -0
- package/dist/testing-vitest.d.ts.map +1 -0
- package/dist/testing-vitest.js +26 -0
- package/dist/testing-vitest.js.map +1 -0
- package/dist/testing.d.ts +17 -1
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +18 -3
- package/dist/testing.js.map +1 -1
- package/dist/zod-jitless.d.ts +2 -0
- package/dist/zod-jitless.d.ts.map +1 -0
- package/dist/zod-jitless.js +4 -0
- package/dist/zod-jitless.js.map +1 -0
- package/package.json +1 -1
- package/sidecar/server.mjs +109 -15
- package/skills/netjes/SKILL.md +15 -7
- package/skills/netjes/references/caching-and-streaming.md +37 -4
- package/skills/netjes/references/data-and-actions.md +15 -7
- package/skills/netjes/references/deployment.md +7 -3
- package/skills/netjes/references/routing.md +32 -9
- package/skills/netjes/references/testing.md +23 -7
- package/src/actions.tsx +9 -3
- package/src/client-entry.tsx +20 -2
- package/src/deferred.ts +14 -1
- package/src/document.tsx +22 -3
- package/src/errors.ts +12 -0
- package/src/form.tsx +10 -7
- package/src/href.ts +137 -11
- package/src/image.tsx +3 -3
- package/src/index.ts +19 -1
- package/src/link.tsx +47 -13
- package/src/metadata.tsx +19 -6
- package/src/modules.ts +15 -2
- package/src/mounted.tsx +160 -0
- package/src/navigation-focus.ts +75 -0
- package/src/payload.ts +32 -0
- package/src/query.tsx +14 -8
- package/src/render-document.tsx +483 -0
- package/src/route-announcer.tsx +69 -0
- package/src/route-tree.tsx +17 -6
- package/src/router.tsx +116 -31
- package/src/script.tsx +214 -23
- package/src/server-render.tsx +4 -1
- package/src/testing-vitest.ts +37 -0
- package/src/testing.tsx +38 -5
- package/src/virtual.d.ts +8 -1
- package/src/zod-jitless.ts +15 -0
- package/template/.dockerignore +3 -0
- package/template/Dockerfile +11 -1
- package/template/README.md +11 -7
- package/template/_AGENTS.md +38 -9
- package/template/__NAME__.csproj +2 -0
- package/template/app/document.test.ts +29 -0
- package/template/app/error.tsx +3 -2
- package/template/app/layout.tsx +10 -1
- package/template/docker-compose.override.yml +4 -3
- package/template/docker-compose.prod.yml +2 -1
- package/template/e2e/home.spec.ts +32 -0
- package/template/generated/netjes.ts +43 -14
- package/template/package.json +4 -1
- package/template/playwright.config.ts +36 -0
- package/template/tests/__NAME__.Tests/AppFactory.cs +31 -0
- package/template/tests/__NAME__.Tests/HomePageTests.cs +55 -0
- package/template/tests/__NAME__.Tests/__NAME__.Tests.csproj +30 -0
- package/template/tsconfig.app.json +20 -0
- package/template/tsconfig.json +2 -17
- package/template/tsconfig.node.json +8 -0
- package/vite/packages.mjs +345 -0
- package/vite/plugin.d.ts +16 -1
- package/vite/plugin.mjs +224 -34
package/template/_AGENTS.md
CHANGED
|
@@ -12,11 +12,17 @@ How this app is built, for coding agents and the people working with them. It is
|
|
|
12
12
|
|
|
13
13
|
| Path | What |
|
|
14
14
|
|---|---|
|
|
15
|
-
| `app/` | Pages, as in the Next.js App Router: `page.tsx` is `/`, `about/page.tsx` is `/about`, `blog/[slug]/page.tsx` is `/blog/{slug}`. `layout.tsx` wraps the pages below it and stays mounted
|
|
15
|
+
| `app/` | Pages, as in the Next.js App Router: `page.tsx` is `/`, `about/page.tsx` is `/about`, `blog/[slug]/page.tsx` is `/blog/{slug}`. `layout.tsx` wraps the pages below it and stays mounted. Netjes renders `<html>`, `<head>` and `<body>` around the root layout, so no layout renders them: the root one returns what goes inside `<body>`. `loading.tsx`, `error.tsx`, `not-found.tsx`, `forbidden.tsx` and `unauthorized.tsx` are boundaries. `(group)` folders add no URL segment, `_private` folders are not routed. A mounted page package's pages, layouts and boundaries merge into these folders under its prefix, from `node_modules`. `document.test.ts` renders the home page's whole document, as the server sends it, with `renderDocument` from `netjes/testing`. |
|
|
16
16
|
| `Features/` | The C# side, one folder per feature (`namespace __NAME__.Features.Home`): page loaders (`MapPageLoader`), layout loaders (`MapLayoutLoader`), server actions (`MapAction`) and JSON endpoints, with records for what they take and return. Each feature has a `Map...` extension method that `Program.cs` calls. |
|
|
17
|
-
| `Program.cs` | Services and the pipeline: `AddNetjes`, `UseNetjes`, authentication (when you add it), `UseAntiforgery`, each feature's `Map...`, and `app.MapPages()` last. |
|
|
17
|
+
| `Program.cs` | Services and the pipeline: `AddNetjes`, `UseNetjes`, authentication (when you add it), `UseAntiforgery`, each page package's mount and each feature's `Map...`, and `app.MapPages()` last. |
|
|
18
18
|
| `components/` | React components, with their Vitest tests next to them (`*.test.tsx`). |
|
|
19
|
-
|
|
19
|
+
<!--netjes:if tests-->
|
|
20
|
+
| `tests/__NAME__.Tests/` | The C# tests: a new test of a loader or an action goes here. In the project `netjes new` writes, on xUnit v3, `AppFactory.cs` runs the real app in memory with `WebApplicationFactory`, with no Node and no client build, and `HomePageTests.cs` requests the home page and posts its form as the client router does. C# for the app does not go in the root folders `tests/` and `e2e/`, which `netjes new` leaves out of the app's build (`DefaultItemExcludes` in `__NAME__.csproj`). |
|
|
21
|
+
<!--netjes:end-->
|
|
22
|
+
<!--netjes:if e2e-->
|
|
23
|
+
| `e2e/` | Playwright tests, which `npm run test:e2e` runs. The `playwright.config.ts` that `netjes new` writes runs them against the app twice: in Development (the Vite dev server) and in Production (the build). |
|
|
24
|
+
<!--netjes:end-->
|
|
25
|
+
| `generated/netjes.ts` | Generated from the C# types and the page files: zod schemas for page and layout props (`pages`, `layouts`), `sharedProps`, typed `serverActions`, `api` schemas for JSON endpoints, `endpointUrl()` for the URLs of the app's endpoints, `route()` and the `Href` type, the pages of mounted page packages included. Never edit it. |
|
|
20
26
|
| `styles/global.css` | Tailwind CSS (`@import 'tailwindcss'`), imported by `app/layout.tsx`. |
|
|
21
27
|
| `public/` | Static files, served at the site root. `favicon.svg` is the Netjes mark, linked by `icons` in the root layout's `metadata`; replace it with the app's own. |
|
|
22
28
|
|
|
@@ -25,8 +31,14 @@ How this app is built, for coding agents and the people working with them. It is
|
|
|
25
31
|
```bash
|
|
26
32
|
npm run dev # dotnet watch + Vite HMR on http://localhost:5080; rewrites generated/netjes.ts on start
|
|
27
33
|
npm test # Vitest
|
|
28
|
-
npm run typecheck # strict TypeScript
|
|
34
|
+
npm run typecheck # strict TypeScript; in the configs netjes new writes, the app's code without Node's types
|
|
29
35
|
dotnet build # C#, warnings are errors
|
|
36
|
+
<!--netjes:if tests-->
|
|
37
|
+
dotnet test tests/__NAME__.Tests # the C# tests; a plain dotnet test here finds only the app's project and runs none
|
|
38
|
+
<!--netjes:end-->
|
|
39
|
+
<!--netjes:if e2e-->
|
|
40
|
+
npm run test:e2e # Playwright (run npx playwright install chromium once first)
|
|
41
|
+
<!--netjes:end-->
|
|
30
42
|
npx netjes generate # rewrite generated/netjes.ts after changing a C# type (--check: fail when stale)
|
|
31
43
|
npm run routes # the route table as ASP.NET Core sees it, with route warnings
|
|
32
44
|
npx netjes build # types, then the client and server bundles
|
|
@@ -39,7 +51,7 @@ dotnet publish -c Release # runs the build and ships dist/
|
|
|
39
51
|
2. Call its `Map...` method from `Program.cs`, before `app.MapPages()`.
|
|
40
52
|
3. Run `npx netjes generate` (or restart `npm run dev`).
|
|
41
53
|
4. Write `app/blog/[slug]/page.tsx`: `export default definePage(pages['/blog/[slug]'], function Post({ post }) { ... })`.
|
|
42
|
-
5. Test the component with Vitest and `createTestApp` from `netjes/testing`; run the checks below.
|
|
54
|
+
5. Test the loader in C# (in `tests/__NAME__.Tests`, where the app has it) and the component with Vitest and `createTestApp` from `netjes/testing`; run the checks below.
|
|
43
55
|
|
|
44
56
|
A page without a loader is a plain default export and gets empty props. Forms call server actions: `app.MapAction("posts.like", ...)` in C#, `useAction(serverActions['posts.like'])` and `<Form action={formAction}>` in React; they also work without JavaScript.
|
|
45
57
|
|
|
@@ -47,18 +59,35 @@ A page without a loader is a plain default export and gets empty props. Forms ca
|
|
|
47
59
|
|
|
48
60
|
- C# decides: data access, validation, authorization (`.RequireAuthorization()` on a loader, a layout loader or `MapPages()`; `Page.Forbidden()`), redirects and caching. React renders what the loader sends; hiding a button is not access control.
|
|
49
61
|
- Never edit `generated/netjes.ts`. Change the C# type and regenerate; commit the file with the change. CI runs `npx netjes generate --check`.
|
|
50
|
-
- zod at every boundary: `definePage` and `defineLayout` with the generated schemas, `useSearchParams(schema)` and `useParams(schema)` for the URL, `api['GET /api/...'].response.parse(...)` for JSON you fetch, `usePublicConfig(schema)` for public config, `Href` (or `isHref`) from the generated file for URLs that come from data. No `any`, no casts of server data.
|
|
62
|
+
- zod at every boundary: `definePage` and `defineLayout` with the generated schemas (extended, as in `pages['/x'].extend({ link: Href })`, only where no C# type fits), `useSearchParams(schema)` and `useParams(schema)` for the URL, `api['GET /api/...'].response.parse(...)` for JSON you fetch from `useHref(endpointUrl('GET /api/...', params))`, `usePublicConfig(schema)` for public config, `Href` (or `isHref`) from the generated file for URLs that come from data, such as a CMS. No `any`, no casts of server data.
|
|
63
|
+
- A link that C# sends to one of the app's pages is a `Href` in the C# record (`new Href("/about")`): it is generated as the `Href` schema, checked against the pages when the props are parsed, and goes to `<Link href>` as it is. C# checks only its shape, and a root-relative `Href` that no page serves fails the whole page, so a link to an endpoint or a file stays a `string`.
|
|
51
64
|
- Server state goes through TanStack Query (`useQuery`), not `useEffect` and `fetch`. Seed it from the loader with `.WithQuery(key, data)` so it is server-rendered; actions update it with `.Invalidate(key)` or `.WithQuery(key, data)`.
|
|
52
|
-
- Internal links are `<Link href={route('/blog/[slug]', { slug })}>`; a typo in an href is a type error.
|
|
65
|
+
- Internal links are `<Link href={route('/blog/[slug]', { slug })}>`; a typo in an href is a type error. `route(pattern, params, search, { hash })` builds the query from `search` and a `#fragment` from `hash`, the id of an element on the page; pass `undefined` as `search` for a link with a fragment and no query.
|
|
66
|
+
- After a client navigation to another page, the router starts the focus over at the top of the document, as a page load does, and announces the page to screen readers by its title. Keep the "Skip to content" link the first thing in `app/layout.tsx` that takes the focus, with the content in `<main id="content">`, and give each page a title of its own (`metadata`, or `PageMetadata` from C#): the router says nothing for a page whose title is the previous page's.
|
|
53
67
|
- Action inputs are records with `init` properties: `public sealed record NoteInput { [Required] public string? Text { get; init; } public bool Pinned { get; init; } }`. A field the post leaves out, such as an unchecked checkbox, keeps its default; form binding refuses (400) a post that lacks one of a positional record's constructor fields. DataAnnotations on them (`[Required]`, `[StringLength]`) are checked before the action runs and come back as field errors, with no setup, also for a post that has none of the input's fields. Return `ServerAction.Invalid(field, message)` for other errors the user can fix.
|
|
54
|
-
- `.CachePage(...)` only for pages that are the same for every visitor: their loaders and shared props must not depend on cookies or other per-visitor state.
|
|
55
|
-
- `
|
|
68
|
+
- `.CachePage(...)` only for pages that are the same for every visitor: their loaders and shared props must not depend on cookies or other per-visitor state. In the Development environment pages are never output-cached (each request renders them), so check caching on `npx netjes build && npx netjes start`, or in a `WebApplicationFactory` test in the `Testing` environment with `Netjes:Mode` set to `Development`, as the `AppFactory.cs` that `netjes new` writes runs. Each query string as sent is an entry of its own: name the keys a cached page reads with `.CacheQueryKeys("page")`, or `.CacheQueryKeys()` for none, so other query strings render without being stored (`.claude/skills/netjes/references/caching-and-streaming.md`).
|
|
69
|
+
- In the TypeScript configs `netjes new` writes, `tsconfig.app.json` checks the app's code without Node's types, so a component or page that reads `process` or imports `node:*` fails `npm run typecheck`. Code that runs only in Node goes in `scripts/`, which `tsconfig.node.json` checks with them; do not add `node` to the `types` of `tsconfig.app.json`.
|
|
70
|
+
- An `error.tsx` renders `error.message` only when `!error.withheld`, and shows `error.status ?? 500` if it shows a status.
|
|
71
|
+
- `Netjes:PublicConfig` is sent to the browser: never put secrets there. So is the route of each endpoint with an `endpointUrl` key, one mapped only in Development included, once any component calls `endpointUrl`: keep secrets out of routes, and authorize on the endpoint ([security](https://github.com/DennisVerhoeven/netjes/blob/main/docs/guides/security.md#errors-and-configuration)).
|
|
72
|
+
- Page packages (pages, layouts, loaders and actions from an npm and NuGet package pair) are mounted, never copied into `app/`: the package's extension in `Program.cs` before `app.MapPages()`, at a prefix in folder syntax (`app.MapHelpCenter("/help")`, `"/(marketing)/help"`), and the npm name in `netjes({ packages })` in `vite.config.ts`. Install both halves at one version. Its files merge into `app/` folder by folder, so an app page in its folder renders inside its layouts. An app page of the same shape as one of its pages stops the app: replace the package's page with `.Override("/help/contact")` on the mount, reading the props from the package's generated file. Conventions on the mount cover only the package's pages; protect the app's own pages under the prefix as usual. For a package that draws its own shell, keep `app/layout.tsx` to providers and the skip link, and put the site's chrome in `app/(site)/layout.tsx`. Run `npx netjes generate` after updating a package ([page packages](https://github.com/DennisVerhoeven/netjes/blob/main/docs/guides/page-packages.md)).
|
|
56
73
|
- Write the failing test first, then the change.
|
|
57
74
|
|
|
58
75
|
## Before you finish
|
|
59
76
|
|
|
77
|
+
<!--netjes:if tests-->
|
|
78
|
+
```bash
|
|
79
|
+
npm test && npm run typecheck && dotnet build && dotnet test tests/__NAME__.Tests && npx netjes generate --check
|
|
80
|
+
```
|
|
81
|
+
<!--netjes:else-->
|
|
60
82
|
```bash
|
|
61
83
|
npm test && npm run typecheck && dotnet build && npx netjes generate --check
|
|
62
84
|
```
|
|
63
85
|
|
|
86
|
+
No C# test project was at `tests/__NAME__.Tests/` when this file was written. If the app has C# tests elsewhere, run them too; [testing](https://github.com/DennisVerhoeven/netjes/blob/main/docs/guides/testing.md#c-tests-of-loaders-and-actions) says how to add a project there. After adding one, run `npx netjes agents --force`, which writes this file again with it, and `CLAUDE.md` and `.claude/skills/netjes/` too; keep your own additions to them with `git diff`.
|
|
87
|
+
<!--netjes:end-->
|
|
88
|
+
|
|
64
89
|
Then `npm run routes` should print no warnings, and the page should work in `npm run dev`, including with JavaScript turned off for forms.
|
|
90
|
+
<!--netjes:if e2e-->
|
|
91
|
+
|
|
92
|
+
For a change to a page or a form, also run `npm run test:e2e`.
|
|
93
|
+
<!--netjes:end-->
|
package/template/__NAME__.csproj
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
<Nullable>enable</Nullable>
|
|
6
6
|
<ImplicitUsings>enable</ImplicitUsings>
|
|
7
7
|
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
|
8
|
+
<!-- The SDK compiles everything below this file: the C# tests and the Playwright specs have builds of their own. -->
|
|
9
|
+
<DefaultItemExcludes>$(DefaultItemExcludes);tests/**;e2e/**</DefaultItemExcludes>
|
|
8
10
|
</PropertyGroup>
|
|
9
11
|
|
|
10
12
|
<!--NETJES_REFERENCES-->
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// @vitest-environment node
|
|
2
|
+
// The home page's whole document, as the server sends it to a crawler, from the props its C# loader returns
|
|
3
|
+
// (Features/Home/HomePage.cs). renderDocument renders without a DOM, as the server does, hence the node environment.
|
|
4
|
+
import { CSP_SCRIPT_HASHES } from 'netjes';
|
|
5
|
+
import { inlineScriptHashes, renderDocument } from 'netjes/testing';
|
|
6
|
+
import { describe, expect, it } from 'vitest';
|
|
7
|
+
|
|
8
|
+
const home = () =>
|
|
9
|
+
renderDocument('app/page.tsx', {
|
|
10
|
+
props: { message: 'Hello from C#', renderedAt: '2026-11-05T12:00:00Z' },
|
|
11
|
+
shared: { siteName: '__TITLE__' },
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
describe('the home page', () => {
|
|
15
|
+
it("is a document with the loader's heading, inside the root layout", async () => {
|
|
16
|
+
const html = await home();
|
|
17
|
+
|
|
18
|
+
expect(html.startsWith('<!DOCTYPE html><html lang="en">')).toBe(true);
|
|
19
|
+
expect(html).toMatch(/<h1[^>]*>Hello from C#<\/h1>/);
|
|
20
|
+
expect(html).toContain('<nav aria-label="Main"');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('has no inline script in its document but the one whose hash Netjes lists for a policy without a nonce', async () => {
|
|
24
|
+
// NetjesContentSecurityPolicy.ScriptHashes in C# holds the same hashes. The inline code of an afterInteractive or
|
|
25
|
+
// lazyOnload <Script> is added in the browser and is in no document: its hash goes in the policy, not here. That of
|
|
26
|
+
// a beforeInteractive <Script> is in the document: its hash goes in both.
|
|
27
|
+
expect(await inlineScriptHashes(await home())).toEqual(CSP_SCRIPT_HASHES.split(' '));
|
|
28
|
+
});
|
|
29
|
+
});
|
package/template/app/error.tsx
CHANGED
|
@@ -5,8 +5,9 @@ export default function ErrorPage({ error, reset }: ErrorPageProps) {
|
|
|
5
5
|
return (
|
|
6
6
|
<section role="alert" className="space-y-4">
|
|
7
7
|
<h1 className="text-3xl font-bold tracking-tight">Something went wrong</h1>
|
|
8
|
-
{/*
|
|
9
|
-
|
|
8
|
+
{/* Withheld: a stand-in for a message Netjes does not show, such as an exception's with error details off. A
|
|
9
|
+
message can take several lines: in development a syntax error comes with a code frame. */}
|
|
10
|
+
{!error.withheld && <p className="whitespace-pre-wrap wrap-anywhere text-slate-600">{error.message}</p>}
|
|
10
11
|
<button type="button" onClick={reset} className="cursor-pointer rounded-full bg-slate-900 px-4 py-2 font-medium text-white">
|
|
11
12
|
Try again
|
|
12
13
|
</button>
|
package/template/app/layout.tsx
CHANGED
|
@@ -14,6 +14,15 @@ export default function RootLayout({ children }: LayoutProps) {
|
|
|
14
14
|
const site = useSharedProps(sharedProps);
|
|
15
15
|
return (
|
|
16
16
|
<div className="mx-auto min-h-screen max-w-3xl px-6 py-10 font-sans text-slate-900">
|
|
17
|
+
{/* The first element Tab reaches, after a page load and after every client navigation, when the router starts
|
|
18
|
+
the focus over at the top of the document. It points to a place on this page, which needs no prefetch. */}
|
|
19
|
+
<Link
|
|
20
|
+
href="#content"
|
|
21
|
+
prefetch="none"
|
|
22
|
+
className="sr-only focus:not-sr-only focus:fixed focus:top-3 focus:left-3 focus:z-50 focus:rounded focus:bg-white focus:px-4 focus:py-2 focus:shadow"
|
|
23
|
+
>
|
|
24
|
+
Skip to content
|
|
25
|
+
</Link>
|
|
17
26
|
<header className="mb-12 flex items-baseline justify-between">
|
|
18
27
|
<Link href="/" className="text-xl font-semibold no-underline">
|
|
19
28
|
{site.siteName}
|
|
@@ -27,7 +36,7 @@ export default function RootLayout({ children }: LayoutProps) {
|
|
|
27
36
|
</Link>
|
|
28
37
|
</nav>
|
|
29
38
|
</header>
|
|
30
|
-
<main>{children}</main>
|
|
39
|
+
<main id="content">{children}</main>
|
|
31
40
|
</div>
|
|
32
41
|
);
|
|
33
42
|
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
# Development: dotnet watch for C#, the Vite dev server for pages (HMR on 5173). Open http://localhost:8080
|
|
2
|
-
# Both ports are bound to localhost only: dev error pages show stack traces and Vite serves the source.
|
|
1
|
+
# Development: dotnet watch for C#, the Vite dev server for pages (HMR on 5173). Open http://localhost:8080, or the port
|
|
2
|
+
# APP_PORT names. Both ports are bound to localhost only: dev error pages show stack traces and Vite serves the source.
|
|
3
3
|
services:
|
|
4
4
|
app:
|
|
5
5
|
build:
|
|
6
6
|
target: dev
|
|
7
7
|
ports:
|
|
8
|
-
|
|
8
|
+
# The host's port for the app: APP_PORT, from the shell or a .env file next to this file, or 8080. Vite keeps 5173.
|
|
9
|
+
- "127.0.0.1:${APP_PORT:-8080}:8080"
|
|
9
10
|
- "127.0.0.1:5173:5173"
|
|
10
11
|
volumes:
|
|
11
12
|
- .:/src
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { expect, test } from '@playwright/test';
|
|
2
|
+
|
|
3
|
+
// These tests pass with or without hydration: a link and a form work before the client router runs. A test that needs
|
|
4
|
+
// the client, such as one that clicks a button with an onClick handler, first waits for it after each full page load:
|
|
5
|
+
// await expect(page.locator('html')).toHaveAttribute('data-netjes-hydrated', '');
|
|
6
|
+
|
|
7
|
+
test('the home page shows the message from its C# loader', async ({ page }) => {
|
|
8
|
+
await page.goto('/');
|
|
9
|
+
|
|
10
|
+
await expect(page.getByRole('heading', { level: 1 })).toHaveText('Hello from C#');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test('the link on the home page leads to the about page', async ({ page }) => {
|
|
14
|
+
await page.goto('/');
|
|
15
|
+
|
|
16
|
+
await page.getByRole('link', { name: 'About this app' }).click();
|
|
17
|
+
|
|
18
|
+
await expect(page).toHaveURL('/about');
|
|
19
|
+
await expect(page.getByRole('heading', { level: 1 })).toHaveText('About');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test.describe('with JavaScript turned off', () => {
|
|
23
|
+
test.use({ javaScriptEnabled: false });
|
|
24
|
+
|
|
25
|
+
test('the form asks for the name it did not get', async ({ page }) => {
|
|
26
|
+
await page.goto('/');
|
|
27
|
+
|
|
28
|
+
await page.getByRole('button', { name: 'Say hello' }).click();
|
|
29
|
+
|
|
30
|
+
await expect(page.getByText('Tell us your name.')).toBeVisible();
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -1,8 +1,19 @@
|
|
|
1
1
|
// Generated by Netjes from the app's C# types and page files. Do not edit: the app rewrites it when it starts in
|
|
2
2
|
// Development, and `netjes generate` writes it for CI (`netjes generate --check` fails when it is out of date).
|
|
3
|
-
import { buildHref, isRouteHref, serverAction, type SearchParamsInput } from 'netjes';
|
|
3
|
+
import { buildEndpointUrl, buildHref, isRouteHref, serverAction, type SearchParamsInput } from 'netjes';
|
|
4
4
|
import { z } from 'zod';
|
|
5
5
|
|
|
6
|
+
/** Every page's URL pattern. */
|
|
7
|
+
export const routePatterns = ["/", "/about"] as const;
|
|
8
|
+
|
|
9
|
+
/** Whether a URL that comes from data (a CMS, shared props) is one of the app's pages or another site. */
|
|
10
|
+
export function isHref(value: string): value is Href {
|
|
11
|
+
return isRouteHref(value, routePatterns);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Checks such a URL at the boundary: `<Link href={Href.parse(item.href)}>`. The schemas below use it for each C# `Netjes.Href`. */
|
|
15
|
+
export const Href = z.custom<Href>((value) => typeof value === 'string' && isHref(value), 'Not a URL this app serves');
|
|
16
|
+
|
|
6
17
|
export const HomeProps = z.object({
|
|
7
18
|
message: z.string(),
|
|
8
19
|
renderedAt: z.iso.datetime({ offset: true, local: true }),
|
|
@@ -29,6 +40,29 @@ export const sharedProps = SiteProps;
|
|
|
29
40
|
/** JSON endpoints by method and route: what they accept and return. */
|
|
30
41
|
export const api = {} as const;
|
|
31
42
|
|
|
43
|
+
/**
|
|
44
|
+
* What the app's endpoints read from their URLs, by method and route as in `api`: the route values, then the
|
|
45
|
+
* query string values the handler binds. A JSON endpoint has a key for each of its methods except HEAD, and any
|
|
46
|
+
* other endpoint one for GET, if it answers GET.
|
|
47
|
+
*/
|
|
48
|
+
export type EndpointParams = Record<never, never>;
|
|
49
|
+
export type EndpointKey = keyof EndpointParams;
|
|
50
|
+
const endpointTemplates: Record<EndpointKey, string> = {};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The URL of one of the app's endpoints, with its values encoded: `endpointUrl('GET /api/posts/{slug}/likes/', { slug })`.
|
|
54
|
+
* The values the route does not name go in the query string, followed by the third argument. The URL has no base
|
|
55
|
+
* path: pass it through `useHref` for a `fetch` or an `<a href>`.
|
|
56
|
+
*/
|
|
57
|
+
export function endpointUrl<K extends EndpointKey>(
|
|
58
|
+
key: K,
|
|
59
|
+
...args: {} extends EndpointParams[K]
|
|
60
|
+
? [params?: EndpointParams[K], search?: SearchParamsInput]
|
|
61
|
+
: [params: EndpointParams[K], search?: SearchParamsInput]
|
|
62
|
+
): string {
|
|
63
|
+
return buildEndpointUrl(endpointTemplates[key], args[0] ?? {}, args[1], key);
|
|
64
|
+
}
|
|
65
|
+
|
|
32
66
|
/** Form fields of page actions (`MapPageAction`), by page pattern. */
|
|
33
67
|
export const forms = {} as const;
|
|
34
68
|
|
|
@@ -48,23 +82,18 @@ export type Route = '/' | '/about';
|
|
|
48
82
|
/** What `<Link href>` and `router.push` accept: the app's routes, with a query or fragment, or another site. */
|
|
49
83
|
export type Href = Route | `${Route}?${string}` | `${Route}#${string}` | `?${string}` | `#${string}` | `http://${string}` | `https://${string}` | `mailto:${string}` | `tel:${string}`;
|
|
50
84
|
|
|
51
|
-
/**
|
|
85
|
+
/**
|
|
86
|
+
* The URL of a page, with its parameters encoded: `route('/blog/[slug]', { slug })`. The third argument is the
|
|
87
|
+
* query, and `{ hash }` in the fourth adds a fragment: `route('/docs/[...path]', { path }, undefined, { hash: 'keys' })`.
|
|
88
|
+
*/
|
|
52
89
|
export function route<P extends RoutePattern>(
|
|
53
90
|
pattern: P,
|
|
54
|
-
...args: {} extends RouteParams[P]
|
|
91
|
+
...args: {} extends RouteParams[P]
|
|
92
|
+
? [params?: RouteParams[P], search?: SearchParamsInput, options?: { hash?: string | undefined }]
|
|
93
|
+
: [params: RouteParams[P], search?: SearchParamsInput, options?: { hash?: string | undefined }]
|
|
55
94
|
): Href {
|
|
56
|
-
return buildHref(pattern, args[0] ?? {}, args[1]) as Href;
|
|
95
|
+
return buildHref(pattern, args[0] ?? {}, args[1], args[2]) as Href;
|
|
57
96
|
}
|
|
58
|
-
/** Every page's URL pattern. */
|
|
59
|
-
export const routePatterns = ["/", "/about"] as const;
|
|
60
|
-
|
|
61
|
-
/** Whether a URL that comes from data (a CMS, shared props) is one of the app's pages or another site. */
|
|
62
|
-
export function isHref(value: string): value is Href {
|
|
63
|
-
return isRouteHref(value, routePatterns);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/** Checks such a URL at the boundary: `<Link href={Href.parse(item.href)}>`. */
|
|
67
|
-
export const Href = z.custom<Href>((value) => typeof value === 'string' && isHref(value), 'Not a URL this app serves');
|
|
68
97
|
|
|
69
98
|
declare module 'netjes' {
|
|
70
99
|
interface Register {
|
package/template/package.json
CHANGED
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
"dev": "netjes dev",
|
|
10
10
|
"build": "vite build && vite build --ssr",
|
|
11
11
|
"test": "vitest run",
|
|
12
|
-
"
|
|
12
|
+
"test:e2e": "dotnet build && playwright test",
|
|
13
|
+
"typecheck": "tsc -p tsconfig.app.json --noEmit && tsc -p tsconfig.node.json --noEmit",
|
|
13
14
|
"generate": "netjes generate",
|
|
14
15
|
"routes": "netjes routes"
|
|
15
16
|
},
|
|
@@ -21,10 +22,12 @@
|
|
|
21
22
|
"zod": "^4.6.7"
|
|
22
23
|
},
|
|
23
24
|
"devDependencies": {
|
|
25
|
+
"@playwright/test": "^1.64.1",
|
|
24
26
|
"@tailwindcss/vite": "^4.3.3",
|
|
25
27
|
"@testing-library/dom": "^10.4.2",
|
|
26
28
|
"@testing-library/react": "^16.3.3",
|
|
27
29
|
"@testing-library/user-event": "^14.6.8",
|
|
30
|
+
"@types/node": "^22.20.5",
|
|
28
31
|
"@types/react": "^19.3.0",
|
|
29
32
|
"@types/react-dom": "^19.3.0",
|
|
30
33
|
"jsdom": "^30.1.3",
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { defineConfig, devices } from '@playwright/test';
|
|
2
|
+
|
|
3
|
+
// Each project runs the app as its own ASP.NET Core process: one in Development (the Vite dev server) and one in
|
|
4
|
+
// Production (the `npm run build` output). `npm run test:e2e` builds the .NET app first; install the browser once with
|
|
5
|
+
// `npx playwright install chromium`. E2E_DEV_PORT, E2E_PROD_PORT and E2E_VITE_PORT move the ports, so that several runs
|
|
6
|
+
// can share a machine. With CI set, Playwright always starts fresh servers; otherwise it reuses one that is listening.
|
|
7
|
+
const DEV_URL = `http://127.0.0.1:${process.env['E2E_DEV_PORT'] ?? '5090'}`;
|
|
8
|
+
const PROD_URL = `http://127.0.0.1:${process.env['E2E_PROD_PORT'] ?? '5091'}`;
|
|
9
|
+
const VITE_PORT = process.env['E2E_VITE_PORT'] ?? '5190';
|
|
10
|
+
|
|
11
|
+
export default defineConfig({
|
|
12
|
+
testDir: './e2e',
|
|
13
|
+
forbidOnly: !!process.env['CI'],
|
|
14
|
+
retries: process.env['CI'] ? 1 : 0,
|
|
15
|
+
use: { ...devices['Desktop Chrome'], trace: 'retain-on-failure' },
|
|
16
|
+
projects: [
|
|
17
|
+
{ name: 'development', use: { baseURL: DEV_URL } },
|
|
18
|
+
{ name: 'production', use: { baseURL: PROD_URL } },
|
|
19
|
+
],
|
|
20
|
+
webServer: [
|
|
21
|
+
{
|
|
22
|
+
command: 'dotnet run --no-build --no-launch-profile',
|
|
23
|
+
url: `${DEV_URL}/healthz`,
|
|
24
|
+
env: { ASPNETCORE_ENVIRONMENT: 'Development', ASPNETCORE_URLS: DEV_URL, Netjes__ViteDevPort: VITE_PORT },
|
|
25
|
+
reuseExistingServer: !process.env['CI'],
|
|
26
|
+
timeout: 120_000,
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
command: 'npm run build && dotnet run --no-build --no-launch-profile',
|
|
30
|
+
url: `${PROD_URL}/healthz`,
|
|
31
|
+
env: { ASPNETCORE_ENVIRONMENT: 'Production', ASPNETCORE_URLS: PROD_URL },
|
|
32
|
+
reuseExistingServer: !process.env['CI'],
|
|
33
|
+
timeout: 180_000,
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
using Microsoft.AspNetCore.Hosting;
|
|
2
|
+
using Microsoft.AspNetCore.Mvc.Testing;
|
|
3
|
+
using Microsoft.Extensions.DependencyInjection;
|
|
4
|
+
using Netjes.Rendering;
|
|
5
|
+
|
|
6
|
+
namespace __NAME__.Tests;
|
|
7
|
+
|
|
8
|
+
/// <summary>Runs the real app in memory, with its pipeline and services, and without a client build or Node.</summary>
|
|
9
|
+
public sealed class AppFactory : WebApplicationFactory<Program>
|
|
10
|
+
{
|
|
11
|
+
protected override void ConfigureWebHost(IWebHostBuilder builder)
|
|
12
|
+
{
|
|
13
|
+
// Not Development: there Netjes output-caches no page, so a test of CachePage would see every request render.
|
|
14
|
+
builder.UseEnvironment("Testing");
|
|
15
|
+
// Development mode reads the routes from app/ on disk; nothing is rendered, so no Vite or Node is needed.
|
|
16
|
+
builder.UseSetting("Netjes:Mode", "Development");
|
|
17
|
+
builder.ConfigureServices(services =>
|
|
18
|
+
{
|
|
19
|
+
services.AddSingleton<ISsrRenderer, FixedHtml>();
|
|
20
|
+
// Development mode would rewrite generated/netjes.ts: a test run leaves it as it is, for `netjes generate --check`.
|
|
21
|
+
services.Configure<NetjesOptions>(options => options.TypesFile = null);
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/// <summary>Stands in for the Node process: a document request gets this HTML. A payload request goes through no renderer.</summary>
|
|
26
|
+
private sealed class FixedHtml : ISsrRenderer
|
|
27
|
+
{
|
|
28
|
+
public Task<string> RenderAsync(string payloadJson, CancellationToken cancellationToken) =>
|
|
29
|
+
Task.FromResult("<!DOCTYPE html><html><body>rendered</body></html>");
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
using System.Net;
|
|
2
|
+
using System.Text.Json.Nodes;
|
|
3
|
+
|
|
4
|
+
namespace __NAME__.Tests;
|
|
5
|
+
|
|
6
|
+
/// <summary>The home page's loader and server action (Features/Home/HomePage.cs), requested as the client router does.</summary>
|
|
7
|
+
public sealed class HomePageTests(AppFactory app) : IClassFixture<AppFactory>
|
|
8
|
+
{
|
|
9
|
+
[Fact]
|
|
10
|
+
public async ValueTask TheHomePage_HasTheLoadersMessage()
|
|
11
|
+
{
|
|
12
|
+
var client = app.CreateClient();
|
|
13
|
+
client.DefaultRequestHeaders.Add(NetjesHeaders.Data, "1");
|
|
14
|
+
|
|
15
|
+
var response = await client.GetAsync("/", TestContext.Current.CancellationToken);
|
|
16
|
+
var payload = JsonNode.Parse(await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken))!;
|
|
17
|
+
|
|
18
|
+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
|
19
|
+
Assert.Equal("Hello from C#", (string?)payload["props"]!["message"]);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
[Fact]
|
|
23
|
+
public async ValueTask Greeting_WithoutAName_IsAFieldError()
|
|
24
|
+
{
|
|
25
|
+
var (status, result) = await Greet("");
|
|
26
|
+
|
|
27
|
+
Assert.Equal(HttpStatusCode.UnprocessableEntity, status);
|
|
28
|
+
Assert.Equal("Tell us your name.", (string?)result["errors"]!["name"]![0]);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
[Fact]
|
|
32
|
+
public async ValueTask Greeting_WithAName_SaysHello()
|
|
33
|
+
{
|
|
34
|
+
var (status, result) = await Greet("Ada");
|
|
35
|
+
|
|
36
|
+
Assert.Equal(HttpStatusCode.OK, status);
|
|
37
|
+
Assert.Equal("Hello, Ada!", (string?)result["result"]!["text"]);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/// <summary>Posts the form with an antiforgery token: the header takes the token, the client's cookies its cookie.</summary>
|
|
41
|
+
private async Task<(HttpStatusCode, JsonNode)> Greet(string name)
|
|
42
|
+
{
|
|
43
|
+
var client = app.CreateClient();
|
|
44
|
+
var token = JsonNode.Parse(await client.GetStringAsync("/_netjes/antiforgery", TestContext.Current.CancellationToken))!;
|
|
45
|
+
client.DefaultRequestHeaders.Add(NetjesHeaders.Data, "1");
|
|
46
|
+
client.DefaultRequestHeaders.Add((string)token["headerName"]!, (string)token["requestToken"]!);
|
|
47
|
+
|
|
48
|
+
var response = await client.PostAsync(
|
|
49
|
+
"/_netjes/actions/home.greet",
|
|
50
|
+
new FormUrlEncodedContent([new("name", name)]),
|
|
51
|
+
TestContext.Current.CancellationToken);
|
|
52
|
+
|
|
53
|
+
return (response.StatusCode, JsonNode.Parse(await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken))!);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<Project Sdk="Microsoft.NET.Sdk">
|
|
2
|
+
|
|
3
|
+
<PropertyGroup>
|
|
4
|
+
<TargetFramework>net10.0</TargetFramework>
|
|
5
|
+
<Nullable>enable</Nullable>
|
|
6
|
+
<ImplicitUsings>enable</ImplicitUsings>
|
|
7
|
+
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
|
8
|
+
<IsPackable>false</IsPackable>
|
|
9
|
+
<OutputType>Exe</OutputType>
|
|
10
|
+
</PropertyGroup>
|
|
11
|
+
|
|
12
|
+
<!-- xunit.v3.mtp-off is xUnit v3 for dotnet test's default runner, VSTest, which works in any folder. In a repository
|
|
13
|
+
whose global.json selects Microsoft.Testing.Platform as the test runner, reference xunit.v3 in its place. -->
|
|
14
|
+
<ItemGroup>
|
|
15
|
+
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.14" />
|
|
16
|
+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.11.0" />
|
|
17
|
+
<PackageReference Include="xunit.v3.mtp-off" Version="4.0.2" />
|
|
18
|
+
<PackageReference Include="xunit.runner.visualstudio" Version="4.0.1" />
|
|
19
|
+
</ItemGroup>
|
|
20
|
+
|
|
21
|
+
<ItemGroup>
|
|
22
|
+
<ProjectReference Include="../../__NAME__.csproj" />
|
|
23
|
+
</ItemGroup>
|
|
24
|
+
|
|
25
|
+
<ItemGroup>
|
|
26
|
+
<Using Include="Netjes" />
|
|
27
|
+
<Using Include="Xunit" />
|
|
28
|
+
</ItemGroup>
|
|
29
|
+
|
|
30
|
+
</Project>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"lib": ["ES2023", "DOM", "DOM.Iterable"],
|
|
7
|
+
"types": ["vite/client"],
|
|
8
|
+
"jsx": "react-jsx",
|
|
9
|
+
"strict": true,
|
|
10
|
+
"noUncheckedIndexedAccess": true,
|
|
11
|
+
"noImplicitOverride": true,
|
|
12
|
+
"isolatedModules": true,
|
|
13
|
+
"verbatimModuleSyntax": true,
|
|
14
|
+
"allowImportingTsExtensions": true,
|
|
15
|
+
"skipLibCheck": true,
|
|
16
|
+
"noEmit": true
|
|
17
|
+
},
|
|
18
|
+
"include": ["**/*.ts", "**/*.tsx"],
|
|
19
|
+
"exclude": ["node_modules", "dist", "**/bin", "**/obj", "publish", "out", "e2e", "scripts", "vite.config.ts", "playwright.config.ts"]
|
|
20
|
+
}
|
package/template/tsconfig.json
CHANGED
|
@@ -1,19 +1,4 @@
|
|
|
1
1
|
{
|
|
2
|
-
"
|
|
3
|
-
|
|
4
|
-
"module": "ESNext",
|
|
5
|
-
"moduleResolution": "bundler",
|
|
6
|
-
"lib": ["ES2023", "DOM", "DOM.Iterable"],
|
|
7
|
-
"types": ["vite/client"],
|
|
8
|
-
"jsx": "react-jsx",
|
|
9
|
-
"strict": true,
|
|
10
|
-
"noUncheckedIndexedAccess": true,
|
|
11
|
-
"noImplicitOverride": true,
|
|
12
|
-
"isolatedModules": true,
|
|
13
|
-
"verbatimModuleSyntax": true,
|
|
14
|
-
"allowImportingTsExtensions": true,
|
|
15
|
-
"skipLibCheck": true,
|
|
16
|
-
"noEmit": true
|
|
17
|
-
},
|
|
18
|
-
"include": ["app", "components", "generated", "vite.config.ts"]
|
|
2
|
+
"files": [],
|
|
3
|
+
"references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }]
|
|
19
4
|
}
|