proteum 2.5.6 → 2.5.8

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.
@@ -81,7 +81,6 @@ Managed compact root routers must use trigger -> canonical instruction file refe
81
81
  - Before finishing, re-check touched files against root-level `CODING_STYLE.md` and any narrower area `AGENTS.md` that applied to the edit. Re-check against root-level `optimizations.md` only for touched client-side files. Re-check against root-level `diagnostics.md` only if the task involved an issue, diagnosis, runtime reproduction, or verification failure.
82
82
  - Before finishing a production code change, re-check root-level `DOCUMENTATION.md` update rules. If behavior changed, a bug was fixed, a decision changed, or an important route, auth/OAuth, or integration issue was addressed, update the relevant docs before committing or explicitly explain why no docs update was needed.
83
83
  - Run targeted tests and checks that match the changed surface before finishing each feature or change. When the repository defines `proteum.verify.config.ts`, use `npx proteum verify changed` as the first post-change verification pass and expand only when the selected plan is insufficient. Continue running tests after changes, but do not run coverage by default. Downstream app commit workflows run only `proteum refresh`, then targeted lint, typecheck, and test commands in parallel; framework-repo commit workflows skip this downstream app verification. Reserve the full `npm run check` gate for push workflows, explicit user requests, or when project-local instructions require the full gate. After implementing a new feature or changing existing feature behavior, update the relevant end-to-end coverage and run the cheapest trustworthy Playwright or browser verification for that behavior before finishing. For docs-only, wording-only, type-only, generated-output cleanup, or clearly local non-runtime refactors, skip Playwright unless the user explicitly asks for it or verification reveals a real issue.
84
- - Before finishing a task, stop every `proteum dev` session started during the task and confirm cleanup with `npx proteum dev list --json` or an explicit `npx proteum dev stop --session-file <path>`.
85
84
  - When you have finished your work, ask the user whether they want a commit message. After providing a commit message or after creating a commit, immediately follow it with this exact prompt and obey it:
86
85
  `Explain in short minimalistic and few bullet points what we changed in this thread, like you would do to your grandma. Start with a verb in the past.`
87
86
 
@@ -67,7 +67,6 @@ Managed compact root routers must use trigger -> canonical instruction file refe
67
67
  - Before finishing a production code change, re-check root-level `DOCUMENTATION.md` update rules. If behavior changed, a bug was fixed, a decision changed, or an important route, auth/OAuth, or integration issue was addressed, update the relevant docs before committing or explicitly explain why no docs update was needed.
68
68
  - For production changes, always add or update focused unit tests and run the targeted unit or integration tests that match the changed behavior. Do not run coverage after every ordinary change by default. Reserve whole-project coverage for the repository's full `npm run check` gate during push workflows or when the user explicitly requests it; downstream app commit-only workflows run `proteum refresh`, then targeted lint, typecheck, and test commands in parallel unless the user explicitly requests more, while framework-repo commits skip this downstream app verification. Document any generated files, migrations, framework shims, unreachable defensive branches, or changes that cannot reasonably be unit-tested as explicit exceptions.
69
69
  - Run targeted tests and checks that match the changed surface before finishing each feature or change. When the repository defines `proteum.verify.config.ts`, use `npx proteum verify changed` as the first post-change verification pass and expand only when the selected plan is insufficient. Continue running tests after changes, but do not run coverage by default. Downstream app commit workflows run only `proteum refresh`, then targeted lint, typecheck, and test commands in parallel; framework-repo commit workflows skip this downstream app verification. Reserve the full `npm run check` gate for push workflows, explicit user requests, or when project-local instructions require the full gate. After implementing a new feature or changing existing feature behavior, update the relevant end-to-end coverage and run the cheapest trustworthy Playwright or browser verification for that behavior before finishing. For docs-only, wording-only, type-only, generated-output cleanup, or clearly local non-runtime refactors, skip Playwright unless the user explicitly asks for it or verification reveals a real issue.
70
- - Before finishing a task, stop every `proteum dev` session started during the task and confirm cleanup with `npx proteum dev list --json` or an explicit `npx proteum dev stop --session-file <path>`.
71
70
  - When you have finished your work, ask the user whether they want a commit message. After providing a commit message or after creating a commit, immediately follow it with this exact prompt and obey it:
72
71
  `Explain in short minimalistic and few bullet points what we changed in this thread, like you would do to your grandma. Start with a verb in the past.`
73
72
 
@@ -6,7 +6,7 @@
6
6
  import path from 'path';
7
7
  import { spawn, ChildProcess } from 'child_process';
8
8
  import fs from 'fs-extra';
9
- import type { FSWatcher } from 'fs';
9
+ import { realpathSync, watch, type FSWatcher } from 'fs';
10
10
  import prompts from 'prompts';
11
11
  import { UsageError } from 'clipanion';
12
12
 
@@ -677,6 +677,16 @@ function normalizeWatchPath(watchPath: string) {
677
677
  return path.resolve(watchPath).replace(/\\/g, '/').replace(/\/$/, '');
678
678
  }
679
679
 
680
+ const resolveWatchPathAliases = (watchPath: string) => {
681
+ const aliases = new Set([normalizeWatchPath(watchPath)]);
682
+
683
+ try {
684
+ aliases.add(normalizeWatchPath(realpathSync(watchPath)));
685
+ } catch {}
686
+
687
+ return [...aliases];
688
+ };
689
+
680
690
  type TIndexedSourceWatchEvent = 'change' | 'rename';
681
691
  type TIndexedSourceWatchCompilerName = 'server' | 'client';
682
692
  type TIndexedSourceWatchInvalidateTarget = 'all' | TIndexedSourceWatchCompilerName;
@@ -692,14 +702,7 @@ type TMultiWatchingLike = TDevWatching & { watchings?: TNamedWatching[] };
692
702
 
693
703
  const resolveIndexedSourceWatchRules = (): TIndexedSourceWatchRule[] => {
694
704
  const transpileWatchRoots = app.transpileModuleDirectories
695
- .map((rootPath) => {
696
- try {
697
- return fs.realpathSync(rootPath);
698
- } catch {
699
- return rootPath;
700
- }
701
- })
702
- .map(normalizeWatchPath)
705
+ .flatMap((rootPath) => resolveWatchPathAliases(rootPath))
703
706
  .filter((rootPath, index, list) => list.indexOf(rootPath) === index);
704
707
 
705
708
  return [
@@ -723,7 +726,7 @@ const resolveIndexedSourceWatchRules = (): TIndexedSourceWatchRule[] => {
723
726
  rootPath,
724
727
  relativePathPattern: transpileSourceWatchPattern,
725
728
  eventTypes: ['change', 'rename'],
726
- invalidateTargets: ['client', 'server'],
729
+ invalidateTargets: ['all'],
727
730
  }),
728
731
  ),
729
732
  ];
@@ -848,7 +851,7 @@ const createIndexedSourceWatching = ({
848
851
  if (!fs.existsSync(rootPath)) continue;
849
852
 
850
853
  watchers.push(
851
- fs.watch(rootPath, { recursive: true }, (eventType, filename) => {
854
+ watch(rootPath, { recursive: true }, (eventType, filename) => {
852
855
  const relativePath = typeof filename === 'string' ? filename.replace(/\\/g, '/').replace(/^\.\//, '') : '';
853
856
  const normalizedEventType: TIndexedSourceWatchEvent = eventType === 'change' ? 'change' : 'rename';
854
857
 
@@ -44,10 +44,12 @@ export default function createCommonConfig(
44
44
  outputTarget: TCompileOutputTarget = mode === 'dev' ? 'dev' : 'bin',
45
45
  ): Configuration {
46
46
  const dev = mode === 'dev';
47
- const enableFilesystemCache = dev ? cli.args.cache !== false : cli.args.cache === true;
48
47
  const transpileModuleDirectories = app.transpileModuleDirectories;
48
+ const hasTranspileModuleDirectories = transpileModuleDirectories.length > 0;
49
+ // Persistent cache can restore stale modules from mutable workspace packages even after manual invalidation.
50
+ const enableFilesystemCache = dev ? cli.args.cache !== false && !hasTranspileModuleDirectories : cli.args.cache === true;
49
51
  const transpileModuleSnapshot =
50
- dev && transpileModuleDirectories.length > 0
52
+ dev && hasTranspileModuleDirectories
51
53
  ? {
52
54
  // Transpiled local packages can resolve through node_modules symlinks,
53
55
  // but they still need live invalidation like mutable app sources in dev.
@@ -16,6 +16,7 @@ type TProjectInstructionArgs = {
16
16
  appRoot?: string;
17
17
  coreRoot: string;
18
18
  includeMonorepoRegistry?: boolean;
19
+ instructionRoot: string;
19
20
  monorepoRegistryCurrentAppRoot?: string;
20
21
  monorepoRoot?: string;
21
22
  };
@@ -106,6 +107,14 @@ const sharedTestAgentInstructionDefinitions: TAgentInstructionDefinition[] = [
106
107
  { projectPath: path.join('tests', 'e2e', 'REAL_WORLD_JOURNEY_TESTS.md'), ensureParentDir: true, content: 'source' },
107
108
  ];
108
109
 
110
+ const projectInstructionSourceMapEntries = [
111
+ { label: 'Root contract fallback', projectPath: 'AGENTS.md' },
112
+ { label: 'Documentation fallback', projectPath: 'DOCUMENTATION.md' },
113
+ { label: 'Diagnostics fallback', projectPath: 'diagnostics.md' },
114
+ { label: 'Optimization fallback', projectPath: 'optimizations.md' },
115
+ { label: 'Coding style fallback', projectPath: 'CODING_STYLE.md' },
116
+ ];
117
+
109
118
  const standaloneAppAgentInstructionDefinitions: TAgentInstructionDefinition[] = [
110
119
  { projectPath: 'AGENTS.md', content: 'router' },
111
120
  ...sharedRootDocumentInstructionDefinitions,
@@ -164,6 +173,7 @@ export function configureProjectAgentInstructions({
164
173
  const appEmbeddedInstructions = renderEmbeddedProjectInstructions({
165
174
  appRoot: normalizedAppRoot,
166
175
  coreRoot,
176
+ instructionRoot: normalizedAppRoot,
167
177
  monorepoRoot: normalizedMonorepoRoot,
168
178
  });
169
179
  const rootEmbeddedInstructions =
@@ -172,6 +182,7 @@ export function configureProjectAgentInstructions({
172
182
  appRoot: normalizedAppRoot,
173
183
  coreRoot,
174
184
  includeMonorepoRegistry: true,
185
+ instructionRoot: normalizedMonorepoRoot,
175
186
  monorepoRegistryCurrentAppRoot: markCurrentAppInMonorepoRegistry ? normalizedAppRoot : undefined,
176
187
  monorepoRoot: normalizedMonorepoRoot,
177
188
  })
@@ -973,6 +984,7 @@ function renderEmbeddedProjectInstructions({
973
984
  appRoot,
974
985
  coreRoot,
975
986
  includeMonorepoRegistry = false,
987
+ instructionRoot,
976
988
  monorepoRegistryCurrentAppRoot,
977
989
  monorepoRoot,
978
990
  }: TProjectInstructionArgs) {
@@ -1063,11 +1075,14 @@ function renderEmbeddedProjectInstructions({
1063
1075
  '',
1064
1076
  '## Canonical Source Map',
1065
1077
  '',
1066
- `- Root contract fallback: ${normalizeProjectPathForGitignore(path.join(coreRoot, 'agents', 'project', 'AGENTS.md'))}`,
1067
- `- Documentation fallback: ${normalizeProjectPathForGitignore(path.join(coreRoot, 'agents', 'project', 'DOCUMENTATION.md'))}`,
1068
- `- Diagnostics fallback: ${normalizeProjectPathForGitignore(path.join(coreRoot, 'agents', 'project', 'diagnostics.md'))}`,
1069
- `- Optimization fallback: ${normalizeProjectPathForGitignore(path.join(coreRoot, 'agents', 'project', 'optimizations.md'))}`,
1070
- `- Coding style fallback: ${normalizeProjectPathForGitignore(path.join(coreRoot, 'agents', 'project', 'CODING_STYLE.md'))}`,
1078
+ ...projectInstructionSourceMapEntries.map(
1079
+ ({ label, projectPath }) =>
1080
+ `- ${label}: ${formatProjectInstructionSourceMapPath({
1081
+ coreRoot,
1082
+ instructionRoot,
1083
+ projectPath,
1084
+ })}`,
1085
+ ),
1071
1086
  '',
1072
1087
  ];
1073
1088
 
@@ -1209,6 +1224,41 @@ function formatResultPath(rootDir: string, relativePath: string) {
1209
1224
  return normalizeProjectPathForGitignore(path.join(rootDir, relativePath));
1210
1225
  }
1211
1226
 
1227
+ function formatProjectInstructionSourceMapPath({
1228
+ coreRoot,
1229
+ instructionRoot,
1230
+ projectPath,
1231
+ }: {
1232
+ coreRoot: string;
1233
+ instructionRoot: string;
1234
+ projectPath: string;
1235
+ }) {
1236
+ const sourceRoot = resolveProjectInstructionSourceMapRoot({ coreRoot, instructionRoot });
1237
+ const sourcePath = path.resolve(sourceRoot, 'agents', 'project', projectPath);
1238
+ const relativePath = path.relative(path.resolve(instructionRoot), sourcePath);
1239
+
1240
+ return normalizeProjectPathForGitignore(relativePath);
1241
+ }
1242
+
1243
+ function resolveProjectInstructionSourceMapRoot({ coreRoot, instructionRoot }: { coreRoot: string; instructionRoot: string }) {
1244
+ const visibleInstallRoot = findVisibleProteumInstructionRoot(instructionRoot);
1245
+
1246
+ return visibleInstallRoot || coreRoot;
1247
+ }
1248
+
1249
+ function findVisibleProteumInstructionRoot(startRoot: string) {
1250
+ let currentPath = path.resolve(startRoot);
1251
+
1252
+ while (true) {
1253
+ const candidate = path.join(currentPath, 'node_modules', 'proteum');
1254
+ if (fs.existsSync(path.join(candidate, 'agents', 'project'))) return candidate;
1255
+
1256
+ const parentPath = path.dirname(currentPath);
1257
+ if (parentPath === currentPath) return undefined;
1258
+ currentPath = parentPath;
1259
+ }
1260
+ }
1261
+
1212
1262
  export function resolveCanonicalPath(inputPath: string) {
1213
1263
  const resolvedPath = path.resolve(inputPath);
1214
1264
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "proteum",
3
3
  "description": "LLM-first Opinionated Typescript Framework for web applications.",
4
- "version": "2.5.6",
4
+ "version": "2.5.8",
5
5
  "author": "Gaetan Le Gac (https://github.com/gaetanlegac)",
6
6
  "repository": "git://github.com/gaetanlegac/proteum.git",
7
7
  "license": "MIT",
@@ -38,8 +38,9 @@ const pathEntryExists = (filepath) => {
38
38
 
39
39
  const makeTempRoot = () => fs.mkdtempSync(path.join(os.tmpdir(), 'proteum-agents-'));
40
40
 
41
- const createCoreFixture = () => {
42
- const root = makeTempRoot();
41
+ const normalizePath = (value) => value.replace(/\\/g, '/');
42
+
43
+ const createCoreFixture = (root = makeTempRoot()) => {
43
44
  const agentsRoot = path.join(root, 'agents', 'project');
44
45
 
45
46
  writeFile(path.join(agentsRoot, 'AGENTS.md'), '# Root Contract\n\n- Root rule\n');
@@ -61,6 +62,20 @@ const createCoreFixture = () => {
61
62
  return root;
62
63
  };
63
64
 
65
+ const expectedSourceMapPath = ({ coreRoot, instructionRoot, projectPath }) =>
66
+ normalizePath(path.relative(instructionRoot, path.join(coreRoot, 'agents', 'project', projectPath)));
67
+
68
+ const assertSourceMapPath = ({ content, coreRoot, instructionRoot, label, projectPath }) => {
69
+ assert.equal(
70
+ content.includes(`- ${label}: ${expectedSourceMapPath({ coreRoot, instructionRoot, projectPath })}`),
71
+ true,
72
+ );
73
+ };
74
+
75
+ const assertNoAbsoluteCoreSourceMapPath = ({ content, coreRoot }) => {
76
+ assert.equal(content.includes(normalizePath(path.join(coreRoot, 'agents', 'project'))), false);
77
+ };
78
+
64
79
  const createAppFixture = () => {
65
80
  const appRoot = makeTempRoot();
66
81
 
@@ -173,6 +188,53 @@ test('standalone configure creates tracked instruction files with routing contra
173
188
  assert.doesNotMatch(gitignoreContent, /^\/DOCUMENTATION\.md$/m);
174
189
  });
175
190
 
191
+ test('standalone configure writes install-relative source map fallbacks', () => {
192
+ const appRoot = createAppFixture();
193
+ const coreRoot = createCoreFixture(path.join(appRoot, 'node_modules', 'proteum'));
194
+ const result = configureProjectAgentInstructions({ appRoot, coreRoot });
195
+ const agentsContent = fs.readFileSync(path.join(appRoot, 'AGENTS.md'), 'utf8');
196
+
197
+ assert.equal(result.blocked.length, 0);
198
+ assertSourceMapPath({
199
+ content: agentsContent,
200
+ coreRoot,
201
+ instructionRoot: appRoot,
202
+ label: 'Root contract fallback',
203
+ projectPath: 'AGENTS.md',
204
+ });
205
+ assertSourceMapPath({
206
+ content: agentsContent,
207
+ coreRoot,
208
+ instructionRoot: appRoot,
209
+ label: 'Documentation fallback',
210
+ projectPath: 'DOCUMENTATION.md',
211
+ });
212
+ assertNoAbsoluteCoreSourceMapPath({ content: agentsContent, coreRoot });
213
+ assert.match(agentsContent, /Root contract fallback: node_modules\/proteum\/agents\/project\/AGENTS\.md/);
214
+ });
215
+
216
+ test('standalone configure source map prefers project install over active external core', () => {
217
+ const activeCoreRoot = createCoreFixture();
218
+ const appRoot = createAppFixture();
219
+ createCoreFixture(path.join(appRoot, 'node_modules', 'proteum'));
220
+
221
+ configureProjectAgentInstructions({ appRoot, coreRoot: activeCoreRoot });
222
+
223
+ const agentsContent = fs.readFileSync(path.join(appRoot, 'AGENTS.md'), 'utf8');
224
+ assert.match(agentsContent, /Root contract fallback: node_modules\/proteum\/agents\/project\/AGENTS\.md/);
225
+ assert.equal(
226
+ agentsContent.includes(
227
+ `Root contract fallback: ${expectedSourceMapPath({
228
+ coreRoot: activeCoreRoot,
229
+ instructionRoot: appRoot,
230
+ projectPath: 'AGENTS.md',
231
+ })}`,
232
+ ),
233
+ false,
234
+ );
235
+ assertNoAbsoluteCoreSourceMapPath({ content: agentsContent, coreRoot: activeCoreRoot });
236
+ });
237
+
176
238
  test('configure preserves project content outside the managed section', () => {
177
239
  const coreRoot = createCoreFixture();
178
240
  const appRoot = createAppFixture();
@@ -296,6 +358,22 @@ test('monorepo configure writes root and app instruction files', () => {
296
358
  assert.match(appAgentsContent, /## Agent Routing Contract/);
297
359
  assert.doesNotMatch(appAgentsContent, /## Known Proteum Apps/);
298
360
  assert.doesNotMatch(appAgentsContent, /Eligible Proteum commands run across the apps below/);
361
+ assertSourceMapPath({
362
+ content: fs.readFileSync(path.join(monorepoRoot, 'AGENTS.md'), 'utf8'),
363
+ coreRoot,
364
+ instructionRoot: monorepoRoot,
365
+ label: 'Root contract fallback',
366
+ projectPath: 'AGENTS.md',
367
+ });
368
+ assertSourceMapPath({
369
+ content: appAgentsContent,
370
+ coreRoot,
371
+ instructionRoot: appRoot,
372
+ label: 'Root contract fallback',
373
+ projectPath: 'AGENTS.md',
374
+ });
375
+ assertNoAbsoluteCoreSourceMapPath({ content: fs.readFileSync(path.join(monorepoRoot, 'AGENTS.md'), 'utf8'), coreRoot });
376
+ assertNoAbsoluteCoreSourceMapPath({ content: appAgentsContent, coreRoot });
299
377
  assert.match(fs.readFileSync(path.join(appRoot, 'client', 'AGENTS.md'), 'utf8'), /## Source: client\/AGENTS\.md/);
300
378
  assertClaudeSymlink(appRoot);
301
379
  assertClaudeSymlink(appRoot, 'client');
@@ -306,6 +384,28 @@ test('monorepo configure writes root and app instruction files', () => {
306
384
  assert.equal(result.removed.some((entry) => entry.endsWith('/apps/product/CODING_STYLE.md')), true);
307
385
  });
308
386
 
387
+ test('monorepo configure source map uses workspace install from root and nested apps', () => {
388
+ const activeCoreRoot = createCoreFixture();
389
+ const monorepoRoot = makeTempRoot();
390
+ const appRoot = path.join(monorepoRoot, 'apps', 'product');
391
+
392
+ createCoreFixture(path.join(monorepoRoot, 'node_modules', 'proteum'));
393
+ fs.mkdirSync(path.join(monorepoRoot, '.git'));
394
+ fs.mkdirSync(path.join(appRoot, 'client'), { recursive: true });
395
+ writeFile(path.join(appRoot, 'package.json'), '{"name":"product"}\n');
396
+ writeFile(path.join(appRoot, 'identity.config.ts'), 'export default {};\n');
397
+ writeFile(path.join(appRoot, 'proteum.config.ts'), 'export default {};\n');
398
+
399
+ configureProjectAgentInstructions({ appRoot, coreRoot: activeCoreRoot, monorepoRoot });
400
+
401
+ const rootAgentsContent = fs.readFileSync(path.join(monorepoRoot, 'AGENTS.md'), 'utf8');
402
+ const appAgentsContent = fs.readFileSync(path.join(appRoot, 'AGENTS.md'), 'utf8');
403
+ assert.match(rootAgentsContent, /Root contract fallback: node_modules\/proteum\/agents\/project\/AGENTS\.md/);
404
+ assert.match(appAgentsContent, /Root contract fallback: \.\.\/\.\.\/node_modules\/proteum\/agents\/project\/AGENTS\.md/);
405
+ assertNoAbsoluteCoreSourceMapPath({ content: rootAgentsContent, coreRoot: activeCoreRoot });
406
+ assertNoAbsoluteCoreSourceMapPath({ content: appAgentsContent, coreRoot: activeCoreRoot });
407
+ });
408
+
309
409
  test('monorepo-wide configure writes shared root once and all app instruction files', () => {
310
410
  const coreRoot = createCoreFixture();
311
411
  const monorepoRoot = makeTempRoot();
@@ -103,6 +103,21 @@ const waitForAssetContaining = async (appRoot, extension, marker, timeoutMs = 60
103
103
  throw new Error(`Timed out waiting for ${extension} asset containing ${marker}.`);
104
104
  };
105
105
 
106
+ const waitForBodyContaining = async (port, urlPath, marker, timeoutMs = 60000) => {
107
+ const deadline = Date.now() + timeoutMs;
108
+
109
+ while (Date.now() < deadline) {
110
+ try {
111
+ const { body } = await request(port, urlPath, { Accept: 'text/html' });
112
+ if (body.includes(marker)) return body;
113
+ } catch {}
114
+
115
+ await sleep(250);
116
+ }
117
+
118
+ throw new Error(`Timed out waiting for ${urlPath} body containing ${marker}.`);
119
+ };
120
+
106
121
  const waitForSessionReady = async (sessionFile, child, getOutput, timeoutMs = 90000) => {
107
122
  const deadline = Date.now() + timeoutMs;
108
123
 
@@ -199,8 +214,13 @@ const createSharedStyleSource = (marker) => `.shared-style-marker {
199
214
  `;
200
215
 
201
216
  const createFixture = (root, port, options = {}) => {
202
- const appRoot = path.join(root, 'app');
203
- const sharedRoot = path.join(root, 'shared');
217
+ const monorepoRootInstall = options.monorepoRootInstall === true;
218
+ const appRoot = monorepoRootInstall ? path.join(root, 'apps', 'app') : path.join(root, 'app');
219
+ const sharedRoot = monorepoRootInstall ? path.join(root, 'packages', 'shared') : path.join(root, 'shared');
220
+ const sharedDependency = monorepoRootInstall ? 'file:../../packages/shared' : 'file:../shared';
221
+ const sharedInstallRoot = monorepoRootInstall
222
+ ? path.join(root, 'node_modules', '@test', 'shared')
223
+ : path.join(appRoot, 'node_modules', '@test', 'shared');
204
224
  const cacheConfigSource = options.routerCache ? ` cache: ${options.routerCache},\n` : '';
205
225
 
206
226
  fs.mkdirSync(path.join(appRoot, 'public'), { recursive: true });
@@ -217,7 +237,7 @@ const createFixture = (root, port, options = {}) => {
217
237
  private: true,
218
238
  version: '0.0.0',
219
239
  dependencies: {
220
- '@test/shared': 'file:../shared',
240
+ '@test/shared': sharedDependency,
221
241
  proteum: `file:${coreRoot}`,
222
242
  },
223
243
  },
@@ -474,7 +494,7 @@ export default definePageRoute({
474
494
  writeFile(path.join(sharedRoot, 'styles.css'), createSharedStyleSource('STYLE_MARKER_INITIAL'));
475
495
 
476
496
  createSymlink(coreRoot, path.join(appRoot, 'node_modules', 'proteum'));
477
- createSymlink(sharedRoot, path.join(appRoot, 'node_modules', '@test', 'shared'));
497
+ createSymlink(sharedRoot, sharedInstallRoot);
478
498
 
479
499
  return {
480
500
  appRoot,
@@ -500,11 +520,14 @@ const stopDevServer = async (child) => {
500
520
  });
501
521
  };
502
522
 
503
- const startDevServer = (appRoot, port, sessionFile) => {
523
+ const startDevServer = (appRoot, port, sessionFile, options = {}) => {
504
524
  let output = '';
525
+ const args = [cliBin, 'dev', '--cwd', appRoot, '--port', String(port), '--session-file', sessionFile];
526
+ if (options.noCache !== false) args.push('--no-cache');
527
+ args.push('--verbose');
505
528
  const child = spawn(
506
529
  process.execPath,
507
- [cliBin, 'dev', '--cwd', appRoot, '--port', String(port), '--session-file', sessionFile, '--no-cache', '--verbose'],
530
+ args,
508
531
  {
509
532
  cwd: appRoot,
510
533
  env: {
@@ -577,6 +600,41 @@ test('proteum dev invalidates client assets and reloads for transpiled package s
577
600
  }
578
601
  });
579
602
 
603
+ test(
604
+ 'proteum dev invalidates SSR and client assets for monorepo-root transpiled package installs',
605
+ { timeout: 180000 },
606
+ async () => {
607
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'proteum-monorepo-transpile-watch-'));
608
+ const port = await resolvePortPair();
609
+ const { appRoot, sharedRoot } = createFixture(root, port, { monorepoRootInstall: true });
610
+ const sessionFile = path.join(appRoot, 'var', 'run', 'proteum', 'dev', 'monorepo-transpile-watch-test.json');
611
+ const { child, getOutput } = startDevServer(appRoot, port, sessionFile, { noCache: false });
612
+
613
+ try {
614
+ await waitForSessionReady(sessionFile, child, getOutput);
615
+ await waitForBodyContaining(port, '/', 'SCRIPT_MARKER_INITIAL').catch((error) => {
616
+ throw new Error(`${error.message}\n${getOutput()}`);
617
+ });
618
+ await waitForAssetContaining(appRoot, '.js', 'SCRIPT_MARKER_INITIAL').catch((error) => {
619
+ throw new Error(`${error.message}\n${getOutput()}`);
620
+ });
621
+
622
+ const reloadStream = await connectToReloadStream(port + 1);
623
+ writeFile(path.join(sharedRoot, 'index.tsx'), createSharedIndexSource('SCRIPT_MARKER_MONOREPO_UPDATED'));
624
+
625
+ await waitForAssetContaining(appRoot, '.js', 'SCRIPT_MARKER_MONOREPO_UPDATED');
626
+ await waitForBodyContaining(port, '/', 'SCRIPT_MARKER_MONOREPO_UPDATED');
627
+ const reloadEvent = await reloadStream.waitForReload();
628
+ reloadStream.close();
629
+
630
+ assert.equal(reloadEvent.type, 'reload');
631
+ } finally {
632
+ await stopDevServer(child);
633
+ fs.rmSync(root, { recursive: true, force: true });
634
+ }
635
+ },
636
+ );
637
+
580
638
  test('proteum dev applies router HTTP cache config to HTML and public assets', { timeout: 180000 }, async () => {
581
639
  const root = fs.mkdtempSync(path.join(os.tmpdir(), 'proteum-router-cache-'));
582
640
  const port = await resolvePortPair();