@_linked/server 2.0.3 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/.github/workflows/publish.yml +13 -1
  2. package/CHANGELOG.md +26 -0
  3. package/lib/esm/index.d.ts +1 -1
  4. package/lib/esm/index.js +1 -1
  5. package/lib/esm/index.js.map +1 -1
  6. package/lib/esm/ontologies/lincd-server.d.ts +42 -25
  7. package/lib/esm/shapes/LincdAPI.d.ts +3 -3
  8. package/lib/esm/shapes/LincdWebApp.d.ts +1 -1
  9. package/lib/esm/shapes/{LincdServer.d.ts → LinkedServer.d.ts} +30 -2
  10. package/lib/esm/shapes/{LincdServer.js → LinkedServer.js} +442 -155
  11. package/lib/esm/shapes/LinkedServer.js.map +1 -0
  12. package/lib/esm/shapes/quadstores/BackendAPIStore.d.ts +1 -1
  13. package/lib/esm/shapes/quadstores/BackendAPIStore.js +7 -7
  14. package/lib/esm/shapes/quadstores/BackendAPIStore.js.map +1 -1
  15. package/lib/esm/shapes/quadstores/BackendAPIStoreProvider.d.ts +10 -5
  16. package/lib/esm/shapes/quadstores/BackendAPIStoreProvider.js +15 -9
  17. package/lib/esm/shapes/quadstores/BackendAPIStoreProvider.js.map +1 -1
  18. package/lib/esm/utils/Shapes.d.ts +1 -1
  19. package/lib/esm/utils/Shapes.js +4 -3
  20. package/lib/esm/utils/Shapes.js.map +1 -1
  21. package/package.json +15 -17
  22. package/readme.md +100 -0
  23. package/tsconfig-esm.json +1 -1
  24. package/lib/cjs/backend.d.ts +0 -6
  25. package/lib/cjs/backend.js +0 -58
  26. package/lib/cjs/backend.js.map +0 -1
  27. package/lib/cjs/data/lincd-server.json +0 -47
  28. package/lib/cjs/index.d.ts +0 -9
  29. package/lib/cjs/index.js +0 -13
  30. package/lib/cjs/index.js.map +0 -1
  31. package/lib/cjs/ontologies/lincd-server.d.ts +0 -31
  32. package/lib/cjs/ontologies/lincd-server.js +0 -86
  33. package/lib/cjs/ontologies/lincd-server.js.map +0 -1
  34. package/lib/cjs/package.d.ts +0 -4
  35. package/lib/cjs/package.js +0 -7
  36. package/lib/cjs/package.js.map +0 -1
  37. package/lib/cjs/package.json +0 -90
  38. package/lib/cjs/shapes/LincdAPI.d.ts +0 -41
  39. package/lib/cjs/shapes/LincdAPI.js +0 -124
  40. package/lib/cjs/shapes/LincdAPI.js.map +0 -1
  41. package/lib/cjs/shapes/LincdServer.d.ts +0 -79
  42. package/lib/cjs/shapes/LincdServer.js +0 -1339
  43. package/lib/cjs/shapes/LincdServer.js.map +0 -1
  44. package/lib/cjs/shapes/LincdWebApp.d.ts +0 -7
  45. package/lib/cjs/shapes/LincdWebApp.js +0 -43
  46. package/lib/cjs/shapes/LincdWebApp.js.map +0 -1
  47. package/lib/cjs/shapes/filestores/LocalFileStore.d.ts +0 -42
  48. package/lib/cjs/shapes/filestores/LocalFileStore.js +0 -146
  49. package/lib/cjs/shapes/filestores/LocalFileStore.js.map +0 -1
  50. package/lib/cjs/shapes/quadstores/BackendAPIStore.d.ts +0 -33
  51. package/lib/cjs/shapes/quadstores/BackendAPIStore.js +0 -79
  52. package/lib/cjs/shapes/quadstores/BackendAPIStore.js.map +0 -1
  53. package/lib/cjs/shapes/quadstores/BackendAPIStoreProvider.d.ts +0 -13
  54. package/lib/cjs/shapes/quadstores/BackendAPIStoreProvider.js +0 -30
  55. package/lib/cjs/shapes/quadstores/BackendAPIStoreProvider.js.map +0 -1
  56. package/lib/cjs/types/RouteConfig.d.ts +0 -56
  57. package/lib/cjs/types/RouteConfig.js +0 -3
  58. package/lib/cjs/types/RouteConfig.js.map +0 -1
  59. package/lib/cjs/types.d.ts +0 -12
  60. package/lib/cjs/types.js +0 -1
  61. package/lib/cjs/types.js.map +0 -1
  62. package/lib/cjs/utils/Shapes.d.ts +0 -7
  63. package/lib/cjs/utils/Shapes.js +0 -76
  64. package/lib/cjs/utils/Shapes.js.map +0 -1
  65. package/lib/cjs/utils/accessUrl.d.ts +0 -6
  66. package/lib/cjs/utils/accessUrl.js +0 -13
  67. package/lib/cjs/utils/accessUrl.js.map +0 -1
  68. package/lib/esm/package.json +0 -90
  69. package/lib/esm/shapes/LincdServer.js.map +0 -1
@@ -9,13 +9,17 @@ var __metadata = (this && this.__metadata) || function (k, v) {
9
9
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
10
  };
11
11
  import chalk from 'chalk';
12
+ import { timingSafeEqual } from 'crypto';
12
13
  import events from 'events';
13
14
  import express from 'express';
14
15
  import fetchCookie from 'fetch-cookie';
15
16
  import * as fsNative from 'fs';
16
17
  import * as fs from 'fs/promises';
17
- import { getWebpackAppConfig } from '@_linked/cli/config-webpack-app';
18
- import { getLincdPackages } from '@_linked/cli/cli-methods';
18
+ // Plan-011: imports from @_linked/cli now go through ./lifecycle (a
19
+ // dynamic-import-free file) so Vite's SSR module graph doesn't pull
20
+ // in the legacy webpack flow and emit dozens of unanalyzable-import
21
+ // warnings on every boot.
22
+ import { getLincdPackages } from '@_linked/cli/lifecycle';
19
23
  import { getPackageJSON } from '@_linked/cli/utils';
20
24
  import { AppContextProvider } from '@_linked/server-utils/components/AppContext';
21
25
  import { JSONParser } from '@_linked/server-utils/utils/JSONParser';
@@ -34,16 +38,12 @@ import process from 'process';
34
38
  import * as React from 'react';
35
39
  import { renderToPipeableStream, renderToStaticMarkup } from 'react-dom/server';
36
40
  import { StaticRouter } from 'react-router-dom/server.js';
37
- import { rimraf } from 'rimraf';
38
41
  import sharp from 'sharp';
39
42
  import { Transform } from 'stream';
40
43
  import { CookieJar } from 'tough-cookie';
41
- import webpack from 'webpack';
42
- import webpackDevMiddleware from 'webpack-dev-middleware';
43
- import webpackHotMiddleware from 'webpack-hot-middleware';
44
44
  import { lincdServer } from '../ontologies/lincd-server.js';
45
45
  import { linkedShape } from '../package.js';
46
- import { syncShapes } from '../utils/Shapes.js';
46
+ import { indexShapesIntoMemory } from '../utils/Shapes.js';
47
47
  import { LincdAPI } from './LincdAPI.js';
48
48
  //prevent errors in node.js when (s)css files are imported in js
49
49
  const isProduction = process.env.NODE_ENV === 'production';
@@ -79,7 +79,7 @@ events.EventEmitter.prototype.setMaxListeners(500);
79
79
  // global['document'] = document;
80
80
  global['reactStaticRenderer'] = renderToStaticMarkup;
81
81
  autoLoadOntologyData(true);
82
- let LincdServer = class LincdServer extends Shape {
82
+ let LinkedServer = class LinkedServer extends Shape {
83
83
  /**
84
84
  * yarn linked start sends the contents of linked.config.js as an object to this constructor
85
85
  * @param n
@@ -152,9 +152,57 @@ let LincdServer = class LincdServer extends Shape {
152
152
  this.initPackage();
153
153
  this.server = express();
154
154
  await this.initBackendProviders();
155
- syncShapes();
155
+ indexShapesIntoMemory();
156
+ await this.materializeShapesIntoStore();
156
157
  return this;
157
158
  }
159
+ /**
160
+ * Materialize this server's registered shapes into the RDF store as SHACL
161
+ * (core `syncShapes` → pure `sh:NodeShape` + property shapes), so a running
162
+ * app's app-data holds the shapes its instances validate/query against
163
+ * (plan-010 T1e.2). This is the canonical, uniform reuse+create mechanism:
164
+ * whatever shapes the app package registers (published re-exports + generated)
165
+ * get materialized on boot (and re-run on shape-file HMR).
166
+ *
167
+ * Gated to servers whose DEFAULT dataset is a **concrete** materializable store
168
+ * (detected via `rawQuery`) — i.e. an app pointing at its app-data FusekiStore.
169
+ * CN's default is the context-routing `AppDataRouter` (no `rawQuery`; a bare
170
+ * `syncShapes` orphan-read would throw with no active project), so CN is skipped
171
+ * here — it materializes its own pinned native shapes in its storage config.
172
+ */
173
+ async materializeShapesIntoStore() {
174
+ var _a;
175
+ // Opt-out via `linked.config` `syncShapesOnBoot` (default true); env
176
+ // LINKED_SYNC_SHAPES_ON_BOOT overrides for deployment. CN sets
177
+ // `syncShapesOnBoot: false` in its own linked.config — its default dataset is
178
+ // a context-router that can't materialize context-free, and it syncs its own
179
+ // pinned shapes separately (linked.backend.storage.js).
180
+ const configFlag = (_a = this.config) === null || _a === void 0 ? void 0 : _a.syncShapesOnBoot;
181
+ const envFlag = process.env.LINKED_SYNC_SHAPES_ON_BOOT;
182
+ if (configFlag === false || envFlag === 'false') {
183
+ return;
184
+ }
185
+ const appData = LinkedStorage.getDefaultDataset();
186
+ if (!appData)
187
+ return;
188
+ try {
189
+ const { syncShapes } = await import('@_linked/core');
190
+ // Explicit target: materialize EVERY registered shape into the app's own
191
+ // data store regardless of per-shape routing/pins — syncShapes(ds) threads
192
+ // ds through the orphan-read + every delete→recreate. (No reliance on
193
+ // "whatever the default resolves to per shape".)
194
+ const thunks = await syncShapes(appData);
195
+ // Batched (not all-at-once) so we don't overwhelm Fuseki — the API hands
196
+ // back unexecuted thunks precisely so the caller paces them.
197
+ for (let i = 0; i < thunks.length; i += 8) {
198
+ await Promise.all(thunks.slice(i, i + 8).map((run) => run()));
199
+ }
200
+ console.log(`[LinkedServer] materialized ${thunks.length} shape(s) into app-data`);
201
+ }
202
+ catch (err) {
203
+ console.warn('[LinkedServer] shape materialization failed (non-fatal):', err);
204
+ }
205
+ }
158
206
  // async serveData(req,res) {
159
207
  // let nodeURI = `${req.protocol}://${req.get('host')}${req.originalUrl}`;
160
208
  //
@@ -163,6 +211,7 @@ let LincdServer = class LincdServer extends Shape {
163
211
  // })
164
212
  // }
165
213
  async start() {
214
+ var _a, _b, _c, _d;
166
215
  this.initPackage();
167
216
  // Static assets should come from the static store URL (versioned path),
168
217
  // not the upload store URL. storage-config sets STATIC_ACCESS_URL accordingly.
@@ -177,21 +226,40 @@ let LincdServer = class LincdServer extends Shape {
177
226
  LinkedFileStorage.accessURL ||
178
227
  '').replace(/\/$/, '');
179
228
  const staticAsset = (assetPath) => `${staticAccessURL}/public${assetPath}`;
180
- //for apps with multiple bundles this should be read from the webpack build manifest
229
+ // Bundle/manifest read (plan-011: Vite is the only supported build).
230
+ // Vite manifest lives at public/bundles/.vite/manifest.json.
181
231
  this.assets = {
182
- 'main.js': staticAsset('/bundles/main.bundle.js') + '?v=' + this.package.version, //output js bundle from Webpack
183
- 'main.css': staticAsset('/bundles/main.css'), //output css from Webpack
232
+ 'main.js': staticAsset('/bundles/main.bundle.js') + '?v=' + this.package.version,
233
+ 'main.css': staticAsset('/bundles/main.css'),
184
234
  };
185
235
  try {
186
- const manifestPath = path.resolve(process.cwd(), 'public/bundles/manifest.json');
187
- if (fsNative.existsSync(manifestPath)) {
188
- const manifestRaw = fsNative.readFileSync(manifestPath, 'utf-8');
189
- this.assets.manifest = JSON.parse(manifestRaw);
236
+ const viteManifestPath = path.resolve(process.cwd(), 'public/bundles/.vite/manifest.json');
237
+ if (fsNative.existsSync(viteManifestPath)) {
238
+ const manifestRaw = fsNative.readFileSync(viteManifestPath, 'utf-8');
239
+ const viteManifest = JSON.parse(manifestRaw);
240
+ this.assets.manifest = viteManifest;
241
+ // Resolve main entry per Vite manifest shape:
242
+ // { "src/index.tsx": { file: "assets/main-<hash>.js", css: [...] } }
243
+ const mainEntry = viteManifest['src/index.tsx'] ||
244
+ viteManifest['src/index.ts'] ||
245
+ Object.values(viteManifest).find((e) => e === null || e === void 0 ? void 0 : e.isEntry);
246
+ if (mainEntry === null || mainEntry === void 0 ? void 0 : mainEntry.file) {
247
+ this.assets['main.js'] = staticAsset(`/bundles/${mainEntry.file}`);
248
+ }
249
+ if ((_a = mainEntry === null || mainEntry === void 0 ? void 0 : mainEntry.css) === null || _a === void 0 ? void 0 : _a[0]) {
250
+ this.assets['main.css'] = staticAsset(`/bundles/${mainEntry.css[0]}`);
251
+ }
190
252
  }
191
253
  }
192
254
  catch (err) {
193
255
  console.warn('Could not load bundle manifest:', err);
194
256
  }
257
+ // Vite dev marker: when Vite is in use AND no Vite manifest exists yet
258
+ // (= dev mode, no `vite build` run), the HTML renderer skips
259
+ // pre-built CSS/JS link tags. Vite handles asset injection itself.
260
+ if (((_b = this.config.server) === null || _b === void 0 ? void 0 : _b.vite) && !((_c = this.assets.manifest) === null || _c === void 0 ? void 0 : _c['src/index.tsx'])) {
261
+ this.assets['__viteDev'] = '1';
262
+ }
195
263
  const isProduction = process.env.NODE_ENV === 'production';
196
264
  if (!isProduction && this.config.cssMode === 'tailwind') {
197
265
  this.assets['tailwind-cdn'] = 'https://cdn.tailwindcss.com';
@@ -214,7 +282,8 @@ let LincdServer = class LincdServer extends Shape {
214
282
  // });
215
283
  // before controllers
216
284
  await this.initBackendProviders();
217
- syncShapes();
285
+ indexShapesIntoMemory();
286
+ await this.materializeShapesIntoStore();
218
287
  //START OF EXPRESS ROUTES AND MIDDLEWARE
219
288
  //use cors
220
289
  // var corsOptions = {
@@ -230,116 +299,20 @@ let LincdServer = class LincdServer extends Shape {
230
299
  //UPDATE ive set level to 2 (low compression fast speed) because responses were taking too long (98% of server time was used by compress)
231
300
  // this.server.use(compress({level: 2}));
232
301
  await this.callGenericBackendProvidersMethod('setupBeforeControllers');
233
- // if development, run webpack from the server
234
- // for production you need to build the bundles before starting the server
235
- const skipBuild = process.env.NO_WEBPACK === 'true';
236
- if (isDevelopment && !skipBuild) {
237
- //three levels up because of lib/esm (2) instead of src (1)
238
- //@ts-ignore
239
- // const getWebpackConfig = (await import('../../../site.webpack.config.cjs')).default;
240
- // let webpackConfig = await getWebpackConfig();
241
- let webpackConfig = await getWebpackAppConfig();
242
- // const compare = (c1,c2,path?='') => {
243
- // for(let key1 of Object.keys(c1)) {
244
- // if(typeof c1[key1] === 'object') {
245
- // compare(c1[key1],c2[key1],path+='.'+key1);
246
- // } else {
247
- // if(c1[key1] !== c2[key1]) {
248
- // console.log("Difference "+path+": ");
249
- // console.log(c1[key1]);
250
- // console.log(c2[key1]);
251
- // }
252
- // }
253
- // }
254
- // }
255
- // compare(webpackConfig,webpackConfig2);
256
- //default to having webpack cache on and filesystem, unless explicitly set to false
257
- if (this.cacheWebpack === false) {
258
- webpackConfig.cache.type = 'memory';
259
- }
260
- else {
261
- webpackConfig.cache.type = 'filesystem';
262
- }
263
- //clean dist/build folder
264
- await rimraf(webpackConfig.output.path).catch((err) => {
265
- if (err) {
266
- console.warn(err);
267
- }
268
- });
269
- const compiler = webpack(webpackConfig, (err, stats) => {
270
- //watch build completed
271
- if (err) {
272
- console.error(err);
273
- }
274
- // Output stats JSON for analysis
275
- if (this.analyse) {
276
- fs.writeFile('./data/webpack-stats.json', JSON.stringify(stats.toJson({ all: true }), null, 2)).then(() => {
277
- console.log('Webpack stats written to ./data/webpack-stats.json');
278
- });
279
- }
280
- });
281
- if (!compiler) {
282
- //something went wrong with webpack config, error will be logged above
283
- return;
284
- }
285
- compiler.hooks.afterEmit.tap('cleanup-the-require-cache', () => {
286
- // After webpack rebuild, clear the files from the require cache,
287
- // so that next server side render wil be in sync
288
- // console.log(Object.keys(require.cache).filter(k => k.includes(dirName)).join("\n"));
289
- // if (typeof require !== 'undefined') {
290
- // Object.keys(require.cache)
291
- // .filter((key) => key.includes(dirName))
292
- // .forEach((key) => delete require.cache[key]);
293
- // }
294
- });
295
- //after the first emit (which means bundles are ready and the site is running) also rebuild the index files of the site
296
- let updatedMetadata = false;
297
- compiler.hooks.afterEmit.tap('update-metadata', () => {
298
- if (!updatedMetadata) {
299
- updatedMetadata = true;
300
- //log updated paths
301
- // buildMetadata()
302
- // .then((updatedPaths) => {
303
- // // if(updatedPaths && updatedPaths.length)
304
- // // {
305
- // // console.log(chalk.blueBright('Updated metadata:\n - '+updatedPaths.join('\n - ')));
306
- // // }
307
- // })
308
- // .catch((err) => {
309
- // console.warn('Could not update metadata: ' + err);
310
- // });
311
- }
312
- });
313
- this.server.use(webpackDevMiddleware(compiler, {
314
- serverSideRender: true,
315
- publicPath: webpackConfig.output.publicPath,
316
- stats: {
317
- children: true,
318
- version: false,
319
- chunks: false,
320
- assets: false,
321
- entrypoints: false,
322
- modules: false,
323
- },
324
- writeToDisk: true,
325
- }));
326
- compiler.hooks.afterEmit.tap('store-manifest', (compilation) => {
327
- try {
328
- // Read manifest from the filesystem after webpack writes it
329
- const manifestPath = path.resolve(compilation.options.output.path, 'manifest.json');
330
- if (fsNative.existsSync(manifestPath)) {
331
- const manifestContent = fsNative.readFileSync(manifestPath, 'utf-8');
332
- this.latestManifest = JSON.parse(manifestContent);
333
- }
334
- }
335
- catch (err) {
336
- console.warn('Failed to parse manifest from webpack emit:', err);
337
- }
338
- });
339
- this.server.use(webpackHotMiddleware(compiler, {
340
- log: false,
341
- }));
302
+ // Vite middleware (plan 010): when the user app starts via Vite
303
+ // (`linked start --vite`), the orchestrator passes the Vite handle
304
+ // through config.server.viteMiddleware. We mount it and skip the
305
+ // entire webpack-dev-middleware setup below.
306
+ const viteMiddleware = (_d = this.config.server) === null || _d === void 0 ? void 0 : _d.viteMiddleware;
307
+ if (viteMiddleware) {
308
+ this.server.use(viteMiddleware);
342
309
  }
310
+ // Plan-011: legacy webpack-dev-middleware branch removed. Under Vite
311
+ // (the only supported dev path now) `viteMiddleware` is set, which
312
+ // previously forced `skipBuild = true` and made this whole block
313
+ // dead. Removing it ends the static dependency on @_linked/cli/
314
+ // config-webpack-app (full of dynamic imports Vite couldn't analyze)
315
+ // and shrinks LinkedServer's import surface considerably.
343
316
  // //map URL routes to file paths
344
317
  const oneYear = 1000 * 60 * 60 * 24 * 365; // in milliseconds
345
318
  const oneMonth = 1000 * 60 * 60 * 24 * 30; // in milliseconds
@@ -392,6 +365,43 @@ let LincdServer = class LincdServer extends Shape {
392
365
  this.server.head('__health', (_req, res) => {
393
366
  res.sendStatus(200);
394
367
  });
368
+ // CN control channel (plan-010 T1b.3). Present only when CN spawned this
369
+ // process with a shared secret (CN_APP_ADMIN_SECRET); a standalone app run
370
+ // without the secret leaves the channel closed (404). Auth is the
371
+ // x-cn-admin-secret header matching that env value.
372
+ const adminSecret = process.env.CN_APP_ADMIN_SECRET;
373
+ // Constant-time compare so the secret can't be recovered by timing the 401
374
+ // response. timingSafeEqual requires equal-length buffers, so guard length.
375
+ const secretMatches = (provided) => {
376
+ if (!adminSecret || !provided)
377
+ return false;
378
+ const a = Buffer.from(provided);
379
+ const b = Buffer.from(adminSecret);
380
+ return a.length === b.length && timingSafeEqual(a, b);
381
+ };
382
+ const requireAdmin = (req, res) => {
383
+ if (!adminSecret) {
384
+ res.sendStatus(404); // no control channel in this mode
385
+ return false;
386
+ }
387
+ if (!secretMatches(req.get('x-cn-admin-secret'))) {
388
+ res.sendStatus(401);
389
+ return false;
390
+ }
391
+ return true;
392
+ };
393
+ this.server.get('/admin/health', (req, res) => {
394
+ if (!requireAdmin(req, res))
395
+ return;
396
+ res.status(200).json({ status: 'ok' });
397
+ });
398
+ this.server.post('/admin/restart', (req, res) => {
399
+ if (!requireAdmin(req, res))
400
+ return;
401
+ res.sendStatus(202);
402
+ // CN's provisioner respawns on exit (see ChildProcessProvisioner).
403
+ setTimeout(() => process.exit(0), 50);
404
+ });
395
405
  this.server.get('*', this.handleErrors(async (req, res) => {
396
406
  //make sure the frontend bundle has finished building
397
407
  // await this.waitForWebpack();
@@ -491,7 +501,19 @@ let LincdServer = class LincdServer extends Shape {
491
501
  .readFile(path.join(process.cwd(), 'package.json'), 'utf-8')
492
502
  .then(async (contents) => {
493
503
  let pkg = JSON.parse(contents);
494
- await this.indexPackageBackendProviders(pkg.name, false, path.join(process.cwd(), 'src', 'backend.ts'));
504
+ // Route through default ${pkg}/backend resolution. The app's
505
+ // package.json `exports`./backend field handles dev vs prod:
506
+ // "exports": {
507
+ // "./backend": {
508
+ // "development": "./src/backend.ts",
509
+ // "default": "./lib/esm/backend.js"
510
+ // }
511
+ // }
512
+ // Removed plan-010 D10: the source-direct
513
+ // path.join(cwd, 'src', 'backend.ts') override that forced TS
514
+ // runtime loading. Vite SSR's ssrLoadModule handles the
515
+ // transform; production runs from lib/esm/.
516
+ await this.indexPackageBackendProviders(pkg.name, false);
495
517
  });
496
518
  }
497
519
  catch (err) {
@@ -727,6 +749,7 @@ let LincdServer = class LincdServer extends Shape {
727
749
  res.sendFile(resizedFilePath);
728
750
  }
729
751
  async indexLincdPackage(pkg, warnIfNotFound = false) {
752
+ var _a;
730
753
  if (pkg === this.package.name) {
731
754
  return;
732
755
  }
@@ -737,7 +760,22 @@ let LincdServer = class LincdServer extends Shape {
737
760
  //@ts-ignore
738
761
  // await import.meta.resolve(pkg)
739
762
  // );
740
- await import(pkg);
763
+ // plan-011 §P6 — prefer Vite's SSR loader so workspace packages register
764
+ // on the SINGLE @_linked/core instance the rest of the SSR graph uses.
765
+ // The bare Node `import()` fallback resolves `default`→lib, evaluating a
766
+ // SECOND core copy (benign post-§P1, but a needless second instance).
767
+ // Node import stays as the fallback when Vite isn't active (production /
768
+ // Node-only CLI commands). Mirrors indexPackageBackendProviders.
769
+ const vite = (_a = this.config.server) === null || _a === void 0 ? void 0 : _a.vite;
770
+ if (vite && typeof vite.ssrLoadModule === 'function') {
771
+ await vite.ssrLoadModule(pkg);
772
+ }
773
+ else {
774
+ // @vite-ignore — dynamic specifier is intentional: this checks
775
+ // whether `pkg` is resolvable at runtime, then catches the error.
776
+ // eslint-disable-next-line no-unsanitized/method
777
+ await import(/* @vite-ignore */ pkg);
778
+ }
741
779
  // console.log(`✅ Successfully loaded: ${pkg}`);
742
780
  }
743
781
  catch (e) {
@@ -761,13 +799,66 @@ let LincdServer = class LincdServer extends Shape {
761
799
  }
762
800
  }
763
801
  async indexPackageBackendProviders(pkg, warnIfNotFound = false, backendIndexFilePath) {
802
+ var _a;
764
803
  if (!backendIndexFilePath) {
765
804
  backendIndexFilePath = `${pkg}/backend`;
766
805
  }
767
806
  let backendProviderExports;
768
807
  let genericBackendProvider;
769
808
  let shapeProviders = [];
770
- await import(backendIndexFilePath)
809
+ // Plan-010 phase 4/5: when Vite SSR is active, route module loading
810
+ // through vite.ssrLoadModule. This handles the case where the user app
811
+ // is the consuming workspace itself (self-reference like
812
+ // @semantu/create-now/backend), which Node's ESM resolver can't resolve
813
+ // from inside an installed package like @_linked/server.
814
+ const vite = (_a = this.config.server) === null || _a === void 0 ? void 0 : _a.vite;
815
+ const loadModule = async (specifier) => {
816
+ var _a, _b, _c;
817
+ if (vite && typeof vite.ssrLoadModule === 'function') {
818
+ // For the user app's own backend, resolve to ./src/backend.ts directly.
819
+ // Vite reads the app's package.json exports.development condition.
820
+ if (specifier === `${this.package.name}/backend`) {
821
+ return await vite.ssrLoadModule('/src/backend.ts');
822
+ }
823
+ // Plan-011: ssr.external is now an npm-only allowlist. Workspace
824
+ // packages MUST go through Vite's SSR loader so they share the
825
+ // same module instance as everything else in the SSR call graph
826
+ // (otherwise we get duplicate React contexts, duplicate Linked
827
+ // package registrations, etc).
828
+ //
829
+ // Pre-check whether the specifier actually resolves before
830
+ // calling ssrLoadModule — many packages have no `/backend`
831
+ // export, and ssrLoadModule logs a "Failed to load url" error
832
+ // INTERNALLY before throwing, which spams stdout even when the
833
+ // surrounding catch handles it. pluginContainer.resolveId is
834
+ // quiet: it returns null when no plugin can resolve the id.
835
+ try {
836
+ const resolved = await ((_b = (_a = vite.pluginContainer) === null || _a === void 0 ? void 0 : _a.resolveId) === null || _b === void 0 ? void 0 : _b.call(_a, specifier));
837
+ if (!resolved) {
838
+ // Synthesise the same error shape the catch expects so the
839
+ // "no backend export, that's fine" branch fires.
840
+ const err = new Error(`Failed to load url ${specifier} (resolved id: ${specifier}). Does the file exist?`);
841
+ throw err;
842
+ }
843
+ }
844
+ catch (e) {
845
+ // If the pre-check itself errored (e.g. plugin threw on a
846
+ // pkg name it doesn't know), let the call below surface the
847
+ // real error path.
848
+ if (!((_c = e === null || e === void 0 ? void 0 : e.message) === null || _c === void 0 ? void 0 : _c.includes('Failed to load url'))) {
849
+ // fall through to ssrLoadModule which will throw + log
850
+ }
851
+ else {
852
+ throw e;
853
+ }
854
+ }
855
+ return await vite.ssrLoadModule(specifier);
856
+ }
857
+ // No-vite path (e.g. production runtime). Fall back to Node's
858
+ // resolver. The dynamic specifier is intentional.
859
+ return await import(/* @vite-ignore */ specifier);
860
+ };
861
+ await loadModule(backendIndexFilePath)
771
862
  .then((backendProviderExports) => {
772
863
  //instantiate the exported provider classes and add them to the right place
773
864
  Object.keys(backendProviderExports).forEach((key) => {
@@ -793,13 +884,22 @@ let LincdServer = class LincdServer extends Shape {
793
884
  });
794
885
  })
795
886
  .catch((e) => {
796
- const match = e.message.match(/module \'([^\']+)'/);
797
- //check that the imported /backend path is not found (and only that path, not an import IN that file that is not found, that should still throw an error)
798
- let providerNotFound = e.code === 'ERR_MODULE_NOT_FOUND' &&
799
- e.message.indexOf(`Cannot find module`) !== -1 &&
800
- match &&
801
- match[1] &&
802
- match[1].includes('/backend');
887
+ var _a;
888
+ // Recognize "no /backend export" failures across two loader
889
+ // shapes:
890
+ // Node's import() → ERR_MODULE_NOT_FOUND + "Cannot find module 'X/backend'"
891
+ // Vite's ssrLoadModule() → "Failed to load url X/backend"
892
+ // In either case, missing /backend on a package that doesn't
893
+ // ship a backend is expected; loud-error only on REAL load
894
+ // failures (syntax error inside an existing backend.ts, etc).
895
+ const nodeMatch = e.message.match(/module \'([^\']+)'/);
896
+ const viteMatch = e.message.match(/Failed to load url ([^\s]+)/);
897
+ const matchedSpec = (_a = nodeMatch === null || nodeMatch === void 0 ? void 0 : nodeMatch[1]) !== null && _a !== void 0 ? _a : viteMatch === null || viteMatch === void 0 ? void 0 : viteMatch[1];
898
+ let providerNotFound = !!matchedSpec &&
899
+ matchedSpec.includes('/backend') &&
900
+ ((e.code === 'ERR_MODULE_NOT_FOUND' &&
901
+ e.message.indexOf(`Cannot find module`) !== -1) ||
902
+ e.message.indexOf('Failed to load url') !== -1);
803
903
  if (providerNotFound) {
804
904
  // console.warn('Error loading ' + providerPath + ': ' + e.stack);
805
905
  if (warnIfNotFound) {
@@ -818,6 +918,69 @@ let LincdServer = class LincdServer extends Shape {
818
918
  this.shapeProviders.set(pkg, shapeProviders);
819
919
  return { backendProviderExports, shapeProviders };
820
920
  }
921
+ /**
922
+ * Plan-011 phase 2 — HMR re-index entry point.
923
+ *
924
+ * Called by the CLI orchestrator's Vite watcher whenever a `.ts`/`.tsx`
925
+ * file inside a workspace package changes. Disposes the package's
926
+ * existing providers (so routes, listeners, timers don't accumulate),
927
+ * drops them from the registry, and re-runs indexPackageBackendProviders
928
+ * to pick up the freshly-imported module.
929
+ *
930
+ * Dispose calls have a 5 s soft timeout. A hanging dispose logs a
931
+ * warning and is abandoned so HMR doesn't stall the whole dev loop.
932
+ */
933
+ async onSourceChange(pkg) {
934
+ var _a, _b, _c, _d, _e;
935
+ const disposeWithTimeout = async (p, label) => {
936
+ if (!(p === null || p === void 0 ? void 0 : p.dispose))
937
+ return;
938
+ try {
939
+ await Promise.race([
940
+ Promise.resolve().then(() => p.dispose()),
941
+ new Promise((_, reject) => setTimeout(() => reject(new Error(`dispose timeout after 5s`)), 5000)),
942
+ ]);
943
+ }
944
+ catch (err) {
945
+ console.warn(chalk.yellow(`[linked] ${label} dispose failed: ${err.message}`));
946
+ }
947
+ };
948
+ const generic = this.genericProviders.get(pkg);
949
+ if (generic) {
950
+ await disposeWithTimeout(generic, `${pkg} generic provider`);
951
+ this.genericProviders.delete(pkg);
952
+ }
953
+ const shapeProvs = (_a = this.shapeProviders.get(pkg)) !== null && _a !== void 0 ? _a : [];
954
+ for (const p of shapeProvs) {
955
+ await disposeWithTimeout(p, `${pkg} ${(_d = (_c = (_b = Object.getPrototypeOf(p)) === null || _b === void 0 ? void 0 : _b.constructor) === null || _c === void 0 ? void 0 : _c.name) !== null && _d !== void 0 ? _d : 'shape provider'}`);
956
+ }
957
+ this.shapeProviders.delete(pkg);
958
+ await this.indexPackageBackendProviders(pkg, true);
959
+ // Run the boot lifecycle on the FRESHLY-loaded provider so it
960
+ // re-registers its Express routes/middleware after dispose. Without
961
+ // this, dispose() tears the old routes off and the new instance is
962
+ // constructed but never gets a chance to register the replacements.
963
+ const fresh = this.genericProviders.get(pkg);
964
+ if (fresh === null || fresh === void 0 ? void 0 : fresh.setupBeforeControllers) {
965
+ try {
966
+ await fresh.setupBeforeControllers();
967
+ }
968
+ catch (err) {
969
+ console.warn(chalk.yellow(`[linked] ${pkg} setupBeforeControllers after reload failed: ${err.message}`));
970
+ }
971
+ }
972
+ const freshShapes = (_e = this.shapeProviders.get(pkg)) !== null && _e !== void 0 ? _e : [];
973
+ for (const p of freshShapes) {
974
+ if (p === null || p === void 0 ? void 0 : p.setupBeforeControllers) {
975
+ try {
976
+ await p.setupBeforeControllers();
977
+ }
978
+ catch (err) {
979
+ console.warn(chalk.yellow(`[linked] ${pkg} shape-provider setupBeforeControllers after reload failed: ${err.message}`));
980
+ }
981
+ }
982
+ }
983
+ }
821
984
  async processBackendMethodCall(request, response) {
822
985
  this.noCache(response);
823
986
  await this.initRequest(request, response);
@@ -1001,7 +1164,7 @@ let LincdServer = class LincdServer extends Shape {
1001
1164
  }
1002
1165
  }
1003
1166
  async render(req, res) {
1004
- var _a, _b, _c, _d;
1167
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j;
1005
1168
  res.socket.on('error', (error) => {
1006
1169
  console.error('Fatal socket error', error);
1007
1170
  });
@@ -1029,6 +1192,69 @@ let LincdServer = class LincdServer extends Shape {
1029
1192
  let App = ((_b = this.config.server) === null || _b === void 0 ? void 0 : _b.loadAppComponent)
1030
1193
  ? await this.config.server.loadAppComponent()
1031
1194
  : null;
1195
+ // Vite dev CSS collection (plan-010 iteration 1 — gap A):
1196
+ // 1. Preload the matched route's module via ssrLoadModule so all
1197
+ // page-level dependencies (SigninLayout, CreateAccount, etc.)
1198
+ // enter Vite's moduleGraph BEFORE we collect CSS. Without this,
1199
+ // only App's eager imports are in the graph and lazy routes'
1200
+ // CSS would be missing — page renders unstyled until hydration.
1201
+ // 2. Walk moduleGraph for CSS entries, load each via `?inline`
1202
+ // (Vite returns raw CSS string as default export).
1203
+ // 3. Inject as inline <style> in HTML head (Html.tsx).
1204
+ const vite = (_c = this.config.server) === null || _c === void 0 ? void 0 : _c.vite;
1205
+ // Vite SSR module preload (plan-010 iter1 gap A):
1206
+ // Pages are loaded lazily via React.lazy — their modules only enter
1207
+ // Vite's moduleGraph after the lazy resolves. For SSR CSS collection
1208
+ // we need the page modules in the graph BEFORE the first render of
1209
+ // each route. The orchestrator passes a preloadPagesFn that lists
1210
+ // all page files (typically via `import.meta.glob`); we call
1211
+ // ssrLoadModule on each path once. After the first render of a
1212
+ // session everything is cached.
1213
+ const preloadPagesFn = (_d = this.config.server) === null || _d === void 0 ? void 0 : _d.viteSsrPreload;
1214
+ if ((vite === null || vite === void 0 ? void 0 : vite.ssrLoadModule) && preloadPagesFn && !this._viteSsrPreloaded) {
1215
+ try {
1216
+ const pagePaths = await preloadPagesFn();
1217
+ for (const p of pagePaths) {
1218
+ try {
1219
+ await vite.ssrLoadModule(p);
1220
+ }
1221
+ catch ( /* skip — page may not exist or have ssr errors */_k) { /* skip — page may not exist or have ssr errors */ }
1222
+ }
1223
+ }
1224
+ catch ( /* ignore */_l) { /* ignore */ }
1225
+ this._viteSsrPreloaded = true;
1226
+ }
1227
+ let ssrCss = '';
1228
+ if ((_e = vite === null || vite === void 0 ? void 0 : vite.moduleGraph) === null || _e === void 0 ? void 0 : _e.idToModuleMap) {
1229
+ const cssChunks = [];
1230
+ const seen = new Set();
1231
+ // Walk the moduleGraph for CSS modules pulled in by app/pages.
1232
+ // Note: Tailwind v4 `@theme` directives in app theme CSS files are
1233
+ // NOT processed at SSR collection time — the @tailwindcss/vite
1234
+ // plugin expands them at production build only. In dev mode, the
1235
+ // theme variables (—color-primary-*, etc.) are injected by Vite's
1236
+ // runtime AFTER client hydration, which causes a brief flash of
1237
+ // unthemed content (logos render black until ~hydration+200ms).
1238
+ // Production (vite build) is unaffected — the static main.css
1239
+ // contains expanded :root variables.
1240
+ for (const [id] of vite.moduleGraph.idToModuleMap) {
1241
+ if (seen.has(id))
1242
+ continue;
1243
+ if (!/\.(module\.)?css(\?(?!.*inline)[^?]*)?$/.test(id))
1244
+ continue;
1245
+ seen.add(id);
1246
+ try {
1247
+ const inlineUrl = id + (id.includes('?') ? '&' : '?') + 'inline';
1248
+ const cssModule = await vite.ssrLoadModule(inlineUrl);
1249
+ if (typeof (cssModule === null || cssModule === void 0 ? void 0 : cssModule.default) === 'string') {
1250
+ cssChunks.push(cssModule.default);
1251
+ }
1252
+ }
1253
+ catch ( /* skip */_m) { /* skip */ }
1254
+ }
1255
+ ssrCss = cssChunks.join('\n');
1256
+ }
1257
+ this.assets['__viteSsrCss'] = ssrCss;
1032
1258
  await this.initRequest(req, res);
1033
1259
  let { requestLD, requestObject } = await this.getRequestData(req, res);
1034
1260
  //on the backend we need to inform the hook of the request-data value
@@ -1052,12 +1278,18 @@ let LincdServer = class LincdServer extends Shape {
1052
1278
  let preloadScripts = [];
1053
1279
  let preloadStyles = [];
1054
1280
  let matchedRouteKey = null;
1281
+ // Plan-010 Vite dev mode: skip preload resolution entirely. Vite
1282
+ // handles dynamic import() at runtime — there are no pre-built
1283
+ // chunks to preload, and the webpack-shape fallback would pick up
1284
+ // stale bundles on disk (public/bundles/*.bundle.js from a prior
1285
+ // webpack build) and link them, breaking hydration.
1286
+ const usingViteDev = !!((_f = this.config.server) === null || _f === void 0 ? void 0 : _f.vite) && !this.latestManifest;
1055
1287
  // Load routes config if available and extract preload chunks for the current route
1056
- if ((_c = this.config.server) === null || _c === void 0 ? void 0 : _c.loadRoutes) {
1288
+ if ((_g = this.config.server) === null || _g === void 0 ? void 0 : _g.loadRoutes) {
1057
1289
  try {
1058
1290
  const routesModule = await this.config.server.loadRoutes();
1059
1291
  const ROUTES = routesModule.ROUTES ||
1060
- ((_d = routesModule.default) === null || _d === void 0 ? void 0 : _d.ROUTES) ||
1292
+ ((_h = routesModule.default) === null || _h === void 0 ? void 0 : _h.ROUTES) ||
1061
1293
  routesModule;
1062
1294
  // Match the current request path to a route
1063
1295
  let matchedRoute = null;
@@ -1074,18 +1306,52 @@ let LincdServer = class LincdServer extends Shape {
1074
1306
  break;
1075
1307
  }
1076
1308
  }
1077
- // If we found a matching route with preloadChunks, resolve them to URLs (both JS and CSS)
1078
- if ((matchedRoute === null || matchedRoute === void 0 ? void 0 : matchedRoute.preloadChunks) &&
1309
+ // If we found a matching route with preloadChunks, resolve them to URLs (both JS and CSS).
1310
+ // Manifest format detection: Vite manifest entries are objects with .file/.css; webpack
1311
+ // manifest entries are bare strings. Try Vite shape first, fall back to webpack.
1312
+ // Skip entirely in Vite dev mode — no pre-built chunks exist; Vite handles dynamic
1313
+ // imports at runtime.
1314
+ if (!usingViteDev &&
1315
+ (matchedRoute === null || matchedRoute === void 0 ? void 0 : matchedRoute.preloadChunks) &&
1079
1316
  Array.isArray(matchedRoute.preloadChunks)) {
1317
+ const resolveJs = (chunkName) => {
1318
+ // Vite shape: keyed by source path. Look up by source-path keys
1319
+ // that match the chunk name's tail.
1320
+ const viteEntry = manifest[`src/pages/${chunkName}.tsx`] ||
1321
+ manifest[`src/pages/${chunkName}.ts`] ||
1322
+ Object.values(manifest).find((e) => {
1323
+ var _a, _b;
1324
+ return ((_a = e === null || e === void 0 ? void 0 : e.src) === null || _a === void 0 ? void 0 : _a.endsWith(`${chunkName}.tsx`)) ||
1325
+ ((_b = e === null || e === void 0 ? void 0 : e.src) === null || _b === void 0 ? void 0 : _b.endsWith(`${chunkName}.ts`));
1326
+ });
1327
+ if (viteEntry && typeof viteEntry === 'object' && viteEntry.file) {
1328
+ return `/bundles/${viteEntry.file}`;
1329
+ }
1330
+ // Webpack shape: keyed by output name.
1331
+ return (manifest[`${chunkName}.js`] ||
1332
+ manifest[`${chunkName}.bundle.js`] ||
1333
+ manifest[`${chunkName}.mjs`] ||
1334
+ null);
1335
+ };
1336
+ const resolveCss = (chunkName) => {
1337
+ const viteEntry = manifest[`src/pages/${chunkName}.tsx`] ||
1338
+ manifest[`src/pages/${chunkName}.ts`] ||
1339
+ Object.values(manifest).find((e) => {
1340
+ var _a, _b;
1341
+ return ((_a = e === null || e === void 0 ? void 0 : e.src) === null || _a === void 0 ? void 0 : _a.endsWith(`${chunkName}.tsx`)) ||
1342
+ ((_b = e === null || e === void 0 ? void 0 : e.src) === null || _b === void 0 ? void 0 : _b.endsWith(`${chunkName}.ts`));
1343
+ });
1344
+ if (viteEntry && typeof viteEntry === 'object' && Array.isArray(viteEntry.css)) {
1345
+ return viteEntry.css.map((c) => `/bundles/${c}`);
1346
+ }
1347
+ const webpackCss = manifest[`${chunkName}.css`];
1348
+ return webpackCss ? [webpackCss] : [];
1349
+ };
1080
1350
  preloadScripts = matchedRoute.preloadChunks
1081
- .map((chunkName) => manifest[`${chunkName}.js`] ||
1082
- manifest[`${chunkName}.bundle.js`] ||
1083
- manifest[`${chunkName}.mjs`])
1351
+ .map(resolveJs)
1084
1352
  .filter(Boolean);
1085
- // Also collect CSS chunks for the same routes
1086
1353
  preloadStyles = matchedRoute.preloadChunks
1087
- .map((chunkName) => manifest[`${chunkName}.css`])
1088
- .filter(Boolean);
1354
+ .flatMap(resolveCss);
1089
1355
  }
1090
1356
  }
1091
1357
  catch (err) {
@@ -1098,9 +1364,30 @@ let LincdServer = class LincdServer extends Shape {
1098
1364
  React.createElement(StaticRouter, { location: req.url },
1099
1365
  React.createElement(AppContextProvider, { assets: this.assets, requestLD: requestLD, requestObject: requestObject, preloadScripts: preloadScripts, preloadStyles: preloadStyles, expressRequest: req, expressResponse: res },
1100
1366
  React.createElement(App, null)))), {
1101
- bootstrapScripts: [
1102
- this.assets['main.js'], //generated webpack bundle from frontend/src
1103
- ],
1367
+ // Bootstrap scripts:
1368
+ // - Vite dev mode: use bootstrapModules so the browser loads them
1369
+ // as ES modules. Vite middleware intercepts /src/index.tsx and
1370
+ // transforms+serves it; /@vite/client provides the HMR client.
1371
+ // @vitejs/plugin-react requires a "preamble" inline script
1372
+ // defining $RefreshReg$/$RefreshSig$ — without it, the first
1373
+ // React module throws "can't detect preamble" and hydration
1374
+ // blows up. Inline via bootstrapScriptContent.
1375
+ // - Production: bootstrapScripts with the hashed main.js from the
1376
+ // Vite (or legacy webpack) build manifest.
1377
+ ...(((_j = this.config.server) === null || _j === void 0 ? void 0 : _j.vite)
1378
+ ? {
1379
+ bootstrapScriptContent: `
1380
+ import("/@vite/client");
1381
+ import("/@react-refresh").then(RefreshRuntime => {
1382
+ RefreshRuntime.injectIntoGlobalHook(window);
1383
+ window.$RefreshReg$ = () => {};
1384
+ window.$RefreshSig$ = () => (type) => type;
1385
+ window.__vite_plugin_react_preamble_installed__ = true;
1386
+ return import("/src/index.tsx");
1387
+ });
1388
+ `,
1389
+ }
1390
+ : { bootstrapScripts: [this.assets['main.js']] }),
1104
1391
  onShellReady: function () {
1105
1392
  var _a;
1106
1393
  res.statusCode = didError ? 500 : 200;
@@ -1248,10 +1535,10 @@ let LincdServer = class LincdServer extends Shape {
1248
1535
  /**
1249
1536
  * indicates that instances of this shape need to have this rdf.type
1250
1537
  */
1251
- LincdServer.targetClass = lincdServer.LincdServer;
1252
- LincdServer = __decorate([
1538
+ LinkedServer.targetClass = lincdServer.LincdServer;
1539
+ LinkedServer = __decorate([
1253
1540
  linkedShape,
1254
1541
  __metadata("design:paramtypes", [Object])
1255
- ], LincdServer);
1256
- export { LincdServer };
1257
- //# sourceMappingURL=LincdServer.js.map
1542
+ ], LinkedServer);
1543
+ export { LinkedServer };
1544
+ //# sourceMappingURL=LinkedServer.js.map