netjes 0.0.1 → 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.
- package/README.md +11 -11
- package/cli/agents.mjs +25 -2
- package/cli/commands.mjs +56 -6
- package/cli/help.mjs +7 -2
- package/cli/info.mjs +7 -2
- 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 +43 -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 +4 -3
- package/dist/document.d.ts.map +1 -1
- package/dist/document.js +15 -5
- 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 +24 -3
- package/dist/href.d.ts.map +1 -1
- package/dist/href.js +117 -7
- 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 +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -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/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 +2 -0
- package/dist/payload.d.ts.map +1 -1
- package/dist/payload.js +2 -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 +60 -0
- package/dist/render-document.d.ts.map +1 -0
- package/dist/render-document.js +330 -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 +11 -1
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +17 -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 +73 -6
- package/skills/netjes/SKILL.md +13 -6
- package/skills/netjes/references/caching-and-streaming.md +37 -4
- package/skills/netjes/references/data-and-actions.md +9 -5
- package/skills/netjes/references/deployment.md +7 -3
- package/skills/netjes/references/routing.md +18 -6
- package/skills/netjes/references/testing.md +23 -7
- package/src/actions.tsx +9 -3
- package/src/client-entry.tsx +43 -2
- package/src/deferred.ts +14 -1
- package/src/document.tsx +28 -7
- package/src/errors.ts +12 -0
- package/src/form.tsx +10 -7
- package/src/href.ts +123 -8
- package/src/image.tsx +3 -3
- package/src/index.ts +2 -1
- package/src/link.tsx +47 -13
- package/src/metadata.tsx +19 -6
- package/src/modules.ts +15 -2
- package/src/navigation-focus.ts +75 -0
- package/src/payload.ts +2 -0
- package/src/query.tsx +14 -8
- package/src/render-document.tsx +386 -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 +31 -5
- package/src/virtual.d.ts +2 -0
- 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 +36 -8
- 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/plugin.d.ts +9 -1
- 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
|
+
}
|
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
|
}
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
|
|
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;
|