netjes 0.0.2 → 0.0.3

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.
Files changed (148) hide show
  1. package/README.md +11 -11
  2. package/cli/agents.mjs +25 -2
  3. package/cli/commands.mjs +56 -6
  4. package/cli/help.mjs +7 -2
  5. package/cli/info.mjs +7 -2
  6. package/cli/scaffold.mjs +31 -4
  7. package/dist/actions.d.ts +7 -2
  8. package/dist/actions.d.ts.map +1 -1
  9. package/dist/actions.js +9 -3
  10. package/dist/actions.js.map +1 -1
  11. package/dist/client-entry.d.ts +1 -0
  12. package/dist/client-entry.d.ts.map +1 -1
  13. package/dist/client-entry.js +19 -2
  14. package/dist/client-entry.js.map +1 -1
  15. package/dist/deferred.d.ts +11 -0
  16. package/dist/deferred.d.ts.map +1 -1
  17. package/dist/deferred.js +15 -1
  18. package/dist/deferred.js.map +1 -1
  19. package/dist/document.d.ts.map +1 -1
  20. package/dist/document.js +10 -1
  21. package/dist/document.js.map +1 -1
  22. package/dist/errors.d.ts +7 -0
  23. package/dist/errors.d.ts.map +1 -0
  24. package/dist/errors.js +11 -0
  25. package/dist/errors.js.map +1 -0
  26. package/dist/form.d.ts +5 -5
  27. package/dist/form.d.ts.map +1 -1
  28. package/dist/form.js +5 -2
  29. package/dist/form.js.map +1 -1
  30. package/dist/href.d.ts +24 -3
  31. package/dist/href.d.ts.map +1 -1
  32. package/dist/href.js +117 -7
  33. package/dist/href.js.map +1 -1
  34. package/dist/image.d.ts +3 -3
  35. package/dist/image.d.ts.map +1 -1
  36. package/dist/image.js.map +1 -1
  37. package/dist/index.d.ts +2 -1
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +2 -1
  40. package/dist/index.js.map +1 -1
  41. package/dist/link.d.ts +15 -7
  42. package/dist/link.d.ts.map +1 -1
  43. package/dist/link.js +38 -7
  44. package/dist/link.js.map +1 -1
  45. package/dist/metadata.d.ts +8 -2
  46. package/dist/metadata.d.ts.map +1 -1
  47. package/dist/metadata.js +14 -5
  48. package/dist/metadata.js.map +1 -1
  49. package/dist/modules.d.ts +15 -1
  50. package/dist/modules.d.ts.map +1 -1
  51. package/dist/modules.js.map +1 -1
  52. package/dist/navigation-focus.d.ts +38 -0
  53. package/dist/navigation-focus.d.ts.map +1 -0
  54. package/dist/navigation-focus.js +71 -0
  55. package/dist/navigation-focus.js.map +1 -0
  56. package/dist/query.d.ts +5 -2
  57. package/dist/query.d.ts.map +1 -1
  58. package/dist/query.js +11 -8
  59. package/dist/query.js.map +1 -1
  60. package/dist/render-document.d.ts +60 -0
  61. package/dist/render-document.d.ts.map +1 -0
  62. package/dist/render-document.js +330 -0
  63. package/dist/render-document.js.map +1 -0
  64. package/dist/route-announcer.d.ts +18 -0
  65. package/dist/route-announcer.d.ts.map +1 -0
  66. package/dist/route-announcer.js +58 -0
  67. package/dist/route-announcer.js.map +1 -0
  68. package/dist/route-tree.d.ts +6 -2
  69. package/dist/route-tree.d.ts.map +1 -1
  70. package/dist/route-tree.js +17 -7
  71. package/dist/route-tree.js.map +1 -1
  72. package/dist/router.d.ts +28 -5
  73. package/dist/router.d.ts.map +1 -1
  74. package/dist/router.js +71 -26
  75. package/dist/router.js.map +1 -1
  76. package/dist/script.d.ts +28 -5
  77. package/dist/script.d.ts.map +1 -1
  78. package/dist/script.js +192 -21
  79. package/dist/script.js.map +1 -1
  80. package/dist/server-render.d.ts.map +1 -1
  81. package/dist/server-render.js +4 -1
  82. package/dist/server-render.js.map +1 -1
  83. package/dist/testing-vitest.d.ts +8 -0
  84. package/dist/testing-vitest.d.ts.map +1 -0
  85. package/dist/testing-vitest.js +26 -0
  86. package/dist/testing-vitest.js.map +1 -0
  87. package/dist/testing.d.ts +11 -1
  88. package/dist/testing.d.ts.map +1 -1
  89. package/dist/testing.js +17 -3
  90. package/dist/testing.js.map +1 -1
  91. package/dist/zod-jitless.d.ts +2 -0
  92. package/dist/zod-jitless.d.ts.map +1 -0
  93. package/dist/zod-jitless.js +4 -0
  94. package/dist/zod-jitless.js.map +1 -0
  95. package/package.json +1 -1
  96. package/sidecar/server.mjs +73 -6
  97. package/skills/netjes/SKILL.md +13 -6
  98. package/skills/netjes/references/caching-and-streaming.md +37 -4
  99. package/skills/netjes/references/data-and-actions.md +9 -5
  100. package/skills/netjes/references/deployment.md +7 -3
  101. package/skills/netjes/references/routing.md +18 -6
  102. package/skills/netjes/references/testing.md +23 -7
  103. package/src/actions.tsx +9 -3
  104. package/src/client-entry.tsx +20 -2
  105. package/src/deferred.ts +14 -1
  106. package/src/document.tsx +22 -3
  107. package/src/errors.ts +12 -0
  108. package/src/form.tsx +10 -7
  109. package/src/href.ts +123 -8
  110. package/src/image.tsx +3 -3
  111. package/src/index.ts +2 -1
  112. package/src/link.tsx +47 -13
  113. package/src/metadata.tsx +19 -6
  114. package/src/modules.ts +15 -2
  115. package/src/navigation-focus.ts +75 -0
  116. package/src/query.tsx +14 -8
  117. package/src/render-document.tsx +386 -0
  118. package/src/route-announcer.tsx +69 -0
  119. package/src/route-tree.tsx +17 -6
  120. package/src/router.tsx +116 -31
  121. package/src/script.tsx +214 -23
  122. package/src/server-render.tsx +4 -1
  123. package/src/testing-vitest.ts +37 -0
  124. package/src/testing.tsx +31 -5
  125. package/src/virtual.d.ts +2 -0
  126. package/src/zod-jitless.ts +15 -0
  127. package/template/.dockerignore +3 -0
  128. package/template/Dockerfile +11 -1
  129. package/template/README.md +11 -7
  130. package/template/_AGENTS.md +36 -8
  131. package/template/__NAME__.csproj +2 -0
  132. package/template/app/document.test.ts +29 -0
  133. package/template/app/error.tsx +3 -2
  134. package/template/app/layout.tsx +10 -1
  135. package/template/docker-compose.override.yml +4 -3
  136. package/template/docker-compose.prod.yml +2 -1
  137. package/template/e2e/home.spec.ts +32 -0
  138. package/template/generated/netjes.ts +43 -14
  139. package/template/package.json +4 -1
  140. package/template/playwright.config.ts +36 -0
  141. package/template/tests/__NAME__.Tests/AppFactory.cs +31 -0
  142. package/template/tests/__NAME__.Tests/HomePageTests.cs +55 -0
  143. package/template/tests/__NAME__.Tests/__NAME__.Tests.csproj +30 -0
  144. package/template/tsconfig.app.json +20 -0
  145. package/template/tsconfig.json +2 -17
  146. package/template/tsconfig.node.json +8 -0
  147. package/vite/plugin.d.ts +9 -1
  148. package/vite/plugin.mjs +17 -5
@@ -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
+ }
@@ -1,19 +1,4 @@
1
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": ["app", "components", "generated", "vite.config.ts"]
2
+ "files": [],
3
+ "references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }]
19
4
  }
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "./tsconfig.app.json",
3
+ "compilerOptions": {
4
+ "types": ["node"]
5
+ },
6
+ "include": ["vite.config.ts", "playwright.config.ts", "e2e", "scripts"],
7
+ "exclude": ["node_modules"]
8
+ }
package/vite/plugin.d.ts CHANGED
@@ -19,9 +19,17 @@ export type NetjesPluginOptions = {
19
19
  * The built-in TanStack Query client, seeded from C# with `PageResult.WithQuery`. Default true. `false` removes it
20
20
  * (and TanStack Query from the bundle); an object configures it. Options are written into the bundle as literals,
21
21
  * so plain values (numbers including Infinity, strings, booleans, objects, arrays) work and functions are rejected.
22
+ * In tests, unless `test` is false, `createTestApp()` from `netjes/testing` starts from the same `defaultOptions`,
23
+ * except `retryDelay` and the queries' `gcTime`.
22
24
  */
23
25
  query?: boolean | { defaultOptions?: DefaultOptions };
24
- /** Vitest defaults (jsdom, Testing Library cleanup, no e2e/ or build output). Default true; the app's own `test` settings win. */
26
+ /**
27
+ * Vitest defaults (jsdom, Testing Library cleanup, no e2e/ or build output), each only where the app's own `test`
28
+ * settings leave it unset, with two exceptions that are added to what the app sets: Netjes's own setup file, after
29
+ * the app's own `setupFiles`, and `provide['netjes:query']`, the `query` option's `defaultOptions` for
30
+ * `createTestApp()` (none with `query: false`). Default true; `false` adds none of them. Under Vitest 5, inline
31
+ * `test.projects` that share the root's Vite server get none of them unless the root sets `sharedViteServer: false`.
32
+ */
25
33
  test?: boolean;
26
34
  };
27
35
 
package/vite/plugin.mjs CHANGED
@@ -15,6 +15,8 @@ const SERVER_ENTRY = normalizePath(fileURLToPath(new URL('../dist/server-entry.j
15
15
  const SIDECAR = fileURLToPath(new URL('../sidecar/server.mjs', import.meta.url));
16
16
  const QUERY_MODULE = normalizePath(fileURLToPath(new URL('../dist/query.js', import.meta.url)));
17
17
  const TEST_SETUP = normalizePath(fileURLToPath(new URL('../dist/testing-setup.js', import.meta.url)));
18
+ // What the test harness needs from Vitest: the setup file every app's tests get, after the app's own. Not exported.
19
+ const TEST_VITEST = normalizePath(fileURLToPath(new URL('../dist/testing-vitest.js', import.meta.url)));
18
20
 
19
21
  const PAGES_ID = 'virtual:netjes/pages';
20
22
  const RESOLVED_PAGES_ID = `\0${PAGES_ID}`;
@@ -72,18 +74,27 @@ function canResolve(root, name) {
72
74
 
73
75
  /**
74
76
  * Vitest settings for component tests, unless the app set them itself: a DOM (when jsdom is installed), Testing
75
- * Library cleanup (when it is installed), and no Playwright specs (e2e/), build output or .NET bin/obj folders.
77
+ * Library cleanup (when it is installed), and no Playwright specs (e2e/), build output or .NET bin/obj folders. Two
78
+ * exceptions, which Vite merges into what the app set: Netjes's own setup file goes into every app's setupFiles,
79
+ * after the app's own, and provide['netjes:query'] holds the app's query defaultOptions, which that file hands to
80
+ * createTestApp. Only that key is returned, so the app's own provide keys are not merged twice. Vitest 5 builds the
81
+ * test options of an inline test project that shares the root's Vite server from the root's options as written, before
82
+ * this hook runs, so such a project gets none of these; with sharedViteServer: false it resolves the config, and this
83
+ * hook, for each project.
76
84
  * @param {Record<string, unknown> | undefined} test
77
85
  * @param {string} root
86
+ * @param {{ defaultOptions?: import('@tanstack/react-query').DefaultOptions } | null} query
78
87
  */
79
- function testDefaults(test = {}, root) {
88
+ function testDefaults(test = {}, root, query) {
80
89
  /** @type {Record<string, unknown>} */
81
90
  const defaults = {};
82
91
  if (test.environment === undefined && canResolve(root, 'jsdom')) defaults.environment = 'jsdom';
83
92
  if (test.exclude === undefined) {
84
93
  defaults.exclude = ['**/node_modules/**', '**/dist/**', '**/bin/**', '**/obj/**', '**/e2e/**', '**/.git/**'];
85
94
  }
86
- if (test.setupFiles === undefined && canResolve(root, '@testing-library/react')) defaults.setupFiles = [TEST_SETUP];
95
+ const cleanup = test.setupFiles === undefined && canResolve(root, '@testing-library/react');
96
+ defaults.setupFiles = cleanup ? [TEST_SETUP, TEST_VITEST] : [TEST_VITEST];
97
+ if (query) defaults.provide = { 'netjes:query': query.defaultOptions ?? {} };
87
98
  return defaults;
88
99
  }
89
100
 
@@ -174,7 +185,7 @@ export default function netjes(options = {}) {
174
185
  const userTest = /** @type {{ test?: Record<string, unknown> }} */ (/** @type {unknown} */ (config)).test;
175
186
  const root = path.resolve(config.root ?? process.cwd());
176
187
  const workspaceRoot = searchForWorkspaceRoot(root);
177
- const test = env.command === 'serve' && options.test !== false ? { test: testDefaults(userTest, root) } : {};
188
+ const test = env.command === 'serve' && options.test !== false ? { test: testDefaults(userTest, root, query) } : {};
178
189
  // Built asset URLs live under the base path; in development the browser loads modules from the Vite server's root.
179
190
  const base = env.command === 'build' && basePath ? { base: `${basePath}/` } : {};
180
191
  return {
@@ -278,7 +289,8 @@ export default function netjes(options = {}) {
278
289
  }
279
290
  if (id !== RESOLVED_PAGES_ID) return undefined;
280
291
  const glob = JSON.stringify([appFiles]);
281
- return `export const pages = import.meta.glob(${glob});\n`;
292
+ // The folder the keys start with: renderDocument (netjes/testing) builds routes and segment ids from it.
293
+ return `export const pages = import.meta.glob(${glob});\nexport const appDir = ${JSON.stringify(appDir)};\n`;
282
294
  },
283
295
  async writeBundle() {
284
296
  if (!isSsrBuild) return;