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/vite/plugin.mjs
CHANGED
|
@@ -1,12 +1,29 @@
|
|
|
1
1
|
// @ts-check
|
|
2
|
-
import {
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { copyFile, mkdir, readdir, writeFile } from 'node:fs/promises';
|
|
3
4
|
import { createRequire } from 'node:module';
|
|
4
5
|
import path from 'node:path';
|
|
5
6
|
import { fileURLToPath } from 'node:url';
|
|
6
7
|
import react from '@vitejs/plugin-react';
|
|
7
8
|
import { normalizePath, searchForWorkspaceRoot } from 'vite';
|
|
9
|
+
import { findPackage } from '../cli/packages.mjs';
|
|
10
|
+
import {
|
|
11
|
+
CONVENTION_EXTENSIONS,
|
|
12
|
+
CONVENTION_FILES,
|
|
13
|
+
commonJsDependencies,
|
|
14
|
+
fileId,
|
|
15
|
+
filePath,
|
|
16
|
+
isConventionFile,
|
|
17
|
+
isPackageName,
|
|
18
|
+
pagePackageFiles,
|
|
19
|
+
resolvePagePackages,
|
|
20
|
+
unsupportedNetjes,
|
|
21
|
+
withPackageStyles,
|
|
22
|
+
} from './packages.mjs';
|
|
8
23
|
|
|
9
24
|
const PACKAGE_ROOT = fileURLToPath(new URL('..', import.meta.url));
|
|
25
|
+
/** The version of this netjes, the app's: what a page package's peer range has to hold. */
|
|
26
|
+
const VERSION = /** @type {{ version: string }} */ (JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'))).version;
|
|
10
27
|
// The compiled modules the package exports (npm run build writes them), so the app runs the code it type-checked, and
|
|
11
28
|
// gets one copy of each module: the same files the app's own imports of netjes resolve to. Vite names a file by its path
|
|
12
29
|
// with forward slashes, on Windows too, so these are named that way.
|
|
@@ -15,11 +32,20 @@ const SERVER_ENTRY = normalizePath(fileURLToPath(new URL('../dist/server-entry.j
|
|
|
15
32
|
const SIDECAR = fileURLToPath(new URL('../sidecar/server.mjs', import.meta.url));
|
|
16
33
|
const QUERY_MODULE = normalizePath(fileURLToPath(new URL('../dist/query.js', import.meta.url)));
|
|
17
34
|
const TEST_SETUP = normalizePath(fileURLToPath(new URL('../dist/testing-setup.js', import.meta.url)));
|
|
35
|
+
// What the test harness needs from Vitest: the setup file every app's tests get, after the app's own. Not exported.
|
|
36
|
+
const TEST_VITEST = normalizePath(fileURLToPath(new URL('../dist/testing-vitest.js', import.meta.url)));
|
|
18
37
|
|
|
19
38
|
const PAGES_ID = 'virtual:netjes/pages';
|
|
20
39
|
const RESOLVED_PAGES_ID = `\0${PAGES_ID}`;
|
|
21
40
|
const QUERY_ID = 'virtual:netjes/query';
|
|
22
41
|
const RESOLVED_QUERY_ID = `\0${QUERY_ID}`;
|
|
42
|
+
/**
|
|
43
|
+
* A module @tailwindcss/vite builds, by its rule: a `.css` file, unless it is imported as text, as a URL or as a worker,
|
|
44
|
+
* which by the time a plugin transforms it is a JavaScript module that exports the text.
|
|
45
|
+
* @param {string} id
|
|
46
|
+
*/
|
|
47
|
+
const isTailwindStylesheet = (id) =>
|
|
48
|
+
!/[?&](?:worker|sharedworker|raw|url)\b|\?commonjs-proxy/.test(id) && !id.includes('/.vite/') && (id.split('?')[0] ?? id).endsWith('.css');
|
|
23
49
|
/** The URL the .NET dev asset resolver points browsers at. */
|
|
24
50
|
const DEV_CLIENT_ENTRY_URL = '/@netjes/client-entry';
|
|
25
51
|
|
|
@@ -38,10 +64,24 @@ const BACKEND_DIRS = ['bin', 'obj', 'dist/server'];
|
|
|
38
64
|
* @param {string} workspaceRoot
|
|
39
65
|
*/
|
|
40
66
|
function backendDeny(workspaceRoot) {
|
|
41
|
-
|
|
42
|
-
return [...BACKEND_FILES, ...BACKEND_DIRS.map((dir) => `${root}/**/${dir}/**`)];
|
|
67
|
+
return [...BACKEND_FILES, ...backendDirs(workspaceRoot)];
|
|
43
68
|
}
|
|
44
69
|
|
|
70
|
+
/**
|
|
71
|
+
* The backend folders below a folder, such as a page package's own `bin/` and `obj/`.
|
|
72
|
+
* @param {string} dir
|
|
73
|
+
*/
|
|
74
|
+
function backendDirs(dir) {
|
|
75
|
+
const root = normalizePath(dir).replace(/[()[\]{}!+@*?\\]/g, '\\$&');
|
|
76
|
+
return BACKEND_DIRS.map((folder) => `${root}/**/${folder}/**`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* A path as a glob that matches it alone: the characters a glob reads, escaped as Vite's glob (tinyglobby) escapes them.
|
|
81
|
+
* @param {string} file With `/` separators.
|
|
82
|
+
*/
|
|
83
|
+
const literalGlob = (file) => file.replace(/[()[\]{}*?|]|[!+@](?=\()/g, '\\$&');
|
|
84
|
+
|
|
45
85
|
/** Loads @tailwindcss/vite only when Tailwind is on, so apps that turn it off need not install it. */
|
|
46
86
|
async function tailwind() {
|
|
47
87
|
try {
|
|
@@ -72,18 +112,27 @@ function canResolve(root, name) {
|
|
|
72
112
|
|
|
73
113
|
/**
|
|
74
114
|
* 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.
|
|
115
|
+
* Library cleanup (when it is installed), and no Playwright specs (e2e/), build output or .NET bin/obj folders. Two
|
|
116
|
+
* exceptions, which Vite merges into what the app set: Netjes's own setup file goes into every app's setupFiles,
|
|
117
|
+
* after the app's own, and provide['netjes:query'] holds the app's query defaultOptions, which that file hands to
|
|
118
|
+
* createTestApp. Only that key is returned, so the app's own provide keys are not merged twice. Vitest 5 builds the
|
|
119
|
+
* test options of an inline test project that shares the root's Vite server from the root's options as written, before
|
|
120
|
+
* this hook runs, so such a project gets none of these; with sharedViteServer: false it resolves the config, and this
|
|
121
|
+
* hook, for each project.
|
|
76
122
|
* @param {Record<string, unknown> | undefined} test
|
|
77
123
|
* @param {string} root
|
|
124
|
+
* @param {{ defaultOptions?: import('@tanstack/react-query').DefaultOptions } | null} query
|
|
78
125
|
*/
|
|
79
|
-
function testDefaults(test = {}, root) {
|
|
126
|
+
function testDefaults(test = {}, root, query) {
|
|
80
127
|
/** @type {Record<string, unknown>} */
|
|
81
128
|
const defaults = {};
|
|
82
129
|
if (test.environment === undefined && canResolve(root, 'jsdom')) defaults.environment = 'jsdom';
|
|
83
130
|
if (test.exclude === undefined) {
|
|
84
131
|
defaults.exclude = ['**/node_modules/**', '**/dist/**', '**/bin/**', '**/obj/**', '**/e2e/**', '**/.git/**'];
|
|
85
132
|
}
|
|
86
|
-
|
|
133
|
+
const cleanup = test.setupFiles === undefined && canResolve(root, '@testing-library/react');
|
|
134
|
+
defaults.setupFiles = cleanup ? [TEST_SETUP, TEST_VITEST] : [TEST_VITEST];
|
|
135
|
+
if (query) defaults.provide = { 'netjes:query': query.defaultOptions ?? {} };
|
|
87
136
|
return defaults;
|
|
88
137
|
}
|
|
89
138
|
|
|
@@ -150,7 +199,11 @@ async function findRouteHandler(root, appDir) {
|
|
|
150
199
|
export default function netjes(options = {}) {
|
|
151
200
|
const appDir = (options.appDir ?? 'app').replace(/^\/+|\/+$/g, '');
|
|
152
201
|
// Only the convention files are routes; everything else in app/ is colocated code.
|
|
153
|
-
const appFiles = `/${appDir}/**/{
|
|
202
|
+
const appFiles = `/${appDir}/**/{${CONVENTION_FILES.join(',')}}.{${CONVENTION_EXTENSIONS.map((ext) => ext.slice(1)).join(',')}}`;
|
|
203
|
+
const packageNames = options.packages ?? [];
|
|
204
|
+
if (packageNames.length > 0 && options.tailwind === false) {
|
|
205
|
+
throw new Error('netjes: page packages style their pages with Tailwind CSS, which netjes({ tailwind: false }) turns off: turn it on, or list no packages');
|
|
206
|
+
}
|
|
154
207
|
const basePath = (options.basePath ?? '').replace(/\/+$/, '');
|
|
155
208
|
const query = options.query === false ? null : options.query === true || options.query === undefined ? {} : options.query;
|
|
156
209
|
// Fail when the config loads, not in the browser.
|
|
@@ -163,6 +216,47 @@ export default function netjes(options = {}) {
|
|
|
163
216
|
let outDir = '';
|
|
164
217
|
/** @type {import('vite').ViteDevServer | undefined} */
|
|
165
218
|
let devServer;
|
|
219
|
+
/** The page packages the app lists, read when the config is. @type {import('./packages.mjs').PagePackage[]} */
|
|
220
|
+
let packages = [];
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Has the next render, and the next page the browser loads, build the page module map again from the files on disk,
|
|
224
|
+
* as Vite does once its watcher reports a page file added or removed.
|
|
225
|
+
*/
|
|
226
|
+
function rescanPages() {
|
|
227
|
+
const timestamp = Date.now();
|
|
228
|
+
for (const { moduleGraph } of Object.values(devServer?.environments ?? {})) {
|
|
229
|
+
const pages = moduleGraph.getModuleById(RESOLVED_PAGES_ID);
|
|
230
|
+
if (pages) moduleGraph.invalidateModule(pages, new Set(), timestamp, true);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* The listed package a file id is of, with the file's path in its folder of pages.
|
|
236
|
+
* @param {string} id
|
|
237
|
+
*/
|
|
238
|
+
function packageFile(id) {
|
|
239
|
+
for (const pkg of packages) {
|
|
240
|
+
const prefix = `${pkg.name}/${pkg.pages}/`;
|
|
241
|
+
if (id.startsWith(prefix)) return { pkg, file: id.slice(prefix.length) };
|
|
242
|
+
}
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* The name of the installed package whose file a file id names, when the app does not list it in netjes({ packages }):
|
|
248
|
+
* the .NET app mounts it, and the page module map does not have it.
|
|
249
|
+
* @param {string} id
|
|
250
|
+
* @returns {string | null}
|
|
251
|
+
*/
|
|
252
|
+
function unlistedPackage(id) {
|
|
253
|
+
if (id.startsWith(`${appDir}/`) || packageFile(id)) return null;
|
|
254
|
+
const segments = id.split('/');
|
|
255
|
+
const length = id.startsWith('@') ? 2 : 1;
|
|
256
|
+
if (segments.length <= length) return null;
|
|
257
|
+
const name = segments.slice(0, length).join('/');
|
|
258
|
+
return isPackageName(name) && findPackage(rootDir, name) ? name : null;
|
|
259
|
+
}
|
|
166
260
|
|
|
167
261
|
/** @type {import('vite').Plugin} */
|
|
168
262
|
const plugin = {
|
|
@@ -174,7 +268,14 @@ export default function netjes(options = {}) {
|
|
|
174
268
|
const userTest = /** @type {{ test?: Record<string, unknown> }} */ (/** @type {unknown} */ (config)).test;
|
|
175
269
|
const root = path.resolve(config.root ?? process.cwd());
|
|
176
270
|
const workspaceRoot = searchForWorkspaceRoot(root);
|
|
177
|
-
|
|
271
|
+
packages = resolvePagePackages(root, packageNames, appDir);
|
|
272
|
+
// A build stops here, and the development sidecar reports it as Vite failing to start.
|
|
273
|
+
for (const pkg of packages) {
|
|
274
|
+
const unsupported = unsupportedNetjes(pkg, VERSION);
|
|
275
|
+
if (unsupported) throw new Error(`netjes: ${unsupported}`);
|
|
276
|
+
}
|
|
277
|
+
const listed = packages.map((pkg) => pkg.name);
|
|
278
|
+
const test = env.command === 'serve' && options.test !== false ? { test: testDefaults(userTest, root, query) } : {};
|
|
178
279
|
// Built asset URLs live under the base path; in development the browser loads modules from the Vite server's root.
|
|
179
280
|
const base = env.command === 'build' && basePath ? { base: `${basePath}/` } : {};
|
|
180
281
|
return {
|
|
@@ -189,21 +290,30 @@ export default function netjes(options = {}) {
|
|
|
189
290
|
...(query ? ['@tanstack/react-query'] : []),
|
|
190
291
|
// The test setup's cleanup must unmount what the app's tests rendered.
|
|
191
292
|
...(env.mode === 'test' ? ['@testing-library/react'] : []),
|
|
293
|
+
// A page package's pages import netjes and zod, which a linked package would otherwise resolve from its own
|
|
294
|
+
// folder: a second copy, or none at all for one linked from outside the workspace.
|
|
295
|
+
...(packages.length > 0 ? ['netjes', ...(canResolve(root, 'zod') ? ['zod'] : [])] : []),
|
|
192
296
|
],
|
|
193
297
|
},
|
|
194
298
|
// netjes serves its own modules (client entry, runtime) and, for their source maps, its sources; a linked
|
|
195
|
-
// package lives outside the app
|
|
196
|
-
// fs.allow itself: Vite concatenates arrays. The dev root is
|
|
197
|
-
//
|
|
299
|
+
// package lives outside the app, and so does a page package linked from elsewhere. The workspace root keeps
|
|
300
|
+
// Vite's default, but only when the app did not narrow fs.allow itself: Vite concatenates arrays. The dev root is
|
|
301
|
+
// the .NET content root, so deny the backend's files, and a page package's backend folders wherever it is (its
|
|
302
|
+
// C# half's build output, in a checkout linked from outside the workspace). A configured deny list replaces
|
|
303
|
+
// Vite's defaults, which are repeated here.
|
|
198
304
|
server: {
|
|
199
305
|
fs: {
|
|
200
|
-
allow: [...(config.server?.fs?.allow ? [] : [workspaceRoot]), PACKAGE_ROOT],
|
|
201
|
-
deny: [...VITE_DENY, ...backendDeny(workspaceRoot)],
|
|
306
|
+
allow: [...(config.server?.fs?.allow ? [] : [workspaceRoot]), PACKAGE_ROOT, ...packages.map((pkg) => pkg.root)],
|
|
307
|
+
deny: [...VITE_DENY, ...backendDeny(workspaceRoot), ...packages.flatMap((pkg) => backendDirs(pkg.root))],
|
|
202
308
|
},
|
|
203
309
|
},
|
|
204
310
|
optimizeDeps: {
|
|
205
|
-
// There is no index.html to scan; start from the pages so dependencies are pre-bundled up front
|
|
206
|
-
|
|
311
|
+
// There is no index.html to scan; start from the pages so dependencies are pre-bundled up front, a page
|
|
312
|
+
// package's included: Vite globs these from the root, in node_modules and outside the root too.
|
|
313
|
+
entries: [
|
|
314
|
+
appFiles.slice(1),
|
|
315
|
+
...packages.map((pkg) => `${literalGlob(normalizePath(path.relative(root, `${pkg.root}/${pkg.pages}`)))}/${appFiles.slice(appDir.length + 2)}`),
|
|
316
|
+
],
|
|
207
317
|
include: [
|
|
208
318
|
'react',
|
|
209
319
|
'react-dom',
|
|
@@ -211,14 +321,19 @@ export default function netjes(options = {}) {
|
|
|
211
321
|
...(query ? ['@tanstack/react-query'] : []),
|
|
212
322
|
// Imported from inside netjes (netjes/vitals), which the dependency scan skips: found late, it would reload the page.
|
|
213
323
|
...(canResolve(root, 'web-vitals') ? ['web-vitals'] : []),
|
|
324
|
+
// A page package is not pre-bundled (below), so its CommonJS dependencies are, through it, as Vite asks.
|
|
325
|
+
...packages.flatMap((pkg) => commonJsDependencies(pkg).map((dependency) => `${pkg.name} > ${dependency}`)),
|
|
214
326
|
],
|
|
215
327
|
// netjes imports the virtual modules below, which pre-bundling cannot resolve, and the plugin loads its query
|
|
216
328
|
// shell by path: a pre-bundled copy of netjes would hold a second router, whose contexts the app's pages miss.
|
|
217
|
-
|
|
329
|
+
// A page package's pages are loaded by path, so a pre-bundled copy of it would be a second one of each of its
|
|
330
|
+
// modules too.
|
|
331
|
+
exclude: ['netjes', ...listed],
|
|
218
332
|
},
|
|
219
333
|
// netjes imports the plugin's virtual modules and must share its module instances with the app's pages, so Vite
|
|
220
|
-
// processes it in development as well
|
|
221
|
-
|
|
334
|
+
// processes it in development as well, and a page package's TypeScript sources with it; production bundles
|
|
335
|
+
// everything for the sidecar.
|
|
336
|
+
ssr: { noExternal: env.command === 'build' ? true : ['netjes', ...listed] },
|
|
222
337
|
build: isSsrBuild
|
|
223
338
|
? {
|
|
224
339
|
outDir: 'dist/server',
|
|
@@ -243,27 +358,52 @@ export default function netjes(options = {}) {
|
|
|
243
358
|
// takes only the convention files, so it would leave one out without a word: it stops on it too, saying the same.
|
|
244
359
|
async buildStart() {
|
|
245
360
|
if (!isBuild) return;
|
|
246
|
-
|
|
361
|
+
let handler = await findRouteHandler(rootDir, appDir);
|
|
362
|
+
// A package's, by its file id, as the route table names it.
|
|
363
|
+
for (const pkg of packages) {
|
|
364
|
+
if (handler) break;
|
|
365
|
+
const found = await findRouteHandler(pkg.root, pkg.pages);
|
|
366
|
+
if (found) handler = `${pkg.name}/${found}`;
|
|
367
|
+
}
|
|
247
368
|
if (handler) {
|
|
248
369
|
this.error(`'${handler}': route handlers are C# in Netjes. Map a minimal API endpoint instead (app.MapGet, app.MapPost, ...).`);
|
|
249
370
|
}
|
|
250
371
|
},
|
|
251
372
|
configureServer(server) {
|
|
252
373
|
devServer = server;
|
|
374
|
+
// Vite watches the root only, so a linked package's files, which can change while the app runs, are added. The
|
|
375
|
+
// map imports a package's files one by one, so a page that comes or goes builds it again, as the glob would.
|
|
376
|
+
const linked = packages.filter((pkg) => pkg.linked);
|
|
377
|
+
if (linked.length === 0) return;
|
|
378
|
+
server.watcher.add([...new Set(linked.flatMap((pkg) => [pkg.pages, ...pkg.sources].map((dir) => `${pkg.root}/${dir}`)))]);
|
|
379
|
+
/** @param {string} file */
|
|
380
|
+
const pageAddedOrRemoved = (file) => {
|
|
381
|
+
const name = normalizePath(file);
|
|
382
|
+
if (isConventionFile(path.posix.basename(name)) && linked.some((pkg) => name.startsWith(`${pkg.root}/${pkg.pages}/`))) rescanPages();
|
|
383
|
+
};
|
|
384
|
+
server.watcher.on('add', pageAddedOrRemoved);
|
|
385
|
+
server.watcher.on('unlink', pageAddedOrRemoved);
|
|
253
386
|
},
|
|
254
387
|
// For the development sidecar, which finds this plugin in its dev server's config.
|
|
255
388
|
api: {
|
|
256
389
|
/**
|
|
257
|
-
*
|
|
258
|
-
*
|
|
390
|
+
* The URL the dev server loads a file by, from the file's id in a payload: `/app/...` for the app's, and for a
|
|
391
|
+
* listed package's the URL of its real path (`/node_modules/...` inside the root, `/@fs/...` outside it). Null for
|
|
392
|
+
* a file of an installed package that netjes({ packages }) does not list, which the page module map lacks.
|
|
393
|
+
* @param {string} id
|
|
394
|
+
* @returns {string | null}
|
|
259
395
|
*/
|
|
260
|
-
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
}
|
|
396
|
+
pageUrl(id) {
|
|
397
|
+
const owned = packageFile(id);
|
|
398
|
+
if (!owned) return unlistedPackage(id) ? null : `/${id}`;
|
|
399
|
+
const file = filePath(owned.pkg, owned.file);
|
|
400
|
+
const relative = path.posix.relative(normalizePath(rootDir), file);
|
|
401
|
+
return relative.startsWith('../') || path.isAbsolute(relative) ? `/@fs/${file.replace(/^\//, '')}` : `/${relative}`;
|
|
266
402
|
},
|
|
403
|
+
unlistedPackage,
|
|
404
|
+
/** The page packages the app lists. */
|
|
405
|
+
packages: () => packages,
|
|
406
|
+
rescanPages,
|
|
267
407
|
},
|
|
268
408
|
resolveId(id) {
|
|
269
409
|
if (id === PAGES_ID) return RESOLVED_PAGES_ID;
|
|
@@ -271,21 +411,71 @@ export default function netjes(options = {}) {
|
|
|
271
411
|
if (id === DEV_CLIENT_ENTRY_URL) return CLIENT_ENTRY;
|
|
272
412
|
return undefined;
|
|
273
413
|
},
|
|
274
|
-
load(id) {
|
|
414
|
+
async load(id) {
|
|
275
415
|
if (id === RESOLVED_QUERY_ID) {
|
|
276
416
|
if (!query) return 'export const QueryShell = undefined;\n';
|
|
277
417
|
return `import { createQueryShell } from ${JSON.stringify(QUERY_MODULE)};\nexport const QueryShell = createQueryShell(${queryLiteral});\n`;
|
|
278
418
|
}
|
|
279
419
|
if (id !== RESOLVED_PAGES_ID) return undefined;
|
|
280
|
-
const glob = JSON.stringify([appFiles])
|
|
281
|
-
|
|
420
|
+
const glob = `import.meta.glob(${JSON.stringify([appFiles])})`;
|
|
421
|
+
// A package's files are imported one by one, by their real paths: a glob leaves out node_modules, and would name a
|
|
422
|
+
// linked package's files by their paths through the link. Listed again each time, as the glob is.
|
|
423
|
+
const imported = (
|
|
424
|
+
await Promise.all(
|
|
425
|
+
packages.map(async (pkg) =>
|
|
426
|
+
(await pagePackageFiles(pkg)).map((file) => `${JSON.stringify(`/${fileId(pkg, file)}`)}: () => import(${JSON.stringify(filePath(pkg, file))})`),
|
|
427
|
+
),
|
|
428
|
+
)
|
|
429
|
+
).flat();
|
|
430
|
+
const pages = packages.length === 0 ? glob : `{ ...${glob}, ${imported.join(', ')} }`;
|
|
431
|
+
// The folder the keys start with: renderDocument (netjes/testing) builds routes and segment ids from it.
|
|
432
|
+
return (
|
|
433
|
+
`export const pages = ${pages};\nexport const appDir = ${JSON.stringify(appDir)};\n` +
|
|
434
|
+
`export const packages = ${JSON.stringify(packages.map(({ name, pages }) => ({ name, pages })))};\n`
|
|
435
|
+
);
|
|
282
436
|
},
|
|
283
437
|
async writeBundle() {
|
|
284
|
-
if (
|
|
285
|
-
|
|
286
|
-
|
|
438
|
+
if (isSsrBuild) {
|
|
439
|
+
await mkdir(outDir, { recursive: true });
|
|
440
|
+
await copyFile(SIDECAR, path.join(outDir, 'netjes-sidecar.mjs'));
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
if (packages.length === 0) return;
|
|
444
|
+
// Where the .NET app finds the packages in production, next to the manifest: no node_modules there to read them
|
|
445
|
+
// from, and a manifest key alone does not say which package a file is of.
|
|
446
|
+
const record = {
|
|
447
|
+
packages: await Promise.all(
|
|
448
|
+
packages.map(async (pkg) => ({
|
|
449
|
+
name: pkg.name,
|
|
450
|
+
version: pkg.version,
|
|
451
|
+
netjes: pkg.netjes,
|
|
452
|
+
pages: pkg.pages,
|
|
453
|
+
// Each file's id, and its key in the manifest: its path from the root, as Vite names a chunk's source.
|
|
454
|
+
files: Object.fromEntries((await pagePackageFiles(pkg)).map((file) => [fileId(pkg, file), normalizePath(path.relative(rootDir, filePath(pkg, file)))])),
|
|
455
|
+
// The NuGet package of its C# half, which the app names when the two halves are different versions.
|
|
456
|
+
...(pkg.nuget === null ? {} : { nuget: pkg.nuget }),
|
|
457
|
+
})),
|
|
458
|
+
),
|
|
459
|
+
};
|
|
460
|
+
await mkdir(path.join(outDir, '.vite'), { recursive: true });
|
|
461
|
+
await writeFile(path.join(outDir, '.vite', 'netjes.json'), `${JSON.stringify(record, null, 2)}\n`);
|
|
462
|
+
},
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Adds each page package's stylesheet and source folders to the app's Tailwind CSS build. Before @tailwindcss/vite,
|
|
467
|
+
* whose plugins run before the others too (`enforce: 'pre'`), so the order in the list decides.
|
|
468
|
+
* @type {import('vite').Plugin}
|
|
469
|
+
*/
|
|
470
|
+
const packageStyles = {
|
|
471
|
+
name: 'netjes:package-styles',
|
|
472
|
+
enforce: 'pre',
|
|
473
|
+
transform(code, id) {
|
|
474
|
+
if (!isTailwindStylesheet(id)) return undefined;
|
|
475
|
+
const css = withPackageStyles(code, id.split('?')[0] ?? id, packages);
|
|
476
|
+
return css === code ? undefined : { code: css, map: null };
|
|
287
477
|
},
|
|
288
478
|
};
|
|
289
479
|
|
|
290
|
-
return [react(options.react), plugin, options.tailwind === false ? null : tailwind()];
|
|
480
|
+
return [react(options.react), plugin, packageNames.length > 0 ? packageStyles : null, options.tailwind === false ? null : tailwind()];
|
|
291
481
|
}
|