proteum 2.1.7 → 2.1.9-1
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/AGENTS.md +16 -5
- package/README.md +5 -1
- package/agents/project/AGENTS.md +13 -2
- package/agents/project/diagnostics.md +4 -0
- package/agents/project/optimizations.md +1 -0
- package/cli/app/index.ts +33 -9
- package/cli/bin.js +0 -8
- package/cli/commands/build.ts +60 -9
- package/cli/commands/dev.ts +232 -5
- package/cli/compiler/artifacts/commands.ts +20 -5
- package/cli/compiler/client/index.ts +46 -23
- package/cli/compiler/common/bundleAnalysis.ts +56 -1
- package/cli/compiler/common/index.ts +16 -5
- package/cli/compiler/index.ts +12 -5
- package/cli/compiler/server/index.ts +39 -13
- package/cli/index.ts +43 -2
- package/cli/paths.ts +341 -10
- package/cli/presentation/commands.ts +30 -4
- package/cli/presentation/devSession.ts +27 -34
- package/cli/presentation/help.ts +4 -0
- package/cli/presentation/ink.ts +10 -5
- package/cli/presentation/welcome.ts +67 -0
- package/cli/runtime/commands.ts +40 -3
- package/cli/runtime/devSessions.ts +337 -0
- package/cli/scaffold/index.ts +27 -4
- package/cli/scaffold/templates.ts +34 -20
- package/cli/utils/check.ts +5 -11
- package/client/app/index.ts +17 -2
- package/client/app.tsconfig.json +11 -10
- package/common/connectedProjects.ts +7 -0
- package/common/dev/serverHotReload.ts +22 -1
- package/package.json +2 -1
- package/server/app.tsconfig.json +10 -9
- package/server/services/router/http/index.ts +72 -10
package/cli/commands/dev.ts
CHANGED
|
@@ -23,6 +23,20 @@ import Compiler from '../compiler';
|
|
|
23
23
|
import { createDevEventServer } from './devEvents';
|
|
24
24
|
import { ensureProjectAgentSymlinks } from '../utils/agents';
|
|
25
25
|
import { renderDevSession, renderServerReadyBanner, renderDevShutdownBanner } from '../presentation/devSession';
|
|
26
|
+
import { clearInteractiveConsole } from '../presentation/welcome';
|
|
27
|
+
import {
|
|
28
|
+
createDevSessionRecord,
|
|
29
|
+
inspectDevSessionFile,
|
|
30
|
+
listDevSessionInspections,
|
|
31
|
+
removeDevSessionRecord,
|
|
32
|
+
removeDevSessionRecordSync,
|
|
33
|
+
resolveDevSessionFilePath,
|
|
34
|
+
stopDevSessionFile,
|
|
35
|
+
updateDevSessionRecord,
|
|
36
|
+
type TDevSessionInspection,
|
|
37
|
+
type TStopDevSessionResult,
|
|
38
|
+
} from '../runtime/devSessions';
|
|
39
|
+
import { resolveFrameworkInstallInfo } from '../paths';
|
|
26
40
|
import { logVerbose } from '../runtime/verbose';
|
|
27
41
|
|
|
28
42
|
// Core
|
|
@@ -51,6 +65,8 @@ const hotReloadableRoots = [() => app.paths.root, () => cli.paths.core.root];
|
|
|
51
65
|
let cp: ChildProcess | undefined = undefined;
|
|
52
66
|
let devSessionStopping = false;
|
|
53
67
|
let appProcessOperation: Promise<void> = Promise.resolve();
|
|
68
|
+
let currentDevSessionFilePath: string | undefined = undefined;
|
|
69
|
+
let devSessionExitCleanupRegistered = false;
|
|
54
70
|
type TDevWatching = ReturnType<Awaited<ReturnType<Compiler['create']>>['watch']>;
|
|
55
71
|
type TIndexedSourceWatching = { close: () => Promise<void> };
|
|
56
72
|
|
|
@@ -122,6 +138,7 @@ const createIgnoredWatchMatcher = (outputPaths: string[]) => (watchPath: string)
|
|
|
122
138
|
|
|
123
139
|
return ignoredWatchPathPatterns.test(normalizedWatchPath);
|
|
124
140
|
};
|
|
141
|
+
|
|
125
142
|
const getDevAppName = (app: App) =>
|
|
126
143
|
app.identity.web?.fullTitle || app.identity.web?.title || app.identity.name || app.packageJson.name || app.paths.root;
|
|
127
144
|
|
|
@@ -159,6 +176,190 @@ const signalAppProcess = (child: ChildProcess, signal: NodeJS.Signals) => {
|
|
|
159
176
|
}
|
|
160
177
|
};
|
|
161
178
|
|
|
179
|
+
const getRequestedSessionFilePath = () =>
|
|
180
|
+
typeof cli.args.sessionFile === 'string' && cli.args.sessionFile.trim() ? cli.args.sessionFile : undefined;
|
|
181
|
+
|
|
182
|
+
const getResolvedDevSessionFilePath = () =>
|
|
183
|
+
resolveDevSessionFilePath({
|
|
184
|
+
appRoot: app.paths.root,
|
|
185
|
+
port: app.env.router.port,
|
|
186
|
+
sessionFilePath: getRequestedSessionFilePath(),
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
const registerDevSessionExitCleanup = () => {
|
|
190
|
+
if (devSessionExitCleanupRegistered) return;
|
|
191
|
+
|
|
192
|
+
devSessionExitCleanupRegistered = true;
|
|
193
|
+
process.once('exit', () => {
|
|
194
|
+
if (!currentDevSessionFilePath) return;
|
|
195
|
+
removeDevSessionRecordSync(currentDevSessionFilePath);
|
|
196
|
+
});
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const updateCurrentDevSession = async (patch: { publicUrl?: string; state?: 'starting' | 'ready' }) => {
|
|
200
|
+
if (!currentDevSessionFilePath) return;
|
|
201
|
+
|
|
202
|
+
await updateDevSessionRecord({
|
|
203
|
+
sessionFilePath: currentDevSessionFilePath,
|
|
204
|
+
patch,
|
|
205
|
+
});
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const cleanupCurrentDevSession = async () => {
|
|
209
|
+
if (!currentDevSessionFilePath) return;
|
|
210
|
+
|
|
211
|
+
const sessionFilePath = currentDevSessionFilePath;
|
|
212
|
+
currentDevSessionFilePath = undefined;
|
|
213
|
+
await removeDevSessionRecord(sessionFilePath);
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const describeInspection = (inspection: TDevSessionInspection) => {
|
|
217
|
+
if (!inspection.record) {
|
|
218
|
+
return [
|
|
219
|
+
'stale invalid',
|
|
220
|
+
inspection.sessionFilePath,
|
|
221
|
+
inspection.parseError || 'Unreadable session file.',
|
|
222
|
+
].join(' | ');
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const parts = [
|
|
226
|
+
inspection.live ? 'live' : 'stale',
|
|
227
|
+
inspection.record.state,
|
|
228
|
+
`pid ${inspection.record.pid}`,
|
|
229
|
+
`port ${inspection.record.routerPort}`,
|
|
230
|
+
];
|
|
231
|
+
|
|
232
|
+
if (inspection.record.publicUrl) parts.push(inspection.record.publicUrl);
|
|
233
|
+
parts.push(inspection.sessionFilePath);
|
|
234
|
+
|
|
235
|
+
return parts.join(' | ');
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
const describeStopResult = (result: TStopDevSessionResult) => {
|
|
239
|
+
if (!result.matched) return `missing | ${result.sessionFilePath}`;
|
|
240
|
+
if (result.invalid)
|
|
241
|
+
return `removed stale invalid | ${result.sessionFilePath} | ${result.parseError || 'Unreadable session file.'}`;
|
|
242
|
+
if (result.removed && result.stopped && !result.live) {
|
|
243
|
+
return [
|
|
244
|
+
result.pid !== null ? `stopped pid ${result.pid}` : 'stopped',
|
|
245
|
+
result.routerPort !== null ? `port ${result.routerPort}` : '',
|
|
246
|
+
result.publicUrl,
|
|
247
|
+
result.sessionFilePath,
|
|
248
|
+
]
|
|
249
|
+
.filter(Boolean)
|
|
250
|
+
.join(' | ');
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return [
|
|
254
|
+
'failed',
|
|
255
|
+
result.pid !== null ? `pid ${result.pid}` : '',
|
|
256
|
+
result.routerPort !== null ? `port ${result.routerPort}` : '',
|
|
257
|
+
result.publicUrl,
|
|
258
|
+
result.sessionFilePath,
|
|
259
|
+
]
|
|
260
|
+
.filter(Boolean)
|
|
261
|
+
.join(' | ');
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
const printJson = (payload: unknown) => {
|
|
265
|
+
process.stdout.write(JSON.stringify(payload, null, 2) + '\n');
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
const runListCommand = async () => {
|
|
269
|
+
const inspections = await listDevSessionInspections({
|
|
270
|
+
appRoot: app.paths.root,
|
|
271
|
+
sessionFilePath: getRequestedSessionFilePath(),
|
|
272
|
+
});
|
|
273
|
+
const filteredInspections = cli.args.stale === true ? inspections.filter((inspection) => inspection.stale) : inspections;
|
|
274
|
+
|
|
275
|
+
if (cli.args.json === true) {
|
|
276
|
+
printJson({
|
|
277
|
+
appRoot: app.paths.root,
|
|
278
|
+
sessions: filteredInspections.map((inspection) => ({
|
|
279
|
+
sessionFilePath: inspection.sessionFilePath,
|
|
280
|
+
live: inspection.live,
|
|
281
|
+
stale: inspection.stale,
|
|
282
|
+
invalid: inspection.invalid,
|
|
283
|
+
parseError: inspection.parseError,
|
|
284
|
+
record: inspection.record,
|
|
285
|
+
})),
|
|
286
|
+
});
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (filteredInspections.length === 0) {
|
|
291
|
+
console.info(`No Proteum dev sessions found for ${app.paths.root}.`);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
console.info(filteredInspections.map(describeInspection).join('\n'));
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
const runStopCommand = async () => {
|
|
299
|
+
const stopAll = cli.args.all === true;
|
|
300
|
+
const filterStale = cli.args.stale === true;
|
|
301
|
+
|
|
302
|
+
const targetSessionFilePaths = stopAll
|
|
303
|
+
? (await listDevSessionInspections({
|
|
304
|
+
appRoot: app.paths.root,
|
|
305
|
+
sessionFilePath: getRequestedSessionFilePath(),
|
|
306
|
+
}))
|
|
307
|
+
.filter((inspection) => !filterStale || inspection.stale)
|
|
308
|
+
.map((inspection) => inspection.sessionFilePath)
|
|
309
|
+
: [getResolvedDevSessionFilePath()];
|
|
310
|
+
|
|
311
|
+
const results = await Promise.all(targetSessionFilePaths.map((sessionFilePath) => stopDevSessionFile(sessionFilePath)));
|
|
312
|
+
const failedResults = results.filter((result) => result.matched && !result.stopped);
|
|
313
|
+
|
|
314
|
+
if (cli.args.json === true) {
|
|
315
|
+
printJson({ appRoot: app.paths.root, results });
|
|
316
|
+
} else if (results.length === 0) {
|
|
317
|
+
console.info(`No Proteum dev sessions matched for ${app.paths.root}.`);
|
|
318
|
+
} else {
|
|
319
|
+
console.info(results.map(describeStopResult).join('\n'));
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (failedResults.length > 0) {
|
|
323
|
+
process.exitCode = 1;
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
const ensureDevSessionSlot = async () => {
|
|
328
|
+
const sessionFilePath = getResolvedDevSessionFilePath();
|
|
329
|
+
const existingInspection = await inspectDevSessionFile(sessionFilePath);
|
|
330
|
+
|
|
331
|
+
if (existingInspection?.record && existingInspection.live && existingInspection.record.pid !== process.pid) {
|
|
332
|
+
if (cli.args.replaceExisting !== true) {
|
|
333
|
+
throw new Error(
|
|
334
|
+
`A Proteum dev session is already registered at ${sessionFilePath} (pid ${existingInspection.record.pid}, port ${existingInspection.record.routerPort}). ` +
|
|
335
|
+
'Use `proteum dev stop` or restart with `proteum dev --replace-existing`.',
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const stopResult = await stopDevSessionFile(sessionFilePath);
|
|
340
|
+
if (!stopResult.stopped) {
|
|
341
|
+
throw new Error(`Could not stop the existing Proteum dev session registered at ${sessionFilePath}.`);
|
|
342
|
+
}
|
|
343
|
+
} else if (existingInspection) {
|
|
344
|
+
await stopDevSessionFile(sessionFilePath);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
currentDevSessionFilePath = sessionFilePath;
|
|
348
|
+
registerDevSessionExitCleanup();
|
|
349
|
+
await fs.ensureDir(path.dirname(sessionFilePath));
|
|
350
|
+
await fs.writeJson(
|
|
351
|
+
sessionFilePath,
|
|
352
|
+
createDevSessionRecord({
|
|
353
|
+
appRoot: app.paths.root,
|
|
354
|
+
port: app.env.router.port,
|
|
355
|
+
sessionFilePath,
|
|
356
|
+
}),
|
|
357
|
+
{ spaces: 2 },
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
logVerbose(`Registered Proteum dev session at ${sessionFilePath}.`);
|
|
361
|
+
};
|
|
362
|
+
|
|
162
363
|
async function startApp(app: App) {
|
|
163
364
|
await runSerializedAppProcessOperation(async () => {
|
|
164
365
|
if (devSessionStopping) return;
|
|
@@ -166,6 +367,7 @@ async function startApp(app: App) {
|
|
|
166
367
|
await stopAppInternal('Restart asked');
|
|
167
368
|
if (devSessionStopping) return;
|
|
168
369
|
|
|
370
|
+
await updateCurrentDevSession({ state: 'starting', publicUrl: '' });
|
|
169
371
|
logVerbose('Launching new server ...');
|
|
170
372
|
cp = spawn('node', ['--preserve-symlinks', app.outputPath('dev') + '/server.js'], {
|
|
171
373
|
// stdin, stdout, stderr
|
|
@@ -191,10 +393,11 @@ async function startApp(app: App) {
|
|
|
191
393
|
if (isServerReadyMessage(message)) {
|
|
192
394
|
childReady = true;
|
|
193
395
|
void (async () => {
|
|
396
|
+
await updateCurrentDevSession({ publicUrl: message.publicUrl, state: 'ready' });
|
|
194
397
|
console.info(
|
|
195
398
|
await renderServerReadyBanner({
|
|
196
399
|
appName: getDevAppName(app),
|
|
197
|
-
|
|
400
|
+
connectedProjects: message.connectedProjects,
|
|
198
401
|
publicUrl: message.publicUrl,
|
|
199
402
|
routerPort: app.env.router.port,
|
|
200
403
|
}),
|
|
@@ -351,12 +554,15 @@ const createIndexedSourceWatching = ({
|
|
|
351
554
|
};
|
|
352
555
|
};
|
|
353
556
|
|
|
354
|
-
|
|
355
|
-
- MAIN PROCESS
|
|
356
|
-
----------------------------------*/
|
|
357
|
-
export const run = async () => {
|
|
557
|
+
const runDevLoop = async () => {
|
|
358
558
|
devSessionStopping = false;
|
|
559
|
+
clearInteractiveConsole();
|
|
359
560
|
ensureProjectAgentSymlinks({ appRoot: app.paths.root, coreRoot: cli.paths.core.root });
|
|
561
|
+
await ensureDevSessionSlot();
|
|
562
|
+
const proteumInstall = resolveFrameworkInstallInfo({
|
|
563
|
+
appRoot: app.paths.root,
|
|
564
|
+
framework: cli.paths.framework,
|
|
565
|
+
});
|
|
360
566
|
|
|
361
567
|
const devEventServer = await createDevEventServer(app.env.router.port + 1);
|
|
362
568
|
app.devEventPort = devEventServer.port;
|
|
@@ -367,6 +573,7 @@ export const run = async () => {
|
|
|
367
573
|
connectedProjects: Object.values(app.env.connectedProjects),
|
|
368
574
|
routerPort: app.env.router.port,
|
|
369
575
|
devEventPort: devEventServer.port,
|
|
576
|
+
proteumInstallSummary: proteumInstall.summary,
|
|
370
577
|
proteumVersion: String(cli.packageJson.version || ''),
|
|
371
578
|
}),
|
|
372
579
|
);
|
|
@@ -472,6 +679,7 @@ export const run = async () => {
|
|
|
472
679
|
await stopApp(reason);
|
|
473
680
|
await cleanupPersistedDevTraces(app);
|
|
474
681
|
await devEventServer.close();
|
|
682
|
+
await cleanupCurrentDevSession();
|
|
475
683
|
console.info(await renderDevShutdownBanner());
|
|
476
684
|
})();
|
|
477
685
|
|
|
@@ -508,3 +716,22 @@ export const run = async () => {
|
|
|
508
716
|
process.once('SIGTERM', () => exitAfterShutdown('SIGTERM', 0));
|
|
509
717
|
process.once('SIGHUP', () => exitAfterShutdown('SIGHUP', 0));
|
|
510
718
|
};
|
|
719
|
+
|
|
720
|
+
/*----------------------------------
|
|
721
|
+
- MAIN PROCESS
|
|
722
|
+
----------------------------------*/
|
|
723
|
+
export const run = async () => {
|
|
724
|
+
const action = typeof cli.args.action === 'string' ? cli.args.action : 'start';
|
|
725
|
+
|
|
726
|
+
if (action === 'list') {
|
|
727
|
+
await runListCommand();
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
if (action === 'stop') {
|
|
732
|
+
await runStopCommand();
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
await runDevLoop();
|
|
737
|
+
};
|
|
@@ -22,8 +22,15 @@ const readServerTsconfigPaths = () => {
|
|
|
22
22
|
return compilerOptions.paths || {};
|
|
23
23
|
};
|
|
24
24
|
|
|
25
|
-
const
|
|
26
|
-
|
|
25
|
+
const getCommandsTsconfigFilepath = () => path.join(app.paths.root, 'commands', 'tsconfig.json');
|
|
26
|
+
|
|
27
|
+
const getCommandsGlobalTypesPath = (commandsTsconfigFilepath: string) =>
|
|
28
|
+
cli.paths.relativeFrameworkPathFrom(commandsTsconfigFilepath, 'types', 'global');
|
|
29
|
+
|
|
30
|
+
const createCommandsTsconfigContent = () => {
|
|
31
|
+
const commandsTsconfigFilepath = getCommandsTsconfigFilepath();
|
|
32
|
+
|
|
33
|
+
return `${JSON.stringify(
|
|
27
34
|
{
|
|
28
35
|
extends: '../server/tsconfig.json',
|
|
29
36
|
compilerOptions: {
|
|
@@ -35,12 +42,13 @@ const createCommandsTsconfigContent = () =>
|
|
|
35
42
|
'@models/types': ['./.proteum/server/models.ts'],
|
|
36
43
|
},
|
|
37
44
|
},
|
|
38
|
-
include: ['.', '../var/typings',
|
|
45
|
+
include: ['.', '../var/typings', getCommandsGlobalTypesPath(commandsTsconfigFilepath), '../.proteum/server/commands.d.ts'],
|
|
39
46
|
},
|
|
40
47
|
null,
|
|
41
48
|
4,
|
|
42
49
|
)}
|
|
43
50
|
`;
|
|
51
|
+
};
|
|
44
52
|
|
|
45
53
|
const legacyCommandsTsconfigContent = `{
|
|
46
54
|
"extends": "../server/tsconfig.json",
|
|
@@ -105,8 +113,15 @@ const isManagedCommandsTsconfig = (content: string) => {
|
|
|
105
113
|
};
|
|
106
114
|
|
|
107
115
|
if (parsed.extends !== '../server/tsconfig.json') return false;
|
|
108
|
-
if (
|
|
116
|
+
if (!Array.isArray(parsed.include) || parsed.include.length !== 4) return false;
|
|
117
|
+
if (parsed.include[0] !== '.' || parsed.include[1] !== '../var/typings') return false;
|
|
118
|
+
if (parsed.include[3] !== '../.proteum/server/commands.d.ts') return false;
|
|
119
|
+
if (
|
|
120
|
+
parsed.include[2] !== getCommandsGlobalTypesPath(getCommandsTsconfigFilepath()) &&
|
|
121
|
+
!parsed.include[2].includes('node_modules/proteum/types/global')
|
|
122
|
+
) {
|
|
109
123
|
return false;
|
|
124
|
+
}
|
|
110
125
|
|
|
111
126
|
if (parsed.compilerOptions?.baseUrl !== undefined && parsed.compilerOptions.baseUrl !== '..') return false;
|
|
112
127
|
if (parsed.compilerOptions?.rootDir !== undefined && parsed.compilerOptions.rootDir !== '..') return false;
|
|
@@ -119,7 +134,7 @@ const isManagedCommandsTsconfig = (content: string) => {
|
|
|
119
134
|
|
|
120
135
|
const ensureCommandsTsconfig = () => {
|
|
121
136
|
const commandsRoot = path.join(app.paths.root, 'commands');
|
|
122
|
-
const commandsTsconfigFilepath =
|
|
137
|
+
const commandsTsconfigFilepath = getCommandsTsconfigFilepath();
|
|
123
138
|
const nextContent = createCommandsTsconfigContent();
|
|
124
139
|
|
|
125
140
|
if (!fs.existsSync(commandsRoot)) return;
|
|
@@ -21,22 +21,40 @@ import type { App } from '../../app';
|
|
|
21
21
|
|
|
22
22
|
const debug = false;
|
|
23
23
|
const ssrScriptPattern = /\.ssr\.(ts|tsx)$/;
|
|
24
|
-
const normalizedCoreRoot = cli.paths.core.root.replace(/\\/g, '/');
|
|
25
|
-
const hmrClientEntry = path.join(cli.paths.core.root, 'client', 'dev', 'hmr.ts');
|
|
26
|
-
|
|
27
24
|
const normalizeModulePath = (value?: string) => (value || '').replace(/\\/g, '/');
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const activeCoreRoot = normalizeModulePath(cli.paths.
|
|
25
|
+
const getFrameworkSourceRoot = () => {
|
|
26
|
+
const installedCoreRoot = cli.paths.framework.installedRoot
|
|
27
|
+
? normalizeModulePath(cli.paths.framework.installedRoot)
|
|
28
|
+
: undefined;
|
|
29
|
+
const activeCoreRoot = normalizeModulePath(cli.paths.framework.activeRoot);
|
|
30
|
+
|
|
31
|
+
if (installedCoreRoot && activeCoreRoot.includes('/node_modules/')) {
|
|
32
|
+
return installedCoreRoot;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return activeCoreRoot;
|
|
36
|
+
};
|
|
33
37
|
|
|
34
|
-
|
|
38
|
+
const resolveFromAppOrCore = (_app: App, request: string) => cli.paths.resolveRequest(request);
|
|
39
|
+
const rewriteFrameworkAliasTargets = (aliases: Record<string, string | string[]>) => {
|
|
40
|
+
const visibleFrameworkRoots = [
|
|
41
|
+
...cli.paths.getVisiblePackageInstallRoots('proteum'),
|
|
42
|
+
cli.paths.framework.installedRoot,
|
|
43
|
+
cli.paths.framework.activeRoot,
|
|
44
|
+
]
|
|
45
|
+
.filter((rootPath): rootPath is string => typeof rootPath === 'string' && rootPath !== '')
|
|
46
|
+
.map((rootPath) => normalizeModulePath(rootPath))
|
|
47
|
+
.filter((rootPath, index, list) => list.indexOf(rootPath) === index);
|
|
48
|
+
const frameworkSourceRoot = getFrameworkSourceRoot();
|
|
35
49
|
|
|
36
50
|
const rewriteCandidate = (candidate: string) =>
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
51
|
+
visibleFrameworkRoots.reduce((nextCandidate, rootPath) => {
|
|
52
|
+
const normalizedCandidate = normalizeModulePath(nextCandidate);
|
|
53
|
+
|
|
54
|
+
return normalizedCandidate.startsWith(rootPath + '/')
|
|
55
|
+
? frameworkSourceRoot + normalizedCandidate.substring(rootPath.length)
|
|
56
|
+
: nextCandidate;
|
|
57
|
+
}, candidate);
|
|
40
58
|
|
|
41
59
|
return Object.fromEntries(
|
|
42
60
|
Object.entries(aliases).map(([alias, value]) => [
|
|
@@ -61,8 +79,9 @@ const isExternalVendorModule = (module: Module) => {
|
|
|
61
79
|
|
|
62
80
|
const isCoreSourceModule = (module: Module) => {
|
|
63
81
|
const modulePath = getModulePath(module);
|
|
82
|
+
const frameworkSourceRoot = getFrameworkSourceRoot();
|
|
64
83
|
|
|
65
|
-
return modulePath.startsWith(
|
|
84
|
+
return modulePath.startsWith(frameworkSourceRoot + '/') || modulePath.includes('/node_modules/proteum/');
|
|
66
85
|
};
|
|
67
86
|
|
|
68
87
|
/*----------------------------------
|
|
@@ -76,9 +95,12 @@ export default function createCompiler(
|
|
|
76
95
|
logVerbose(`Creating compiler for client (${mode}).`);
|
|
77
96
|
const dev = mode === 'dev';
|
|
78
97
|
const outputPath = app.outputPath(outputTarget);
|
|
79
|
-
const
|
|
80
|
-
const frameworkRoots = [cli.paths.
|
|
98
|
+
const frameworkSourceRoot = getFrameworkSourceRoot();
|
|
99
|
+
const frameworkRoots = [frameworkSourceRoot, ...cli.paths.getFrameworkRoots()].filter(
|
|
100
|
+
(rootPath, index, list) => list.indexOf(rootPath) === index,
|
|
101
|
+
);
|
|
81
102
|
const transpileModuleDirectories = app.transpileModuleDirectories;
|
|
103
|
+
const hmrClientEntry = path.join(frameworkSourceRoot, 'client', 'dev', 'hmr.ts');
|
|
82
104
|
|
|
83
105
|
const commonConfig = createCommonConfig(app, 'client', mode, outputTarget);
|
|
84
106
|
|
|
@@ -93,14 +115,15 @@ export default function createCompiler(
|
|
|
93
115
|
);*/
|
|
94
116
|
|
|
95
117
|
// Convert tsconfig paths into bundler aliases.
|
|
96
|
-
const { aliases } = app.aliases.client.forWebpack({ modulesPath:
|
|
97
|
-
const resolvedAliases = rewriteFrameworkAliasTargets(
|
|
118
|
+
const { aliases } = app.aliases.client.forWebpack({ modulesPath: cli.paths.framework.appNodeModulesRoot });
|
|
119
|
+
const resolvedAliases = rewriteFrameworkAliasTargets(aliases);
|
|
98
120
|
|
|
99
121
|
// We're not supposed in any case to import server libs from client
|
|
100
122
|
delete resolvedAliases['@server'];
|
|
101
123
|
delete resolvedAliases['@/server'];
|
|
102
124
|
const rspackAliases = toRspackAliases(resolvedAliases);
|
|
103
|
-
rspackAliases['
|
|
125
|
+
rspackAliases['proteum'] = frameworkSourceRoot;
|
|
126
|
+
rspackAliases['@/client/router$'] = frameworkSourceRoot + '/client/router.ts';
|
|
104
127
|
rspackAliases['preact/jsx-runtime$'] = resolveFromAppOrCore(app, 'preact/jsx-runtime');
|
|
105
128
|
rspackAliases['react/jsx-runtime$'] = resolveFromAppOrCore(app, 'preact/jsx-runtime');
|
|
106
129
|
rspackAliases['react/jsx-dev-runtime$'] = resolveFromAppOrCore(app, 'preact/jsx-dev-runtime');
|
|
@@ -113,8 +136,8 @@ export default function createCompiler(
|
|
|
113
136
|
target: 'web',
|
|
114
137
|
entry: {
|
|
115
138
|
client: dev
|
|
116
|
-
? [hmrClientEntry,
|
|
117
|
-
: [
|
|
139
|
+
? [hmrClientEntry, frameworkSourceRoot + '/client/index.ts']
|
|
140
|
+
: [frameworkSourceRoot + '/client/index.ts'],
|
|
118
141
|
},
|
|
119
142
|
|
|
120
143
|
output: {
|
|
@@ -168,7 +191,7 @@ export default function createCompiler(
|
|
|
168
191
|
...transpileModuleDirectories,
|
|
169
192
|
],
|
|
170
193
|
loader: path.join(
|
|
171
|
-
|
|
194
|
+
frameworkSourceRoot,
|
|
172
195
|
'cli',
|
|
173
196
|
'compiler',
|
|
174
197
|
'common',
|
|
@@ -229,11 +252,11 @@ export default function createCompiler(
|
|
|
229
252
|
: [
|
|
230
253
|
new rspack.NormalModuleReplacementPlugin(
|
|
231
254
|
/^@client\/dev\/profiler$/,
|
|
232
|
-
|
|
255
|
+
frameworkSourceRoot + '/client/dev/profiler/noop.tsx',
|
|
233
256
|
),
|
|
234
257
|
new rspack.NormalModuleReplacementPlugin(
|
|
235
258
|
/^@client\/dev\/profiler\/runtime$/,
|
|
236
|
-
|
|
259
|
+
frameworkSourceRoot + '/client/dev/profiler/runtime.noop.ts',
|
|
237
260
|
),
|
|
238
261
|
]),
|
|
239
262
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'fs-extra';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import type { RspackPluginInstance } from '@rspack/core';
|
|
4
|
+
import { UsageError } from 'clipanion';
|
|
4
5
|
|
|
5
6
|
import cli from '../..';
|
|
6
7
|
import type { App } from '../../app';
|
|
@@ -8,8 +9,56 @@ import type { App } from '../../app';
|
|
|
8
9
|
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
|
|
9
10
|
|
|
10
11
|
export type TBundleAnalysisReportPaths = { reportPath: string; statsPath: string };
|
|
12
|
+
export type TBundleAnalysisMode = 'server' | 'static';
|
|
13
|
+
type TBundleAnalysisServerUrlArgs = {
|
|
14
|
+
listenHost: string;
|
|
15
|
+
listenPort: number | 'auto';
|
|
16
|
+
boundAddress?: string | { address?: string; port?: number } | null;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const defaultAnalyzerHost = '127.0.0.1';
|
|
20
|
+
const defaultAnalyzerPort = 8888;
|
|
21
|
+
let latestClientBundleAnalysisServerUrl: string | undefined;
|
|
11
22
|
|
|
12
23
|
export const isBundleAnalysisEnabled = () => cli.args.analyze === true;
|
|
24
|
+
export const isBundleAnalysisServerEnabled = () => cli.args.analyzeServe === true;
|
|
25
|
+
export const getBundleAnalysisMode = (): TBundleAnalysisMode => (isBundleAnalysisServerEnabled() ? 'server' : 'static');
|
|
26
|
+
|
|
27
|
+
const hasCliStringArg = (name: string) => typeof cli.args[name] === 'string' && (cli.args[name] as string).trim().length > 0;
|
|
28
|
+
|
|
29
|
+
export const hasBundleAnalysisServerOverrides = () => hasCliStringArg('analyzeHost') || hasCliStringArg('analyzePort');
|
|
30
|
+
|
|
31
|
+
export const getBundleAnalysisServerHost = () =>
|
|
32
|
+
hasCliStringArg('analyzeHost') ? String(cli.args.analyzeHost).trim() : defaultAnalyzerHost;
|
|
33
|
+
|
|
34
|
+
export const getBundleAnalysisServerPort = (): number | 'auto' => {
|
|
35
|
+
const rawPort = hasCliStringArg('analyzePort') ? String(cli.args.analyzePort).trim() : '';
|
|
36
|
+
if (!rawPort) return defaultAnalyzerPort;
|
|
37
|
+
if (rawPort === 'auto') return 'auto';
|
|
38
|
+
|
|
39
|
+
const parsedPort = Number.parseInt(rawPort, 10);
|
|
40
|
+
if (!Number.isInteger(parsedPort) || parsedPort < 1 || parsedPort > 65535) {
|
|
41
|
+
throw new UsageError(`Invalid analyzer port "${rawPort}". Use a number between 1 and 65535, or \`auto\`.`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return parsedPort;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const createBundleAnalysisServerUrl = ({ listenHost, listenPort, boundAddress }: TBundleAnalysisServerUrlArgs) => {
|
|
48
|
+
const port =
|
|
49
|
+
typeof boundAddress === 'object' && boundAddress !== null && typeof boundAddress.port === 'number'
|
|
50
|
+
? boundAddress.port
|
|
51
|
+
: listenPort;
|
|
52
|
+
const url = `http://${listenHost}:${port}`;
|
|
53
|
+
latestClientBundleAnalysisServerUrl = url;
|
|
54
|
+
return url;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export const consumeClientBundleAnalysisServerUrl = () => {
|
|
58
|
+
const url = latestClientBundleAnalysisServerUrl;
|
|
59
|
+
latestClientBundleAnalysisServerUrl = undefined;
|
|
60
|
+
return url;
|
|
61
|
+
};
|
|
13
62
|
|
|
14
63
|
export const getClientBundleAnalysisReportPaths = (
|
|
15
64
|
app: App,
|
|
@@ -23,19 +72,25 @@ export const getClientBundleAnalysisReportPaths = (
|
|
|
23
72
|
export const createClientBundleAnalysisPlugins = (app: App, outputTarget: 'dev' | 'bin'): RspackPluginInstance[] => {
|
|
24
73
|
if (!isBundleAnalysisEnabled()) return [];
|
|
25
74
|
|
|
75
|
+
latestClientBundleAnalysisServerUrl = undefined;
|
|
76
|
+
|
|
26
77
|
const { reportPath, statsPath } = getClientBundleAnalysisReportPaths(app, outputTarget);
|
|
78
|
+
const analyzerMode = getBundleAnalysisMode();
|
|
27
79
|
|
|
28
80
|
fs.ensureDirSync(path.dirname(reportPath));
|
|
29
81
|
|
|
30
82
|
return [
|
|
31
83
|
new BundleAnalyzerPlugin({
|
|
32
|
-
analyzerMode
|
|
84
|
+
analyzerMode,
|
|
85
|
+
analyzerHost: getBundleAnalysisServerHost(),
|
|
86
|
+
analyzerPort: getBundleAnalysisServerPort(),
|
|
33
87
|
openAnalyzer: false,
|
|
34
88
|
defaultSizes: 'parsed',
|
|
35
89
|
reportFilename: reportPath,
|
|
36
90
|
generateStatsFile: true,
|
|
37
91
|
statsFilename: statsPath,
|
|
38
92
|
logLevel: 'info',
|
|
93
|
+
analyzerUrl: createBundleAnalysisServerUrl,
|
|
39
94
|
}),
|
|
40
95
|
];
|
|
41
96
|
};
|
|
@@ -45,6 +45,21 @@ export default function createCommonConfig(
|
|
|
45
45
|
): Configuration {
|
|
46
46
|
const dev = mode === 'dev';
|
|
47
47
|
const enableFilesystemCache = dev ? cli.args.cache !== false : cli.args.cache === true;
|
|
48
|
+
const frameworkPackageRoots = [cli.paths.framework.installedRoot, cli.paths.framework.activeRoot].filter(
|
|
49
|
+
(rootPath, index, list): rootPath is string => typeof rootPath === 'string' && list.indexOf(rootPath) === index,
|
|
50
|
+
);
|
|
51
|
+
const visibleNodeModulesRoots = [
|
|
52
|
+
...cli.paths.getVisibleNodeModulesRootsForPath(app.paths.root),
|
|
53
|
+
...frameworkPackageRoots.flatMap((rootPath) => cli.paths.getVisibleNodeModulesRootsForPath(rootPath)),
|
|
54
|
+
...cli.paths.getVisibleNodeModulesRootsForPath(cli.paths.core.cli),
|
|
55
|
+
].filter((moduleRoot, index, list) => list.indexOf(moduleRoot) === index);
|
|
56
|
+
const loaderModuleRoots = [
|
|
57
|
+
...visibleNodeModulesRoots,
|
|
58
|
+
...frameworkPackageRoots.map((rootPath) => path.join(rootPath, 'node_modules')),
|
|
59
|
+
cli.paths.framework.appNodeModulesRoot,
|
|
60
|
+
cli.paths.framework.frameworkNodeModulesRoot,
|
|
61
|
+
path.join(cli.paths.core.cli, 'node_modules'),
|
|
62
|
+
].filter((moduleRoot, index, list) => list.indexOf(moduleRoot) === index);
|
|
48
63
|
const config: Configuration = {
|
|
49
64
|
// Project root
|
|
50
65
|
context: app.paths.root,
|
|
@@ -55,11 +70,7 @@ export default function createCommonConfig(
|
|
|
55
70
|
// Support both install modes:
|
|
56
71
|
// - npm i: loaders are often hoisted in app/node_modules
|
|
57
72
|
// - npm link: loaders often live in framework/node_modules
|
|
58
|
-
modules:
|
|
59
|
-
app.paths.root + '/node_modules',
|
|
60
|
-
cli.paths.core.root + '/node_modules',
|
|
61
|
-
cli.paths.core.cli + '/node_modules',
|
|
62
|
-
],
|
|
73
|
+
modules: loaderModuleRoots,
|
|
63
74
|
mainFields: ['loader', 'main'],
|
|
64
75
|
},
|
|
65
76
|
|
package/cli/compiler/index.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { rspack, type Compiler as RspackCompiler } from '@rspack/core';
|
|
|
9
9
|
|
|
10
10
|
// Core
|
|
11
11
|
import app from '../app';
|
|
12
|
+
import cli from '..';
|
|
12
13
|
import createServerConfig from './server';
|
|
13
14
|
import createClientConfig from './client';
|
|
14
15
|
import { TCompileMode, TCompileOutputTarget } from './common';
|
|
@@ -62,15 +63,21 @@ export default class Compiler {
|
|
|
62
63
|
- Including React, so VSCode shows that JSX is missing
|
|
63
64
|
*/
|
|
64
65
|
public fixNpmLinkIssues() {
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
const installedFrameworkRoot = cli.paths.framework.installedRoot;
|
|
67
|
+
|
|
68
|
+
if (!installedFrameworkRoot || !fs.existsSync(installedFrameworkRoot)) {
|
|
69
|
+
return logVerbose("Not fixing npm link issues because the app can't see an installed Proteum package.");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!fs.lstatSync(installedFrameworkRoot).isSymbolicLink()) {
|
|
73
|
+
return logVerbose("Not fixing npm link issues because Proteum wasn't installed with npm link.");
|
|
74
|
+
}
|
|
68
75
|
|
|
69
76
|
this.debug && logVerbose(`Fix NPM link issues ...`);
|
|
70
77
|
const outputPath = app.outputPath(this.outputTarget);
|
|
71
78
|
|
|
72
|
-
const appModules =
|
|
73
|
-
const coreModules =
|
|
79
|
+
const appModules = cli.paths.framework.appNodeModulesRoot;
|
|
80
|
+
const coreModules = cli.paths.framework.frameworkNodeModulesRoot;
|
|
74
81
|
|
|
75
82
|
// When the 5htp package is installed from npm link,
|
|
76
83
|
// Modules are installed locally and not glbally as with with the 5htp package from NPM.
|