netjes 0.0.2 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -11
- package/cli/agents.mjs +25 -2
- package/cli/commands.mjs +56 -6
- package/cli/help.mjs +7 -2
- package/cli/info.mjs +275 -33
- package/cli/routes.mjs +35 -8
- package/cli/scaffold.mjs +31 -4
- package/dist/actions.d.ts +7 -2
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +9 -3
- package/dist/actions.js.map +1 -1
- package/dist/client-entry.d.ts +1 -0
- package/dist/client-entry.d.ts.map +1 -1
- package/dist/client-entry.js +19 -2
- package/dist/client-entry.js.map +1 -1
- package/dist/deferred.d.ts +11 -0
- package/dist/deferred.d.ts.map +1 -1
- package/dist/deferred.js +15 -1
- package/dist/deferred.js.map +1 -1
- package/dist/document.d.ts.map +1 -1
- package/dist/document.js +10 -1
- package/dist/document.js.map +1 -1
- package/dist/errors.d.ts +7 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +11 -0
- package/dist/errors.js.map +1 -0
- package/dist/form.d.ts +5 -5
- package/dist/form.d.ts.map +1 -1
- package/dist/form.js +5 -2
- package/dist/form.js.map +1 -1
- package/dist/href.d.ts +31 -3
- package/dist/href.d.ts.map +1 -1
- package/dist/href.js +131 -10
- package/dist/href.js.map +1 -1
- package/dist/image.d.ts +3 -3
- package/dist/image.d.ts.map +1 -1
- package/dist/image.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/link.d.ts +15 -7
- package/dist/link.d.ts.map +1 -1
- package/dist/link.js +38 -7
- package/dist/link.js.map +1 -1
- package/dist/metadata.d.ts +8 -2
- package/dist/metadata.d.ts.map +1 -1
- package/dist/metadata.js +14 -5
- package/dist/metadata.js.map +1 -1
- package/dist/modules.d.ts +15 -1
- package/dist/modules.d.ts.map +1 -1
- package/dist/modules.js.map +1 -1
- package/dist/mounted.d.ts +55 -0
- package/dist/mounted.d.ts.map +1 -0
- package/dist/mounted.js +110 -0
- package/dist/mounted.js.map +1 -0
- package/dist/navigation-focus.d.ts +38 -0
- package/dist/navigation-focus.d.ts.map +1 -0
- package/dist/navigation-focus.js +71 -0
- package/dist/navigation-focus.js.map +1 -0
- package/dist/payload.d.ts +8 -0
- package/dist/payload.d.ts.map +1 -1
- package/dist/payload.js +28 -0
- package/dist/payload.js.map +1 -1
- package/dist/query.d.ts +5 -2
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +11 -8
- package/dist/query.js.map +1 -1
- package/dist/render-document.d.ts +74 -0
- package/dist/render-document.d.ts.map +1 -0
- package/dist/render-document.js +393 -0
- package/dist/render-document.js.map +1 -0
- package/dist/route-announcer.d.ts +18 -0
- package/dist/route-announcer.d.ts.map +1 -0
- package/dist/route-announcer.js +58 -0
- package/dist/route-announcer.js.map +1 -0
- package/dist/route-tree.d.ts +6 -2
- package/dist/route-tree.d.ts.map +1 -1
- package/dist/route-tree.js +17 -7
- package/dist/route-tree.js.map +1 -1
- package/dist/router.d.ts +28 -5
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +71 -26
- package/dist/router.js.map +1 -1
- package/dist/script.d.ts +28 -5
- package/dist/script.d.ts.map +1 -1
- package/dist/script.js +192 -21
- package/dist/script.js.map +1 -1
- package/dist/server-render.d.ts.map +1 -1
- package/dist/server-render.js +4 -1
- package/dist/server-render.js.map +1 -1
- package/dist/testing-vitest.d.ts +8 -0
- package/dist/testing-vitest.d.ts.map +1 -0
- package/dist/testing-vitest.js +26 -0
- package/dist/testing-vitest.js.map +1 -0
- package/dist/testing.d.ts +17 -1
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +18 -3
- package/dist/testing.js.map +1 -1
- package/dist/zod-jitless.d.ts +2 -0
- package/dist/zod-jitless.d.ts.map +1 -0
- package/dist/zod-jitless.js +4 -0
- package/dist/zod-jitless.js.map +1 -0
- package/package.json +1 -1
- package/sidecar/server.mjs +109 -15
- package/skills/netjes/SKILL.md +15 -7
- package/skills/netjes/references/caching-and-streaming.md +37 -4
- package/skills/netjes/references/data-and-actions.md +15 -7
- package/skills/netjes/references/deployment.md +7 -3
- package/skills/netjes/references/routing.md +32 -9
- package/skills/netjes/references/testing.md +23 -7
- package/src/actions.tsx +9 -3
- package/src/client-entry.tsx +20 -2
- package/src/deferred.ts +14 -1
- package/src/document.tsx +22 -3
- package/src/errors.ts +12 -0
- package/src/form.tsx +10 -7
- package/src/href.ts +137 -11
- package/src/image.tsx +3 -3
- package/src/index.ts +19 -1
- package/src/link.tsx +47 -13
- package/src/metadata.tsx +19 -6
- package/src/modules.ts +15 -2
- package/src/mounted.tsx +160 -0
- package/src/navigation-focus.ts +75 -0
- package/src/payload.ts +32 -0
- package/src/query.tsx +14 -8
- package/src/render-document.tsx +483 -0
- package/src/route-announcer.tsx +69 -0
- package/src/route-tree.tsx +17 -6
- package/src/router.tsx +116 -31
- package/src/script.tsx +214 -23
- package/src/server-render.tsx +4 -1
- package/src/testing-vitest.ts +37 -0
- package/src/testing.tsx +38 -5
- package/src/virtual.d.ts +8 -1
- package/src/zod-jitless.ts +15 -0
- package/template/.dockerignore +3 -0
- package/template/Dockerfile +11 -1
- package/template/README.md +11 -7
- package/template/_AGENTS.md +38 -9
- package/template/__NAME__.csproj +2 -0
- package/template/app/document.test.ts +29 -0
- package/template/app/error.tsx +3 -2
- package/template/app/layout.tsx +10 -1
- package/template/docker-compose.override.yml +4 -3
- package/template/docker-compose.prod.yml +2 -1
- package/template/e2e/home.spec.ts +32 -0
- package/template/generated/netjes.ts +43 -14
- package/template/package.json +4 -1
- package/template/playwright.config.ts +36 -0
- package/template/tests/__NAME__.Tests/AppFactory.cs +31 -0
- package/template/tests/__NAME__.Tests/HomePageTests.cs +55 -0
- package/template/tests/__NAME__.Tests/__NAME__.Tests.csproj +30 -0
- package/template/tsconfig.app.json +20 -0
- package/template/tsconfig.json +2 -17
- package/template/tsconfig.node.json +8 -0
- package/vite/packages.mjs +345 -0
- package/vite/plugin.d.ts +16 -1
- package/vite/plugin.mjs +224 -34
package/README.md
CHANGED
|
@@ -12,12 +12,12 @@ cd my-app
|
|
|
12
12
|
npm run dev # http://localhost:5080
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
`netjes new` writes an ASP.NET Core app with React pages, Tailwind CSS, TanStack Query, zod, Vitest and Docker files, and references `netjes` and `Netjes` at the same exact version.
|
|
15
|
+
`netjes new` writes an ASP.NET Core app with React pages, Tailwind CSS, TanStack Query, zod, Vitest, a C# test project, a Playwright suite and Docker files, and references `netjes` and `Netjes` at the same exact version.
|
|
16
16
|
|
|
17
17
|
## Add it to a project
|
|
18
18
|
|
|
19
19
|
```bash
|
|
20
|
-
npm install netjes@0.0.
|
|
20
|
+
npm install netjes@0.0.4 react react-dom zod @tanstack/react-query
|
|
21
21
|
npm install -D vite tailwindcss @tailwindcss/vite
|
|
22
22
|
```
|
|
23
23
|
|
|
@@ -31,25 +31,27 @@ export default defineConfig({ plugins: [netjes()] });
|
|
|
31
31
|
|
|
32
32
|
Tailwind CSS and the TanStack Query client are on by default; `netjes({ tailwind: false, query: false })` turns them off, and then they need not be installed. `vitest`, `jsdom`, `@testing-library/react` and `web-vitals` are optional peers, used when installed. Use the version of `netjes` that matches the app's `Netjes` NuGet package; `npx netjes info` checks it.
|
|
33
33
|
|
|
34
|
+
A page package, an npm package that brings pages for the app to mount at a prefix, is listed in the plugin's options, `netjes({ packages: ['@acme/help-center'] })`, so the app's build compiles its pages and its Tailwind classes, with Tailwind on ([Page packages](https://github.com/DennisVerhoeven/netjes/blob/v0.0.4/docs/guides/page-packages.md)).
|
|
35
|
+
|
|
34
36
|
## Entry points
|
|
35
37
|
|
|
36
38
|
| Import | What it holds |
|
|
37
39
|
|---|---|
|
|
38
|
-
| `netjes` | Components and hooks: `Link`, `Form`, `Image`, `Script`, `definePage`, `useAction`, `useRouter` and the rest |
|
|
40
|
+
| `netjes` | Components and hooks: `Link`, `Form`, `Image`, `Script`, `definePage`, `useAction`, `useRouter` and the rest, `mounted`, which a page package's generated file calls for links below its mount, and `CSP_SCRIPT_HASHES`, the hashes of Netjes's inline scripts for a Content-Security-Policy without a nonce |
|
|
39
41
|
| `netjes/vite` | The Vite plugin |
|
|
40
|
-
| `netjes/testing` | `createTestApp()`, for rendering pages and components in Vitest |
|
|
42
|
+
| `netjes/testing` | `createTestApp()`, for rendering pages and components in Vitest, and `renderDocument()` and `inlineScriptHashes()`, for a page's whole document and the inline scripts in it |
|
|
41
43
|
| `netjes/vitals` | `useReportWebVitals` and `<WebVitals />` |
|
|
42
44
|
| `netjes/zod` | `deferred(schema)`, for values that loaders stream with `Page.Defer` |
|
|
43
45
|
| `netjes` (bin) | The command: `new`, `dev`, `build`, `start`, `generate`, `routes`, `export`, `info`, `agents` |
|
|
44
46
|
|
|
45
|
-
Each import is an ES module with its type declarations, so an app's `tsc` reads the declarations and checks only the app's own code, also under stricter compiler options such as `exactOptionalPropertyTypes` or `noPropertyAccessFromIndexSignature` (see [TypeScript setup](https://github.com/DennisVerhoeven/netjes/blob/
|
|
47
|
+
Each import is an ES module with its type declarations, so an app's `tsc` reads the declarations and checks only the app's own code, also under stricter compiler options such as `exactOptionalPropertyTypes` or `noPropertyAccessFromIndexSignature` (see [TypeScript setup](https://github.com/DennisVerhoeven/netjes/blob/v0.0.4/docs/reference/vite-plugin.md#typescript-setup)). The TypeScript sources are in the package too, for the source maps: an editor's go to definition and a stack trace lead to them.
|
|
46
48
|
|
|
47
49
|
## Documentation
|
|
48
50
|
|
|
49
|
-
- [Getting started](https://github.com/DennisVerhoeven/netjes/blob/
|
|
50
|
-
- [All guides](https://github.com/DennisVerhoeven/netjes/blob/
|
|
51
|
-
- [React API](https://github.com/DennisVerhoeven/netjes/blob/
|
|
52
|
-
- [Vite plugin](https://github.com/DennisVerhoeven/netjes/blob/
|
|
53
|
-
- [The netjes command](https://github.com/DennisVerhoeven/netjes/blob/
|
|
51
|
+
- [Getting started](https://github.com/DennisVerhoeven/netjes/blob/v0.0.4/docs/getting-started.md)
|
|
52
|
+
- [All guides](https://github.com/DennisVerhoeven/netjes/blob/v0.0.4/docs/README.md)
|
|
53
|
+
- [React API](https://github.com/DennisVerhoeven/netjes/blob/v0.0.4/docs/reference/react-api.md)
|
|
54
|
+
- [Vite plugin](https://github.com/DennisVerhoeven/netjes/blob/v0.0.4/docs/reference/vite-plugin.md)
|
|
55
|
+
- [The netjes command](https://github.com/DennisVerhoeven/netjes/blob/v0.0.4/docs/reference/cli.md)
|
|
54
56
|
|
|
55
|
-
Report bugs in the [issue tracker](https://github.com/DennisVerhoeven/netjes/issues), and security problems privately, as [
|
|
57
|
+
Report bugs in the [issue tracker](https://github.com/DennisVerhoeven/netjes/issues), and security problems privately, as the [security policy](https://github.com/DennisVerhoeven/netjes/security/policy) describes. MIT licensed.
|
package/cli/agents.mjs
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { existsSync, statSync } from 'node:fs';
|
|
5
5
|
import { mkdir, readFile, readdir, writeFile } from 'node:fs/promises';
|
|
6
6
|
import path from 'node:path';
|
|
7
|
-
import { fillNames, projectNames, readTemplate, skillFiles } from './scaffold.mjs';
|
|
7
|
+
import { fillNames, keepSections, projectNames, readTemplate, skillFiles } from './scaffold.mjs';
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* The app's names: from `--name`, else its one project file (whose name is its root namespace), else its directory,
|
|
@@ -26,6 +26,20 @@ async function namesOf(io, dir, name) {
|
|
|
26
26
|
return projectNames(folder);
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Whether the app's package.json has that script; one that is missing or not valid JSON has none.
|
|
31
|
+
* @param {string} dir
|
|
32
|
+
* @param {string} name
|
|
33
|
+
*/
|
|
34
|
+
async function hasScript(dir, name) {
|
|
35
|
+
try {
|
|
36
|
+
const manifest = /** @type {{ scripts?: Record<string, unknown> }} */ (JSON.parse(await readFile(path.join(dir, 'package.json'), 'utf8')));
|
|
37
|
+
return typeof manifest.scripts?.[name] === 'string';
|
|
38
|
+
} catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
29
43
|
/**
|
|
30
44
|
* @param {import('./main.mjs').CliIo} io
|
|
31
45
|
* @param {string} dir
|
|
@@ -38,8 +52,17 @@ export async function agents(io, dir, options) {
|
|
|
38
52
|
return 1;
|
|
39
53
|
}
|
|
40
54
|
const names = await namesOf(io, dir, options.name);
|
|
55
|
+
// AGENTS.md sends an agent only to the test suites this app has where `netjes new` puts them, and to the script
|
|
56
|
+
// it names.
|
|
57
|
+
const has = {
|
|
58
|
+
tests: existsSync(path.join(dir, 'tests', `${names.dotnet}.Tests`, `${names.dotnet}.Tests.csproj`)),
|
|
59
|
+
e2e:
|
|
60
|
+
existsSync(path.join(dir, 'playwright.config.ts')) &&
|
|
61
|
+
statSync(path.join(dir, 'e2e'), { throwIfNoEntry: false })?.isDirectory() === true &&
|
|
62
|
+
(await hasScript(dir, 'test:e2e')),
|
|
63
|
+
};
|
|
41
64
|
const files = [
|
|
42
|
-
{ path: 'AGENTS.md', text: fillNames(await readTemplate('_AGENTS.md'), names) },
|
|
65
|
+
{ path: 'AGENTS.md', text: fillNames(keepSections(await readTemplate('_AGENTS.md'), has), names) },
|
|
43
66
|
{ path: 'CLAUDE.md', text: await readTemplate('_CLAUDE.md') },
|
|
44
67
|
...(await skillFiles()),
|
|
45
68
|
];
|
package/cli/commands.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// (NETJES_COMMAND=generate|routes|export) it binds a throwaway loopback port, writes its result and exits. Only export
|
|
4
4
|
// starts the Node sidecar, to render the pages.
|
|
5
5
|
import { existsSync, statSync } from 'node:fs';
|
|
6
|
-
import { mkdtemp, readFile, rm } from 'node:fs/promises';
|
|
6
|
+
import { mkdtemp, readFile, realpath, rm } from 'node:fs/promises';
|
|
7
7
|
import { tmpdir } from 'node:os';
|
|
8
8
|
import path from 'node:path';
|
|
9
9
|
import { agents } from './agents.mjs';
|
|
@@ -116,6 +116,42 @@ async function build(io, cwd) {
|
|
|
116
116
|
return 0;
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
/**
|
|
120
|
+
* Whether a path that path.relative returned leads below the folder it started from: not the folder itself (''), not
|
|
121
|
+
* up out of it (`..`, `../site`) and not on another drive (absolute). A name that only starts with two dots (`..site`)
|
|
122
|
+
* is below.
|
|
123
|
+
* @param {string} relative
|
|
124
|
+
*/
|
|
125
|
+
function isBelow(relative) {
|
|
126
|
+
return relative !== '' && relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Whether `target` is a folder inside the app in `app`, comparing paths as .NET does: on macOS and Windows it
|
|
131
|
+
* upper-cases each letter, so paths that differ only in case are one folder there (and a long s, ſ, is an s).
|
|
132
|
+
* @param {string} app
|
|
133
|
+
* @param {string} target
|
|
134
|
+
*/
|
|
135
|
+
function insideApp(app, target) {
|
|
136
|
+
const fold = process.platform === 'darwin' || process.platform === 'win32';
|
|
137
|
+
return isBelow(fold ? path.relative(app.toUpperCase(), target.toUpperCase()) : path.relative(app, target));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* The folder an app run in `cwd` sees as its own: its working directory, which macOS and Linux report by the real
|
|
142
|
+
* path, as getcwd() does (every symlink resolved, each name as the disk spells it), and Windows as it was given. `cwd`
|
|
143
|
+
* itself when its real path cannot be read.
|
|
144
|
+
* @param {string} cwd
|
|
145
|
+
*/
|
|
146
|
+
async function appFolder(cwd) {
|
|
147
|
+
if (process.platform === 'win32') return cwd;
|
|
148
|
+
try {
|
|
149
|
+
return await realpath(cwd);
|
|
150
|
+
} catch {
|
|
151
|
+
return cwd;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
119
155
|
/**
|
|
120
156
|
* `target` as the user would type it from `from`: relative when it is below, else absolute.
|
|
121
157
|
* @param {string} from
|
|
@@ -123,7 +159,7 @@ async function build(io, cwd) {
|
|
|
123
159
|
*/
|
|
124
160
|
function displayPath(from, target) {
|
|
125
161
|
const relative = path.relative(from, target);
|
|
126
|
-
return relative
|
|
162
|
+
return relative === '' ? '.' : isBelow(relative) ? relative : target;
|
|
127
163
|
}
|
|
128
164
|
|
|
129
165
|
/**
|
|
@@ -204,6 +240,8 @@ function layout(name) {
|
|
|
204
240
|
['Features/', 'the C# side, one folder per feature: loaders and server actions'],
|
|
205
241
|
['Program.cs', `the ASP.NET Core app (${name}.csproj)`],
|
|
206
242
|
['generated/netjes.ts', 'types and zod schemas from C#, rewritten by netjes generate; never edit it'],
|
|
243
|
+
[`tests/${name}.Tests/`, `C# tests of loaders and actions, run by dotnet test tests/${name}.Tests`],
|
|
244
|
+
['e2e/', 'Playwright tests against a development and a production server, run by npm run test:e2e'],
|
|
207
245
|
['Dockerfile', 'and the docker-compose files: Docker for development and production'],
|
|
208
246
|
];
|
|
209
247
|
const width = Math.max(...rows.map(([entry]) => (entry ?? '').length)) + 2;
|
|
@@ -213,7 +251,7 @@ function layout(name) {
|
|
|
213
251
|
/** @type {Record<string, Command>} */
|
|
214
252
|
export const COMMANDS = {
|
|
215
253
|
new: {
|
|
216
|
-
summary: 'Create an app: ASP.NET Core with React pages, Tailwind CSS, TanStack Query, zod and
|
|
254
|
+
summary: 'Create an app: ASP.NET Core with React pages, Tailwind CSS, TanStack Query, zod, Vitest, xUnit and Playwright',
|
|
217
255
|
args: '<dir>',
|
|
218
256
|
arguments: { '<dir>': { description: 'The folder to create the app in; it must not exist yet, or be empty' } },
|
|
219
257
|
options: {
|
|
@@ -401,7 +439,7 @@ export const COMMANDS = {
|
|
|
401
439
|
},
|
|
402
440
|
|
|
403
441
|
export: {
|
|
404
|
-
summary: 'Build, then write every page
|
|
442
|
+
summary: 'Build, then write every page, and each endpoint marked IncludeInExport, into a folder as static files',
|
|
405
443
|
args: '[dir]',
|
|
406
444
|
arguments: { '[dir]': { description: 'The folder to write the site to, inside the app', default: 'out' } },
|
|
407
445
|
options: {
|
|
@@ -410,7 +448,19 @@ export const COMMANDS = {
|
|
|
410
448
|
examples: ['netjes export', 'netjes export site --no-build'],
|
|
411
449
|
project: () => true,
|
|
412
450
|
async run({ io, cwd, args, options }) {
|
|
451
|
+
// A relative folder is resolved against cwd as the app will see it, so that a --cwd through a symlink still
|
|
452
|
+
// names a folder inside the app; the app compares paths as text.
|
|
453
|
+
const app = await appFolder(cwd);
|
|
454
|
+
const output = path.resolve(app, args[0] ?? 'out');
|
|
413
455
|
if (!options['no-build']) {
|
|
456
|
+
// The app decides where the export may write (StaticExportService.OutputProblems, ADR 0007), before it
|
|
457
|
+
// renders anything: only it knows Netjes:Root and the folders it needs. A folder outside the app is refused
|
|
458
|
+
// here too, before the build, which runs in cwd, so the app is there. With --no-build only the app checks,
|
|
459
|
+
// since Netjes:Root can put the app above cwd.
|
|
460
|
+
if (!insideApp(app, output)) {
|
|
461
|
+
printError(io, `netjes export: the output folder ${output} must be inside the app (${app}), such as 'out'; nothing was built`);
|
|
462
|
+
return 1;
|
|
463
|
+
}
|
|
414
464
|
const code = await build(io, cwd);
|
|
415
465
|
if (code !== 0) return code;
|
|
416
466
|
}
|
|
@@ -423,7 +473,7 @@ export const COMMANDS = {
|
|
|
423
473
|
...commandEnv(io),
|
|
424
474
|
ASPNETCORE_ENVIRONMENT: io.env.ASPNETCORE_ENVIRONMENT ?? 'Production',
|
|
425
475
|
NETJES_COMMAND: 'export',
|
|
426
|
-
NETJES_OUTPUT:
|
|
476
|
+
NETJES_OUTPUT: output,
|
|
427
477
|
},
|
|
428
478
|
});
|
|
429
479
|
},
|
|
@@ -451,7 +501,7 @@ export const COMMANDS = {
|
|
|
451
501
|
name: {
|
|
452
502
|
type: 'string',
|
|
453
503
|
value: '<Name>',
|
|
454
|
-
description: "The app's name in AGENTS.md",
|
|
504
|
+
description: "The app's name in AGENTS.md, and the <Name> of the test project tests/<Name>.Tests it looks for",
|
|
455
505
|
default: 'the name of its .csproj, else the folder name',
|
|
456
506
|
},
|
|
457
507
|
force: { type: 'boolean', description: 'Replace the files that exist', default: 'off: keep them' },
|
package/cli/help.mjs
CHANGED
|
@@ -6,7 +6,12 @@
|
|
|
6
6
|
* @typedef {import('./commands.mjs').OptionSpec} OptionSpec
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
/**
|
|
10
|
+
* The CLI reference at the release tag of `version`: the page on main can describe commands and options that this
|
|
11
|
+
* version does not have.
|
|
12
|
+
* @param {string} version
|
|
13
|
+
*/
|
|
14
|
+
export const docsUrl = (version) => `https://github.com/DennisVerhoeven/netjes/blob/v${version}/docs/reference/cli.md`;
|
|
10
15
|
|
|
11
16
|
/**
|
|
12
17
|
* Options every command takes.
|
|
@@ -55,7 +60,7 @@ export function overviewHelp(commands, version) {
|
|
|
55
60
|
columns(Object.entries(GLOBAL_OPTIONS).map(([name, option]) => [label(name, option), describe(option)])),
|
|
56
61
|
'',
|
|
57
62
|
'Run `netjes help <command>` for its options and examples.',
|
|
58
|
-
`Docs: ${
|
|
63
|
+
`Docs: ${docsUrl(version)}`,
|
|
59
64
|
'',
|
|
60
65
|
].join('\n');
|
|
61
66
|
}
|
package/cli/info.mjs
CHANGED
|
@@ -24,8 +24,10 @@ export const MIN_DOTNET = 10;
|
|
|
24
24
|
export const DOTNET_DOWNLOAD = 'https://dotnet.microsoft.com/download';
|
|
25
25
|
/** The npm packages a Netjes app runs on. */
|
|
26
26
|
const PACKAGES = ['netjes', 'vite', 'react', 'react-dom'];
|
|
27
|
-
/** Folders that hold no C# source of the app's own. */
|
|
27
|
+
/** Folders that hold no C# source of the app's own, at any depth. */
|
|
28
28
|
const SKIPPED = new Set(['bin', 'obj', 'node_modules', 'dist', 'out']);
|
|
29
|
+
/** The folder at the app's root that holds its test project, which the app's project leaves out, so its code generates no types. */
|
|
30
|
+
const TEST_PROJECTS = 'tests';
|
|
29
31
|
/** Files written within this long of each other, as a checkout or `netjes new` does, count as written together. */
|
|
30
32
|
const SAME_TIME_MS = 2000;
|
|
31
33
|
/** The Next.js pages router's special files, at the root of pages/, by the App Router file that takes their place. */
|
|
@@ -56,31 +58,166 @@ function parseVersion(version) {
|
|
|
56
58
|
const compare = (a, b) => a[0] - b[0] || a[1] - b[1] || a[2] - b[2];
|
|
57
59
|
|
|
58
60
|
/**
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
+
* A version as npm reads one: major.minor.patch, a prerelease and build metadata, which no comparison looks at.
|
|
62
|
+
* @typedef {{ main: [number, number, number]; pre: (string | number)[] }} SemVer
|
|
63
|
+
* @typedef {{ op: '<' | '<=' | '>' | '>=' | '='; version: SemVer }} Comparator
|
|
64
|
+
*/
|
|
65
|
+
|
|
66
|
+
/** A number in a version: no leading zeros. */
|
|
67
|
+
const NUMBER = '0|[1-9]\\d*';
|
|
68
|
+
const IDENTIFIER = `(?:${NUMBER}|\\d*[A-Za-z-][0-9A-Za-z-]*)`;
|
|
69
|
+
const PRERELEASE = `(?:-(${IDENTIFIER}(?:\\.${IDENTIFIER})*))?`;
|
|
70
|
+
const BUILD = '(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?';
|
|
71
|
+
const SEMVER = new RegExp(`^v?(${NUMBER})\\.(${NUMBER})\\.(${NUMBER})${PRERELEASE}${BUILD}$`);
|
|
72
|
+
/** A version in a range, which may leave out its minor and patch, or write them as x, X or *. */
|
|
73
|
+
const PARTIAL = new RegExp(`^v?(${NUMBER}|[xX*])(?:\\.(${NUMBER}|[xX*])(?:\\.(${NUMBER}|[xX*])${PRERELEASE}${BUILD})?)?$`);
|
|
74
|
+
const OPERATOR = /^(<=|>=|<|>|=|\^|~>|~)?/;
|
|
75
|
+
|
|
76
|
+
/** @param {string} pre @returns {(string | number)[]} */
|
|
77
|
+
const identifiers = (pre) => (pre ? pre.split('.').map((id) => (/^\d+$/.test(id) ? Number(id) : id)) : []);
|
|
78
|
+
|
|
79
|
+
/** @param {string} version @returns {SemVer | null} */
|
|
80
|
+
function parseSemver(version) {
|
|
81
|
+
const match = SEMVER.exec(version.trim());
|
|
82
|
+
return match ? { main: [Number(match[1]), Number(match[2]), Number(match[3])], pre: identifiers(match[4] ?? '') } : null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** @param {SemVer} a @param {SemVer} b */
|
|
86
|
+
function compareSemver(a, b) {
|
|
87
|
+
const main = compare(a.main, b.main);
|
|
88
|
+
if (main !== 0) return main;
|
|
89
|
+
// A version with a prerelease comes before the version itself, and identifiers compare one by one: numbers by value,
|
|
90
|
+
// and before words.
|
|
91
|
+
if (a.pre.length === 0 || b.pre.length === 0) return b.pre.length - a.pre.length;
|
|
92
|
+
for (let i = 0; i < Math.max(a.pre.length, b.pre.length); i++) {
|
|
93
|
+
const [x, y] = [a.pre[i], b.pre[i]];
|
|
94
|
+
if (x === undefined || y === undefined) return x === undefined ? -1 : 1;
|
|
95
|
+
if (x === y) continue;
|
|
96
|
+
if (typeof x !== typeof y) return typeof x === 'number' ? -1 : 1;
|
|
97
|
+
return x < y ? -1 : 1;
|
|
98
|
+
}
|
|
99
|
+
return 0;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** @param {number} major @param {number} minor @param {number} patch @param {(string | number)[]} [pre] @returns {SemVer} */
|
|
103
|
+
const semver = (major, minor, patch, pre = []) => ({ main: [major, minor, patch], pre });
|
|
104
|
+
/** The lowest version above every prerelease of a release below this one: what npm's `<1.3.0-0` bounds are. */
|
|
105
|
+
const before = (/** @type {number} */ major, /** @type {number} */ minor, /** @type {number} */ patch) => semver(major, minor, patch, [0]);
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The comparators one comparator of a range stands for, as npm reads it: `^1.2` is `>=1.2.0 <2.0.0-0`. Null for one
|
|
109
|
+
* that is none. With `includePrerelease`, a lower bound that the range leaves to npm (`1.2` in `^1.2`, `~1.2`, `>=1.2`)
|
|
110
|
+
* is the lowest prerelease, `>=1.2.0-0`, as npm makes it.
|
|
111
|
+
* @param {string} token
|
|
112
|
+
* @param {boolean} includePrerelease
|
|
113
|
+
* @returns {Comparator[] | null}
|
|
114
|
+
*/
|
|
115
|
+
function comparators(token, includePrerelease) {
|
|
116
|
+
const op = OPERATOR.exec(token)?.[1] ?? '';
|
|
117
|
+
const match = PARTIAL.exec(token.slice(op.length));
|
|
118
|
+
if (!match) return null;
|
|
119
|
+
const [major, minor, patch] = [match[1], match[2], match[3]].map((part) => (part === undefined || /^[xX*]$/.test(part) ? null : Number(part)));
|
|
120
|
+
// A number after an x, as in `1.x.3`, makes it none.
|
|
121
|
+
if ((major === null && (minor ?? patch ?? null) !== null) || (minor === null && (patch ?? null) !== null)) return null;
|
|
122
|
+
const pre = identifiers(match[4] ?? '');
|
|
123
|
+
/** @param {number} major @param {number} minor @returns {SemVer} */
|
|
124
|
+
const lowest = (major, minor) => semver(major, minor, 0, includePrerelease ? [0] : []);
|
|
125
|
+
if (major === null || major === undefined) return op === '<' || op === '>' ? [{ op: '<', version: before(0, 0, 0) }] : [];
|
|
126
|
+
if (minor === null || minor === undefined) {
|
|
127
|
+
/** @type {Record<string, Comparator[]>} */
|
|
128
|
+
const byMajor = {
|
|
129
|
+
'>': [{ op: '>=', version: lowest(major + 1, 0) }],
|
|
130
|
+
'>=': [{ op: '>=', version: lowest(major, 0) }],
|
|
131
|
+
'<': [{ op: '<', version: before(major, 0, 0) }],
|
|
132
|
+
'<=': [{ op: '<', version: before(major + 1, 0, 0) }],
|
|
133
|
+
};
|
|
134
|
+
return byMajor[op] ?? [{ op: '>=', version: lowest(major, 0) }, { op: '<', version: before(major + 1, 0, 0) }];
|
|
135
|
+
}
|
|
136
|
+
if (patch === null || patch === undefined) {
|
|
137
|
+
/** @type {Record<string, Comparator[]>} */
|
|
138
|
+
const byMinor = {
|
|
139
|
+
'>': [{ op: '>=', version: lowest(major, minor + 1) }],
|
|
140
|
+
'>=': [{ op: '>=', version: lowest(major, minor) }],
|
|
141
|
+
'<': [{ op: '<', version: before(major, minor, 0) }],
|
|
142
|
+
'<=': [{ op: '<', version: before(major, minor + 1, 0) }],
|
|
143
|
+
'^': [{ op: '>=', version: lowest(major, minor) }, { op: '<', version: major > 0 ? before(major + 1, 0, 0) : before(0, minor + 1, 0) }],
|
|
144
|
+
};
|
|
145
|
+
return byMinor[op] ?? [{ op: '>=', version: lowest(major, minor) }, { op: '<', version: before(major, minor + 1, 0) }];
|
|
146
|
+
}
|
|
147
|
+
const version = semver(major, minor, patch, pre);
|
|
148
|
+
switch (op) {
|
|
149
|
+
case '^': {
|
|
150
|
+
// npm's caret allows the changes that keep the first non-zero part: ^0.0.4 is 0.0.4 alone, and its prereleases.
|
|
151
|
+
const upper = major > 0 ? before(major + 1, 0, 0) : minor > 0 ? before(0, minor + 1, 0) : before(0, 0, patch + 1);
|
|
152
|
+
return [{ op: '>=', version }, { op: '<', version: upper }];
|
|
153
|
+
}
|
|
154
|
+
case '~':
|
|
155
|
+
case '~>':
|
|
156
|
+
return [{ op: '>=', version }, { op: '<', version: before(major, minor + 1, 0) }];
|
|
157
|
+
case '':
|
|
158
|
+
return [{ op: '=', version }];
|
|
159
|
+
default:
|
|
160
|
+
return [{ op: /** @type {Comparator['op']} */ (op), version }];
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* The comparators of one part of a range, between `||`s: a hyphen range (`1.2 - 2.3.4`), or comparators that a version
|
|
166
|
+
* has to pass all of, separated by spaces. Null for a part that is none.
|
|
167
|
+
* @param {string} part
|
|
168
|
+
* @param {boolean} includePrerelease
|
|
169
|
+
* @returns {Comparator[] | null}
|
|
170
|
+
*/
|
|
171
|
+
function comparatorSet(part, includePrerelease) {
|
|
172
|
+
const hyphen = /^(\S+)\s+-\s+(\S+)$/.exec(part);
|
|
173
|
+
if (hyphen) {
|
|
174
|
+
// From the first version, up to and with the second: `<=2.3` is below 2.4.0, as npm reads a partial version there.
|
|
175
|
+
// With prereleases included, npm starts a hyphen range at the first version's lowest prerelease, unless it names one.
|
|
176
|
+
const from = comparators(`>=${hyphen[1]}`, includePrerelease)?.map((c) =>
|
|
177
|
+
includePrerelease && c.version.pre.length === 0 ? { ...c, version: { ...c.version, pre: [0] } } : c,
|
|
178
|
+
);
|
|
179
|
+
const to = comparators(`<=${hyphen[2]}`, includePrerelease);
|
|
180
|
+
return from && to ? [...from, ...to] : null;
|
|
181
|
+
}
|
|
182
|
+
// `>= 1.2.3` is `>=1.2.3`.
|
|
183
|
+
const tokens = part.replace(/(<=|>=|<|>|=|\^|~>|~)\s+/g, '$1').split(/\s+/).filter(Boolean);
|
|
184
|
+
/** @type {Comparator[]} */
|
|
185
|
+
const set = [];
|
|
186
|
+
for (const token of tokens) {
|
|
187
|
+
const parsed = comparators(token, includePrerelease);
|
|
188
|
+
if (!parsed) return null;
|
|
189
|
+
set.push(...parsed);
|
|
190
|
+
}
|
|
191
|
+
return set;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** @param {SemVer} version @param {Comparator} comparator */
|
|
195
|
+
function passes(version, { op, version: bound }) {
|
|
196
|
+
const order = compareSemver(version, bound);
|
|
197
|
+
return op === '<' ? order < 0 : op === '<=' ? order <= 0 : op === '>' ? order > 0 : op === '>=' ? order >= 0 : order === 0;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Whether `version` is in `range`, as npm reads ranges: `^`, `~`, `x` and partial versions, `<`, `<=`, `>`, `>=` and
|
|
202
|
+
* `=`, comparators separated by spaces, hyphen ranges and `||`. `^0.0.4` is `>=0.0.4 <0.0.5-0`, and a prerelease is in
|
|
203
|
+
* a range only when a comparator names a prerelease of the same major.minor.patch: `0.0.5-rc.1` is not in `^0.0.4`.
|
|
204
|
+
* Build metadata (`+sha`) is left out. In strict mode a range or a version that is none satisfies nothing; otherwise
|
|
205
|
+
* it passes, so that only a definite mismatch is reported. With `includePrerelease`, as npm checks `engines`, a
|
|
206
|
+
* prerelease is in a range by its place in the order alone: `24.16.0-rc.1` is in `^24.15.0`.
|
|
61
207
|
* @param {string | undefined} version
|
|
62
208
|
* @param {string} range
|
|
209
|
+
* @param {{ strict?: boolean; includePrerelease?: boolean }} [options]
|
|
63
210
|
*/
|
|
64
|
-
export function satisfies(version, range) {
|
|
65
|
-
const have =
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
case '>=':
|
|
75
|
-
return atLeast;
|
|
76
|
-
case '^':
|
|
77
|
-
return atLeast && (want[0] > 0 ? have[0] === want[0] : have[1] === want[1]);
|
|
78
|
-
case '~':
|
|
79
|
-
return atLeast && have[0] === want[0] && have[1] === want[1];
|
|
80
|
-
default:
|
|
81
|
-
return match[3] === undefined ? have[0] === want[0] : compare(have, want) === 0;
|
|
82
|
-
}
|
|
83
|
-
});
|
|
211
|
+
export function satisfies(version, range, { strict = false, includePrerelease = false } = {}) {
|
|
212
|
+
const have = parseSemver(version ?? '');
|
|
213
|
+
const sets = range.split('||').map((part) => comparatorSet(part.trim(), includePrerelease));
|
|
214
|
+
if (!have || sets.some((set) => set === null)) return !strict;
|
|
215
|
+
return sets.some(
|
|
216
|
+
(set) =>
|
|
217
|
+
set !== null &&
|
|
218
|
+
set.every((comparator) => passes(have, comparator)) &&
|
|
219
|
+
(includePrerelease || have.pre.length === 0 || set.some(({ version: bound }) => bound.pre.length > 0 && compare(bound.main, have.main) === 0)),
|
|
220
|
+
);
|
|
84
221
|
}
|
|
85
222
|
|
|
86
223
|
/** @param {string} file @returns {Promise<any>} */
|
|
@@ -97,7 +234,8 @@ async function readJson(file) {
|
|
|
97
234
|
* @returns {Check}
|
|
98
235
|
*/
|
|
99
236
|
export function checkNode(version) {
|
|
100
|
-
|
|
237
|
+
// A prerelease of Node.js is checked by its place in the order, as npm checks a package's engines.
|
|
238
|
+
if (satisfies(version, NODE_RANGE, { includePrerelease: true })) return ok(`Node.js ${version}`);
|
|
101
239
|
const major = parseVersion(version)?.[0] ?? 0;
|
|
102
240
|
const fix = `Install a Node.js release that matches ${NODE_RANGE}: ${NODE_DOWNLOAD}`;
|
|
103
241
|
// The odd release lines between the supported ones (23, 25) are short-lived; they may work, but are not supported.
|
|
@@ -234,6 +372,25 @@ async function checkPackages(cwd, cliVersion) {
|
|
|
234
372
|
return { checks, netjes: netjes?.version };
|
|
235
373
|
}
|
|
236
374
|
|
|
375
|
+
/** @param {string} text */
|
|
376
|
+
const escapeRegExp = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* The project file's PackageReference of the NuGet package `name`, in any case as NuGet compares ids: the Version it
|
|
380
|
+
* gives, and that version when it is an exact one, 1.2.3 or [1.2.3]. A range, a floating version or an MSBuild property
|
|
381
|
+
* such as $(NetjesVersion) is resolved by NuGet and MSBuild, which this does not repeat, so it has no exact version.
|
|
382
|
+
* Null when the project does not reference the package.
|
|
383
|
+
* @param {string} text
|
|
384
|
+
* @param {string} name
|
|
385
|
+
* @returns {{ reference: string | undefined; version: string | undefined } | null}
|
|
386
|
+
*/
|
|
387
|
+
function packageReference(text, name) {
|
|
388
|
+
const element = new RegExp(`<PackageReference\\s+[^>]*Include="${escapeRegExp(name)}"[^>]*>`, 'i').exec(text)?.[0];
|
|
389
|
+
if (!element) return null;
|
|
390
|
+
const reference = /\sVersion="([^"]*)"/i.exec(element)?.[1];
|
|
391
|
+
return { reference, version: /^\[?(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)\]?$/.exec(reference?.trim() ?? '')?.[1] };
|
|
392
|
+
}
|
|
393
|
+
|
|
237
394
|
/**
|
|
238
395
|
* How the project file references Netjes, and whether that is the version of the npm package.
|
|
239
396
|
* @param {string} cwd
|
|
@@ -251,17 +408,14 @@ function checkReference(cwd, csproj, text, npmVersion, cliVersion) {
|
|
|
251
408
|
? ok(`${csproj} references the Netjes checkout at ${file}`)
|
|
252
409
|
: fail(`${csproj} references Netjes at ${file}, which does not exist`, 'Point its ProjectReference (and the Netjes.targets import) at src/Netjes in a Netjes checkout');
|
|
253
410
|
}
|
|
254
|
-
const
|
|
255
|
-
if (!
|
|
411
|
+
const found = packageReference(text, 'Netjes');
|
|
412
|
+
if (!found) {
|
|
256
413
|
return warn(
|
|
257
414
|
`${csproj} does not reference the Netjes package`,
|
|
258
415
|
`Add <PackageReference Include="Netjes" Version="[${npmVersion ?? cliVersion}]" />, unless a Directory.Packages.props does`,
|
|
259
416
|
);
|
|
260
417
|
}
|
|
261
|
-
const reference =
|
|
262
|
-
// Only an exact version, 1.2.3 or [1.2.3], is compared: a range, a floating version or an MSBuild property such as
|
|
263
|
-
// $(NetjesVersion) is resolved by NuGet and MSBuild, which this does not repeat.
|
|
264
|
-
const version = /^\[?(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)\]?$/.exec(reference?.trim() ?? '')?.[1];
|
|
418
|
+
const { reference, version } = found;
|
|
265
419
|
const shown = version ?? reference;
|
|
266
420
|
if (!version || !npmVersion) return ok(`${csproj} references Netjes${shown ? ` ${shown}` : ''}`);
|
|
267
421
|
return version === npmVersion
|
|
@@ -272,6 +426,80 @@ function checkReference(cwd, csproj, text, npmVersion, cliVersion) {
|
|
|
272
426
|
);
|
|
273
427
|
}
|
|
274
428
|
|
|
429
|
+
/**
|
|
430
|
+
* The page packages the app depends on: each dependency whose package.json has a `netjes` field, as the Vite plugin
|
|
431
|
+
* reads them. One line names them with their versions, and a line follows for each one that the app would refuse to
|
|
432
|
+
* mount (no peer range, or one that leaves out the installed netjes), whose NuGet half the project references at another
|
|
433
|
+
* exact version, or that `netjes({ packages })` does not list, so that no build holds its pages. Nothing when the app
|
|
434
|
+
* depends on none.
|
|
435
|
+
* @param {string} cwd
|
|
436
|
+
* @param {{
|
|
437
|
+
* manifest: Record<string, any> | null;
|
|
438
|
+
* netjes: string | undefined;
|
|
439
|
+
* csproj: { file: string; text: string } | null;
|
|
440
|
+
* viteConfig: { file: string; text: string } | null;
|
|
441
|
+
* }} project the app's package.json, the installed netjes's version, the project file, and the Vite config that uses netjes()
|
|
442
|
+
* @returns {Promise<Check[]>}
|
|
443
|
+
*/
|
|
444
|
+
async function checkPagePackages(cwd, { manifest, netjes, csproj, viteConfig }) {
|
|
445
|
+
/** @param {unknown} deps */
|
|
446
|
+
const namesIn = (deps) => (typeof deps === 'object' && deps !== null ? Object.keys(deps) : []);
|
|
447
|
+
const names = [...new Set([...namesIn(manifest?.dependencies), ...namesIn(manifest?.devDependencies)])]
|
|
448
|
+
.filter((name) => name !== 'netjes')
|
|
449
|
+
.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
450
|
+
/** @type {{ name: string; version: string; range: unknown; nuget: unknown }[]} */
|
|
451
|
+
const found = [];
|
|
452
|
+
for (const name of names) {
|
|
453
|
+
const file = findPackage(cwd, name);
|
|
454
|
+
const json = file ? await readJson(file) : null;
|
|
455
|
+
const field = json?.netjes;
|
|
456
|
+
if (typeof field !== 'object' || field === null || Array.isArray(field)) continue;
|
|
457
|
+
found.push({ name, version: typeof json.version === 'string' ? json.version : '?', range: json.peerDependencies?.netjes, nuget: field.nuget });
|
|
458
|
+
}
|
|
459
|
+
if (!found.length) return [];
|
|
460
|
+
|
|
461
|
+
/** @type {Check[]} */
|
|
462
|
+
const checks = [ok(`page packages: ${found.map((p) => `${p.name} ${p.version}`).join(', ')}`)];
|
|
463
|
+
for (const p of found) {
|
|
464
|
+
const named = `${p.name} ${p.version}`;
|
|
465
|
+
if (typeof p.range !== 'string' || p.range.trim() === '') {
|
|
466
|
+
checks.push(
|
|
467
|
+
fail(
|
|
468
|
+
`${named} has no peerDependencies.netjes, so the app refuses to mount it`,
|
|
469
|
+
`A page package names the Netjes versions it supports there: install a version of ${p.name} that does`,
|
|
470
|
+
),
|
|
471
|
+
);
|
|
472
|
+
} else if (netjes && !satisfies(netjes, p.range, { strict: true })) {
|
|
473
|
+
// As the app and the Vite plugin say it; both stop on it.
|
|
474
|
+
checks.push(
|
|
475
|
+
fail(
|
|
476
|
+
`${named} supports Netjes ${p.range}; this app runs Netjes ${netjes}`,
|
|
477
|
+
`Install a version of ${p.name} that supports netjes ${netjes}, or a netjes in ${p.range}: the app stops at startup otherwise`,
|
|
478
|
+
),
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
const reference = csproj && typeof p.nuget === 'string' ? packageReference(csproj.text, p.nuget) : null;
|
|
482
|
+
if (reference?.version && reference.version !== p.version) {
|
|
483
|
+
checks.push(
|
|
484
|
+
fail(
|
|
485
|
+
`${csproj?.file} references ${p.nuget} ${reference.version}, but node_modules has ${named}`,
|
|
486
|
+
'Use one version of both halves of the page package: change the PackageReference or package.json; the app stops at startup otherwise',
|
|
487
|
+
),
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
// The name in quotes, so that '@acme/help-center-extra' does not list '@acme/help-center'.
|
|
491
|
+
if (viteConfig && !new RegExp(`(['"\`])${escapeRegExp(p.name)}\\1`).test(viteConfig.text)) {
|
|
492
|
+
checks.push(
|
|
493
|
+
warn(
|
|
494
|
+
`${named} is not in netjes({ packages }) in ${viteConfig.file}`,
|
|
495
|
+
`List it there to build its pages, as in netjes({ packages: ['${p.name}'] }), and mount it with app.MapPagePackage; or uninstall it`,
|
|
496
|
+
),
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
return checks;
|
|
501
|
+
}
|
|
502
|
+
|
|
275
503
|
/**
|
|
276
504
|
* The newest .cs file of the app's own, relative to `cwd` with / separators.
|
|
277
505
|
* @param {string} cwd
|
|
@@ -280,12 +508,15 @@ function checkReference(cwd, csproj, text, npmVersion, cliVersion) {
|
|
|
280
508
|
async function newestSource(cwd) {
|
|
281
509
|
/** @type {{ file: string; mtimeMs: number } | null} */
|
|
282
510
|
let newest = null;
|
|
511
|
+
// The build leaves out `Tests/` too where the file system ignores case, as it then finds `tests/` there.
|
|
512
|
+
const isTestProjects = (/** @type {string} */ file) =>
|
|
513
|
+
file.toLowerCase() === TEST_PROJECTS && (file === TEST_PROJECTS || existsSync(path.join(cwd, TEST_PROJECTS)));
|
|
283
514
|
/** @param {string} relative */
|
|
284
515
|
async function walk(relative) {
|
|
285
516
|
for (const entry of await readdir(path.join(cwd, relative), { withFileTypes: true })) {
|
|
286
517
|
const file = relative ? `${relative}/${entry.name}` : entry.name;
|
|
287
518
|
if (entry.isDirectory()) {
|
|
288
|
-
if (!SKIPPED.has(entry.name) && !entry.name.startsWith('.')) await walk(file);
|
|
519
|
+
if (!SKIPPED.has(entry.name) && !entry.name.startsWith('.') && !isTestProjects(file)) await walk(file);
|
|
289
520
|
} else if (entry.name.endsWith('.cs')) {
|
|
290
521
|
const { mtimeMs } = await stat(path.join(cwd, file));
|
|
291
522
|
if (!newest || mtimeMs > newest.mtimeMs) newest = { file, mtimeMs };
|
|
@@ -325,14 +556,25 @@ export async function checkProject(cwd, cliVersion) {
|
|
|
325
556
|
);
|
|
326
557
|
|
|
327
558
|
const viteConfig = entries.find((e) => /^vite\.config\.(ts|mts|js|mjs)$/.test(e));
|
|
559
|
+
const viteText = viteConfig ? await readFile(path.join(cwd, viteConfig), 'utf8') : null;
|
|
328
560
|
if (!viteConfig) {
|
|
329
561
|
checks.push(fail('No vite.config.ts', "Add one: export default defineConfig({ plugins: [netjes()] }), with netjes from 'netjes/vite'"));
|
|
330
|
-
} else if (!
|
|
562
|
+
} else if (!viteText?.includes('netjes(')) {
|
|
331
563
|
checks.push(fail(`${viteConfig} does not use netjes() from 'netjes/vite'`, 'Add netjes() to its plugins'));
|
|
332
564
|
} else checks.push(ok(`${viteConfig} uses netjes()`));
|
|
333
565
|
|
|
566
|
+
const manifest = hasManifest ? await readJson(path.join(cwd, 'package.json')) : null;
|
|
567
|
+
checks.push(
|
|
568
|
+
...(await checkPagePackages(cwd, {
|
|
569
|
+
manifest,
|
|
570
|
+
netjes: packages.netjes,
|
|
571
|
+
csproj: csproj ? { file: csproj, text: await readFile(path.join(cwd, csproj), 'utf8') } : null,
|
|
572
|
+
// A config without netjes() is flagged above already.
|
|
573
|
+
viteConfig: viteConfig && viteText?.includes('netjes(') ? { file: viteConfig, text: viteText } : null,
|
|
574
|
+
})),
|
|
575
|
+
);
|
|
576
|
+
|
|
334
577
|
if (hasManifest) {
|
|
335
|
-
const manifest = await readJson(path.join(cwd, 'package.json'));
|
|
336
578
|
if (!manifest) checks.push(fail('package.json is not valid JSON', 'Fix its syntax; npm and Vite read it'));
|
|
337
579
|
else if (manifest.scripts?.build) checks.push(ok('package.json has a build script (dotnet publish runs it)'));
|
|
338
580
|
else {
|