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
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { DefaultOptions } from '@tanstack/react-query';
|
|
2
|
+
declare module 'vitest' {
|
|
3
|
+
interface ProvidedContext {
|
|
4
|
+
/** The app's `netjes({ query: { defaultOptions } })`, which netjes() provides to its tests. */
|
|
5
|
+
'netjes:query': DefaultOptions;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
//# sourceMappingURL=testing-vitest.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"testing-vitest.d.ts","sourceRoot":"","sources":["../src/testing-vitest.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAG5D,OAAO,QAAQ,QAAQ,CAAC;IACtB,UAAU,eAAe;QACvB,+FAA+F;QAC/F,cAAc,EAAE,cAAc,CAAC;KAChC;CACF"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { inject, vi } from 'vitest';
|
|
2
|
+
// The app's query defaults, for createTestApp. They wait under a global symbol rather than in a module: the app's
|
|
3
|
+
// imports of netjes/testing need not share module instances with this file, which Vitest loads by its path.
|
|
4
|
+
const appQueryDefaults = inject('netjes:query');
|
|
5
|
+
if (typeof appQueryDefaults === 'object' && appQueryDefaults !== null) {
|
|
6
|
+
globalThis[Symbol.for('netjes.testing.queryDefaults')] = appQueryDefaults;
|
|
7
|
+
}
|
|
8
|
+
// Testing Library advances fake timers only through Jest's global: without one, waitFor, findBy and every user-event
|
|
9
|
+
// call wait on a faked setTimeout that nothing advances, so they never return. While Vitest's timers are fake, `jest`
|
|
10
|
+
// is a bridge to them; otherwise it is undefined, so code that looks for Jest finds none. A jest global that was
|
|
11
|
+
// already there is left alone, and one a test assigns replaces the bridge.
|
|
12
|
+
if (!('jest' in globalThis)) {
|
|
13
|
+
const bridge = {
|
|
14
|
+
advanceTimersByTime(ms) {
|
|
15
|
+
vi.advanceTimersByTime(ms);
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
Object.defineProperty(globalThis, 'jest', {
|
|
19
|
+
configurable: true,
|
|
20
|
+
get: () => (vi.isFakeTimers() ? bridge : undefined),
|
|
21
|
+
set(value) {
|
|
22
|
+
Object.defineProperty(globalThis, 'jest', { configurable: true, enumerable: true, writable: true, value });
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=testing-vitest.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"testing-vitest.js","sourceRoot":"","sources":["../src/testing-vitest.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AASpC,kHAAkH;AAClH,4GAA4G;AAC5G,MAAM,gBAAgB,GAAY,MAAM,CAAC,cAAc,CAAC,CAAC;AACzD,IAAI,OAAO,gBAAgB,KAAK,QAAQ,IAAI,gBAAgB,KAAK,IAAI,EAAE,CAAC;IACrE,UAAsC,CAAC,MAAM,CAAC,GAAG,CAAC,8BAA8B,CAAC,CAAC,GAAG,gBAAgB,CAAC;AACzG,CAAC;AAED,qHAAqH;AACrH,sHAAsH;AACtH,iHAAiH;AACjH,2EAA2E;AAC3E,IAAI,CAAC,CAAC,MAAM,IAAI,UAAU,CAAC,EAAE,CAAC;IAC5B,MAAM,MAAM,GAAG;QACb,mBAAmB,CAAC,EAAU;YAC5B,EAAE,CAAC,mBAAmB,CAAC,EAAE,CAAC,CAAC;QAC7B,CAAC;KACF,CAAC;IACF,MAAM,CAAC,cAAc,CAAC,UAAU,EAAE,MAAM,EAAE;QACxC,YAAY,EAAE,IAAI;QAClB,GAAG,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,CAAC,YAAY,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC;QACnD,GAAG,CAAC,KAAc;YAChB,MAAM,CAAC,cAAc,CAAC,UAAU,EAAE,MAAM,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;QAC7G,CAAC;KACF,CAAC,CAAC;AACL,CAAC"}
|
package/dist/testing.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { type ReactNode } from 'react';
|
|
|
3
3
|
import { type ActionState } from './actions.tsx';
|
|
4
4
|
import type { AntiforgeryToken, Params } from './payload.ts';
|
|
5
5
|
import { type NavigationState } from './router.tsx';
|
|
6
|
+
export { type RenderDocumentOptions, inlineScriptHashes, renderDocument } from './render-document.tsx';
|
|
6
7
|
/** What the fake app of `createTestApp` reports to the components under test. */
|
|
7
8
|
export type TestAppOptions = {
|
|
8
9
|
/** The current URL. Default `/`. */
|
|
@@ -18,12 +19,21 @@ export type TestAppOptions = {
|
|
|
18
19
|
params?: Params;
|
|
19
20
|
/** What the server would send as `SharedProps`. */
|
|
20
21
|
shared?: unknown;
|
|
22
|
+
/**
|
|
23
|
+
* Each mounted page package's URL for the page, by npm name, as the server sends it: `{ '@acme/help-center': '/help' }`,
|
|
24
|
+
* without the base path, or `null` for a prefix whose value the page has none for. The links, forms and router hooks
|
|
25
|
+
* of the package's generated file go below it.
|
|
26
|
+
*/
|
|
27
|
+
mounts?: Record<string, string | null>;
|
|
21
28
|
/** Seeded into the QueryClient the way `PageResult.WithQuery` seeds it, as of now. */
|
|
22
29
|
queries?: ReadonlyArray<{
|
|
23
30
|
key: readonly unknown[];
|
|
24
31
|
data: unknown;
|
|
25
32
|
}>;
|
|
26
|
-
/**
|
|
33
|
+
/**
|
|
34
|
+
* Like `netjes({ query: { defaultOptions } })`, merged over the harness's own: failed queries and mutations are tried
|
|
35
|
+
* as often as in the app, with no wait between the tries, and no query's data is garbage-collected.
|
|
36
|
+
*/
|
|
27
37
|
defaultOptions?: DefaultOptions;
|
|
28
38
|
/** What `useNavigation()` returns. Default idle. */
|
|
29
39
|
navigation?: NavigationState;
|
|
@@ -45,6 +55,12 @@ export type TestActionAnswer = ActionState<unknown> & {
|
|
|
45
55
|
invalidate?: ReadonlyArray<readonly unknown[]>;
|
|
46
56
|
/** After a failure: the submitted fields the state's `values` keep, as the server names them (the action's declared, non-private fields). Default: all. */
|
|
47
57
|
fields?: readonly string[];
|
|
58
|
+
/**
|
|
59
|
+
* After a failure other than a 422: the message the server would send, which the `ServerActionError` then carries, as
|
|
60
|
+
* the server does for a 4xx (`Forbidden`) and for a 5xx with error details on. Without one, the call fails as a 5xx
|
|
61
|
+
* does with error details off: the error's message is Netjes's own and `withheld` is true.
|
|
62
|
+
*/
|
|
63
|
+
message?: string;
|
|
48
64
|
};
|
|
49
65
|
/** One thing the component under test did: a navigation, a router call, a form post or a server action call. */
|
|
50
66
|
export type TestNavigation = {
|
package/dist/testing.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../src/testing.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../src/testing.tsx"],"names":[],"mappings":"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"}
|
package/dist/testing.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
// Test harness for components and pages that use Netjes: `render(<Page />, { wrapper: createTestApp().Wrapper })`.
|
|
3
3
|
// The router is a fake: it records what the component asked for and moves to the new URL, but loads nothing.
|
|
4
|
+
// renderDocument renders a page's whole document instead, as the server would send it.
|
|
4
5
|
import { QueryClient, QueryClientProvider, hydrate } from '@tanstack/react-query';
|
|
5
6
|
import { useLayoutEffect, useMemo, useState } from 'react';
|
|
6
7
|
import { setActionClient } from './actions.js';
|
|
7
8
|
import { PublicConfigProvider } from './public-config.js';
|
|
8
9
|
import { dehydratedStateOf, mergeOptions } from './query.js';
|
|
9
10
|
import { RouterContexts } from './router.js';
|
|
11
|
+
export { inlineScriptHashes, renderDocument } from './render-document.js';
|
|
10
12
|
/**
|
|
11
13
|
* Registers the stand-in in the commit, and only then: a render React throws away (`renderToString`, children that
|
|
12
14
|
* threw) leaves calls made outside any Wrapper failing at once. React runs the effects of earlier siblings first, and
|
|
@@ -18,15 +20,26 @@ function RegisterActionClient({ client }) {
|
|
|
18
20
|
return null;
|
|
19
21
|
}
|
|
20
22
|
const TEST_TOKEN = { formFieldName: '__RequestVerificationToken', headerName: 'RequestVerificationToken', requestToken: 'test-token' };
|
|
23
|
+
/**
|
|
24
|
+
* The harness's own query defaults, over the app's: a failed query or mutation is tried as often as in the app, but
|
|
25
|
+
* without the wait between the tries (TanStack Query's 1, 2 and 4 seconds would outlast a test), and data stays in the
|
|
26
|
+
* client for the whole test.
|
|
27
|
+
*/
|
|
28
|
+
const HARNESS_DEFAULTS = { queries: { retryDelay: 0, gcTime: Number.POSITIVE_INFINITY }, mutations: { retryDelay: 0 } };
|
|
29
|
+
/** The app's `netjes({ query })` defaults, where Netjes's Vitest setup file keeps them (src/testing-vitest.ts). */
|
|
30
|
+
const APP_QUERY_DEFAULTS = Symbol.for('netjes.testing.queryDefaults');
|
|
31
|
+
function appQueryDefaults() {
|
|
32
|
+
return globalThis[APP_QUERY_DEFAULTS];
|
|
33
|
+
}
|
|
21
34
|
function dataOf(body) {
|
|
22
35
|
return body.contentType === null ? body.body : new URLSearchParams(body.body);
|
|
23
36
|
}
|
|
24
37
|
/** Creates a fake app around the components under test: pass its `Wrapper` to Testing Library's `render`. The router loads nothing; it records what was asked of it. */
|
|
25
38
|
export function createTestApp(options = {}) {
|
|
26
39
|
const navigations = [];
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
|
|
40
|
+
// The Netjes defaults, then the app's, then the harness's, then the test's.
|
|
41
|
+
const defaultOptions = mergeOptions(appQueryDefaults(), HARNESS_DEFAULTS, options.defaultOptions);
|
|
42
|
+
const queryClient = options.queryClient ?? new QueryClient({ defaultOptions });
|
|
30
43
|
const now = Date.now();
|
|
31
44
|
const seeded = dehydratedStateOf(options.queries?.map((query) => ({ ...query, updatedAt: now })));
|
|
32
45
|
if (seeded)
|
|
@@ -52,6 +65,7 @@ export function createTestApp(options = {}) {
|
|
|
52
65
|
status: answer.status,
|
|
53
66
|
errors: answer.errors,
|
|
54
67
|
...(answer.fields ? { fields: [...answer.fields] } : {}),
|
|
68
|
+
...(answer.message !== undefined ? { message: answer.message } : {}),
|
|
55
69
|
revalidate: false,
|
|
56
70
|
};
|
|
57
71
|
},
|
|
@@ -98,6 +112,7 @@ export function createTestApp(options = {}) {
|
|
|
98
112
|
...(antiforgery ? { antiforgery } : {}),
|
|
99
113
|
...(options.draftMode ? { draftMode: true } : {}),
|
|
100
114
|
...(options.locale ? { locale: options.locale } : {}),
|
|
115
|
+
...(options.mounts ? { mounts: options.mounts } : {}),
|
|
101
116
|
}), [url]);
|
|
102
117
|
return (_jsx(QueryClientProvider, { client: queryClient, children: _jsx(PublicConfigProvider, { value: options.publicConfig ?? {}, children: _jsxs(RouterContexts, { router: router, payload: payload, url: url, navigation: options.navigation ?? { state: 'idle' }, children: [_jsx(RegisterActionClient, { client: actionClient }), children] }) }) }));
|
|
103
118
|
}
|
package/dist/testing.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"testing.js","sourceRoot":"","sources":["../src/testing.tsx"],"names":[],"mappings":";AAAA,mHAAmH;AACnH,6GAA6G;AAC7G,OAAO,EAAuB,WAAW,EAAE,mBAAmB,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAC;AACvG,OAAO,EAAkB,eAAe,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAC3E,OAAO,EAAuC,eAAe,EAAE,MAAM,cAAe,CAAC;AAErF,OAAO,EAAE,oBAAoB,EAAE,MAAM,oBAAqB,CAAC;AAC3D,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,YAAa,CAAC;AAC9D,OAAO,EAA6C,cAAc,EAAmB,MAAM,aAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"testing.js","sourceRoot":"","sources":["../src/testing.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"zod-jitless.d.ts","sourceRoot":"","sources":["../src/zod-jitless.ts"],"names":[],"mappings":"AAcA,OAAO,EAAE,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"zod-jitless.js","sourceRoot":"","sources":["../src/zod-jitless.ts"],"names":[],"mappings":"AAWA,CAAE,UAAoC,CAAC,kBAAkB,KAAK,EAAE,CAAC,CAAC,OAAO,GAAG,IAAI,CAAC;AAEjF,gEAAgE;AAChE,OAAO,EAAE,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "netjes",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"description": "Build React apps and sites on ASP.NET Core: the Next.js App Router conventions, C# loaders and server actions, streaming SSR and types from C# to the browser",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
package/sidecar/server.mjs
CHANGED
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
// production: renders with the prebuilt dist/server/server-entry.js.
|
|
5
5
|
// Protocol: POST /render with the HTML-safe page payload JSON as body -> 200 text/html, streamed from the moment the
|
|
6
6
|
// shell is ready (or once everything is, with X-Netjes-Wait-All: 1), or 500 {"error":{message,stack}} in plain text
|
|
7
|
-
// (no terminal colours) if the shell fails.
|
|
8
|
-
//
|
|
7
|
+
// (no terminal colours) if the shell fails. GET /health -> 200 at once, without a body read. In production the parent
|
|
8
|
+
// pings a process that has answered no render for a while; any answer will do, and a process that sends nothing back
|
|
9
|
+
// within the render timeout (at least 5 s) is killed and restarted. Every request must carry the X-Netjes-Token the
|
|
10
|
+
// parent passed in NETJES_TOKEN. Renders are aborted when the caller disconnects, when the shell is not ready after
|
|
9
11
|
// NETJES_RENDER_TIMEOUT_MS, or when the whole stream takes longer than NETJES_STREAM_TIMEOUT_MS. Exits when stdin
|
|
10
12
|
// closes.
|
|
11
13
|
// Startup: prints NETJES_READY {"port","viteOrigin"} on stdout once it listens. When the Vite dev server does not start,
|
|
@@ -14,6 +16,7 @@
|
|
|
14
16
|
// that is not installed (message in plain text; code is the error's when it has one, such as ERR_MODULE_NOT_FOUND, or
|
|
15
17
|
// MODULE_NOT_FOUND from a CommonJS config).
|
|
16
18
|
import http from 'node:http';
|
|
19
|
+
import path from 'node:path';
|
|
17
20
|
import { pipeline } from 'node:stream/promises';
|
|
18
21
|
import { pathToFileURL } from 'node:url';
|
|
19
22
|
|
|
@@ -39,6 +42,10 @@ const TERMINAL_ESCAPES = /(?:\u001b\[|\u009b)[0-?]*[ -/]*[@-~]|\u001b[\]PX^_][^\
|
|
|
39
42
|
const plainText = (text) => text.replace(TERMINAL_ESCAPES, '');
|
|
40
43
|
|
|
41
44
|
/** @typedef {{ signal: AbortSignal, waitForAll?: boolean, deferred?: (id: string) => PromiseLike<unknown> }} RenderOptions */
|
|
45
|
+
/**
|
|
46
|
+
* The netjes plugin's development API (vite/plugin.mjs).
|
|
47
|
+
* @typedef {{ rescanPages(): void, pageUrl(id: string): string | null, unlistedPackage(id: string): string | null }} NetjesDevApi
|
|
48
|
+
*/
|
|
42
49
|
/** @type {(data: string, options: RenderOptions) => Promise<string>} */
|
|
43
50
|
let render;
|
|
44
51
|
/** @type {((data: string, options: RenderOptions) => Promise<import('node:stream').Readable>) | undefined} */
|
|
@@ -92,8 +99,21 @@ if (mode === 'development') {
|
|
|
92
99
|
}
|
|
93
100
|
await viteFailed(error);
|
|
94
101
|
}
|
|
95
|
-
/**
|
|
96
|
-
|
|
102
|
+
const netjes = /** @type {NetjesDevApi | undefined} */ (dev.config.plugins.find((plugin) => plugin.name === 'netjes')?.api);
|
|
103
|
+
/** @param {unknown} error @returns {error is Error & { file: string }} */
|
|
104
|
+
const isMissingModule = (error) => error instanceof Error && error.name === 'MissingModuleError' && 'file' in error && typeof error.file === 'string';
|
|
105
|
+
/**
|
|
106
|
+
* Says what to do when the missing file is of an installed page package that the Vite config does not list.
|
|
107
|
+
* @param {Error & { file: string }} error
|
|
108
|
+
*/
|
|
109
|
+
const withPackageHint = (error) => {
|
|
110
|
+
const name = netjes?.unlistedPackage(error.file);
|
|
111
|
+
if (!name) return error;
|
|
112
|
+
const message = `${error.message}: add '${name}' to netjes({ packages }) in ${path.basename(dev.config.configFile ?? 'vite.config.ts')}`;
|
|
113
|
+
error.stack = error.stack?.replace(error.message, message);
|
|
114
|
+
error.message = message;
|
|
115
|
+
return error;
|
|
116
|
+
};
|
|
97
117
|
/**
|
|
98
118
|
* Runs a render with the server entry. .NET routes a page file as soon as its own watcher sees it, but the page
|
|
99
119
|
* module map changes only when Vite's watcher reports a page file, which for this one can be much later (a file it
|
|
@@ -106,17 +126,21 @@ if (mode === 'development') {
|
|
|
106
126
|
try {
|
|
107
127
|
return await run(await dev.ssrLoadModule('netjes/server-entry'));
|
|
108
128
|
} catch (error) {
|
|
109
|
-
if (!netjes || !(error
|
|
129
|
+
if (!netjes || !isMissingModule(error)) throw error;
|
|
110
130
|
netjes.rescanPages();
|
|
111
|
-
|
|
131
|
+
try {
|
|
132
|
+
return await run(await dev.ssrLoadModule('netjes/server-entry'));
|
|
133
|
+
} catch (again) {
|
|
134
|
+
throw isMissingModule(again) ? withPackageHint(again) : again;
|
|
135
|
+
}
|
|
112
136
|
}
|
|
113
137
|
};
|
|
114
138
|
render = async (data, options) => {
|
|
115
|
-
const styled = await withDevStyles(dev, data);
|
|
139
|
+
const styled = await withDevStyles(dev, data, netjes);
|
|
116
140
|
return withServerEntry((entry) => entry.render(styled, options));
|
|
117
141
|
};
|
|
118
142
|
renderToStream = async (data, options) => {
|
|
119
|
-
const styled = await withDevStyles(dev, data);
|
|
143
|
+
const styled = await withDevStyles(dev, data, netjes);
|
|
120
144
|
return withServerEntry((entry) => entry.renderToStream(styled, options));
|
|
121
145
|
};
|
|
122
146
|
} else {
|
|
@@ -127,11 +151,17 @@ if (mode === 'development') {
|
|
|
127
151
|
|
|
128
152
|
/**
|
|
129
153
|
* Inlines the CSS imported by the page and its segment files, so the server-rendered page is styled before the client
|
|
130
|
-
* script runs. Vite's client adopts these <style data-vite-dev-id> tags and keeps them updated through HMR
|
|
154
|
+
* script runs. Vite's client adopts these <style data-vite-dev-id> tags and keeps them updated through HMR: each CSS
|
|
155
|
+
* module sets its tag's text to its own as it loads, before hydration. The files are found in the SSR environment,
|
|
156
|
+
* which rendered the page, and their text comes from the client environment's build, which that module carries
|
|
157
|
+
* (clientCss): React then hydrates the text the server rendered. A page package's files are loaded by the URLs of
|
|
158
|
+
* their real paths, which the plugin knows; a file of a package the plugin does not list is left to the render, which
|
|
159
|
+
* says what to do.
|
|
131
160
|
* @param {import('vite').ViteDevServer} server
|
|
132
161
|
* @param {string} data
|
|
162
|
+
* @param {NetjesDevApi | undefined} netjes
|
|
133
163
|
*/
|
|
134
|
-
async function withDevStyles(server, data) {
|
|
164
|
+
async function withDevStyles(server, data, netjes) {
|
|
135
165
|
const payload = JSON.parse(data);
|
|
136
166
|
/** @type {Array<Record<string, unknown>>} */
|
|
137
167
|
const segments = payload.segments ?? [];
|
|
@@ -149,22 +179,83 @@ async function withDevStyles(server, data) {
|
|
|
149
179
|
if (!node?.id || seen.has(node.id)) return;
|
|
150
180
|
seen.add(node.id);
|
|
151
181
|
if (node.file && CSS_FILE.test(node.file) && !node.id.includes('?')) {
|
|
152
|
-
|
|
153
|
-
styles.set(node.file, String(css.default));
|
|
182
|
+
styles.set(node.file, await clientCss(server, node));
|
|
154
183
|
return;
|
|
155
184
|
}
|
|
156
185
|
for (const imported of node.importedModules) await collect(imported);
|
|
157
186
|
}
|
|
158
187
|
|
|
159
188
|
for (const file of files) {
|
|
160
|
-
|
|
161
|
-
|
|
189
|
+
const url = netjes ? netjes.pageUrl(file) : `/${file}`;
|
|
190
|
+
if (url === null) continue;
|
|
191
|
+
await server.ssrLoadModule(url);
|
|
192
|
+
await collect(await environment.moduleGraph.getModuleByUrl(url));
|
|
162
193
|
}
|
|
163
194
|
if (styles.size === 0) return data;
|
|
164
195
|
payload.document.devStyles = [...styles].map(([id, css]) => ({ id, css }));
|
|
165
196
|
return JSON.stringify(payload).replace(/[<>&\u2028\u2029]/g, (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, '0')}`);
|
|
166
197
|
}
|
|
167
198
|
|
|
199
|
+
/** How a CSS module in the client environment declares the text it sets: this, then the text as a JSON string. */
|
|
200
|
+
const CLIENT_CSS_LINE = 'const __vite__css = ';
|
|
201
|
+
/** Whether a module without that line, or without a JSON string on it, has been reported: once per process. */
|
|
202
|
+
let clientCssMissing = false;
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* A CSS file's text as Vite's client sets it in the page: the string that the file's module in the client environment
|
|
206
|
+
* declares and passes to updateStyle. The browser imports that module and gets it from the same cache, until an edit
|
|
207
|
+
* invalidates it; a module that does not accept its own updates, and that no browser has loaded yet, is built again
|
|
208
|
+
* for the browser (below). The SSR environment's `?inline` build is a second build of the file, which a plugin with
|
|
209
|
+
* state of its own in each environment, as Tailwind CSS has, can make differently. A module without that line, or with
|
|
210
|
+
* something other than a JSON string after it, from a Vite or a plugin that writes it otherwise, gets the `?inline`
|
|
211
|
+
* build all the same, and a warning.
|
|
212
|
+
* @param {import('vite').ViteDevServer} server
|
|
213
|
+
* @param {import('vite').EnvironmentModuleNode} node
|
|
214
|
+
*/
|
|
215
|
+
async function clientCss(server, node) {
|
|
216
|
+
const client = server.environments.client;
|
|
217
|
+
const built = await client.transformRequest(node.url);
|
|
218
|
+
const module = await client.moduleGraph.getModuleByUrl(node.url);
|
|
219
|
+
if (module && module.importers.size === 0 && module.isSelfAccepting === false) {
|
|
220
|
+
// Nothing in the client environment imports the module, so no browser has loaded a page with it, and the module
|
|
221
|
+
// does not accept its own updates, as a CSS Modules file's does not. Vite would take an edit to the file for one
|
|
222
|
+
// that no page can apply, and reload every open page, where without this read the module would not be there and
|
|
223
|
+
// the edit would reach no browser. So that it still reaches none, the module counts as not yet analyzed, where Vite
|
|
224
|
+
// stops an update, and its build is dropped: Vite builds it again for the browser that loads the page, and learns
|
|
225
|
+
// from that build how the module takes updates.
|
|
226
|
+
client.moduleGraph.invalidateModule(module);
|
|
227
|
+
module.isSelfAccepting = undefined;
|
|
228
|
+
}
|
|
229
|
+
// By lines, not with a regular expression: JSON.stringify leaves U+2028 and U+2029 in a string, and `.` stops at both.
|
|
230
|
+
const line = built?.code.split('\n').find((text) => text.startsWith(CLIENT_CSS_LINE));
|
|
231
|
+
const css = line === undefined ? undefined : jsonString(line.slice(CLIENT_CSS_LINE.length));
|
|
232
|
+
if (css !== undefined) return css;
|
|
233
|
+
if (!clientCssMissing) {
|
|
234
|
+
clientCssMissing = true;
|
|
235
|
+
const unread = line === undefined ? 'has no line that starts with' : 'has no JSON string after';
|
|
236
|
+
console.warn(
|
|
237
|
+
`netjes: Vite's client module for ${node.url} ${unread} "${CLIENT_CSS_LINE}", so development pages inline the ` +
|
|
238
|
+
"SSR build of their CSS: it can differ from the browser's build, which Vite's client puts in its place once " +
|
|
239
|
+
"the page's scripts run. Logged once, for the first such file.",
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
const inline = await server.ssrLoadModule(`${node.url}?inline`);
|
|
243
|
+
return String(inline.default);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* @param {string} json
|
|
248
|
+
* @returns {string | undefined} The string `json` holds, or undefined when it holds no string.
|
|
249
|
+
*/
|
|
250
|
+
function jsonString(json) {
|
|
251
|
+
try {
|
|
252
|
+
const value = JSON.parse(json);
|
|
253
|
+
return typeof value === 'string' ? value : undefined;
|
|
254
|
+
} catch {
|
|
255
|
+
return undefined;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
168
259
|
/**
|
|
169
260
|
* Page.Defer values per render, as .NET posts them to /resolve. Values may arrive before the render asks for them;
|
|
170
261
|
* entries nobody claims are dropped after the stream timeout.
|
|
@@ -240,7 +331,10 @@ const server = http.createServer(async (req, res) => {
|
|
|
240
331
|
const { renderId, id, value, error } = JSON.parse(await readBody(req));
|
|
241
332
|
if (typeof renderId !== 'string' || typeof id !== 'string') throw new Error('renderId and id are required');
|
|
242
333
|
const pending = pendingValue(renderId, id);
|
|
243
|
-
|
|
334
|
+
// An empty error is a failure whose message .NET withheld (error details are off): the render gets a stand-in
|
|
335
|
+
// marked as one, as the browser does (src/errors.ts), and hands the browser an empty error again.
|
|
336
|
+
if (error === '') pending.reject(Object.assign(new Error('Something went wrong'), { withheld: true }));
|
|
337
|
+
else if (typeof error === 'string') pending.reject(new Error(error));
|
|
244
338
|
else pending.resolve(value);
|
|
245
339
|
res.writeHead(204).end();
|
|
246
340
|
} catch (error) {
|
package/skills/netjes/SKILL.md
CHANGED
|
@@ -12,14 +12,19 @@ Netjes renders React pages from ASP.NET Core. C# receives every request and owns
|
|
|
12
12
|
- `app/**/page.tsx`: a page. `app/blog/[slug]/page.tsx` serves `/blog/{slug}`. Layouts, boundaries and folder rules: [references/routing.md](references/routing.md).
|
|
13
13
|
- `Features/<Feature>/*.cs`: the C# side of a feature, mapped by an extension method that `Program.cs` calls before `app.MapPages()`. Loaders and actions: [references/data-and-actions.md](references/data-and-actions.md).
|
|
14
14
|
- `generated/netjes.ts`: zod schemas and typed references generated from the C# types and the page files. Read it to see what a page receives; never edit it.
|
|
15
|
+
- Page packages: pages, layouts and loaders from an npm and NuGet package pair, mounted at a prefix by the package's extension in `Program.cs` (`app.MapHelpCenter("/help")`, before `app.MapPages()`) and listed in `netjes({ packages })` in `vite.config.ts`. Their files stay in `node_modules` and merge into `app/` folder by folder; never copy them into `app/`. Mounting, replacing a page and the root layout: [references/routing.md](references/routing.md#page-packages).
|
|
15
16
|
- `components/`: React components and their tests. Testing: [references/testing.md](references/testing.md).
|
|
17
|
+
- `tests/<App>.Tests/`: the C# tests (xUnit v3 and `WebApplicationFactory`), where the app has them, as an app made with `netjes new` does.
|
|
18
|
+
- `e2e/`: the Playwright specs, which `npm run test:e2e` runs.
|
|
19
|
+
- The root folders `tests/` and `e2e/` are outside the app's build (`DefaultItemExcludes` in a new app's project file), so the app's own C# does not go there.
|
|
20
|
+
- `scripts/`: code that runs only in Node. In the TypeScript configs `netjes new` writes, `tsconfig.app.json` checks the app's code without Node's types, so a component that reads `process` or imports `node:*` fails `npm run typecheck`; `tsconfig.node.json` checks `scripts/`, `e2e/`, `vite.config.ts` and `playwright.config.ts` with them. Do not add `node` to the `types` of `tsconfig.app.json`.
|
|
16
21
|
|
|
17
22
|
## The loop for any change
|
|
18
23
|
|
|
19
|
-
1. Write the failing test first: a Vitest component test, or a C# test for loader and action logic.
|
|
20
|
-
2. Change the C# record, loader or action, then run `npx netjes generate` so `generated/netjes.ts` matches (a running `npm run dev` rewrites it when it restarts).
|
|
21
|
-
3. Use the generated names in React: `pages['/pattern']`, `layouts['app/folder']`, `serverActions['name']`, `api['GET /api/...']`, `route('/pattern', params)`.
|
|
22
|
-
4. Check: `npm test`, `npm run typecheck`, `dotnet build` (warnings are errors), `npx netjes generate --check`, and `npm run routes` for route warnings.
|
|
24
|
+
1. Write the failing test first: a Vitest component test, or a C# test for loader and action logic (in `tests/<App>.Tests`, where a new app has its test project).
|
|
25
|
+
2. Change the C# record, loader or action, then run `npx netjes generate` so `generated/netjes.ts` matches (a running `npm run dev` rewrites it when it restarts). Run it after updating a page package too: the app's file routes the package's pages.
|
|
26
|
+
3. Use the generated names in React: `pages['/pattern']`, `layouts['app/folder']`, `serverActions['name']`, `api['GET /api/...']`, `route('/pattern', params)`, `endpointUrl('GET /api/...', params)`.
|
|
27
|
+
4. Check: `npm test`, `npm run typecheck`, `dotnet build` (warnings are errors), `npx netjes generate --check`, and `npm run routes` for route warnings. Where the app has them, which an app made with `netjes new` does: `dotnet test tests/<App>.Tests` for the C# tests, and `npm run test:e2e` for a change to a page or a form.
|
|
23
28
|
|
|
24
29
|
## A page with data, end to end
|
|
25
30
|
|
|
@@ -65,11 +70,12 @@ export default definePage(pages['/blog/[slug]'], function Post({ title, body })
|
|
|
65
70
|
|
|
66
71
|
- Decisions live in C#: data access, validation, authorization (`.RequireAuthorization()`, `Page.Forbidden()`), redirects, caching. React renders what it is sent.
|
|
67
72
|
- Loaders and actions return Netjes results (`Page.Props(...)`, `Page.NotFound()`, `ServerAction.Ok(...)`), so their types reach `generated/netjes.ts`. Use records for everything that crosses to the browser.
|
|
68
|
-
- zod at every boundary: `definePage`/`defineLayout` with the generated schemas, `useSearchParams(schema)` and `useParams(schema)` (schemas at module level), `api[...].response.parse(...)` for fetched JSON, `usePublicConfig(schema)`. No `any`, no casts of server data.
|
|
73
|
+
- zod at every boundary: `definePage`/`defineLayout` with the generated schemas, `useSearchParams(schema)` and `useParams(schema)` (schemas at module level), `api[...].response.parse(...)` for fetched JSON (from `useHref(endpointUrl(...))`, never a hand-built URL), `usePublicConfig(schema)`, `Href.parse(...)` or `isHref(...)` for URLs from a CMS. A link C# sends to one of the app's pages is a `Href` in the record (`new Href("/about")`), generated as the `Href` schema, so it reaches `<Link href>` checked; a link to an endpoint or a file stays a `string`. No `any`, no casts of server data.
|
|
69
74
|
- Server state goes through TanStack Query, seeded from the loader with `.WithQuery(key, data)`; no `useEffect` fetching.
|
|
70
75
|
- Internal links use `<Link>` and `route()`; forms use `<Form>` with `useAction`, and must work without JavaScript.
|
|
76
|
+
- Give each page a title of its own (`metadata`, or `PageMetadata` from C#), and keep the "Skip to content" link the first thing in the root layout, `app/layout.tsx`, that takes the focus, also when the root layout is kept bare and the site's chrome is in a group layout such as `app/(site)/layout.tsx`: after a client navigation the router announces the page by its title and starts the focus over at the top ([references/routing.md](references/routing.md)).
|
|
71
77
|
- Never edit `generated/netjes.ts`; regenerate it and commit it with the change.
|
|
72
|
-
- Anything in `Netjes:PublicConfig` or shared props is sent to the browser.
|
|
78
|
+
- Anything in `Netjes:PublicConfig` or shared props is sent to the browser. 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.
|
|
73
79
|
- Caching, streaming and static export have sharp edges: read [references/caching-and-streaming.md](references/caching-and-streaming.md) before using `.CachePage`, `Page.Defer` or `netjes export`.
|
|
74
80
|
- Shipping: [references/deployment.md](references/deployment.md).
|
|
75
81
|
|
|
@@ -79,6 +85,8 @@ export default definePage(pages['/blog/[slug]'], function Post({ title, body })
|
|
|
79
85
|
|---|---|
|
|
80
86
|
| `npm run dev` | `dotnet watch` with Vite HMR on http://localhost:5080 |
|
|
81
87
|
| `npm test`, `npm run typecheck` | Vitest; strict TypeScript |
|
|
88
|
+
| `dotnet test tests/<App>.Tests` | The C# tests, in an app that has that project; no Node and no client build |
|
|
89
|
+
| `npm run test:e2e` | Playwright, in an app that has that script; `npx playwright install chromium` once first |
|
|
82
90
|
| `npx netjes generate [--check]` | Rewrite `generated/netjes.ts`; `--check` fails when it is stale |
|
|
83
91
|
| `npm run routes` (`netjes routes [--json]`) | The route table and route warnings, as ASP.NET Core sees them |
|
|
84
92
|
| `npx netjes build` | Types, then the client and server bundles |
|
|
@@ -88,6 +96,6 @@ export default definePage(pages['/blog/[slug]'], function Post({ title, body })
|
|
|
88
96
|
|
|
89
97
|
## More
|
|
90
98
|
|
|
91
|
-
The guides go further than these references: [routing](https://github.com/DennisVerhoeven/netjes/blob/main/docs/guides/routing.md), [data loading](https://github.com/DennisVerhoeven/netjes/blob/main/docs/guides/data-loading.md), [actions and forms](https://github.com/DennisVerhoeven/netjes/blob/main/docs/guides/actions-and-forms.md), [
|
|
99
|
+
The guides go further than these references: [routing](https://github.com/DennisVerhoeven/netjes/blob/main/docs/guides/routing.md), [data loading](https://github.com/DennisVerhoeven/netjes/blob/main/docs/guides/data-loading.md), [actions and forms](https://github.com/DennisVerhoeven/netjes/blob/main/docs/guides/actions-and-forms.md), [page packages](https://github.com/DennisVerhoeven/netjes/blob/main/docs/guides/page-packages.md), [rendering and streaming](https://github.com/DennisVerhoeven/netjes/blob/main/docs/guides/rendering-and-streaming.md), [caching](https://github.com/DennisVerhoeven/netjes/blob/main/docs/guides/caching.md), [images](https://github.com/DennisVerhoeven/netjes/blob/main/docs/guides/images.md), [metadata](https://github.com/DennisVerhoeven/netjes/blob/main/docs/guides/metadata.md), [security](https://github.com/DennisVerhoeven/netjes/blob/main/docs/guides/security.md), [testing](https://github.com/DennisVerhoeven/netjes/blob/main/docs/guides/testing.md) and [deployment](https://github.com/DennisVerhoeven/netjes/blob/main/docs/guides/deployment.md), with the [React API](https://github.com/DennisVerhoeven/netjes/blob/main/docs/reference/react-api.md), the [C# API](https://github.com/DennisVerhoeven/netjes/blob/main/docs/reference/csharp-api.md) and the [options](https://github.com/DennisVerhoeven/netjes/blob/main/docs/reference/options.md) as reference.
|
|
92
100
|
|
|
93
101
|
The same docs as plain text: https://raw.githubusercontent.com/DennisVerhoeven/netjes/main/llms.txt (an index) and https://raw.githubusercontent.com/DennisVerhoeven/netjes/main/llms-full.txt (everything in one file). They follow the main branch; the installed version's sources and types are in `node_modules/netjes/`.
|
|
@@ -17,15 +17,43 @@ app.MapPageLoader("/blog/[slug]", (string slug, PostStore posts) => ...)
|
|
|
17
17
|
|
|
18
18
|
- A stored page is served to every visitor, shared props included. Cache only pages that are the same for everyone: their loaders and shared props must not depend on cookies or other per-visitor state (Netjes warns once per page when a cached render reads `Request.Cookies`). Signed-in users and requests with an `Authorization` header bypass the cache.
|
|
19
19
|
- Evict from an action with `ServerAction.Ok().Revalidate("posts")` or `.RevalidatePath("/blog/hello")`, and from anywhere else (a webhook, a background job) with `IPageCache.RevalidateTagAsync(tag)` / `RevalidatePathAsync(path)`.
|
|
20
|
+
- Each query string as sent is an entry of its own, so `?utm_source=...` and a made-up `?&&` each store another copy of the page, and `SetVaryByQuery` cannot narrow that. `.CacheQueryKeys("page")` names the keys a page reads, and `.CacheQueryKeys()` says it reads none: a request with another key, an empty segment (`?page=2&`) or a segment without a name renders each time and is never stored; the page rendered for it goes out `no-store, private` unless the page sets its own `Cache-Control`, and keeps its URL as sent. Do not leave out a key that a loader or `useSearchParams()` reads: those requests still work but are not cached, and nothing warns. Weigh it on a page with many visits through tagged links, which then renders for each of them. It does nothing on an endpoint that is neither a page nor under `CachePage`, so it can stay on a group that mixes pages and APIs.
|
|
20
21
|
- Cached pages carry no antiforgery token: forms on them fetch one first, so they need JavaScript.
|
|
21
|
-
- A Content-Security-Policy is off unless the app sets one. A document with a CSP nonce is never stored, so cached pages under a policy need a hash-based one (`ContentSecurityPolicy` without `{nonce}`), and under such a policy a page must not stream.
|
|
22
|
+
- A Content-Security-Policy is off unless the app sets one. A document with a CSP nonce is never stored, so cached pages under a policy need a hash-based one (`ContentSecurityPolicy` without `{nonce}`), and under such a policy a page must not stream. Build it from `NetjesContentSecurityPolicy.ScriptHashes`, the hashes of Netjes's inline scripts, never from a pasted hash, which a Netjes upgrade can make stale: `$"script-src 'self' {NetjesContentSecurityPolicy.ScriptHashes}; object-src 'none'; base-uri 'self'"`. Keep `'self'` for the built app's module scripts, and leave out `'strict-dynamic'`, which needs a nonce. In the Development environment the module scripts come from the Vite dev server, so add its origin there, and only there, from a variable:
|
|
23
|
+
|
|
24
|
+
```csharp
|
|
25
|
+
builder.Services.AddNetjes(options =>
|
|
26
|
+
{
|
|
27
|
+
var devServer = builder.Environment.IsDevelopment() ? $" http://localhost:{options.ViteDevPort}" : "";
|
|
28
|
+
options.ContentSecurityPolicy =
|
|
29
|
+
$"script-src 'self'{devServer} {NetjesContentSecurityPolicy.ScriptHashes}; object-src 'none'; base-uri 'self'";
|
|
30
|
+
});
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
When browsers reach the dev server at another address, the one set in `NETJES_VITE_ORIGIN`, list that origin instead. In Development a policy also needs these, or set it outside Development only:
|
|
34
|
+
|
|
35
|
+
- With `connect-src`, or `default-src` without `connect-src`: Vite's HMR socket, `ws://` (`wss://` for an https origin) and the dev server's host and port, such as `ws://localhost:{options.ViteDevPort}`, which neither `'self'` nor a nonce covers.
|
|
36
|
+
- With `style-src-elem` or `style-src`, or `default-src` without them: the document's nonce or `'unsafe-inline'`, for the CSS that Development inlines.
|
|
37
|
+
- With `img-src` or `font-src`, or `default-src` without them: the dev server's origin, for the images and fonts Vite serves.
|
|
38
|
+
|
|
39
|
+
Neither the nonce-based nor the hash-based policy needs `'unsafe-eval'`: zod parses without compiled code in the browser, from zod 4.4.0 on. Under a hash-based policy, give forms server actions through `useAction` and `<Form action={formAction}>`: any other function action, and any function on `<SubmitButton formAction>`, makes React add an inline script whose hash follows the React version and is not in `ScriptHashes`. In JavaScript, such as a static host's headers for exported pages, the same value is `CSP_SCRIPT_HASHES` from `netjes`. A nonce-based policy that the app writes itself takes its nonce from `HttpContext.GetCspNonce()`, the one Netjes puts on the document. Netjes logs a warning, once per policy, when the policy on a document refuses Netjes's bootstrap or module scripts or, in Development, the dev server. It does not check the inline code of a `<Script>`, so list that hash yourself ([routing.md](routing.md)).
|
|
22
40
|
- Keys include the host and the full URL, case included. In production pin `AllowedHosts` and set the output cache's `SizeLimit`.
|
|
23
41
|
- Pages the output cache does not store go out `Cache-Control: no-store, private`, for visitors and signed-in users alike, unless the page sets its own header.
|
|
42
|
+
- In the Development environment pages are never output-cached: each request renders the page, nothing is stored, and an output-cached page goes out `no-store, private` unless it sets its own `Cache-Control`, so `UseOutputCache()` can stay in every environment, with no Development guard. Cache slow or billed upstream data in the service that loads it (`IMemoryCache`, `HybridCache`). Try caching on the built app (`npx netjes build && npx netjes start`); a `WebApplicationFactory` test of it runs in the `Testing` environment with `Netjes:Mode=Development`.
|
|
24
43
|
- Draft pages and draft redirects (a loader's `Page.Redirect` in draft mode), the client-rendered fallback, the page for a failed form post, action answers and antiforgery tokens always go out `no-store`, and without the headers a CDN reads ahead of `Cache-Control` (`CDN-Cache-Control`, any other `*-Cache-Control`, `Surrogate-Control`, `Edge-Control`, `X-Accel-Expires`), whatever the page set.
|
|
25
44
|
|
|
45
|
+
## Endpoints that are not pages
|
|
46
|
+
|
|
47
|
+
JSON APIs, text files and webhooks are not pages, unless `CachePage` is put on them or on their group. The page rules do not apply to their cache entries.
|
|
48
|
+
|
|
49
|
+
- Netjes adds no `Cache-Control` to their answers, including the 400 that minimal API binding answers, and none under `CachePage` either (the page rules decide entries, not headers): the handler's own goes out as set, and the output cache sends the headers set before the body is written again with every stored answer, with `Age`, and adds none. Netjes's own endpoints: `/_netjes/image` (`public, max-age=` the image `CacheDuration` with an image or a 304, none with an error), `MapSitemap` (`public, max-age=` its `cacheFor`), action answers and `/_netjes/antiforgery` (`no-store`); `MapRobotsTxt` and `MapLocaleRedirect` send none.
|
|
50
|
+
- ASP.NET Core's keys apply. The path in any case shares one entry, route values included (`/codes/Ab` and `/codes/aB`): `AddOutputCache(o => o.UseCaseSensitivePaths = true)` gives each spelling its own entry, app-wide, and `.CacheOutput(p => p.With(c => c.HttpContext.Request.Path.Value == "/llms.txt"))` stores that one spelling only. Every query key varies the entry: `SetVaryByQuery([])` keeps one entry whatever the query (there is no call without arguments), `SetVaryByQuery("page")` varies on the keys it lists. Both are safe only when the answer does not depend on the keys they leave out and its body does not repeat the URL.
|
|
51
|
+
- Only the answer to a `GET` or `HEAD` without an `Authorization` header or a signed-in user is stored, when it is a 200 that sets no cookie. `RevalidatePath` never evicts these entries: give the policy a tag (`.CacheOutput(p => p.Tag("docs"))`) and use `.Revalidate("docs")` or `IPageCache.RevalidateTagAsync("docs")`.
|
|
52
|
+
- They stay output-cached in the Development environment, where only pages are not. Draft mode bypasses them, the image endpoint apart.
|
|
53
|
+
|
|
26
54
|
## TanStack Query
|
|
27
55
|
|
|
28
|
-
Netjes creates one `QueryClient` per server render and per page load. `.WithQuery(key, data)` on a loader result seeds it, so `useQuery` with that key has data on the first render and fetches nothing until the data is stale (default `staleTime` one minute). Data the browser fetched more recently is kept. After an action, `.Invalidate(key)` refetches and `.WithQuery(key, data)` sets the new value. Configure the client with `netjes({ query: { defaultOptions } })` in `vite.config.ts`.
|
|
56
|
+
Netjes creates one `QueryClient` per server render and per page load. `.WithQuery(key, data)` on a loader result seeds it, so `useQuery` with that key has data on the first render and fetches nothing until the data is stale (default `staleTime` one minute). Data the browser fetched more recently is kept. After an action, `.Invalidate(key)` refetches and `.WithQuery(key, data)` sets the new value. A fresh query is not fetched again on a mount or a window focus, whatever `Cache-Control` its endpoint sent; the `fetch` of a refetch can be answered by the browser's HTTP cache, and `staleTime: 0` leaves a query to HTTP caching. Configure the client with `netjes({ query: { defaultOptions } })` in `vite.config.ts`.
|
|
29
57
|
|
|
30
58
|
## Streaming slow data
|
|
31
59
|
|
|
@@ -52,14 +80,19 @@ function ActivityFeed({ activity }: { activity: PromiseLike<ActivityItem[]> }) {
|
|
|
52
80
|
- A deferred task runs alongside the rest of the request: give it its own `DbContext`, never the request's, and pass `http.RequestAborted`. Either create one from `IDbContextFactory<T>`, as above, or use the overload that runs the work in its own DI scope, started once the page's payload is written: `Page.Defer(services => services.GetRequiredService<AppDb>().ActivityAsync(id, http.RequestAborted))`.
|
|
53
81
|
- Without JavaScript, a fallback that is still pending stays. Crawlers (`CrawlerUserAgents`), apps with `StreamDocuments = false` and `netjes export` get the complete document.
|
|
54
82
|
- A boundary that is ready when the shell goes out is written in place, so it shows without JavaScript too.
|
|
83
|
+
- A value that fails rejects, so `use()` throws to the nearest `error.tsx`: with error details off its message is a stand-in marked `withheld`, and a cancelled task fails with "The value was cancelled".
|
|
55
84
|
|
|
56
85
|
## Static export
|
|
57
86
|
|
|
58
|
-
`npx netjes export [dir]` builds, runs the app once in Production and writes every page as static HTML (default `out/`). Dynamic pages need `.StaticParams(...)` (`.StaticParams<PostStore>(s => s.All.Select(p => new { slug = p.Slug }))`, arrays for catch-all parameters); pages that need a server take `.ExcludeFromExport()`. The export fails before writing anything for a dynamic page without static params, a page with a page action, or a page that requires authorization. Exported pages navigate with full page loads and carry no antiforgery token.
|
|
87
|
+
`npx netjes export [dir]` builds, runs the app once in Production and writes every page as static HTML (default `out/`). `dir` must be inside the app: `netjes export` refuses any other folder before it builds. Dynamic pages need `.StaticParams(...)` (`.StaticParams<PostStore>(s => s.All.Select(p => new { slug = p.Slug }))`, arrays for catch-all parameters); pages that need a server take `.ExcludeFromExport()`. The export fails before writing anything for a dynamic page without static params, a page with a page action, or a page that requires authorization. Exported pages navigate with full page loads and carry no antiforgery token.
|
|
88
|
+
|
|
89
|
+
It also writes `sitemap.xml` and `robots.txt` when the app maps `MapSitemap` and `MapRobotsTxt`, the sitemap listing only exported pages (none marked `.ExcludeFromExport()`), and any other `GET` endpoint marked `.IncludeInExport()` as a file at its URL: `app.MapGet("/llms.txt", ...).IncludeInExport()` writes `out/llms.txt`, the body only. The export requests it with no query string, and any status but 200, a redirect included, fails the export; one that reads the query string is warned about in `Netjes.Export.QueryString`, and one that requires a value there fails the export. Such an endpoint needs a URL with a file extension and no route parameters, and must allow anonymous requests; a file in `public/` with its path (a `public/robots.txt` next to `MapRobotsTxt`) fails the export. `.ExcludeFromExport()` on the Map call leaves a file out, and wins over `.IncludeInExport()`; when you leave `MapSitemap` out, leave `MapRobotsTxt` out too, since `robots.txt` always names the sitemap. Both files use the `siteUrl` of the environment the export runs in, Production unless `ASPNETCORE_ENVIRONMENT` is set, so set the static host's URL for the export (`SiteUrl=https://www.example.com npx netjes export` when the app reads `app.Configuration["SiteUrl"]`); a loopback one, such as a `http://localhost:5080` fallback, is written and warned about.
|
|
90
|
+
|
|
91
|
+
The export requests every URL without a query string, and a static host serves each file for every query string. A page whose loader, or a layout loader above it, reads the query string is still exported, as the page without one, and the export logs a warning in `Netjes.Export.QueryString`. To export a page for each value, make it a route parameter with `.StaticParams(...)` (`/blog/tags/[tag]`, not `/blog?tag=`); to keep the page on a server, exclude it. A loader parameter with no default value and a type that is not nullable (`int page`, `string q`) requires the value: without one the page answers 400 and the export fails, so give it a default value or a nullable type (`int page = 1`, `string? q`) to export the page without one. On an exported page `useSearchParams()` sees no query string either, whatever the address bar shows: read `window.location.search` in an effect there, and parse it with the same schema.
|
|
59
92
|
|
|
60
93
|
## Sitemap and robots
|
|
61
94
|
|
|
62
|
-
`app.MapSitemap(siteUrl)` serves `/sitemap.xml` from the route table (dynamic pages through `.StaticParams`, pages that need authorization or call `.ExcludeFromSitemap()` left out) and `app.MapRobotsTxt(siteUrl, disallow)` serves `/robots.txt`.
|
|
95
|
+
`app.MapSitemap(siteUrl)` serves `/sitemap.xml` from the route table (dynamic pages through `.StaticParams`, pages that need authorization or call `.ExcludeFromSitemap()` left out) and `app.MapRobotsTxt(siteUrl, disallow)` serves `/robots.txt`. `netjes export` writes both ([Static export](#static-export)).
|
|
63
96
|
|
|
64
97
|
Leave `/_netjes/` out of `disallow`: `<Image>` serves every image through `/_netjes/image`, so a crawler that may not fetch it cannot index your images. The other endpoints there take posts or hand out an antiforgery token, which a crawler has no use for. Under a base path, `disallow` paths are written as given, so include the base path in them. See [Metadata](https://github.com/DennisVerhoeven/netjes/blob/main/docs/guides/metadata.md).
|
|
65
98
|
|