@rs-x/cli 2.0.0-next.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.
- package/LICENSE +21 -0
- package/README.md +92 -0
- package/bin/rsx.cjs +3509 -0
- package/package.json +28 -0
- package/rs-x-vscode-extension-2.0.0-next.0.vsix +0 -0
- package/scripts/postinstall.cjs +90 -0
- package/scripts/prepare-vsix.cjs +59 -0
package/bin/rsx.cjs
ADDED
|
@@ -0,0 +1,3509 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const readline = require('node:readline/promises');
|
|
6
|
+
const { spawnSync } = require('node:child_process');
|
|
7
|
+
|
|
8
|
+
const CLI_VERSION = '0.2.0';
|
|
9
|
+
const VS_CODE_EXTENSION_ID = 'rs-x.rs-x-vscode-extension';
|
|
10
|
+
const RUNTIME_PACKAGES = [
|
|
11
|
+
'@rs-x/core',
|
|
12
|
+
'@rs-x/state-manager',
|
|
13
|
+
'@rs-x/expression-parser',
|
|
14
|
+
];
|
|
15
|
+
const COMPILER_PACKAGES = ['@rs-x/compiler', '@rs-x/typescript-plugin'];
|
|
16
|
+
const RSX_PACKAGE_VERSION = '^1.0.2';
|
|
17
|
+
const PROJECT_TEMPLATES = ['angular', 'vuejs', 'react', 'nextjs', 'nodejs'];
|
|
18
|
+
const TS_RESERVED_WORDS = new Set([
|
|
19
|
+
'abstract',
|
|
20
|
+
'any',
|
|
21
|
+
'as',
|
|
22
|
+
'asserts',
|
|
23
|
+
'async',
|
|
24
|
+
'await',
|
|
25
|
+
'bigint',
|
|
26
|
+
'boolean',
|
|
27
|
+
'break',
|
|
28
|
+
'case',
|
|
29
|
+
'catch',
|
|
30
|
+
'class',
|
|
31
|
+
'const',
|
|
32
|
+
'continue',
|
|
33
|
+
'debugger',
|
|
34
|
+
'declare',
|
|
35
|
+
'default',
|
|
36
|
+
'delete',
|
|
37
|
+
'do',
|
|
38
|
+
'else',
|
|
39
|
+
'enum',
|
|
40
|
+
'export',
|
|
41
|
+
'extends',
|
|
42
|
+
'false',
|
|
43
|
+
'finally',
|
|
44
|
+
'for',
|
|
45
|
+
'from',
|
|
46
|
+
'function',
|
|
47
|
+
'get',
|
|
48
|
+
'if',
|
|
49
|
+
'implements',
|
|
50
|
+
'import',
|
|
51
|
+
'in',
|
|
52
|
+
'infer',
|
|
53
|
+
'instanceof',
|
|
54
|
+
'interface',
|
|
55
|
+
'is',
|
|
56
|
+
'keyof',
|
|
57
|
+
'let',
|
|
58
|
+
'module',
|
|
59
|
+
'namespace',
|
|
60
|
+
'never',
|
|
61
|
+
'new',
|
|
62
|
+
'null',
|
|
63
|
+
'number',
|
|
64
|
+
'object',
|
|
65
|
+
'of',
|
|
66
|
+
'package',
|
|
67
|
+
'private',
|
|
68
|
+
'protected',
|
|
69
|
+
'public',
|
|
70
|
+
'readonly',
|
|
71
|
+
'return',
|
|
72
|
+
'satisfies',
|
|
73
|
+
'set',
|
|
74
|
+
'static',
|
|
75
|
+
'string',
|
|
76
|
+
'super',
|
|
77
|
+
'switch',
|
|
78
|
+
'symbol',
|
|
79
|
+
'this',
|
|
80
|
+
'throw',
|
|
81
|
+
'true',
|
|
82
|
+
'try',
|
|
83
|
+
'type',
|
|
84
|
+
'typeof',
|
|
85
|
+
'undefined',
|
|
86
|
+
'unique',
|
|
87
|
+
'unknown',
|
|
88
|
+
'var',
|
|
89
|
+
'void',
|
|
90
|
+
'while',
|
|
91
|
+
'with',
|
|
92
|
+
'yield',
|
|
93
|
+
]);
|
|
94
|
+
|
|
95
|
+
function parseArgs(argv) {
|
|
96
|
+
const raw = argv.slice(2);
|
|
97
|
+
const positionals = [];
|
|
98
|
+
const flags = {};
|
|
99
|
+
|
|
100
|
+
for (let index = 0; index < raw.length; index += 1) {
|
|
101
|
+
const token = raw[index];
|
|
102
|
+
if (!token.startsWith('--')) {
|
|
103
|
+
positionals.push(token);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const key = token.slice(2);
|
|
108
|
+
const next = raw[index + 1];
|
|
109
|
+
if (next && !next.startsWith('--')) {
|
|
110
|
+
flags[key] = next;
|
|
111
|
+
index += 1;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
flags[key] = true;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return { positionals, flags };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function run(command, args, options = {}) {
|
|
122
|
+
const { dryRun, cwd = process.cwd() } = options;
|
|
123
|
+
const printable = [command, ...args].join(' ');
|
|
124
|
+
|
|
125
|
+
if (dryRun) {
|
|
126
|
+
logInfo(`[dry-run] ${printable}`);
|
|
127
|
+
return { status: 0 };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const result = spawnSync(command, args, {
|
|
131
|
+
cwd,
|
|
132
|
+
stdio: 'inherit',
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
if (result.error) {
|
|
136
|
+
throw result.error;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (typeof result.status === 'number' && result.status !== 0) {
|
|
140
|
+
process.exit(result.status);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return result;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function runCapture(command, args) {
|
|
147
|
+
return spawnSync(command, args, {
|
|
148
|
+
cwd: process.cwd(),
|
|
149
|
+
encoding: 'utf8',
|
|
150
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function hasCommand(command) {
|
|
155
|
+
const result = runCapture(command, ['--version']);
|
|
156
|
+
return !result.error && result.status === 0;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function detectPackageManager(explicitPm) {
|
|
160
|
+
if (explicitPm) {
|
|
161
|
+
return explicitPm;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const cwd = process.cwd();
|
|
165
|
+
if (fs.existsSync(path.join(cwd, 'pnpm-lock.yaml'))) {
|
|
166
|
+
return 'pnpm';
|
|
167
|
+
}
|
|
168
|
+
if (fs.existsSync(path.join(cwd, 'package-lock.json'))) {
|
|
169
|
+
return 'npm';
|
|
170
|
+
}
|
|
171
|
+
if (fs.existsSync(path.join(cwd, 'yarn.lock'))) {
|
|
172
|
+
return 'yarn';
|
|
173
|
+
}
|
|
174
|
+
if (fs.existsSync(path.join(cwd, 'bun.lockb'))) {
|
|
175
|
+
return 'bun';
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return 'npm';
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function installPackages(pm, packages, options = {}) {
|
|
182
|
+
const { dev = false, dryRun = false, label = 'packages' } = options;
|
|
183
|
+
const argsByPm = {
|
|
184
|
+
pnpm: dev ? ['add', '-D', ...packages] : ['add', ...packages],
|
|
185
|
+
npm: dev
|
|
186
|
+
? ['install', '--save-dev', ...packages]
|
|
187
|
+
: ['install', '--save', ...packages],
|
|
188
|
+
yarn: dev ? ['add', '--dev', ...packages] : ['add', ...packages],
|
|
189
|
+
bun: dev ? ['add', '--dev', ...packages] : ['add', ...packages],
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const installArgs = argsByPm[pm];
|
|
193
|
+
if (!installArgs) {
|
|
194
|
+
logError(`Unsupported package manager: ${pm}`);
|
|
195
|
+
process.exit(1);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
logInfo(`Installing ${label} with ${pm}...`);
|
|
199
|
+
run(pm, installArgs, { dryRun });
|
|
200
|
+
logOk(`Installed ${label}.`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function installRuntimePackages(pm, dryRun) {
|
|
204
|
+
installPackages(pm, RUNTIME_PACKAGES, {
|
|
205
|
+
dev: false,
|
|
206
|
+
dryRun,
|
|
207
|
+
label: 'runtime RS-X packages',
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function installCompilerPackages(pm, dryRun) {
|
|
212
|
+
installPackages(pm, COMPILER_PACKAGES, {
|
|
213
|
+
dev: true,
|
|
214
|
+
dryRun,
|
|
215
|
+
label: 'compiler tooling',
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function installVsCodeExtension(flags) {
|
|
220
|
+
const dryRun = Boolean(flags['dry-run']);
|
|
221
|
+
const force = Boolean(flags.force);
|
|
222
|
+
const local = Boolean(flags.local);
|
|
223
|
+
|
|
224
|
+
if (!hasCommand('code')) {
|
|
225
|
+
logWarn(
|
|
226
|
+
'VS Code CLI `code` is not available on PATH. Skipping VS Code extension installation.',
|
|
227
|
+
);
|
|
228
|
+
logInfo(
|
|
229
|
+
'In VS Code: Command Palette -> "Shell Command: Install code command in PATH".',
|
|
230
|
+
);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (local) {
|
|
235
|
+
installLocalVsix(dryRun, force);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const args = ['--install-extension', VS_CODE_EXTENSION_ID];
|
|
240
|
+
if (force) {
|
|
241
|
+
args.push('--force');
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
logInfo(`Installing ${VS_CODE_EXTENSION_ID} from VS Code marketplace...`);
|
|
245
|
+
run('code', args, { dryRun });
|
|
246
|
+
logOk('VS Code extension installed.');
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function installLocalVsix(dryRun, force) {
|
|
250
|
+
const repoRoot = findRepoRoot(process.cwd());
|
|
251
|
+
if (!repoRoot) {
|
|
252
|
+
logWarn('Could not locate rs-x repository root for --local VSIX install.');
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const extensionDir = path.join(repoRoot, 'rs-x-vscode-extension');
|
|
257
|
+
const extensionPackagePath = path.join(extensionDir, 'package.json');
|
|
258
|
+
if (!fs.existsSync(extensionPackagePath)) {
|
|
259
|
+
logWarn(`Missing ${extensionPackagePath}. Skipping local VSIX install.`);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const extensionPackage = JSON.parse(
|
|
264
|
+
fs.readFileSync(extensionPackagePath, 'utf8'),
|
|
265
|
+
);
|
|
266
|
+
const vsixFileName = `${extensionPackage.name}-${extensionPackage.version}.vsix`;
|
|
267
|
+
const preferredVsixPath = path.join(extensionDir, 'dist', vsixFileName);
|
|
268
|
+
const legacyVsixPath = path.join(extensionDir, vsixFileName);
|
|
269
|
+
|
|
270
|
+
logInfo('Packaging local rs-x-vscode-extension...');
|
|
271
|
+
run('pnpm', ['--filter', 'rs-x-vscode-extension', 'run', 'package'], {
|
|
272
|
+
dryRun,
|
|
273
|
+
cwd: repoRoot,
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
const vsixPath =
|
|
277
|
+
!dryRun && fs.existsSync(preferredVsixPath)
|
|
278
|
+
? preferredVsixPath
|
|
279
|
+
: !dryRun && fs.existsSync(legacyVsixPath)
|
|
280
|
+
? legacyVsixPath
|
|
281
|
+
: preferredVsixPath;
|
|
282
|
+
|
|
283
|
+
if (!dryRun && !fs.existsSync(vsixPath)) {
|
|
284
|
+
logWarn(
|
|
285
|
+
`Expected VSIX not found at ${preferredVsixPath}. Skipping installation.`,
|
|
286
|
+
);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const args = ['--install-extension', vsixPath];
|
|
291
|
+
if (force) {
|
|
292
|
+
args.push('--force');
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
logInfo(`Installing local VSIX from ${vsixPath}...`);
|
|
296
|
+
run('code', args, { dryRun });
|
|
297
|
+
logOk('Local VS Code extension installed.');
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function findRepoRoot(startDir) {
|
|
301
|
+
let current = startDir;
|
|
302
|
+
const root = path.parse(startDir).root;
|
|
303
|
+
|
|
304
|
+
while (current !== root) {
|
|
305
|
+
const marker = path.join(current, 'pnpm-workspace.yaml');
|
|
306
|
+
const extensionDir = path.join(current, 'rs-x-vscode-extension');
|
|
307
|
+
if (fs.existsSync(marker) && fs.existsSync(extensionDir)) {
|
|
308
|
+
return current;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
current = path.dirname(current);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function runDoctor() {
|
|
318
|
+
const nodeMajor = Number.parseInt(process.versions.node.split('.')[0], 10);
|
|
319
|
+
const hasCode = hasCommand('code');
|
|
320
|
+
const checks = [
|
|
321
|
+
{
|
|
322
|
+
name: 'Node.js >= 20',
|
|
323
|
+
ok: Number.isFinite(nodeMajor) && nodeMajor >= 20,
|
|
324
|
+
details: `detected ${process.versions.node}`,
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
name: 'VS Code CLI (code)',
|
|
328
|
+
ok: hasCode,
|
|
329
|
+
details: hasCode ? 'available' : 'not found in PATH',
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
name: 'Package manager (pnpm/npm/yarn/bun)',
|
|
333
|
+
ok:
|
|
334
|
+
hasCommand('pnpm') ||
|
|
335
|
+
hasCommand('npm') ||
|
|
336
|
+
hasCommand('yarn') ||
|
|
337
|
+
hasCommand('bun'),
|
|
338
|
+
details: 'required for compiler package installation',
|
|
339
|
+
},
|
|
340
|
+
];
|
|
341
|
+
|
|
342
|
+
for (const check of checks) {
|
|
343
|
+
const tag = check.ok ? '[OK]' : '[WARN]';
|
|
344
|
+
console.log(`${tag} ${check.name} - ${check.details}`);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function isValidTsIdentifier(input) {
|
|
349
|
+
if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/u.test(input)) {
|
|
350
|
+
return false;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return !TS_RESERVED_WORDS.has(input);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function toKebabCase(input) {
|
|
357
|
+
return input
|
|
358
|
+
.replace(/([a-z0-9])([A-Z])/gu, '$1-$2')
|
|
359
|
+
.replace(/[_\s]+/gu, '-')
|
|
360
|
+
.replace(/[^a-zA-Z0-9-]/gu, '-')
|
|
361
|
+
.replace(/-+/gu, '-')
|
|
362
|
+
.replace(/^-|-$/gu, '')
|
|
363
|
+
.toLowerCase();
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function ensureTsExtension(fileName) {
|
|
367
|
+
if (/\.[cm]?[jt]sx?$/u.test(fileName)) {
|
|
368
|
+
return fileName;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return `${fileName}.ts`;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
async function askUntilNonEmpty(rl, prompt) {
|
|
375
|
+
while (true) {
|
|
376
|
+
const answer = (await rl.question(prompt)).trim();
|
|
377
|
+
if (answer.length > 0) {
|
|
378
|
+
return answer;
|
|
379
|
+
}
|
|
380
|
+
logWarn('Value is required.');
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
async function askUntilValidIdentifier(rl) {
|
|
385
|
+
while (true) {
|
|
386
|
+
const answer = (
|
|
387
|
+
await rl.question('Expression export name (TS identifier): ')
|
|
388
|
+
).trim();
|
|
389
|
+
if (!answer) {
|
|
390
|
+
logWarn('Name is required.');
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (isValidTsIdentifier(answer)) {
|
|
395
|
+
return answer;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
logWarn(`"${answer}" is not a valid TypeScript identifier.`);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function normalizeYesNo(answer, defaultValue) {
|
|
403
|
+
const normalized = answer.trim().toLowerCase();
|
|
404
|
+
if (!normalized) {
|
|
405
|
+
return defaultValue;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (normalized === 'y' || normalized === 'yes') {
|
|
409
|
+
return true;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (normalized === 'n' || normalized === 'no') {
|
|
413
|
+
return false;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
return defaultValue;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function stripTsLikeExtension(fileName) {
|
|
420
|
+
return fileName.replace(/\.[cm]?[jt]sx?$/u, '');
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function createModelTemplate() {
|
|
424
|
+
return `export const model = {
|
|
425
|
+
a: 1,
|
|
426
|
+
};
|
|
427
|
+
`;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function createExpressionTemplate(
|
|
431
|
+
expressionName,
|
|
432
|
+
modelImportPath,
|
|
433
|
+
modelExportName,
|
|
434
|
+
) {
|
|
435
|
+
return `import { rsx } from '@rs-x/expression-parser';
|
|
436
|
+
import { ${modelExportName} } from '${modelImportPath}';
|
|
437
|
+
|
|
438
|
+
export const ${expressionName} = rsx('a')(${modelExportName});
|
|
439
|
+
`;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
async function askForIdentifierWithDefault(rl, prompt, defaultValue) {
|
|
443
|
+
while (true) {
|
|
444
|
+
const answer = (await rl.question(prompt)).trim();
|
|
445
|
+
if (!answer) {
|
|
446
|
+
return defaultValue;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
if (isValidTsIdentifier(answer)) {
|
|
450
|
+
return answer;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
logWarn(`"${answer}" is not a valid TypeScript identifier.`);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
async function runAdd() {
|
|
458
|
+
const rl = readline.createInterface({
|
|
459
|
+
input: process.stdin,
|
|
460
|
+
output: process.stdout,
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
try {
|
|
464
|
+
const expressionName = await askUntilValidIdentifier(rl);
|
|
465
|
+
|
|
466
|
+
const kebabAnswer = await rl.question('Use kebab-case file name? [Y/n]: ');
|
|
467
|
+
const useKebabCase = normalizeYesNo(kebabAnswer, true);
|
|
468
|
+
|
|
469
|
+
const directoryInput = await askUntilNonEmpty(
|
|
470
|
+
rl,
|
|
471
|
+
'Directory path (relative or absolute): ',
|
|
472
|
+
);
|
|
473
|
+
const resolvedDirectory = path.isAbsolute(directoryInput)
|
|
474
|
+
? directoryInput
|
|
475
|
+
: path.resolve(process.cwd(), directoryInput);
|
|
476
|
+
|
|
477
|
+
const baseFileName = useKebabCase
|
|
478
|
+
? toKebabCase(expressionName)
|
|
479
|
+
: expressionName;
|
|
480
|
+
const expressionFileName = ensureTsExtension(baseFileName);
|
|
481
|
+
const expressionFileBase = stripTsLikeExtension(expressionFileName);
|
|
482
|
+
const modelFileName = `${expressionFileBase}.model.ts`;
|
|
483
|
+
const expressionPath = path.join(resolvedDirectory, expressionFileName);
|
|
484
|
+
const modelPath = path.join(resolvedDirectory, modelFileName);
|
|
485
|
+
const useExistingModelAnswer = await rl.question(
|
|
486
|
+
'Use existing model file? [y/N]: ',
|
|
487
|
+
);
|
|
488
|
+
const useExistingModel = normalizeYesNo(useExistingModelAnswer, false);
|
|
489
|
+
|
|
490
|
+
if (
|
|
491
|
+
fs.existsSync(expressionPath) ||
|
|
492
|
+
(!useExistingModel && fs.existsSync(modelPath))
|
|
493
|
+
) {
|
|
494
|
+
const overwriteAnswer = await rl.question(
|
|
495
|
+
`One or more target files already exist. Overwrite? [y/N]: `,
|
|
496
|
+
);
|
|
497
|
+
const shouldOverwrite = normalizeYesNo(overwriteAnswer, false);
|
|
498
|
+
if (!shouldOverwrite) {
|
|
499
|
+
logInfo('Cancelled. Existing file was not modified.');
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
fs.mkdirSync(resolvedDirectory, { recursive: true });
|
|
505
|
+
let modelImportPath = `./${expressionFileBase}.model`;
|
|
506
|
+
let modelExportName = 'model';
|
|
507
|
+
|
|
508
|
+
if (useExistingModel) {
|
|
509
|
+
const existingModelPathInput = await askUntilNonEmpty(
|
|
510
|
+
rl,
|
|
511
|
+
'Existing model file path (relative to output dir or absolute): ',
|
|
512
|
+
);
|
|
513
|
+
const resolvedExistingModelPath = path.isAbsolute(existingModelPathInput)
|
|
514
|
+
? existingModelPathInput
|
|
515
|
+
: path.resolve(resolvedDirectory, existingModelPathInput);
|
|
516
|
+
|
|
517
|
+
if (!fs.existsSync(resolvedExistingModelPath)) {
|
|
518
|
+
logError(`Model file not found: ${resolvedExistingModelPath}`);
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
modelImportPath = toModuleImportPath(
|
|
523
|
+
expressionPath,
|
|
524
|
+
resolvedExistingModelPath,
|
|
525
|
+
);
|
|
526
|
+
modelExportName = await askForIdentifierWithDefault(
|
|
527
|
+
rl,
|
|
528
|
+
'Model export name [model]: ',
|
|
529
|
+
'model',
|
|
530
|
+
);
|
|
531
|
+
} else {
|
|
532
|
+
fs.writeFileSync(modelPath, createModelTemplate(), 'utf8');
|
|
533
|
+
logOk(`Created ${modelPath}`);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
fs.writeFileSync(
|
|
537
|
+
expressionPath,
|
|
538
|
+
createExpressionTemplate(
|
|
539
|
+
expressionName,
|
|
540
|
+
modelImportPath,
|
|
541
|
+
modelExportName,
|
|
542
|
+
),
|
|
543
|
+
'utf8',
|
|
544
|
+
);
|
|
545
|
+
|
|
546
|
+
logOk(`Created ${expressionPath}`);
|
|
547
|
+
} finally {
|
|
548
|
+
rl.close();
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function writeFileWithDryRun(filePath, content, dryRun) {
|
|
553
|
+
if (dryRun) {
|
|
554
|
+
logInfo(`[dry-run] create ${filePath}`);
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
559
|
+
fs.writeFileSync(filePath, content, 'utf8');
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function toFileDependencySpec(fromDir, targetPath) {
|
|
563
|
+
const relative = path.relative(fromDir, targetPath).replace(/\\/gu, '/');
|
|
564
|
+
const normalized = relative.startsWith('.') ? relative : `./${relative}`;
|
|
565
|
+
return `file:${normalized}`;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function findLatestTarball(packageDir, packageSlug) {
|
|
569
|
+
if (!fs.existsSync(packageDir)) {
|
|
570
|
+
return null;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
const candidates = [];
|
|
574
|
+
const stack = [packageDir];
|
|
575
|
+
|
|
576
|
+
while (stack.length > 0) {
|
|
577
|
+
const currentDir = stack.pop();
|
|
578
|
+
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
579
|
+
for (const entry of entries) {
|
|
580
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
581
|
+
if (entry.isDirectory()) {
|
|
582
|
+
stack.push(fullPath);
|
|
583
|
+
continue;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
if (
|
|
587
|
+
entry.isFile() &&
|
|
588
|
+
entry.name.startsWith(`${packageSlug}-`) &&
|
|
589
|
+
entry.name.endsWith('.tgz')
|
|
590
|
+
) {
|
|
591
|
+
candidates.push(fullPath);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
candidates.sort();
|
|
597
|
+
if (candidates.length === 0) {
|
|
598
|
+
return null;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
return candidates[candidates.length - 1];
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function resolveProjectRsxSpecs(
|
|
605
|
+
projectRoot,
|
|
606
|
+
workspaceRoot,
|
|
607
|
+
tarballsDir,
|
|
608
|
+
options = {},
|
|
609
|
+
) {
|
|
610
|
+
const includeAngularPackage = Boolean(options.includeAngularPackage);
|
|
611
|
+
const defaults = {
|
|
612
|
+
'@rs-x/core': RSX_PACKAGE_VERSION,
|
|
613
|
+
'@rs-x/state-manager': RSX_PACKAGE_VERSION,
|
|
614
|
+
'@rs-x/expression-parser': RSX_PACKAGE_VERSION,
|
|
615
|
+
'@rs-x/compiler': RSX_PACKAGE_VERSION,
|
|
616
|
+
'@rs-x/typescript-plugin': RSX_PACKAGE_VERSION,
|
|
617
|
+
...(includeAngularPackage ? { '@rs-x/angular': RSX_PACKAGE_VERSION } : {}),
|
|
618
|
+
'@rs-x/cli': null,
|
|
619
|
+
};
|
|
620
|
+
|
|
621
|
+
const tarballSlugs = {
|
|
622
|
+
'@rs-x/core': 'rs-x-core',
|
|
623
|
+
'@rs-x/state-manager': 'rs-x-state-manager',
|
|
624
|
+
'@rs-x/expression-parser': 'rs-x-expression-parser',
|
|
625
|
+
'@rs-x/compiler': 'rs-x-compiler',
|
|
626
|
+
'@rs-x/typescript-plugin': 'rs-x-typescript-plugin',
|
|
627
|
+
...(includeAngularPackage ? { '@rs-x/angular': 'rs-x-angular' } : {}),
|
|
628
|
+
'@rs-x/cli': 'rs-x-cli',
|
|
629
|
+
};
|
|
630
|
+
|
|
631
|
+
if (tarballsDir) {
|
|
632
|
+
const specs = { ...defaults };
|
|
633
|
+
const packageDirBySlug = {
|
|
634
|
+
'rs-x-core': path.join(tarballsDir, 'rs-x-core'),
|
|
635
|
+
'rs-x-state-manager': path.join(tarballsDir, 'rs-x-state-manager'),
|
|
636
|
+
'rs-x-expression-parser': path.join(
|
|
637
|
+
tarballsDir,
|
|
638
|
+
'rs-x-expression-parser',
|
|
639
|
+
),
|
|
640
|
+
'rs-x-compiler': path.join(tarballsDir, 'rs-x-compiler'),
|
|
641
|
+
'rs-x-typescript-plugin': path.join(
|
|
642
|
+
tarballsDir,
|
|
643
|
+
'rs-x-typescript-plugin',
|
|
644
|
+
),
|
|
645
|
+
...(includeAngularPackage
|
|
646
|
+
? {
|
|
647
|
+
'rs-x-angular': path.join(tarballsDir, 'rs-x-angular'),
|
|
648
|
+
}
|
|
649
|
+
: {}),
|
|
650
|
+
'rs-x-cli': path.join(tarballsDir, 'rs-x-cli'),
|
|
651
|
+
};
|
|
652
|
+
|
|
653
|
+
for (const packageName of Object.keys(tarballSlugs)) {
|
|
654
|
+
const slug = tarballSlugs[packageName];
|
|
655
|
+
const tarball = findLatestTarball(tarballsDir, slug);
|
|
656
|
+
if (tarball) {
|
|
657
|
+
specs[packageName] = toFileDependencySpec(projectRoot, tarball);
|
|
658
|
+
continue;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
const packageDir = packageDirBySlug[slug];
|
|
662
|
+
if (packageDir && fs.existsSync(packageDir)) {
|
|
663
|
+
specs[packageName] = toFileDependencySpec(projectRoot, packageDir);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
return specs;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
if (!workspaceRoot) {
|
|
670
|
+
return defaults;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
const packageDirs = {
|
|
674
|
+
'@rs-x/core': path.join(workspaceRoot, 'rs-x-core'),
|
|
675
|
+
'@rs-x/state-manager': path.join(workspaceRoot, 'rs-x-state-manager'),
|
|
676
|
+
'@rs-x/expression-parser': path.join(
|
|
677
|
+
workspaceRoot,
|
|
678
|
+
'rs-x-expression-parser',
|
|
679
|
+
),
|
|
680
|
+
'@rs-x/compiler': path.join(workspaceRoot, 'rs-x-compiler'),
|
|
681
|
+
'@rs-x/typescript-plugin': path.join(
|
|
682
|
+
workspaceRoot,
|
|
683
|
+
'rs-x-typescript-plugin',
|
|
684
|
+
),
|
|
685
|
+
...(includeAngularPackage
|
|
686
|
+
? {
|
|
687
|
+
'@rs-x/angular': path.join(
|
|
688
|
+
workspaceRoot,
|
|
689
|
+
'rs-x-angular/projects/rsx',
|
|
690
|
+
),
|
|
691
|
+
}
|
|
692
|
+
: {}),
|
|
693
|
+
'@rs-x/cli': path.join(workspaceRoot, 'rs-x-cli'),
|
|
694
|
+
};
|
|
695
|
+
|
|
696
|
+
const specs = { ...defaults };
|
|
697
|
+
for (const packageName of Object.keys(packageDirs)) {
|
|
698
|
+
const packageDir = packageDirs[packageName];
|
|
699
|
+
if (!fs.existsSync(packageDir)) {
|
|
700
|
+
continue;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
if (packageName === '@rs-x/cli') {
|
|
704
|
+
const tarball =
|
|
705
|
+
findLatestTarball(path.join(packageDir, 'dist'), 'rs-x-cli') ??
|
|
706
|
+
findLatestTarball(packageDir, 'rs-x-cli');
|
|
707
|
+
if (tarball) {
|
|
708
|
+
specs[packageName] = toFileDependencySpec(projectRoot, tarball);
|
|
709
|
+
continue;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
specs[packageName] = toFileDependencySpec(projectRoot, packageDir);
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
return specs;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
function createProjectPackageJson(projectName, rsxSpecs) {
|
|
720
|
+
const devDependencies = {
|
|
721
|
+
'@rs-x/compiler': rsxSpecs['@rs-x/compiler'],
|
|
722
|
+
'@rs-x/typescript-plugin': rsxSpecs['@rs-x/typescript-plugin'],
|
|
723
|
+
typescript: '^5.9.3',
|
|
724
|
+
};
|
|
725
|
+
|
|
726
|
+
return (
|
|
727
|
+
JSON.stringify(
|
|
728
|
+
{
|
|
729
|
+
name: projectName,
|
|
730
|
+
version: '0.1.0',
|
|
731
|
+
private: true,
|
|
732
|
+
type: 'commonjs',
|
|
733
|
+
scripts: {
|
|
734
|
+
build: 'rsx build --project tsconfig.json',
|
|
735
|
+
'typecheck:rsx': 'rsx typecheck --project tsconfig.json',
|
|
736
|
+
start: 'node dist/main.js',
|
|
737
|
+
},
|
|
738
|
+
dependencies: {
|
|
739
|
+
'@rs-x/core': rsxSpecs['@rs-x/core'],
|
|
740
|
+
'@rs-x/state-manager': rsxSpecs['@rs-x/state-manager'],
|
|
741
|
+
'@rs-x/expression-parser': rsxSpecs['@rs-x/expression-parser'],
|
|
742
|
+
},
|
|
743
|
+
devDependencies: {
|
|
744
|
+
...devDependencies,
|
|
745
|
+
},
|
|
746
|
+
},
|
|
747
|
+
null,
|
|
748
|
+
2,
|
|
749
|
+
) + '\n'
|
|
750
|
+
);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
function createProjectTsConfig() {
|
|
754
|
+
return (
|
|
755
|
+
JSON.stringify(
|
|
756
|
+
{
|
|
757
|
+
compilerOptions: {
|
|
758
|
+
target: 'ES2022',
|
|
759
|
+
module: 'CommonJS',
|
|
760
|
+
moduleResolution: 'Node',
|
|
761
|
+
strict: true,
|
|
762
|
+
esModuleInterop: true,
|
|
763
|
+
skipLibCheck: true,
|
|
764
|
+
experimentalDecorators: true,
|
|
765
|
+
emitDecoratorMetadata: true,
|
|
766
|
+
outDir: 'dist',
|
|
767
|
+
rootDir: 'src',
|
|
768
|
+
plugins: [
|
|
769
|
+
{
|
|
770
|
+
name: '@rs-x/typescript-plugin',
|
|
771
|
+
},
|
|
772
|
+
],
|
|
773
|
+
},
|
|
774
|
+
include: ['src/**/*.ts'],
|
|
775
|
+
},
|
|
776
|
+
null,
|
|
777
|
+
2,
|
|
778
|
+
) + '\n'
|
|
779
|
+
);
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
function normalizeProjectTemplate(value) {
|
|
783
|
+
if (typeof value !== 'string') {
|
|
784
|
+
return null;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
const normalized = value.trim().toLowerCase();
|
|
788
|
+
if (normalized === 'angular' || normalized === 'a' || normalized === 'ng') {
|
|
789
|
+
return 'angular';
|
|
790
|
+
}
|
|
791
|
+
if (normalized === 'vue' || normalized === 'vuejs' || normalized === 'v') {
|
|
792
|
+
return 'vuejs';
|
|
793
|
+
}
|
|
794
|
+
if (normalized === 'react' || normalized === 'r') {
|
|
795
|
+
return 'react';
|
|
796
|
+
}
|
|
797
|
+
if (
|
|
798
|
+
normalized === 'next' ||
|
|
799
|
+
normalized === 'nextjs' ||
|
|
800
|
+
normalized === 'n' ||
|
|
801
|
+
normalized === 'nx'
|
|
802
|
+
) {
|
|
803
|
+
return 'nextjs';
|
|
804
|
+
}
|
|
805
|
+
if (
|
|
806
|
+
normalized === 'node' ||
|
|
807
|
+
normalized === 'nodejs' ||
|
|
808
|
+
normalized === 'generic' ||
|
|
809
|
+
normalized === 'js'
|
|
810
|
+
) {
|
|
811
|
+
return 'nodejs';
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
return null;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
async function promptProjectTemplate() {
|
|
818
|
+
const rl = readline.createInterface({
|
|
819
|
+
input: process.stdin,
|
|
820
|
+
output: process.stdout,
|
|
821
|
+
});
|
|
822
|
+
try {
|
|
823
|
+
console.log('Choose a project template:');
|
|
824
|
+
PROJECT_TEMPLATES.forEach((template, index) => {
|
|
825
|
+
console.log(` ${index + 1}) ${template}`);
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
while (true) {
|
|
829
|
+
const answer = (await rl.question('Template (name or number): '))
|
|
830
|
+
.trim()
|
|
831
|
+
.toLowerCase();
|
|
832
|
+
if (answer.length === 0) {
|
|
833
|
+
continue;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
const byName = normalizeProjectTemplate(answer);
|
|
837
|
+
if (byName) {
|
|
838
|
+
return byName;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
const byNumber = Number.parseInt(answer, 10);
|
|
842
|
+
if (
|
|
843
|
+
Number.isInteger(byNumber) &&
|
|
844
|
+
byNumber >= 1 &&
|
|
845
|
+
byNumber <= PROJECT_TEMPLATES.length
|
|
846
|
+
) {
|
|
847
|
+
return PROJECT_TEMPLATES[byNumber - 1];
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
logWarn(
|
|
851
|
+
`Invalid template '${answer}'. Choose one of: ${PROJECT_TEMPLATES.join(', ')}`,
|
|
852
|
+
);
|
|
853
|
+
}
|
|
854
|
+
} finally {
|
|
855
|
+
rl.close();
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
function withWorkingDirectory(nextCwd, work) {
|
|
860
|
+
const previousCwd = process.cwd();
|
|
861
|
+
process.chdir(nextCwd);
|
|
862
|
+
try {
|
|
863
|
+
return work();
|
|
864
|
+
} finally {
|
|
865
|
+
process.chdir(previousCwd);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
function createVueRsxAppTemplate() {
|
|
870
|
+
return `<script setup lang="ts">
|
|
871
|
+
import { reactive } from 'vue';
|
|
872
|
+
|
|
873
|
+
import { useRsxExpression } from '@rs-x/vue';
|
|
874
|
+
|
|
875
|
+
const model = reactive<Record<string, number>>({
|
|
876
|
+
a: 2,
|
|
877
|
+
b: 3,
|
|
878
|
+
});
|
|
879
|
+
|
|
880
|
+
const result = useRsxExpression<number>('a + b', { model });
|
|
881
|
+
|
|
882
|
+
function incrementA(): void {
|
|
883
|
+
model.a += 1;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
function incrementB(): void {
|
|
887
|
+
model.b += 1;
|
|
888
|
+
}
|
|
889
|
+
</script>
|
|
890
|
+
|
|
891
|
+
<template>
|
|
892
|
+
<main style="font-family: sans-serif; max-width: 640px; margin: 2rem auto; line-height: 1.5;">
|
|
893
|
+
<h1>RS-X + Vue</h1>
|
|
894
|
+
<p>Expression: <code>rsx('a + b')</code></p>
|
|
895
|
+
<p>Model: a={{ model.a }}, b={{ model.b }}</p>
|
|
896
|
+
<p>Result (from RS-X expression value): <strong>{{ result ?? 0 }}</strong></p>
|
|
897
|
+
<div style="display: flex; gap: 0.75rem;">
|
|
898
|
+
<button @click="incrementA">Increment a</button>
|
|
899
|
+
<button @click="incrementB">Increment b</button>
|
|
900
|
+
</div>
|
|
901
|
+
</main>
|
|
902
|
+
</template>
|
|
903
|
+
`;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
function applyVueRsxTemplate(projectRoot, dryRun) {
|
|
907
|
+
const appVuePath = path.join(projectRoot, 'src/App.vue');
|
|
908
|
+
if (!fs.existsSync(appVuePath)) {
|
|
909
|
+
logWarn(
|
|
910
|
+
`Vue app file not found at ${appVuePath}. Skipping RS-X example patch.`,
|
|
911
|
+
);
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
writeFileWithDryRun(appVuePath, createVueRsxAppTemplate(), dryRun);
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
async function runProject(flags) {
|
|
919
|
+
const dryRun = Boolean(flags['dry-run']);
|
|
920
|
+
const skipInstall = Boolean(flags['skip-install']);
|
|
921
|
+
const pm = detectPackageManager(flags.pm);
|
|
922
|
+
let projectName = typeof flags.name === 'string' ? flags.name.trim() : '';
|
|
923
|
+
|
|
924
|
+
if (!projectName) {
|
|
925
|
+
const rl = readline.createInterface({
|
|
926
|
+
input: process.stdin,
|
|
927
|
+
output: process.stdout,
|
|
928
|
+
});
|
|
929
|
+
try {
|
|
930
|
+
projectName = await askUntilNonEmpty(rl, 'Project name: ');
|
|
931
|
+
} finally {
|
|
932
|
+
rl.close();
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
const projectRoot = path.resolve(process.cwd(), projectName);
|
|
937
|
+
const tarballsDir =
|
|
938
|
+
typeof flags['tarballs-dir'] === 'string'
|
|
939
|
+
? path.resolve(process.cwd(), flags['tarballs-dir'])
|
|
940
|
+
: typeof process.env.RSX_TARBALLS_DIR === 'string' &&
|
|
941
|
+
process.env.RSX_TARBALLS_DIR.trim().length > 0
|
|
942
|
+
? path.resolve(process.cwd(), process.env.RSX_TARBALLS_DIR)
|
|
943
|
+
: null;
|
|
944
|
+
const workspaceRoot = findRepoRoot(process.cwd());
|
|
945
|
+
const rsxSpecs = resolveProjectRsxSpecs(
|
|
946
|
+
projectRoot,
|
|
947
|
+
workspaceRoot,
|
|
948
|
+
tarballsDir,
|
|
949
|
+
);
|
|
950
|
+
if (fs.existsSync(projectRoot) && fs.readdirSync(projectRoot).length > 0) {
|
|
951
|
+
logError(`Target directory is not empty: ${projectRoot}`);
|
|
952
|
+
process.exit(1);
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
if (!dryRun) {
|
|
956
|
+
fs.mkdirSync(projectRoot, { recursive: true });
|
|
957
|
+
} else {
|
|
958
|
+
logInfo(`[dry-run] create directory ${projectRoot}`);
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
writeFileWithDryRun(
|
|
962
|
+
path.join(projectRoot, 'package.json'),
|
|
963
|
+
createProjectPackageJson(projectName, rsxSpecs),
|
|
964
|
+
dryRun,
|
|
965
|
+
);
|
|
966
|
+
writeFileWithDryRun(
|
|
967
|
+
path.join(projectRoot, 'tsconfig.json'),
|
|
968
|
+
createProjectTsConfig(),
|
|
969
|
+
dryRun,
|
|
970
|
+
);
|
|
971
|
+
writeFileWithDryRun(
|
|
972
|
+
path.join(projectRoot, '.gitignore'),
|
|
973
|
+
'node_modules\ndist\n',
|
|
974
|
+
dryRun,
|
|
975
|
+
);
|
|
976
|
+
writeFileWithDryRun(
|
|
977
|
+
path.join(projectRoot, '.vscode/extensions.json'),
|
|
978
|
+
JSON.stringify({ recommendations: [VS_CODE_EXTENSION_ID] }, null, 2) + '\n',
|
|
979
|
+
dryRun,
|
|
980
|
+
);
|
|
981
|
+
|
|
982
|
+
writeFileWithDryRun(
|
|
983
|
+
path.join(projectRoot, 'src/rsx-bootstrap.ts'),
|
|
984
|
+
`import { InjectionContainer } from '@rs-x/core';
|
|
985
|
+
import { RsXExpressionParserModule } from '@rs-x/expression-parser';
|
|
986
|
+
|
|
987
|
+
export async function initRsx(): Promise<void> {
|
|
988
|
+
await InjectionContainer.load(RsXExpressionParserModule);
|
|
989
|
+
}
|
|
990
|
+
`,
|
|
991
|
+
dryRun,
|
|
992
|
+
);
|
|
993
|
+
|
|
994
|
+
writeFileWithDryRun(
|
|
995
|
+
path.join(projectRoot, 'src/model.ts'),
|
|
996
|
+
`export interface IModel {
|
|
997
|
+
a: number;
|
|
998
|
+
b: number;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
export const model: IModel = {
|
|
1002
|
+
a: 2,
|
|
1003
|
+
b: 3,
|
|
1004
|
+
};
|
|
1005
|
+
`,
|
|
1006
|
+
dryRun,
|
|
1007
|
+
);
|
|
1008
|
+
|
|
1009
|
+
writeFileWithDryRun(
|
|
1010
|
+
path.join(projectRoot, 'src/expressions/sample.expression.ts'),
|
|
1011
|
+
`import { rsx } from '@rs-x/expression-parser';
|
|
1012
|
+
|
|
1013
|
+
import { model } from '../model';
|
|
1014
|
+
|
|
1015
|
+
export const sampleExpression = rsx('a + b')(model);
|
|
1016
|
+
`,
|
|
1017
|
+
dryRun,
|
|
1018
|
+
);
|
|
1019
|
+
|
|
1020
|
+
writeFileWithDryRun(
|
|
1021
|
+
path.join(projectRoot, 'src/main.ts'),
|
|
1022
|
+
`import { sampleExpression } from './expressions/sample.expression';
|
|
1023
|
+
import { initRsx } from './rsx-bootstrap';
|
|
1024
|
+
|
|
1025
|
+
async function main(): Promise<void> {
|
|
1026
|
+
await initRsx();
|
|
1027
|
+
console.log('RS-X sample expression initialized:', Boolean(sampleExpression));
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
void main();
|
|
1031
|
+
`,
|
|
1032
|
+
dryRun,
|
|
1033
|
+
);
|
|
1034
|
+
|
|
1035
|
+
if (!skipInstall) {
|
|
1036
|
+
const installArgsByPm = {
|
|
1037
|
+
pnpm: ['install'],
|
|
1038
|
+
npm: ['install'],
|
|
1039
|
+
yarn: ['install'],
|
|
1040
|
+
bun: ['install'],
|
|
1041
|
+
};
|
|
1042
|
+
const installArgs = installArgsByPm[pm];
|
|
1043
|
+
if (!installArgs) {
|
|
1044
|
+
logError(`Unsupported package manager: ${pm}`);
|
|
1045
|
+
process.exit(1);
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
logInfo(`Installing dependencies with ${pm}...`);
|
|
1049
|
+
run(pm, installArgs, { dryRun, cwd: projectRoot });
|
|
1050
|
+
logOk('Dependencies installed.');
|
|
1051
|
+
} else {
|
|
1052
|
+
logInfo('Skipping dependency install (--skip-install).');
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
logOk(`Created RS-X project: ${projectRoot}`);
|
|
1056
|
+
logInfo('Next steps:');
|
|
1057
|
+
console.log(` cd ${projectName}`);
|
|
1058
|
+
if (skipInstall) {
|
|
1059
|
+
console.log(' npm install');
|
|
1060
|
+
}
|
|
1061
|
+
console.log(' npm run build');
|
|
1062
|
+
console.log(' npm run start');
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
async function resolveProjectName(nameFromFlags, fallbackName) {
|
|
1066
|
+
const fromFlags =
|
|
1067
|
+
typeof nameFromFlags === 'string' ? nameFromFlags.trim() : '';
|
|
1068
|
+
if (fromFlags.length > 0) {
|
|
1069
|
+
return fromFlags;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
const fromFallback =
|
|
1073
|
+
typeof fallbackName === 'string' ? fallbackName.trim() : '';
|
|
1074
|
+
if (fromFallback.length > 0) {
|
|
1075
|
+
return fromFallback;
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
const rl = readline.createInterface({
|
|
1079
|
+
input: process.stdin,
|
|
1080
|
+
output: process.stdout,
|
|
1081
|
+
});
|
|
1082
|
+
try {
|
|
1083
|
+
return await askUntilNonEmpty(rl, 'Project name: ');
|
|
1084
|
+
} finally {
|
|
1085
|
+
rl.close();
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
function scaffoldProjectTemplate(template, projectName, pm, flags) {
|
|
1090
|
+
const dryRun = Boolean(flags['dry-run']);
|
|
1091
|
+
const skipInstall = Boolean(flags['skip-install']);
|
|
1092
|
+
|
|
1093
|
+
if (template === 'angular') {
|
|
1094
|
+
const args = [
|
|
1095
|
+
'-y',
|
|
1096
|
+
'@angular/cli@latest',
|
|
1097
|
+
'new',
|
|
1098
|
+
projectName,
|
|
1099
|
+
'--defaults',
|
|
1100
|
+
'--standalone',
|
|
1101
|
+
'--routing',
|
|
1102
|
+
'--style',
|
|
1103
|
+
'css',
|
|
1104
|
+
'--skip-git',
|
|
1105
|
+
];
|
|
1106
|
+
if (skipInstall) {
|
|
1107
|
+
args.push('--skip-install');
|
|
1108
|
+
}
|
|
1109
|
+
run('npx', args, { dryRun });
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
if (template === 'react') {
|
|
1114
|
+
run('npx', ['create-vite@latest', projectName, '--template', 'react-ts'], {
|
|
1115
|
+
dryRun,
|
|
1116
|
+
});
|
|
1117
|
+
return;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
if (template === 'vuejs') {
|
|
1121
|
+
run('npx', ['create-vite@latest', projectName, '--template', 'vue-ts'], {
|
|
1122
|
+
dryRun,
|
|
1123
|
+
});
|
|
1124
|
+
return;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
if (template === 'nextjs') {
|
|
1128
|
+
const packageManagerFlagByPm = {
|
|
1129
|
+
npm: '--use-npm',
|
|
1130
|
+
pnpm: '--use-pnpm',
|
|
1131
|
+
yarn: '--use-yarn',
|
|
1132
|
+
bun: '--use-bun',
|
|
1133
|
+
};
|
|
1134
|
+
const args = [
|
|
1135
|
+
'create-next-app@latest',
|
|
1136
|
+
projectName,
|
|
1137
|
+
'--yes',
|
|
1138
|
+
'--ts',
|
|
1139
|
+
'--app',
|
|
1140
|
+
'--eslint',
|
|
1141
|
+
'--import-alias',
|
|
1142
|
+
'@/*',
|
|
1143
|
+
packageManagerFlagByPm[pm] ?? '--use-npm',
|
|
1144
|
+
];
|
|
1145
|
+
if (skipInstall) {
|
|
1146
|
+
args.push('--skip-install');
|
|
1147
|
+
}
|
|
1148
|
+
run('npx', args, { dryRun });
|
|
1149
|
+
return;
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
logError(`Unknown project template: ${template}`);
|
|
1153
|
+
process.exit(1);
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
async function runProjectWithTemplate(template, flags) {
|
|
1157
|
+
const normalizedTemplate = normalizeProjectTemplate(template);
|
|
1158
|
+
if (!normalizedTemplate) {
|
|
1159
|
+
logError(
|
|
1160
|
+
`Unsupported template '${template}'. Choose one of: ${PROJECT_TEMPLATES.join(', ')}`,
|
|
1161
|
+
);
|
|
1162
|
+
process.exit(1);
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
if (normalizedTemplate === 'nodejs') {
|
|
1166
|
+
await runProject(flags);
|
|
1167
|
+
return;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
const pm = detectPackageManager(flags.pm);
|
|
1171
|
+
const projectName = await resolveProjectName(flags.name, flags._nameHint);
|
|
1172
|
+
const projectRoot = path.resolve(process.cwd(), projectName);
|
|
1173
|
+
if (fs.existsSync(projectRoot) && fs.readdirSync(projectRoot).length > 0) {
|
|
1174
|
+
logError(`Target directory is not empty: ${projectRoot}`);
|
|
1175
|
+
process.exit(1);
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
scaffoldProjectTemplate(normalizedTemplate, projectName, pm, flags);
|
|
1179
|
+
const dryRun = Boolean(flags['dry-run']);
|
|
1180
|
+
if (dryRun) {
|
|
1181
|
+
logInfo(`[dry-run] setup RS-X in ${projectRoot}`);
|
|
1182
|
+
return;
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
withWorkingDirectory(projectRoot, () => {
|
|
1186
|
+
if (normalizedTemplate === 'angular') {
|
|
1187
|
+
runSetupAngular(flags);
|
|
1188
|
+
return;
|
|
1189
|
+
}
|
|
1190
|
+
if (normalizedTemplate === 'react') {
|
|
1191
|
+
runSetupReact({
|
|
1192
|
+
...flags,
|
|
1193
|
+
entry: flags.entry ?? 'src/main.tsx',
|
|
1194
|
+
});
|
|
1195
|
+
return;
|
|
1196
|
+
}
|
|
1197
|
+
if (normalizedTemplate === 'nextjs') {
|
|
1198
|
+
runSetupNext(flags);
|
|
1199
|
+
return;
|
|
1200
|
+
}
|
|
1201
|
+
if (normalizedTemplate === 'vuejs') {
|
|
1202
|
+
runSetupVue({
|
|
1203
|
+
...flags,
|
|
1204
|
+
entry: flags.entry ?? 'src/main.ts',
|
|
1205
|
+
});
|
|
1206
|
+
applyVueRsxTemplate(projectRoot, dryRun);
|
|
1207
|
+
}
|
|
1208
|
+
});
|
|
1209
|
+
|
|
1210
|
+
logOk(`Created RS-X ${normalizedTemplate} project: ${projectRoot}`);
|
|
1211
|
+
logInfo('Next steps:');
|
|
1212
|
+
console.log(` cd ${projectName}`);
|
|
1213
|
+
if (Boolean(flags['skip-install'])) {
|
|
1214
|
+
console.log(` ${pm} install`);
|
|
1215
|
+
}
|
|
1216
|
+
if (normalizedTemplate === 'angular') {
|
|
1217
|
+
console.log(` ${pm} run start`);
|
|
1218
|
+
} else {
|
|
1219
|
+
console.log(` ${pm} run dev`);
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
function detectProjectContext(projectRoot) {
|
|
1224
|
+
const packageJsonPath = path.join(projectRoot, 'package.json');
|
|
1225
|
+
let dependencies = {};
|
|
1226
|
+
|
|
1227
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
1228
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
1229
|
+
dependencies = {
|
|
1230
|
+
...(packageJson.dependencies ?? {}),
|
|
1231
|
+
...(packageJson.devDependencies ?? {}),
|
|
1232
|
+
...(packageJson.peerDependencies ?? {}),
|
|
1233
|
+
};
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
if (
|
|
1237
|
+
fs.existsSync(path.join(projectRoot, 'angular.json')) ||
|
|
1238
|
+
Object.prototype.hasOwnProperty.call(dependencies, '@angular/core')
|
|
1239
|
+
) {
|
|
1240
|
+
return 'angular';
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
if (Object.prototype.hasOwnProperty.call(dependencies, 'next')) {
|
|
1244
|
+
return 'next';
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
if (Object.prototype.hasOwnProperty.call(dependencies, 'react')) {
|
|
1248
|
+
return 'react';
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
if (Object.prototype.hasOwnProperty.call(dependencies, 'vue')) {
|
|
1252
|
+
return 'vuejs';
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
return 'generic';
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
function resolveEntryFile(projectRoot, context, explicitEntry) {
|
|
1259
|
+
if (explicitEntry) {
|
|
1260
|
+
const resolved = path.resolve(projectRoot, explicitEntry);
|
|
1261
|
+
return fs.existsSync(resolved) ? resolved : null;
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
const candidatesByContext = {
|
|
1265
|
+
angular: ['src/main.ts', 'src/main.js'],
|
|
1266
|
+
react: ['src/main.tsx', 'src/main.jsx', 'src/index.tsx', 'src/index.jsx'],
|
|
1267
|
+
vuejs: ['src/main.ts', 'src/main.js'],
|
|
1268
|
+
next: [
|
|
1269
|
+
'app/layout.tsx',
|
|
1270
|
+
'app/layout.jsx',
|
|
1271
|
+
'pages/_app.tsx',
|
|
1272
|
+
'pages/_app.jsx',
|
|
1273
|
+
],
|
|
1274
|
+
generic: [
|
|
1275
|
+
'src/main.ts',
|
|
1276
|
+
'src/main.js',
|
|
1277
|
+
'src/index.ts',
|
|
1278
|
+
'src/index.js',
|
|
1279
|
+
'main.ts',
|
|
1280
|
+
'main.js',
|
|
1281
|
+
'index.ts',
|
|
1282
|
+
'index.js',
|
|
1283
|
+
],
|
|
1284
|
+
};
|
|
1285
|
+
|
|
1286
|
+
const candidates =
|
|
1287
|
+
candidatesByContext[context] ?? candidatesByContext.generic;
|
|
1288
|
+
for (const candidate of candidates) {
|
|
1289
|
+
const fullPath = path.join(projectRoot, candidate);
|
|
1290
|
+
if (fs.existsSync(fullPath)) {
|
|
1291
|
+
return fullPath;
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
return null;
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
function inferContextFromEntryFile(entryFile) {
|
|
1299
|
+
if (!entryFile || !fs.existsSync(entryFile)) {
|
|
1300
|
+
return null;
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
const normalizedPath = entryFile.replace(/\\/gu, '/');
|
|
1304
|
+
if (
|
|
1305
|
+
/\/app\/layout\.[jt]sx?$/u.test(normalizedPath) ||
|
|
1306
|
+
/\/pages\/_app\.[jt]sx?$/u.test(normalizedPath)
|
|
1307
|
+
) {
|
|
1308
|
+
return 'next';
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
const content = fs.readFileSync(entryFile, 'utf8');
|
|
1312
|
+
if (
|
|
1313
|
+
content.includes('bootstrapApplication(') ||
|
|
1314
|
+
content.includes('platformBrowserDynamic()')
|
|
1315
|
+
) {
|
|
1316
|
+
return 'angular';
|
|
1317
|
+
}
|
|
1318
|
+
if (content.includes('createRoot(') || content.includes('ReactDOM.render(')) {
|
|
1319
|
+
return 'react';
|
|
1320
|
+
}
|
|
1321
|
+
if (content.includes('createApp(') && content.includes('.mount(')) {
|
|
1322
|
+
return 'vuejs';
|
|
1323
|
+
}
|
|
1324
|
+
if (content.includes("from 'next/") || content.includes('from "next/')) {
|
|
1325
|
+
return 'next';
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
return 'generic';
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
function rsxBootstrapFilePath(entryFile) {
|
|
1332
|
+
const ext = path.extname(entryFile).toLowerCase();
|
|
1333
|
+
const fileName =
|
|
1334
|
+
ext === '.js' || ext === '.jsx' ? 'rsx-bootstrap.js' : 'rsx-bootstrap.ts';
|
|
1335
|
+
return path.join(path.dirname(entryFile), fileName);
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
function ensureRsxBootstrapFile(bootstrapFile, dryRun) {
|
|
1339
|
+
const content = `import { InjectionContainer } from '@rs-x/core';\nimport { RsXExpressionParserModule } from '@rs-x/expression-parser';\n\n// Generated by rsx init\nexport async function initRsx(): Promise<void> {\n await InjectionContainer.load(RsXExpressionParserModule);\n}\n`;
|
|
1340
|
+
|
|
1341
|
+
if (fs.existsSync(bootstrapFile)) {
|
|
1342
|
+
const existing = fs.readFileSync(bootstrapFile, 'utf8');
|
|
1343
|
+
if (existing.includes('export async function initRsx')) {
|
|
1344
|
+
logInfo(`Bootstrap module already exists: ${bootstrapFile}`);
|
|
1345
|
+
return;
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
logWarn(`Bootstrap file exists but is unmanaged: ${bootstrapFile}`);
|
|
1349
|
+
logWarn('Skipping overwrite; please add `initRsx` manually.');
|
|
1350
|
+
return;
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
if (dryRun) {
|
|
1354
|
+
logInfo(`[dry-run] create ${bootstrapFile}`);
|
|
1355
|
+
return;
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
fs.writeFileSync(bootstrapFile, content, 'utf8');
|
|
1359
|
+
logOk(`Created ${bootstrapFile}`);
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
function stripFileExtension(filePath) {
|
|
1363
|
+
return filePath.replace(/\.[^.]+$/u, '');
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
function toModuleImportPath(fromFile, targetFile) {
|
|
1367
|
+
const relative = path
|
|
1368
|
+
.relative(path.dirname(fromFile), targetFile)
|
|
1369
|
+
.replace(/\\/gu, '/');
|
|
1370
|
+
const withDot = relative.startsWith('.') ? relative : `./${relative}`;
|
|
1371
|
+
return stripFileExtension(withDot);
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
function injectImport(source, importStatement) {
|
|
1375
|
+
if (source.includes(importStatement)) {
|
|
1376
|
+
return source;
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
const lines = source.split('\n');
|
|
1380
|
+
let insertAt = 0;
|
|
1381
|
+
|
|
1382
|
+
while (
|
|
1383
|
+
insertAt < lines.length &&
|
|
1384
|
+
lines[insertAt].trim().startsWith('import ')
|
|
1385
|
+
) {
|
|
1386
|
+
insertAt += 1;
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
const next = [
|
|
1390
|
+
...lines.slice(0, insertAt),
|
|
1391
|
+
importStatement,
|
|
1392
|
+
...lines.slice(insertAt),
|
|
1393
|
+
];
|
|
1394
|
+
return next.join('\n');
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
function indentBlock(text, spaces) {
|
|
1398
|
+
const pad = ' '.repeat(spaces);
|
|
1399
|
+
return text
|
|
1400
|
+
.split('\n')
|
|
1401
|
+
.map((line) => `${pad}${line}`)
|
|
1402
|
+
.join('\n');
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
function wrapReactEntry(source) {
|
|
1406
|
+
const reactStartPattern =
|
|
1407
|
+
/(ReactDOM\s*\.\s*)?createRoot\([\s\S]*?\)\s*\.\s*render\([\s\S]*?\);/mu;
|
|
1408
|
+
const match = source.match(reactStartPattern);
|
|
1409
|
+
if (!match) {
|
|
1410
|
+
return null;
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
const renderCall = match[0].trim();
|
|
1414
|
+
const replacement = `const __rsxStart = async () => {\n await initRsx();\n${indentBlock(renderCall, 2)}\n};\n\nvoid __rsxStart();`;
|
|
1415
|
+
return source.replace(reactStartPattern, replacement);
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
function wrapAngularEntry(source) {
|
|
1419
|
+
const angularBootstrapPattern =
|
|
1420
|
+
/bootstrapApplication\([\s\S]*?\)(?:\s*\.\s*catch\([\s\S]*?\))?\s*;/mu;
|
|
1421
|
+
const angularModulePattern =
|
|
1422
|
+
/platformBrowserDynamic\(\)\s*\.\s*bootstrapModule\([\s\S]*?\)(?:\s*\.\s*catch\([\s\S]*?\))?\s*;/mu;
|
|
1423
|
+
|
|
1424
|
+
const match =
|
|
1425
|
+
source.match(angularBootstrapPattern) ?? source.match(angularModulePattern);
|
|
1426
|
+
if (!match) {
|
|
1427
|
+
return null;
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
const bootstrapCall = match[0].trim();
|
|
1431
|
+
const replacement = `const __rsxBootstrap = async () => {\n await initRsx();\n${indentBlock(bootstrapCall, 2)}\n};\n\nvoid __rsxBootstrap();`;
|
|
1432
|
+
|
|
1433
|
+
const pattern = source.match(angularBootstrapPattern)
|
|
1434
|
+
? angularBootstrapPattern
|
|
1435
|
+
: angularModulePattern;
|
|
1436
|
+
return source.replace(pattern, replacement);
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
function wrapVueEntry(source) {
|
|
1440
|
+
const vueStartPattern =
|
|
1441
|
+
/createApp\([\s\S]*?\)\s*\.\s*mount\([\s\S]*?\)\s*;/mu;
|
|
1442
|
+
const match = source.match(vueStartPattern);
|
|
1443
|
+
if (!match) {
|
|
1444
|
+
return null;
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
const mountCall = match[0].trim();
|
|
1448
|
+
const replacement = `const __rsxBootstrap = async () => {\n await initRsx();\n${indentBlock(mountCall, 2)}\n};\n\nvoid __rsxBootstrap();`;
|
|
1449
|
+
return source.replace(vueStartPattern, replacement);
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
function wrapGenericEntry(source) {
|
|
1453
|
+
const startCallPattern = /^\s*([A-Za-z_$][\w$]*)\(\);\s*$/mu;
|
|
1454
|
+
const match = source.match(startCallPattern);
|
|
1455
|
+
if (!match) {
|
|
1456
|
+
return null;
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
const startCall = match[0].trim();
|
|
1460
|
+
const replacement = `const __rsxBootstrap = async () => {\n await initRsx();\n${indentBlock(startCall, 2)}\n};\n\nvoid __rsxBootstrap();`;
|
|
1461
|
+
return source.replace(startCallPattern, replacement);
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
function nextGateFilePath(entryFile) {
|
|
1465
|
+
const ext = path.extname(entryFile).toLowerCase();
|
|
1466
|
+
const fileName =
|
|
1467
|
+
ext === '.js' || ext === '.jsx'
|
|
1468
|
+
? 'rsx-bootstrap-gate.jsx'
|
|
1469
|
+
: 'rsx-bootstrap-gate.tsx';
|
|
1470
|
+
return path.join(path.dirname(entryFile), fileName);
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
function ensureNextGateFile(gateFile, bootstrapFile, dryRun) {
|
|
1474
|
+
const gateExt = path.extname(gateFile).toLowerCase();
|
|
1475
|
+
const useTypeScript = gateExt === '.tsx';
|
|
1476
|
+
const importPath = toModuleImportPath(gateFile, bootstrapFile);
|
|
1477
|
+
|
|
1478
|
+
const content = useTypeScript
|
|
1479
|
+
? `'use client';
|
|
1480
|
+
|
|
1481
|
+
import { type ReactNode, useEffect, useState } from 'react';
|
|
1482
|
+
|
|
1483
|
+
import { initRsx } from '${importPath}';
|
|
1484
|
+
|
|
1485
|
+
// Generated by rsx init
|
|
1486
|
+
export function RsxBootstrapGate(props: { children: ReactNode }): JSX.Element | null {
|
|
1487
|
+
const [ready, setReady] = useState(false);
|
|
1488
|
+
|
|
1489
|
+
useEffect(() => {
|
|
1490
|
+
let active = true;
|
|
1491
|
+
void initRsx().then(() => {
|
|
1492
|
+
if (active) {
|
|
1493
|
+
setReady(true);
|
|
1494
|
+
}
|
|
1495
|
+
});
|
|
1496
|
+
return () => {
|
|
1497
|
+
active = false;
|
|
1498
|
+
};
|
|
1499
|
+
}, []);
|
|
1500
|
+
|
|
1501
|
+
if (!ready) {
|
|
1502
|
+
return null;
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
return <>{props.children}</>;
|
|
1506
|
+
}
|
|
1507
|
+
`
|
|
1508
|
+
: `'use client';
|
|
1509
|
+
|
|
1510
|
+
import { useEffect, useState } from 'react';
|
|
1511
|
+
|
|
1512
|
+
import { initRsx } from '${importPath}';
|
|
1513
|
+
|
|
1514
|
+
// Generated by rsx init
|
|
1515
|
+
export function RsxBootstrapGate(props) {
|
|
1516
|
+
const [ready, setReady] = useState(false);
|
|
1517
|
+
|
|
1518
|
+
useEffect(() => {
|
|
1519
|
+
let active = true;
|
|
1520
|
+
void initRsx().then(() => {
|
|
1521
|
+
if (active) {
|
|
1522
|
+
setReady(true);
|
|
1523
|
+
}
|
|
1524
|
+
});
|
|
1525
|
+
return () => {
|
|
1526
|
+
active = false;
|
|
1527
|
+
};
|
|
1528
|
+
}, []);
|
|
1529
|
+
|
|
1530
|
+
if (!ready) {
|
|
1531
|
+
return null;
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
return <>{props.children}</>;
|
|
1535
|
+
}
|
|
1536
|
+
`;
|
|
1537
|
+
|
|
1538
|
+
if (fs.existsSync(gateFile)) {
|
|
1539
|
+
const existing = fs.readFileSync(gateFile, 'utf8');
|
|
1540
|
+
if (existing.includes('export function RsxBootstrapGate')) {
|
|
1541
|
+
logInfo(`Next gate module already exists: ${gateFile}`);
|
|
1542
|
+
return;
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
logWarn(`Next gate file exists but is unmanaged: ${gateFile}`);
|
|
1546
|
+
logWarn('Skipping overwrite; please add `RsxBootstrapGate` manually.');
|
|
1547
|
+
return;
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
if (dryRun) {
|
|
1551
|
+
logInfo(`[dry-run] create ${gateFile}`);
|
|
1552
|
+
return;
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
fs.writeFileSync(gateFile, content, 'utf8');
|
|
1556
|
+
logOk(`Created ${gateFile}`);
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
function patchNextLayoutEntry(source) {
|
|
1560
|
+
if (source.includes('<RsxBootstrapGate>')) {
|
|
1561
|
+
return source;
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
const bodyPattern = /<body([^>]*)>([\s\S]*?)<\/body>/u;
|
|
1565
|
+
const match = source.match(bodyPattern);
|
|
1566
|
+
if (!match) {
|
|
1567
|
+
return null;
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
const bodyAttributes = match[1] ?? '';
|
|
1571
|
+
const bodyInner = (match[2] ?? '').trim();
|
|
1572
|
+
const gateInner = bodyInner.length
|
|
1573
|
+
? `\n${indentBlock(bodyInner, 10)}\n`
|
|
1574
|
+
: '\n';
|
|
1575
|
+
const replacement = `<body${bodyAttributes}>\n <RsxBootstrapGate>${gateInner} </RsxBootstrapGate>\n </body>`;
|
|
1576
|
+
const updated = source.replace(bodyPattern, replacement);
|
|
1577
|
+
|
|
1578
|
+
return updated;
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
function patchNextPagesAppEntry(source) {
|
|
1582
|
+
if (source.includes('<RsxBootstrapGate>')) {
|
|
1583
|
+
return source;
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
const componentRenderPattern = /<Component\b[^>]*\/>/u;
|
|
1587
|
+
const match = source.match(componentRenderPattern);
|
|
1588
|
+
if (!match) {
|
|
1589
|
+
return null;
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
const componentRender = match[0];
|
|
1593
|
+
const wrapped = `<RsxBootstrapGate>\n ${componentRender}\n </RsxBootstrapGate>`;
|
|
1594
|
+
return source.replace(componentRenderPattern, wrapped);
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
function patchNextEntryFile(entryFile, gateFile, dryRun) {
|
|
1598
|
+
const original = fs.readFileSync(entryFile, 'utf8');
|
|
1599
|
+
if (
|
|
1600
|
+
original.includes('RsxBootstrapGate') &&
|
|
1601
|
+
original.includes('rsx-bootstrap-gate')
|
|
1602
|
+
) {
|
|
1603
|
+
logInfo(`Entry already wired for Next RS-X bootstrap gate: ${entryFile}`);
|
|
1604
|
+
return true;
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
const gateImportPath = toModuleImportPath(entryFile, gateFile);
|
|
1608
|
+
const importStatement = `import { RsxBootstrapGate } from '${gateImportPath}';`;
|
|
1609
|
+
const sourceWithImport = injectImport(original, importStatement);
|
|
1610
|
+
|
|
1611
|
+
const normalizedPath = entryFile.replace(/\\/gu, '/');
|
|
1612
|
+
const isAppLayout = /\/app\/layout\.[jt]sx?$/u.test(normalizedPath);
|
|
1613
|
+
const isPagesApp = /\/pages\/_app\.[jt]sx?$/u.test(normalizedPath);
|
|
1614
|
+
|
|
1615
|
+
let updated = null;
|
|
1616
|
+
if (isAppLayout) {
|
|
1617
|
+
updated = patchNextLayoutEntry(sourceWithImport);
|
|
1618
|
+
if (!updated) {
|
|
1619
|
+
logWarn(`Could not patch Next app router layout at ${entryFile}.`);
|
|
1620
|
+
return false;
|
|
1621
|
+
}
|
|
1622
|
+
} else if (isPagesApp) {
|
|
1623
|
+
updated = patchNextPagesAppEntry(sourceWithImport);
|
|
1624
|
+
if (!updated) {
|
|
1625
|
+
logWarn(`Could not patch Next pages router app file at ${entryFile}.`);
|
|
1626
|
+
return false;
|
|
1627
|
+
}
|
|
1628
|
+
} else {
|
|
1629
|
+
logWarn(`Unsupported Next entry file shape for ${entryFile}.`);
|
|
1630
|
+
return false;
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
if (dryRun) {
|
|
1634
|
+
logInfo(`[dry-run] patch ${entryFile}`);
|
|
1635
|
+
return true;
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
fs.writeFileSync(entryFile, updated, 'utf8');
|
|
1639
|
+
logOk(`Patched ${entryFile}`);
|
|
1640
|
+
return true;
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
function patchEntryFileForRsx(entryFile, bootstrapFile, context, dryRun) {
|
|
1644
|
+
const original = fs.readFileSync(entryFile, 'utf8');
|
|
1645
|
+
if (original.includes('initRsx') && original.includes('rsx-bootstrap')) {
|
|
1646
|
+
logInfo(`Entry already wired for RS-X bootstrap: ${entryFile}`);
|
|
1647
|
+
return true;
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
const importPath = toModuleImportPath(entryFile, bootstrapFile);
|
|
1651
|
+
const importStatement = `import { initRsx } from '${importPath}';`;
|
|
1652
|
+
|
|
1653
|
+
let updated = injectImport(original, importStatement);
|
|
1654
|
+
|
|
1655
|
+
if (context === 'react') {
|
|
1656
|
+
updated = wrapReactEntry(updated);
|
|
1657
|
+
if (!updated) {
|
|
1658
|
+
logWarn(`Could not find React app bootstrap call in ${entryFile}.`);
|
|
1659
|
+
return false;
|
|
1660
|
+
}
|
|
1661
|
+
} else if (context === 'angular') {
|
|
1662
|
+
updated = wrapAngularEntry(updated);
|
|
1663
|
+
if (!updated) {
|
|
1664
|
+
logWarn(`Could not find Angular bootstrap call in ${entryFile}.`);
|
|
1665
|
+
return false;
|
|
1666
|
+
}
|
|
1667
|
+
} else if (context === 'vuejs') {
|
|
1668
|
+
updated = wrapVueEntry(updated);
|
|
1669
|
+
if (!updated) {
|
|
1670
|
+
logWarn(`Could not find Vue app mount call in ${entryFile}.`);
|
|
1671
|
+
return false;
|
|
1672
|
+
}
|
|
1673
|
+
} else if (context === 'generic') {
|
|
1674
|
+
updated = wrapGenericEntry(updated);
|
|
1675
|
+
if (!updated) {
|
|
1676
|
+
logWarn(
|
|
1677
|
+
`Could not find a generic startup call (for example main();) in ${entryFile}.`,
|
|
1678
|
+
);
|
|
1679
|
+
return false;
|
|
1680
|
+
}
|
|
1681
|
+
} else {
|
|
1682
|
+
logWarn(
|
|
1683
|
+
`Automatic bootstrap wiring is not yet supported for context '${context}'.`,
|
|
1684
|
+
);
|
|
1685
|
+
return false;
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
if (dryRun) {
|
|
1689
|
+
logInfo(`[dry-run] patch ${entryFile}`);
|
|
1690
|
+
return true;
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
fs.writeFileSync(entryFile, updated, 'utf8');
|
|
1694
|
+
logOk(`Patched ${entryFile}`);
|
|
1695
|
+
return true;
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
function runInit(flags) {
|
|
1699
|
+
const dryRun = Boolean(flags['dry-run']);
|
|
1700
|
+
const skipVscode = Boolean(flags['skip-vscode']);
|
|
1701
|
+
const skipInstall = Boolean(flags['skip-install']);
|
|
1702
|
+
const pm = detectPackageManager(flags.pm);
|
|
1703
|
+
const projectRoot = process.cwd();
|
|
1704
|
+
|
|
1705
|
+
if (!skipInstall) {
|
|
1706
|
+
installRuntimePackages(pm, dryRun);
|
|
1707
|
+
installCompilerPackages(pm, dryRun);
|
|
1708
|
+
} else {
|
|
1709
|
+
logInfo('Skipping package installation (--skip-install).');
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
const context = detectProjectContext(projectRoot);
|
|
1713
|
+
const entryFile = resolveEntryFile(projectRoot, context, flags.entry);
|
|
1714
|
+
const effectiveContext = flags.entry
|
|
1715
|
+
? (inferContextFromEntryFile(entryFile) ?? context)
|
|
1716
|
+
: context;
|
|
1717
|
+
|
|
1718
|
+
if (!entryFile) {
|
|
1719
|
+
logWarn('Could not detect an application entry file automatically.');
|
|
1720
|
+
logInfo(
|
|
1721
|
+
'Use `rsx init --entry <path-to-entry-file>` to force bootstrap wiring.',
|
|
1722
|
+
);
|
|
1723
|
+
} else if (effectiveContext === 'next') {
|
|
1724
|
+
logInfo(`Detected context: ${effectiveContext}`);
|
|
1725
|
+
logInfo(`Using entry file: ${entryFile}`);
|
|
1726
|
+
|
|
1727
|
+
const bootstrapFile = rsxBootstrapFilePath(entryFile);
|
|
1728
|
+
const gateFile = nextGateFilePath(entryFile);
|
|
1729
|
+
|
|
1730
|
+
ensureRsxBootstrapFile(bootstrapFile, dryRun);
|
|
1731
|
+
ensureNextGateFile(gateFile, bootstrapFile, dryRun);
|
|
1732
|
+
const patched = patchNextEntryFile(entryFile, gateFile, dryRun);
|
|
1733
|
+
|
|
1734
|
+
if (!patched) {
|
|
1735
|
+
logInfo('Manual fallback snippet:');
|
|
1736
|
+
console.log(" import { RsxBootstrapGate } from './rsx-bootstrap-gate';");
|
|
1737
|
+
console.log(
|
|
1738
|
+
' // wrap app children with <RsxBootstrapGate>...</RsxBootstrapGate>',
|
|
1739
|
+
);
|
|
1740
|
+
}
|
|
1741
|
+
} else {
|
|
1742
|
+
logInfo(`Detected context: ${effectiveContext}`);
|
|
1743
|
+
logInfo(`Using entry file: ${entryFile}`);
|
|
1744
|
+
|
|
1745
|
+
const bootstrapFile = rsxBootstrapFilePath(entryFile);
|
|
1746
|
+
ensureRsxBootstrapFile(bootstrapFile, dryRun);
|
|
1747
|
+
const patched = patchEntryFileForRsx(
|
|
1748
|
+
entryFile,
|
|
1749
|
+
bootstrapFile,
|
|
1750
|
+
effectiveContext,
|
|
1751
|
+
dryRun,
|
|
1752
|
+
);
|
|
1753
|
+
|
|
1754
|
+
if (!patched) {
|
|
1755
|
+
logInfo('Manual fallback snippet:');
|
|
1756
|
+
console.log(" import { initRsx } from './rsx-bootstrap';");
|
|
1757
|
+
console.log(' await initRsx(); // before first rsx(...)');
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
if (!skipVscode) {
|
|
1762
|
+
installVsCodeExtension(flags);
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
logOk('RS-X init completed.');
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
function upsertScriptInPackageJson(
|
|
1769
|
+
projectRoot,
|
|
1770
|
+
scriptName,
|
|
1771
|
+
scriptValue,
|
|
1772
|
+
dryRun,
|
|
1773
|
+
) {
|
|
1774
|
+
const packageJsonPath = path.join(projectRoot, 'package.json');
|
|
1775
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
1776
|
+
return false;
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
1780
|
+
const scripts = packageJson.scripts ?? {};
|
|
1781
|
+
if (scripts[scriptName] === scriptValue) {
|
|
1782
|
+
return true;
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
scripts[scriptName] = scriptValue;
|
|
1786
|
+
packageJson.scripts = scripts;
|
|
1787
|
+
|
|
1788
|
+
if (dryRun) {
|
|
1789
|
+
logInfo(`[dry-run] patch ${packageJsonPath} (scripts.${scriptName})`);
|
|
1790
|
+
return true;
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
fs.writeFileSync(
|
|
1794
|
+
packageJsonPath,
|
|
1795
|
+
`${JSON.stringify(packageJson, null, 2)}\n`,
|
|
1796
|
+
'utf8',
|
|
1797
|
+
);
|
|
1798
|
+
logOk(`Patched ${packageJsonPath} (scripts.${scriptName})`);
|
|
1799
|
+
return true;
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
function createRsxWebpackLoaderFile(projectRoot, dryRun) {
|
|
1803
|
+
const loaderPath = path.join(projectRoot, 'rsx-webpack-loader.cjs');
|
|
1804
|
+
const loaderSource = `const path = require('node:path');
|
|
1805
|
+
const ts = require('typescript');
|
|
1806
|
+
const { createExpressionCachePreloadTransformer } = require('@rs-x/compiler');
|
|
1807
|
+
|
|
1808
|
+
function normalizeFileName(fileName) {
|
|
1809
|
+
return path.resolve(fileName).replace(/\\\\/gu, '/');
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
function buildTransformedSourceMap(tsconfigPath) {
|
|
1813
|
+
const configFile = ts.readConfigFile(tsconfigPath, ts.sys.readFile);
|
|
1814
|
+
if (configFile.error) {
|
|
1815
|
+
return new Map();
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
const parsed = ts.parseJsonConfigFileContent(
|
|
1819
|
+
configFile.config,
|
|
1820
|
+
ts.sys,
|
|
1821
|
+
path.dirname(tsconfigPath),
|
|
1822
|
+
undefined,
|
|
1823
|
+
tsconfigPath,
|
|
1824
|
+
);
|
|
1825
|
+
if (parsed.errors.length > 0) {
|
|
1826
|
+
return new Map();
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
const program = ts.createProgram({
|
|
1830
|
+
rootNames: parsed.fileNames,
|
|
1831
|
+
options: parsed.options,
|
|
1832
|
+
});
|
|
1833
|
+
const transformer = createExpressionCachePreloadTransformer(program);
|
|
1834
|
+
const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
|
|
1835
|
+
const transformedByFile = new Map();
|
|
1836
|
+
|
|
1837
|
+
for (const sourceFile of program.getSourceFiles()) {
|
|
1838
|
+
if (sourceFile.isDeclarationFile) {
|
|
1839
|
+
continue;
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
if (sourceFile.fileName.includes('/node_modules/')) {
|
|
1843
|
+
continue;
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
const transformed = ts.transform(sourceFile, [transformer]);
|
|
1847
|
+
const transformedSource = transformed.transformed[0];
|
|
1848
|
+
const transformedText = printer.printFile(transformedSource);
|
|
1849
|
+
transformed.dispose();
|
|
1850
|
+
|
|
1851
|
+
transformedByFile.set(normalizeFileName(sourceFile.fileName), transformedText);
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
return transformedByFile;
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
module.exports = function rsxWebpackLoader(source) {
|
|
1858
|
+
const callback = this.async();
|
|
1859
|
+
const tsconfigPath = normalizeFileName(
|
|
1860
|
+
path.resolve(this.rootContext || process.cwd(), 'tsconfig.json'),
|
|
1861
|
+
);
|
|
1862
|
+
const transformedByFile = buildTransformedSourceMap(tsconfigPath);
|
|
1863
|
+
const transformed = transformedByFile.get(normalizeFileName(this.resourcePath));
|
|
1864
|
+
callback(null, transformed ?? source);
|
|
1865
|
+
};
|
|
1866
|
+
`;
|
|
1867
|
+
|
|
1868
|
+
if (dryRun) {
|
|
1869
|
+
logInfo(`[dry-run] create ${loaderPath}`);
|
|
1870
|
+
} else {
|
|
1871
|
+
fs.writeFileSync(loaderPath, loaderSource, 'utf8');
|
|
1872
|
+
logOk(`Created ${loaderPath}`);
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
return loaderPath;
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
function wireRsxVitePlugin(projectRoot, dryRun) {
|
|
1879
|
+
const pluginFile = path.join(projectRoot, 'rsx-vite-plugin.mjs');
|
|
1880
|
+
const pluginSource = `import path from 'node:path';
|
|
1881
|
+
|
|
1882
|
+
import ts from 'typescript';
|
|
1883
|
+
|
|
1884
|
+
import { createExpressionCachePreloadTransformer } from '@rs-x/compiler';
|
|
1885
|
+
|
|
1886
|
+
function normalizeFileName(fileName) {
|
|
1887
|
+
return path.resolve(fileName).replace(/\\\\/gu, '/');
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
function buildTransformedSourceMap(tsconfigPath) {
|
|
1891
|
+
const configFile = ts.readConfigFile(tsconfigPath, ts.sys.readFile);
|
|
1892
|
+
if (configFile.error) {
|
|
1893
|
+
return new Map();
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
const parsed = ts.parseJsonConfigFileContent(
|
|
1897
|
+
configFile.config,
|
|
1898
|
+
ts.sys,
|
|
1899
|
+
path.dirname(tsconfigPath),
|
|
1900
|
+
undefined,
|
|
1901
|
+
tsconfigPath,
|
|
1902
|
+
);
|
|
1903
|
+
if (parsed.errors.length > 0) {
|
|
1904
|
+
return new Map();
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1907
|
+
const program = ts.createProgram({
|
|
1908
|
+
rootNames: parsed.fileNames,
|
|
1909
|
+
options: parsed.options,
|
|
1910
|
+
});
|
|
1911
|
+
const transformer = createExpressionCachePreloadTransformer(program);
|
|
1912
|
+
const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
|
|
1913
|
+
const transformedByFile = new Map();
|
|
1914
|
+
|
|
1915
|
+
for (const sourceFile of program.getSourceFiles()) {
|
|
1916
|
+
if (sourceFile.isDeclarationFile) {
|
|
1917
|
+
continue;
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
if (sourceFile.fileName.includes('/node_modules/')) {
|
|
1921
|
+
continue;
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
const transformed = ts.transform(sourceFile, [transformer]);
|
|
1925
|
+
const transformedSource = transformed.transformed[0];
|
|
1926
|
+
const transformedText = printer.printFile(transformedSource);
|
|
1927
|
+
transformed.dispose();
|
|
1928
|
+
|
|
1929
|
+
transformedByFile.set(normalizeFileName(sourceFile.fileName), transformedText);
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
return transformedByFile;
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
export function rsxVitePlugin(tsconfigPath = 'tsconfig.json') {
|
|
1936
|
+
let transformedByFile = new Map();
|
|
1937
|
+
let resolvedTsConfigPath = '';
|
|
1938
|
+
|
|
1939
|
+
const refresh = () => {
|
|
1940
|
+
transformedByFile = buildTransformedSourceMap(resolvedTsConfigPath);
|
|
1941
|
+
};
|
|
1942
|
+
|
|
1943
|
+
return {
|
|
1944
|
+
name: 'rsx-vite-transform',
|
|
1945
|
+
enforce: 'pre',
|
|
1946
|
+
configResolved(config) {
|
|
1947
|
+
resolvedTsConfigPath = normalizeFileName(path.resolve(config.root, tsconfigPath));
|
|
1948
|
+
refresh();
|
|
1949
|
+
},
|
|
1950
|
+
buildStart() {
|
|
1951
|
+
if (!resolvedTsConfigPath) {
|
|
1952
|
+
resolvedTsConfigPath = normalizeFileName(path.resolve(process.cwd(), tsconfigPath));
|
|
1953
|
+
}
|
|
1954
|
+
refresh();
|
|
1955
|
+
},
|
|
1956
|
+
handleHotUpdate() {
|
|
1957
|
+
refresh();
|
|
1958
|
+
},
|
|
1959
|
+
transform(_code, id) {
|
|
1960
|
+
const normalizedId = normalizeFileName(id.split('?')[0]);
|
|
1961
|
+
const transformed = transformedByFile.get(normalizedId);
|
|
1962
|
+
if (!transformed) {
|
|
1963
|
+
return null;
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
return {
|
|
1967
|
+
code: transformed,
|
|
1968
|
+
map: null,
|
|
1969
|
+
};
|
|
1970
|
+
},
|
|
1971
|
+
};
|
|
1972
|
+
}
|
|
1973
|
+
`;
|
|
1974
|
+
|
|
1975
|
+
if (dryRun) {
|
|
1976
|
+
logInfo(`[dry-run] create ${pluginFile}`);
|
|
1977
|
+
} else {
|
|
1978
|
+
fs.writeFileSync(pluginFile, pluginSource, 'utf8');
|
|
1979
|
+
logOk(`Created ${pluginFile}`);
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1982
|
+
const viteConfigCandidates = [
|
|
1983
|
+
'vite.config.ts',
|
|
1984
|
+
'vite.config.mts',
|
|
1985
|
+
'vite.config.js',
|
|
1986
|
+
'vite.config.mjs',
|
|
1987
|
+
].map((fileName) => path.join(projectRoot, fileName));
|
|
1988
|
+
const viteConfigPath = viteConfigCandidates.find((candidate) =>
|
|
1989
|
+
fs.existsSync(candidate),
|
|
1990
|
+
);
|
|
1991
|
+
if (!viteConfigPath) {
|
|
1992
|
+
logWarn(
|
|
1993
|
+
'No vite.config.[ts|mts|js|mjs] found. RS-X Vite plugin file was created, but config patch was skipped.',
|
|
1994
|
+
);
|
|
1995
|
+
logInfo(
|
|
1996
|
+
"Add it manually: import { rsxVitePlugin } from './rsx-vite-plugin.mjs' and include rsxVitePlugin() in plugins.",
|
|
1997
|
+
);
|
|
1998
|
+
return;
|
|
1999
|
+
}
|
|
2000
|
+
|
|
2001
|
+
const original = fs.readFileSync(viteConfigPath, 'utf8');
|
|
2002
|
+
if (original.includes('rsxVitePlugin(')) {
|
|
2003
|
+
logInfo(`Vite config already includes RS-X plugin: ${viteConfigPath}`);
|
|
2004
|
+
return;
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
let updated = original;
|
|
2008
|
+
const importStatement =
|
|
2009
|
+
"import { rsxVitePlugin } from './rsx-vite-plugin.mjs';";
|
|
2010
|
+
if (!updated.includes(importStatement)) {
|
|
2011
|
+
const lines = updated.split('\n');
|
|
2012
|
+
let insertAt = 0;
|
|
2013
|
+
while (
|
|
2014
|
+
insertAt < lines.length &&
|
|
2015
|
+
lines[insertAt].trim().startsWith('import ')
|
|
2016
|
+
) {
|
|
2017
|
+
insertAt += 1;
|
|
2018
|
+
}
|
|
2019
|
+
lines.splice(insertAt, 0, importStatement);
|
|
2020
|
+
updated = lines.join('\n');
|
|
2021
|
+
}
|
|
2022
|
+
|
|
2023
|
+
if (/plugins\s*:\s*\[/u.test(updated)) {
|
|
2024
|
+
updated = updated.replace(
|
|
2025
|
+
/plugins\s*:\s*\[/u,
|
|
2026
|
+
'plugins: [rsxVitePlugin(), ',
|
|
2027
|
+
);
|
|
2028
|
+
} else if (/defineConfig\s*\(\s*\{/u.test(updated)) {
|
|
2029
|
+
updated = updated.replace(
|
|
2030
|
+
/defineConfig\s*\(\s*\{/u,
|
|
2031
|
+
'defineConfig({\n plugins: [rsxVitePlugin()],',
|
|
2032
|
+
);
|
|
2033
|
+
} else {
|
|
2034
|
+
logWarn(`Could not patch Vite config automatically: ${viteConfigPath}`);
|
|
2035
|
+
logInfo('Add `rsxVitePlugin()` to your Vite plugins manually.');
|
|
2036
|
+
return;
|
|
2037
|
+
}
|
|
2038
|
+
|
|
2039
|
+
if (dryRun) {
|
|
2040
|
+
logInfo(`[dry-run] patch ${viteConfigPath}`);
|
|
2041
|
+
return;
|
|
2042
|
+
}
|
|
2043
|
+
|
|
2044
|
+
fs.writeFileSync(viteConfigPath, updated, 'utf8');
|
|
2045
|
+
logOk(`Patched ${viteConfigPath} with RS-X Vite plugin.`);
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
function wireRsxNextWebpack(projectRoot, dryRun) {
|
|
2049
|
+
const loaderPath = createRsxWebpackLoaderFile(projectRoot, dryRun);
|
|
2050
|
+
const nextConfigJs = path.join(projectRoot, 'next.config.js');
|
|
2051
|
+
const nextConfigMjs = path.join(projectRoot, 'next.config.mjs');
|
|
2052
|
+
const nextConfigTs = path.join(projectRoot, 'next.config.ts');
|
|
2053
|
+
|
|
2054
|
+
if (fs.existsSync(nextConfigMjs) || fs.existsSync(nextConfigTs)) {
|
|
2055
|
+
logWarn(
|
|
2056
|
+
'Detected next.config.mjs/ts. Automatic RS-X patch currently supports next.config.js only.',
|
|
2057
|
+
);
|
|
2058
|
+
logInfo(`Add webpack rule manually with loader: ${loaderPath}`);
|
|
2059
|
+
return;
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2062
|
+
const patchBlock = `
|
|
2063
|
+
const __rsxWebpackLoaderPath = require('node:path').resolve(__dirname, './rsx-webpack-loader.cjs');
|
|
2064
|
+
const __rsxApply = (nextConfigOrFactory) => {
|
|
2065
|
+
if (typeof nextConfigOrFactory === 'function') {
|
|
2066
|
+
return (...args) => __rsxApply(nextConfigOrFactory(...args));
|
|
2067
|
+
}
|
|
2068
|
+
|
|
2069
|
+
const nextConfig = nextConfigOrFactory ?? {};
|
|
2070
|
+
const previousWebpack = nextConfig.webpack;
|
|
2071
|
+
return {
|
|
2072
|
+
...nextConfig,
|
|
2073
|
+
webpack(config, options) {
|
|
2074
|
+
config.module.rules.unshift({
|
|
2075
|
+
test: /\\.[jt]sx?$/u,
|
|
2076
|
+
exclude: /node_modules/u,
|
|
2077
|
+
use: [
|
|
2078
|
+
{
|
|
2079
|
+
loader: __rsxWebpackLoaderPath,
|
|
2080
|
+
},
|
|
2081
|
+
],
|
|
2082
|
+
});
|
|
2083
|
+
|
|
2084
|
+
if (typeof previousWebpack === 'function') {
|
|
2085
|
+
return previousWebpack(config, options);
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
return config;
|
|
2089
|
+
},
|
|
2090
|
+
};
|
|
2091
|
+
};
|
|
2092
|
+
|
|
2093
|
+
module.exports = __rsxApply(module.exports);
|
|
2094
|
+
`;
|
|
2095
|
+
|
|
2096
|
+
if (!fs.existsSync(nextConfigJs)) {
|
|
2097
|
+
const source = `/** @type {import('next').NextConfig} */
|
|
2098
|
+
module.exports = {};
|
|
2099
|
+
${patchBlock}
|
|
2100
|
+
`;
|
|
2101
|
+
if (dryRun) {
|
|
2102
|
+
logInfo(`[dry-run] create ${nextConfigJs}`);
|
|
2103
|
+
} else {
|
|
2104
|
+
fs.writeFileSync(nextConfigJs, source, 'utf8');
|
|
2105
|
+
logOk(`Created ${nextConfigJs}`);
|
|
2106
|
+
}
|
|
2107
|
+
return;
|
|
2108
|
+
}
|
|
2109
|
+
|
|
2110
|
+
const original = fs.readFileSync(nextConfigJs, 'utf8');
|
|
2111
|
+
if (original.includes('__rsxWebpackLoaderPath')) {
|
|
2112
|
+
logInfo(
|
|
2113
|
+
`Next config already includes RS-X webpack loader: ${nextConfigJs}`,
|
|
2114
|
+
);
|
|
2115
|
+
return;
|
|
2116
|
+
}
|
|
2117
|
+
|
|
2118
|
+
if (dryRun) {
|
|
2119
|
+
logInfo(`[dry-run] patch ${nextConfigJs}`);
|
|
2120
|
+
return;
|
|
2121
|
+
}
|
|
2122
|
+
|
|
2123
|
+
fs.writeFileSync(nextConfigJs, `${original}\n${patchBlock}\n`, 'utf8');
|
|
2124
|
+
logOk(`Patched ${nextConfigJs} with RS-X webpack loader.`);
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2127
|
+
function wireRsxAngularWebpack(projectRoot, dryRun) {
|
|
2128
|
+
const angularJsonPath = path.join(projectRoot, 'angular.json');
|
|
2129
|
+
if (!fs.existsSync(angularJsonPath)) {
|
|
2130
|
+
logWarn('angular.json not found. Skipping Angular build integration.');
|
|
2131
|
+
return;
|
|
2132
|
+
}
|
|
2133
|
+
|
|
2134
|
+
createRsxWebpackLoaderFile(projectRoot, dryRun);
|
|
2135
|
+
|
|
2136
|
+
const webpackConfigPath = path.join(projectRoot, 'rsx-angular-webpack.cjs');
|
|
2137
|
+
const webpackConfigSource = `const path = require('node:path');
|
|
2138
|
+
|
|
2139
|
+
module.exports = {
|
|
2140
|
+
module: {
|
|
2141
|
+
rules: [
|
|
2142
|
+
{
|
|
2143
|
+
test: /\\.[jt]sx?$/u,
|
|
2144
|
+
exclude: /node_modules/u,
|
|
2145
|
+
use: [
|
|
2146
|
+
{
|
|
2147
|
+
loader: path.resolve(__dirname, './rsx-webpack-loader.cjs'),
|
|
2148
|
+
},
|
|
2149
|
+
],
|
|
2150
|
+
},
|
|
2151
|
+
],
|
|
2152
|
+
},
|
|
2153
|
+
};
|
|
2154
|
+
`;
|
|
2155
|
+
|
|
2156
|
+
if (dryRun) {
|
|
2157
|
+
logInfo(`[dry-run] create ${webpackConfigPath}`);
|
|
2158
|
+
} else {
|
|
2159
|
+
fs.writeFileSync(webpackConfigPath, webpackConfigSource, 'utf8');
|
|
2160
|
+
logOk(`Created ${webpackConfigPath}`);
|
|
2161
|
+
}
|
|
2162
|
+
|
|
2163
|
+
const angularJson = JSON.parse(fs.readFileSync(angularJsonPath, 'utf8'));
|
|
2164
|
+
const projects = angularJson.projects ?? {};
|
|
2165
|
+
const projectNames = Object.keys(projects);
|
|
2166
|
+
if (projectNames.length === 0) {
|
|
2167
|
+
logWarn('No Angular projects found in angular.json.');
|
|
2168
|
+
return;
|
|
2169
|
+
}
|
|
2170
|
+
|
|
2171
|
+
const patchPath = 'rsx-angular-webpack.cjs';
|
|
2172
|
+
for (const projectName of projectNames) {
|
|
2173
|
+
const project = projects[projectName];
|
|
2174
|
+
const architect = project.architect ?? project.targets;
|
|
2175
|
+
if (!architect?.build) {
|
|
2176
|
+
continue;
|
|
2177
|
+
}
|
|
2178
|
+
|
|
2179
|
+
const build = architect.build;
|
|
2180
|
+
if (build.builder !== '@angular-builders/custom-webpack:browser') {
|
|
2181
|
+
build.builder = '@angular-builders/custom-webpack:browser';
|
|
2182
|
+
}
|
|
2183
|
+
build.options = build.options ?? {};
|
|
2184
|
+
build.options.customWebpackConfig = build.options.customWebpackConfig ?? {};
|
|
2185
|
+
build.options.customWebpackConfig.path = patchPath;
|
|
2186
|
+
|
|
2187
|
+
if (architect.serve) {
|
|
2188
|
+
const serve = architect.serve;
|
|
2189
|
+
if (serve.builder !== '@angular-builders/custom-webpack:dev-server') {
|
|
2190
|
+
serve.builder = '@angular-builders/custom-webpack:dev-server';
|
|
2191
|
+
}
|
|
2192
|
+
serve.options = serve.options ?? {};
|
|
2193
|
+
serve.options.buildTarget =
|
|
2194
|
+
serve.options.buildTarget ?? `${projectName}:build`;
|
|
2195
|
+
serve.options.browserTarget =
|
|
2196
|
+
serve.options.browserTarget ?? `${projectName}:build`;
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
|
|
2200
|
+
if (dryRun) {
|
|
2201
|
+
logInfo(`[dry-run] patch ${angularJsonPath}`);
|
|
2202
|
+
} else {
|
|
2203
|
+
fs.writeFileSync(
|
|
2204
|
+
angularJsonPath,
|
|
2205
|
+
`${JSON.stringify(angularJson, null, 2)}\n`,
|
|
2206
|
+
'utf8',
|
|
2207
|
+
);
|
|
2208
|
+
logOk(`Patched ${angularJsonPath} for RS-X Angular webpack integration.`);
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
|
|
2212
|
+
function runSetupReact(flags) {
|
|
2213
|
+
const dryRun = Boolean(flags['dry-run']);
|
|
2214
|
+
const pm = detectPackageManager(flags.pm);
|
|
2215
|
+
const projectRoot = process.cwd();
|
|
2216
|
+
const packageJsonPath = path.join(projectRoot, 'package.json');
|
|
2217
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
2218
|
+
logError(`package.json not found in ${projectRoot}`);
|
|
2219
|
+
process.exit(1);
|
|
2220
|
+
}
|
|
2221
|
+
|
|
2222
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
2223
|
+
const allDependencies = {
|
|
2224
|
+
...(packageJson.dependencies ?? {}),
|
|
2225
|
+
...(packageJson.devDependencies ?? {}),
|
|
2226
|
+
};
|
|
2227
|
+
if (!allDependencies.react) {
|
|
2228
|
+
logWarn(
|
|
2229
|
+
'React dependency not detected in package.json; continuing anyway.',
|
|
2230
|
+
);
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2233
|
+
runInit({
|
|
2234
|
+
...flags,
|
|
2235
|
+
'skip-vscode': true,
|
|
2236
|
+
});
|
|
2237
|
+
if (!Boolean(flags['skip-install'])) {
|
|
2238
|
+
installPackages(pm, ['@rs-x/react'], {
|
|
2239
|
+
dev: false,
|
|
2240
|
+
dryRun,
|
|
2241
|
+
label: 'RS-X React bindings',
|
|
2242
|
+
});
|
|
2243
|
+
} else {
|
|
2244
|
+
logInfo('Skipping RS-X React bindings install (--skip-install).');
|
|
2245
|
+
}
|
|
2246
|
+
wireRsxVitePlugin(projectRoot, dryRun);
|
|
2247
|
+
if (!Boolean(flags['skip-vscode'])) {
|
|
2248
|
+
installVsCodeExtension(flags);
|
|
2249
|
+
}
|
|
2250
|
+
logOk('RS-X React setup completed.');
|
|
2251
|
+
}
|
|
2252
|
+
|
|
2253
|
+
function runSetupNext(flags) {
|
|
2254
|
+
const dryRun = Boolean(flags['dry-run']);
|
|
2255
|
+
const pm = detectPackageManager(flags.pm);
|
|
2256
|
+
runInit({
|
|
2257
|
+
...flags,
|
|
2258
|
+
'skip-vscode': true,
|
|
2259
|
+
});
|
|
2260
|
+
if (!Boolean(flags['skip-install'])) {
|
|
2261
|
+
installPackages(pm, ['@rs-x/react'], {
|
|
2262
|
+
dev: false,
|
|
2263
|
+
dryRun,
|
|
2264
|
+
label: 'RS-X React bindings',
|
|
2265
|
+
});
|
|
2266
|
+
} else {
|
|
2267
|
+
logInfo('Skipping RS-X React bindings install (--skip-install).');
|
|
2268
|
+
}
|
|
2269
|
+
wireRsxNextWebpack(process.cwd(), dryRun);
|
|
2270
|
+
if (!Boolean(flags['skip-vscode'])) {
|
|
2271
|
+
installVsCodeExtension(flags);
|
|
2272
|
+
}
|
|
2273
|
+
logOk('RS-X Next.js setup completed.');
|
|
2274
|
+
}
|
|
2275
|
+
|
|
2276
|
+
function runSetupVue(flags) {
|
|
2277
|
+
const dryRun = Boolean(flags['dry-run']);
|
|
2278
|
+
const pm = detectPackageManager(flags.pm);
|
|
2279
|
+
runInit({
|
|
2280
|
+
...flags,
|
|
2281
|
+
'skip-vscode': true,
|
|
2282
|
+
});
|
|
2283
|
+
if (!Boolean(flags['skip-install'])) {
|
|
2284
|
+
installPackages(pm, ['@rs-x/vue'], {
|
|
2285
|
+
dev: false,
|
|
2286
|
+
dryRun,
|
|
2287
|
+
label: 'RS-X Vue bindings',
|
|
2288
|
+
});
|
|
2289
|
+
} else {
|
|
2290
|
+
logInfo('Skipping RS-X Vue bindings install (--skip-install).');
|
|
2291
|
+
}
|
|
2292
|
+
wireRsxVitePlugin(process.cwd(), dryRun);
|
|
2293
|
+
if (!Boolean(flags['skip-vscode'])) {
|
|
2294
|
+
installVsCodeExtension(flags);
|
|
2295
|
+
}
|
|
2296
|
+
logOk('RS-X Vue setup completed.');
|
|
2297
|
+
}
|
|
2298
|
+
|
|
2299
|
+
function runSetupAngular(flags) {
|
|
2300
|
+
const dryRun = Boolean(flags['dry-run']);
|
|
2301
|
+
const pm = detectPackageManager(flags.pm);
|
|
2302
|
+
|
|
2303
|
+
runInit({
|
|
2304
|
+
...flags,
|
|
2305
|
+
'skip-vscode': true,
|
|
2306
|
+
});
|
|
2307
|
+
|
|
2308
|
+
if (!Boolean(flags['skip-install'])) {
|
|
2309
|
+
installPackages(pm, ['@rs-x/angular'], {
|
|
2310
|
+
dev: false,
|
|
2311
|
+
dryRun,
|
|
2312
|
+
label: 'RS-X Angular bindings',
|
|
2313
|
+
});
|
|
2314
|
+
installPackages(pm, ['@angular-builders/custom-webpack'], {
|
|
2315
|
+
dev: true,
|
|
2316
|
+
dryRun,
|
|
2317
|
+
label: 'Angular custom webpack builder',
|
|
2318
|
+
});
|
|
2319
|
+
} else {
|
|
2320
|
+
logInfo(
|
|
2321
|
+
'Skipping Angular custom webpack builder install (--skip-install).',
|
|
2322
|
+
);
|
|
2323
|
+
}
|
|
2324
|
+
|
|
2325
|
+
wireRsxAngularWebpack(process.cwd(), dryRun);
|
|
2326
|
+
upsertScriptInPackageJson(
|
|
2327
|
+
process.cwd(),
|
|
2328
|
+
'build:rsx',
|
|
2329
|
+
'rsx build --project tsconfig.json',
|
|
2330
|
+
dryRun,
|
|
2331
|
+
);
|
|
2332
|
+
upsertScriptInPackageJson(
|
|
2333
|
+
process.cwd(),
|
|
2334
|
+
'typecheck:rsx',
|
|
2335
|
+
'rsx typecheck --project tsconfig.json',
|
|
2336
|
+
dryRun,
|
|
2337
|
+
);
|
|
2338
|
+
|
|
2339
|
+
if (!Boolean(flags['skip-vscode'])) {
|
|
2340
|
+
installVsCodeExtension(flags);
|
|
2341
|
+
}
|
|
2342
|
+
logOk('RS-X Angular setup completed.');
|
|
2343
|
+
}
|
|
2344
|
+
|
|
2345
|
+
function runSetupAuto(flags) {
|
|
2346
|
+
const projectRoot = process.cwd();
|
|
2347
|
+
const context = detectProjectContext(projectRoot);
|
|
2348
|
+
|
|
2349
|
+
if (context === 'react') {
|
|
2350
|
+
logInfo('Auto-detected framework: react');
|
|
2351
|
+
runSetupReact(flags);
|
|
2352
|
+
return;
|
|
2353
|
+
}
|
|
2354
|
+
|
|
2355
|
+
if (context === 'vuejs') {
|
|
2356
|
+
logInfo('Auto-detected framework: vuejs');
|
|
2357
|
+
runSetupVue(flags);
|
|
2358
|
+
return;
|
|
2359
|
+
}
|
|
2360
|
+
|
|
2361
|
+
if (context === 'next') {
|
|
2362
|
+
logInfo('Auto-detected framework: next');
|
|
2363
|
+
runSetupNext(flags);
|
|
2364
|
+
return;
|
|
2365
|
+
}
|
|
2366
|
+
|
|
2367
|
+
if (context === 'angular') {
|
|
2368
|
+
logInfo('Auto-detected framework: angular');
|
|
2369
|
+
runSetupAngular(flags);
|
|
2370
|
+
return;
|
|
2371
|
+
}
|
|
2372
|
+
|
|
2373
|
+
logInfo('No framework-specific setup detected; running generic setup.');
|
|
2374
|
+
const pm = detectPackageManager(flags.pm);
|
|
2375
|
+
installRuntimePackages(pm, Boolean(flags['dry-run']));
|
|
2376
|
+
installCompilerPackages(pm, Boolean(flags['dry-run']));
|
|
2377
|
+
installVsCodeExtension(flags);
|
|
2378
|
+
}
|
|
2379
|
+
|
|
2380
|
+
function resolveProjectModule(projectRoot, moduleName) {
|
|
2381
|
+
try {
|
|
2382
|
+
const resolvedPath = require.resolve(moduleName, { paths: [projectRoot] });
|
|
2383
|
+
return require(resolvedPath);
|
|
2384
|
+
} catch {
|
|
2385
|
+
return null;
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2388
|
+
|
|
2389
|
+
function runBuild(flags) {
|
|
2390
|
+
const invocationRoot = process.cwd();
|
|
2391
|
+
const dryRun = Boolean(flags['dry-run']);
|
|
2392
|
+
const noEmit = Boolean(flags['no-emit']);
|
|
2393
|
+
const prodMode = parseBooleanFlag(flags.prod, false);
|
|
2394
|
+
const projectArg =
|
|
2395
|
+
typeof flags.project === 'string' ? flags.project : 'tsconfig.json';
|
|
2396
|
+
const configPath = path.resolve(invocationRoot, projectArg);
|
|
2397
|
+
const projectRoot = path.dirname(configPath);
|
|
2398
|
+
const context = detectProjectContext(projectRoot);
|
|
2399
|
+
const rsxBuildConfig = resolveRsxBuildConfig(projectRoot);
|
|
2400
|
+
const defaultPreparseEnabled = prodMode
|
|
2401
|
+
? typeof rsxBuildConfig.preparse === 'boolean'
|
|
2402
|
+
? rsxBuildConfig.preparse
|
|
2403
|
+
: context === 'angular'
|
|
2404
|
+
: context === 'angular';
|
|
2405
|
+
const defaultCompiledEnabled = prodMode
|
|
2406
|
+
? typeof rsxBuildConfig.compiled === 'boolean'
|
|
2407
|
+
? rsxBuildConfig.compiled
|
|
2408
|
+
: true
|
|
2409
|
+
: false;
|
|
2410
|
+
const aotPreparseEnabled = parseBooleanFlag(
|
|
2411
|
+
flags['aot-preparse'],
|
|
2412
|
+
defaultPreparseEnabled,
|
|
2413
|
+
);
|
|
2414
|
+
const aotCompiledEnabled = parseBooleanFlag(
|
|
2415
|
+
flags['aot-compiled'],
|
|
2416
|
+
defaultCompiledEnabled,
|
|
2417
|
+
);
|
|
2418
|
+
const aotPreparseFile =
|
|
2419
|
+
typeof flags['aot-preparse-file'] === 'string'
|
|
2420
|
+
? path.resolve(projectRoot, flags['aot-preparse-file'])
|
|
2421
|
+
: typeof rsxBuildConfig.preparseFile === 'string'
|
|
2422
|
+
? path.resolve(projectRoot, rsxBuildConfig.preparseFile)
|
|
2423
|
+
: context === 'angular'
|
|
2424
|
+
? path.join(projectRoot, 'src', 'rsx-aot-preparsed.generated.ts')
|
|
2425
|
+
: null;
|
|
2426
|
+
const aotCompiledFile =
|
|
2427
|
+
typeof flags['aot-compiled-file'] === 'string'
|
|
2428
|
+
? path.resolve(projectRoot, flags['aot-compiled-file'])
|
|
2429
|
+
: typeof rsxBuildConfig.compiledFile === 'string'
|
|
2430
|
+
? path.resolve(projectRoot, rsxBuildConfig.compiledFile)
|
|
2431
|
+
: context === 'angular'
|
|
2432
|
+
? path.join(projectRoot, 'src', 'rsx-aot-compiled.generated.ts')
|
|
2433
|
+
: null;
|
|
2434
|
+
const aotRegistrationFile =
|
|
2435
|
+
typeof rsxBuildConfig.registrationFile === 'string'
|
|
2436
|
+
? path.resolve(projectRoot, rsxBuildConfig.registrationFile)
|
|
2437
|
+
: context === 'angular'
|
|
2438
|
+
? path.join(projectRoot, 'src', 'rsx-aot-registration.generated.ts')
|
|
2439
|
+
: null;
|
|
2440
|
+
const includeResolvedEvaluator = parseBooleanFlag(
|
|
2441
|
+
flags['compiled-resolved-evaluator'],
|
|
2442
|
+
Boolean(rsxBuildConfig.compiledResolvedEvaluator),
|
|
2443
|
+
);
|
|
2444
|
+
|
|
2445
|
+
if (!fs.existsSync(configPath)) {
|
|
2446
|
+
logError(`TypeScript config not found: ${configPath}`);
|
|
2447
|
+
process.exit(1);
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2450
|
+
const ts = resolveProjectModule(projectRoot, 'typescript');
|
|
2451
|
+
if (!ts) {
|
|
2452
|
+
logError('Missing `typescript` in this project.');
|
|
2453
|
+
logInfo('Install it with: npm i -D typescript');
|
|
2454
|
+
process.exit(1);
|
|
2455
|
+
}
|
|
2456
|
+
|
|
2457
|
+
const readConfig = ts.readConfigFile(configPath, ts.sys.readFile);
|
|
2458
|
+
if (readConfig.error) {
|
|
2459
|
+
const formatted = ts.formatDiagnosticsWithColorAndContext(
|
|
2460
|
+
[readConfig.error],
|
|
2461
|
+
{
|
|
2462
|
+
getCanonicalFileName: (name) => name,
|
|
2463
|
+
getCurrentDirectory: () => projectRoot,
|
|
2464
|
+
getNewLine: () => '\n',
|
|
2465
|
+
},
|
|
2466
|
+
);
|
|
2467
|
+
console.error(formatted);
|
|
2468
|
+
process.exit(1);
|
|
2469
|
+
}
|
|
2470
|
+
|
|
2471
|
+
const parsedConfig = ts.parseJsonConfigFileContent(
|
|
2472
|
+
readConfig.config,
|
|
2473
|
+
ts.sys,
|
|
2474
|
+
path.dirname(configPath),
|
|
2475
|
+
undefined,
|
|
2476
|
+
configPath,
|
|
2477
|
+
);
|
|
2478
|
+
if (parsedConfig.errors.length > 0) {
|
|
2479
|
+
const formatted = ts.formatDiagnosticsWithColorAndContext(
|
|
2480
|
+
parsedConfig.errors,
|
|
2481
|
+
{
|
|
2482
|
+
getCanonicalFileName: (name) => name,
|
|
2483
|
+
getCurrentDirectory: () => projectRoot,
|
|
2484
|
+
getNewLine: () => '\n',
|
|
2485
|
+
},
|
|
2486
|
+
);
|
|
2487
|
+
console.error(formatted);
|
|
2488
|
+
process.exit(1);
|
|
2489
|
+
}
|
|
2490
|
+
|
|
2491
|
+
const outDirOverride =
|
|
2492
|
+
typeof flags['out-dir'] === 'string'
|
|
2493
|
+
? path.resolve(projectRoot, flags['out-dir'])
|
|
2494
|
+
: null;
|
|
2495
|
+
const outDir =
|
|
2496
|
+
outDirOverride ??
|
|
2497
|
+
parsedConfig.options.outDir ??
|
|
2498
|
+
path.join(projectRoot, 'dist');
|
|
2499
|
+
const compilerOptions = {
|
|
2500
|
+
...parsedConfig.options,
|
|
2501
|
+
outDir,
|
|
2502
|
+
};
|
|
2503
|
+
|
|
2504
|
+
const program = ts.createProgram({
|
|
2505
|
+
rootNames: parsedConfig.fileNames,
|
|
2506
|
+
options: compilerOptions,
|
|
2507
|
+
});
|
|
2508
|
+
const ignoredGeneratedFiles = new Set(
|
|
2509
|
+
[aotPreparseFile, aotCompiledFile]
|
|
2510
|
+
.filter((filePath) => typeof filePath === 'string')
|
|
2511
|
+
.map((filePath) => path.resolve(filePath)),
|
|
2512
|
+
);
|
|
2513
|
+
|
|
2514
|
+
let blockingDiagnostics = [];
|
|
2515
|
+
try {
|
|
2516
|
+
const preEmitDiagnostics = ts.getPreEmitDiagnostics(program);
|
|
2517
|
+
blockingDiagnostics = preEmitDiagnostics.filter((diagnostic) => {
|
|
2518
|
+
if (diagnostic.category !== ts.DiagnosticCategory.Error) {
|
|
2519
|
+
return false;
|
|
2520
|
+
}
|
|
2521
|
+
const diagnosticFilePath = diagnostic.file?.fileName
|
|
2522
|
+
? path.resolve(diagnostic.file.fileName)
|
|
2523
|
+
: null;
|
|
2524
|
+
if (diagnosticFilePath && ignoredGeneratedFiles.has(diagnosticFilePath)) {
|
|
2525
|
+
return false;
|
|
2526
|
+
}
|
|
2527
|
+
return true;
|
|
2528
|
+
});
|
|
2529
|
+
} catch (error) {
|
|
2530
|
+
if (
|
|
2531
|
+
error instanceof RangeError &&
|
|
2532
|
+
String(error.message).includes('Maximum call stack size exceeded')
|
|
2533
|
+
) {
|
|
2534
|
+
logWarn(
|
|
2535
|
+
'TypeScript pre-emit diagnostics overflowed (TS internal recursion). Continuing with RS-X semantic validation.',
|
|
2536
|
+
);
|
|
2537
|
+
} else {
|
|
2538
|
+
throw error;
|
|
2539
|
+
}
|
|
2540
|
+
}
|
|
2541
|
+
|
|
2542
|
+
if (blockingDiagnostics.length > 0) {
|
|
2543
|
+
const formatted = ts.formatDiagnosticsWithColorAndContext(
|
|
2544
|
+
blockingDiagnostics,
|
|
2545
|
+
{
|
|
2546
|
+
getCanonicalFileName: (name) => name,
|
|
2547
|
+
getCurrentDirectory: () => projectRoot,
|
|
2548
|
+
getNewLine: () => '\n',
|
|
2549
|
+
},
|
|
2550
|
+
);
|
|
2551
|
+
console.error(formatted);
|
|
2552
|
+
process.exit(1);
|
|
2553
|
+
}
|
|
2554
|
+
|
|
2555
|
+
const compilerModule = resolveRsxCompilerModule(projectRoot);
|
|
2556
|
+
runRsxSemanticValidation(program, projectRoot, compilerModule);
|
|
2557
|
+
runRsxAotPreparseGeneration({
|
|
2558
|
+
program,
|
|
2559
|
+
projectRoot,
|
|
2560
|
+
compilerModule,
|
|
2561
|
+
enabled: aotPreparseEnabled,
|
|
2562
|
+
outputFile: aotPreparseFile,
|
|
2563
|
+
dryRun,
|
|
2564
|
+
});
|
|
2565
|
+
runRsxAotCompiledGeneration({
|
|
2566
|
+
program,
|
|
2567
|
+
projectRoot,
|
|
2568
|
+
compilerModule,
|
|
2569
|
+
enabled: aotCompiledEnabled,
|
|
2570
|
+
outputFile: aotCompiledFile,
|
|
2571
|
+
includeResolvedEvaluator,
|
|
2572
|
+
dryRun,
|
|
2573
|
+
});
|
|
2574
|
+
runRsxAngularAotRegistrationInjection({
|
|
2575
|
+
context,
|
|
2576
|
+
projectRoot,
|
|
2577
|
+
configPath,
|
|
2578
|
+
registrationFile: aotRegistrationFile,
|
|
2579
|
+
preparseEnabled: aotPreparseEnabled,
|
|
2580
|
+
preparseFile: aotPreparseFile,
|
|
2581
|
+
compiledEnabled: aotCompiledEnabled,
|
|
2582
|
+
compiledFile: aotCompiledFile,
|
|
2583
|
+
dryRun,
|
|
2584
|
+
});
|
|
2585
|
+
|
|
2586
|
+
if (dryRun) {
|
|
2587
|
+
logInfo(`[dry-run] rsx build using ${configPath}`);
|
|
2588
|
+
logInfo(`[dry-run] source files: ${parsedConfig.fileNames.length}`);
|
|
2589
|
+
logInfo(`[dry-run] outDir: ${outDir}`);
|
|
2590
|
+
logInfo(`[dry-run] prod mode: ${prodMode ? 'on' : 'off'}`);
|
|
2591
|
+
if (noEmit) {
|
|
2592
|
+
logInfo('[dry-run] no-emit mode enabled');
|
|
2593
|
+
}
|
|
2594
|
+
return;
|
|
2595
|
+
}
|
|
2596
|
+
|
|
2597
|
+
if (noEmit) {
|
|
2598
|
+
logOk('Typecheck completed. No TypeScript or RS-X semantic errors found.');
|
|
2599
|
+
return;
|
|
2600
|
+
}
|
|
2601
|
+
|
|
2602
|
+
try {
|
|
2603
|
+
const emitResult = program.emit();
|
|
2604
|
+
if (emitResult.emitSkipped) {
|
|
2605
|
+
logError('Build failed: TypeScript emit skipped.');
|
|
2606
|
+
process.exit(1);
|
|
2607
|
+
}
|
|
2608
|
+
|
|
2609
|
+
logOk(`Build completed. Output: ${outDir}`);
|
|
2610
|
+
return;
|
|
2611
|
+
} catch (error) {
|
|
2612
|
+
logWarn('TypeScript emit failed; falling back to transpile pipeline.');
|
|
2613
|
+
if (error instanceof Error) {
|
|
2614
|
+
logWarn(error.message);
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2617
|
+
|
|
2618
|
+
fs.rmSync(outDir, { recursive: true, force: true });
|
|
2619
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
2620
|
+
|
|
2621
|
+
const commonSourceDirectory =
|
|
2622
|
+
compilerOptions.rootDir ??
|
|
2623
|
+
program.getCommonSourceDirectory() ??
|
|
2624
|
+
projectRoot;
|
|
2625
|
+
const sourceFiles = program
|
|
2626
|
+
.getSourceFiles()
|
|
2627
|
+
.filter((sourceFile) => !sourceFile.isDeclarationFile)
|
|
2628
|
+
.filter((sourceFile) =>
|
|
2629
|
+
parsedConfig.fileNames.includes(sourceFile.fileName),
|
|
2630
|
+
);
|
|
2631
|
+
|
|
2632
|
+
for (const sourceFile of sourceFiles) {
|
|
2633
|
+
const sourceText = ts
|
|
2634
|
+
.createPrinter({ newLine: ts.NewLineKind.LineFeed })
|
|
2635
|
+
.printFile(sourceFile);
|
|
2636
|
+
|
|
2637
|
+
const transpiled = ts.transpileModule(sourceText, {
|
|
2638
|
+
compilerOptions,
|
|
2639
|
+
fileName: sourceFile.fileName,
|
|
2640
|
+
});
|
|
2641
|
+
|
|
2642
|
+
const relativePath = path.relative(
|
|
2643
|
+
commonSourceDirectory,
|
|
2644
|
+
sourceFile.fileName,
|
|
2645
|
+
);
|
|
2646
|
+
const outputPath = path
|
|
2647
|
+
.join(outDir, relativePath)
|
|
2648
|
+
.replace(/\.[cm]?[jt]sx?$/u, '.js');
|
|
2649
|
+
|
|
2650
|
+
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
2651
|
+
fs.writeFileSync(outputPath, transpiled.outputText, 'utf8');
|
|
2652
|
+
}
|
|
2653
|
+
|
|
2654
|
+
logOk(`Build completed via transpile fallback. Output: ${outDir}`);
|
|
2655
|
+
}
|
|
2656
|
+
|
|
2657
|
+
function runTypecheck(flags) {
|
|
2658
|
+
runBuild({
|
|
2659
|
+
...flags,
|
|
2660
|
+
'no-emit': true,
|
|
2661
|
+
});
|
|
2662
|
+
}
|
|
2663
|
+
|
|
2664
|
+
function resolveRsxCompilerModule(projectRoot) {
|
|
2665
|
+
let compilerModule = resolveProjectModule(projectRoot, '@rs-x/compiler');
|
|
2666
|
+
if (compilerModule) {
|
|
2667
|
+
return compilerModule;
|
|
2668
|
+
}
|
|
2669
|
+
|
|
2670
|
+
const repoRoot = findRepoRoot(projectRoot);
|
|
2671
|
+
const localCompilerPath = repoRoot
|
|
2672
|
+
? path.join(repoRoot, 'rs-x-compiler', 'dist', 'index.cjs')
|
|
2673
|
+
: null;
|
|
2674
|
+
if (localCompilerPath && fs.existsSync(localCompilerPath)) {
|
|
2675
|
+
return require(localCompilerPath);
|
|
2676
|
+
}
|
|
2677
|
+
|
|
2678
|
+
return null;
|
|
2679
|
+
}
|
|
2680
|
+
|
|
2681
|
+
function runRsxSemanticValidation(program, projectRoot, compilerModule) {
|
|
2682
|
+
if (
|
|
2683
|
+
!compilerModule ||
|
|
2684
|
+
typeof compilerModule.validateExpressionSites !== 'function'
|
|
2685
|
+
) {
|
|
2686
|
+
logError('Missing `@rs-x/compiler` in this project.');
|
|
2687
|
+
logInfo('Install it with: npm i -D @rs-x/compiler');
|
|
2688
|
+
process.exit(1);
|
|
2689
|
+
}
|
|
2690
|
+
|
|
2691
|
+
const validatedSites = compilerModule.validateExpressionSites(program);
|
|
2692
|
+
const rsxDiagnostics = validatedSites.flatMap((site) =>
|
|
2693
|
+
site.diagnostics.map((diagnostic) => ({
|
|
2694
|
+
diagnostic,
|
|
2695
|
+
site,
|
|
2696
|
+
})),
|
|
2697
|
+
);
|
|
2698
|
+
|
|
2699
|
+
if (rsxDiagnostics.length === 0) {
|
|
2700
|
+
return;
|
|
2701
|
+
}
|
|
2702
|
+
|
|
2703
|
+
const formatted = rsxDiagnostics
|
|
2704
|
+
.map(({ diagnostic, site }) => {
|
|
2705
|
+
const sourceFile = site.sourceFile;
|
|
2706
|
+
const absolutePath = sourceFile.fileName;
|
|
2707
|
+
const relativePath =
|
|
2708
|
+
path.relative(projectRoot, absolutePath) || absolutePath;
|
|
2709
|
+
const expressionStart = site.expressionLiteral.getStart(sourceFile) + 1;
|
|
2710
|
+
const location =
|
|
2711
|
+
sourceFile.getLineAndCharacterOfPosition(expressionStart);
|
|
2712
|
+
const category = diagnostic.category === 'syntax' ? 'RSX1001' : 'RSX1000';
|
|
2713
|
+
return `${relativePath}:${location.line + 1}:${location.character + 1} - error ${category}: ${diagnostic.message}\n expression: ${site.expression}`;
|
|
2714
|
+
})
|
|
2715
|
+
.join('\n\n');
|
|
2716
|
+
|
|
2717
|
+
console.error('');
|
|
2718
|
+
logError(
|
|
2719
|
+
`RS-X semantic validation failed with ${rsxDiagnostics.length} error(s).`,
|
|
2720
|
+
);
|
|
2721
|
+
console.error(formatted);
|
|
2722
|
+
process.exit(1);
|
|
2723
|
+
}
|
|
2724
|
+
|
|
2725
|
+
function runRsxAotPreparseGeneration({
|
|
2726
|
+
program,
|
|
2727
|
+
projectRoot,
|
|
2728
|
+
compilerModule,
|
|
2729
|
+
enabled,
|
|
2730
|
+
outputFile,
|
|
2731
|
+
dryRun,
|
|
2732
|
+
}) {
|
|
2733
|
+
if (!enabled || !outputFile) {
|
|
2734
|
+
return;
|
|
2735
|
+
}
|
|
2736
|
+
|
|
2737
|
+
if (
|
|
2738
|
+
!compilerModule ||
|
|
2739
|
+
typeof compilerModule.generateAotParsedExpressionCacheModule !== 'function'
|
|
2740
|
+
) {
|
|
2741
|
+
logWarn(
|
|
2742
|
+
'Skipping RS-X preparse generation: compiler does not expose generateAotParsedExpressionCacheModule.',
|
|
2743
|
+
);
|
|
2744
|
+
return;
|
|
2745
|
+
}
|
|
2746
|
+
|
|
2747
|
+
const generated =
|
|
2748
|
+
compilerModule.generateAotParsedExpressionCacheModule(program);
|
|
2749
|
+
const header = [
|
|
2750
|
+
'// @ts-nocheck',
|
|
2751
|
+
'/* eslint-disable */',
|
|
2752
|
+
'/* This file is auto-generated by rsx build. Do not edit manually. */',
|
|
2753
|
+
'',
|
|
2754
|
+
].join('\n');
|
|
2755
|
+
const content = `${header}${generated.code}`;
|
|
2756
|
+
|
|
2757
|
+
if (dryRun) {
|
|
2758
|
+
logInfo(
|
|
2759
|
+
`[dry-run] generate RS-X preparse cache (${generated.expressions.length} expressions): ${outputFile}`,
|
|
2760
|
+
);
|
|
2761
|
+
return;
|
|
2762
|
+
}
|
|
2763
|
+
|
|
2764
|
+
fs.mkdirSync(path.dirname(outputFile), { recursive: true });
|
|
2765
|
+
fs.writeFileSync(outputFile, content, 'utf8');
|
|
2766
|
+
logOk(
|
|
2767
|
+
`Generated RS-X preparse cache (${generated.expressions.length} expressions): ${path.relative(projectRoot, outputFile)}`,
|
|
2768
|
+
);
|
|
2769
|
+
}
|
|
2770
|
+
|
|
2771
|
+
function runRsxAotCompiledGeneration({
|
|
2772
|
+
program,
|
|
2773
|
+
projectRoot,
|
|
2774
|
+
compilerModule,
|
|
2775
|
+
enabled,
|
|
2776
|
+
outputFile,
|
|
2777
|
+
includeResolvedEvaluator,
|
|
2778
|
+
dryRun,
|
|
2779
|
+
}) {
|
|
2780
|
+
if (!enabled || !outputFile) {
|
|
2781
|
+
return;
|
|
2782
|
+
}
|
|
2783
|
+
|
|
2784
|
+
if (
|
|
2785
|
+
!compilerModule ||
|
|
2786
|
+
typeof compilerModule.generateAotCompiledExpressionsModule !== 'function'
|
|
2787
|
+
) {
|
|
2788
|
+
logWarn(
|
|
2789
|
+
'Skipping RS-X compiled generation: compiler does not expose generateAotCompiledExpressionsModule.',
|
|
2790
|
+
);
|
|
2791
|
+
return;
|
|
2792
|
+
}
|
|
2793
|
+
|
|
2794
|
+
const generated = compilerModule.generateAotCompiledExpressionsModule(
|
|
2795
|
+
program,
|
|
2796
|
+
{
|
|
2797
|
+
includeResolvedEvaluator,
|
|
2798
|
+
},
|
|
2799
|
+
);
|
|
2800
|
+
const header = [
|
|
2801
|
+
'// @ts-nocheck',
|
|
2802
|
+
'/* eslint-disable */',
|
|
2803
|
+
'/* This file is auto-generated by rsx build. Do not edit manually. */',
|
|
2804
|
+
'',
|
|
2805
|
+
].join('\n');
|
|
2806
|
+
const content = `${header}${generated.code}`;
|
|
2807
|
+
|
|
2808
|
+
if (dryRun) {
|
|
2809
|
+
logInfo(
|
|
2810
|
+
`[dry-run] generate RS-X compiled cache (${generated.expressions.length} expressions): ${outputFile}`,
|
|
2811
|
+
);
|
|
2812
|
+
return;
|
|
2813
|
+
}
|
|
2814
|
+
|
|
2815
|
+
fs.mkdirSync(path.dirname(outputFile), { recursive: true });
|
|
2816
|
+
fs.writeFileSync(outputFile, content, 'utf8');
|
|
2817
|
+
logOk(
|
|
2818
|
+
`Generated RS-X compiled cache (${generated.expressions.length} expressions): ${path.relative(projectRoot, outputFile)}`,
|
|
2819
|
+
);
|
|
2820
|
+
}
|
|
2821
|
+
|
|
2822
|
+
function runRsxAngularAotRegistrationInjection({
|
|
2823
|
+
context,
|
|
2824
|
+
projectRoot,
|
|
2825
|
+
configPath,
|
|
2826
|
+
registrationFile,
|
|
2827
|
+
preparseEnabled,
|
|
2828
|
+
preparseFile,
|
|
2829
|
+
compiledEnabled,
|
|
2830
|
+
compiledFile,
|
|
2831
|
+
dryRun,
|
|
2832
|
+
}) {
|
|
2833
|
+
if (context !== 'angular') {
|
|
2834
|
+
return;
|
|
2835
|
+
}
|
|
2836
|
+
|
|
2837
|
+
if (!registrationFile) {
|
|
2838
|
+
return;
|
|
2839
|
+
}
|
|
2840
|
+
|
|
2841
|
+
const registrationLines = [
|
|
2842
|
+
'// @ts-nocheck',
|
|
2843
|
+
'/* eslint-disable */',
|
|
2844
|
+
'/* This file is auto-generated by rsx build. Do not edit manually. */',
|
|
2845
|
+
'',
|
|
2846
|
+
];
|
|
2847
|
+
|
|
2848
|
+
if (preparseEnabled && preparseFile) {
|
|
2849
|
+
const preparseImport = toImportSpecifier(registrationFile, preparseFile);
|
|
2850
|
+
registrationLines.push(
|
|
2851
|
+
`import { registerRsxAotParsedExpressionCache } from '${preparseImport}';`,
|
|
2852
|
+
);
|
|
2853
|
+
}
|
|
2854
|
+
|
|
2855
|
+
if (compiledEnabled && compiledFile) {
|
|
2856
|
+
const compiledImport = toImportSpecifier(registrationFile, compiledFile);
|
|
2857
|
+
registrationLines.push(
|
|
2858
|
+
`import { registerRsxAotCompiledExpressions } from '${compiledImport}';`,
|
|
2859
|
+
);
|
|
2860
|
+
}
|
|
2861
|
+
|
|
2862
|
+
registrationLines.push('');
|
|
2863
|
+
|
|
2864
|
+
if (preparseEnabled && preparseFile) {
|
|
2865
|
+
registrationLines.push('registerRsxAotParsedExpressionCache();');
|
|
2866
|
+
}
|
|
2867
|
+
if (compiledEnabled && compiledFile) {
|
|
2868
|
+
registrationLines.push('registerRsxAotCompiledExpressions();');
|
|
2869
|
+
}
|
|
2870
|
+
|
|
2871
|
+
registrationLines.push('');
|
|
2872
|
+
|
|
2873
|
+
const registrationContent = `${registrationLines.join('\n')}`;
|
|
2874
|
+
if (dryRun) {
|
|
2875
|
+
logInfo(
|
|
2876
|
+
`[dry-run] generate RS-X Angular AOT registration: ${registrationFile}`,
|
|
2877
|
+
);
|
|
2878
|
+
} else {
|
|
2879
|
+
fs.mkdirSync(path.dirname(registrationFile), { recursive: true });
|
|
2880
|
+
fs.writeFileSync(registrationFile, registrationContent, 'utf8');
|
|
2881
|
+
logOk(
|
|
2882
|
+
`Generated RS-X Angular AOT registration: ${path.relative(projectRoot, registrationFile)}`,
|
|
2883
|
+
);
|
|
2884
|
+
}
|
|
2885
|
+
|
|
2886
|
+
ensureAngularPolyfillsContainsFile({
|
|
2887
|
+
projectRoot,
|
|
2888
|
+
configPath,
|
|
2889
|
+
filePath: registrationFile,
|
|
2890
|
+
dryRun,
|
|
2891
|
+
});
|
|
2892
|
+
}
|
|
2893
|
+
|
|
2894
|
+
function toImportSpecifier(fromFile, toFile) {
|
|
2895
|
+
const fromDir = path.dirname(fromFile);
|
|
2896
|
+
const relativePath = path.relative(fromDir, toFile).replace(/\\/g, '/');
|
|
2897
|
+
const withoutExtension = relativePath.replace(/\.[cm]?[jt]sx?$/u, '');
|
|
2898
|
+
if (withoutExtension.startsWith('./') || withoutExtension.startsWith('../')) {
|
|
2899
|
+
return withoutExtension;
|
|
2900
|
+
}
|
|
2901
|
+
return `./${withoutExtension}`;
|
|
2902
|
+
}
|
|
2903
|
+
|
|
2904
|
+
function ensureAngularPolyfillsContainsFile({
|
|
2905
|
+
projectRoot,
|
|
2906
|
+
configPath,
|
|
2907
|
+
filePath,
|
|
2908
|
+
dryRun,
|
|
2909
|
+
}) {
|
|
2910
|
+
const angularJsonPath = path.join(projectRoot, 'angular.json');
|
|
2911
|
+
if (!fs.existsSync(angularJsonPath)) {
|
|
2912
|
+
logWarn('angular.json not found. Skipping RS-X AOT runtime injection.');
|
|
2913
|
+
return;
|
|
2914
|
+
}
|
|
2915
|
+
|
|
2916
|
+
let angularJson;
|
|
2917
|
+
try {
|
|
2918
|
+
angularJson = JSON.parse(fs.readFileSync(angularJsonPath, 'utf8'));
|
|
2919
|
+
} catch {
|
|
2920
|
+
logWarn(
|
|
2921
|
+
'Failed to parse angular.json. Skipping RS-X AOT runtime injection.',
|
|
2922
|
+
);
|
|
2923
|
+
return;
|
|
2924
|
+
}
|
|
2925
|
+
|
|
2926
|
+
const projects = angularJson.projects ?? {};
|
|
2927
|
+
const entries = Object.entries(projects);
|
|
2928
|
+
if (entries.length === 0) {
|
|
2929
|
+
return;
|
|
2930
|
+
}
|
|
2931
|
+
|
|
2932
|
+
const normalizedConfigPath = path.resolve(configPath);
|
|
2933
|
+
const targetEntries = entries.filter(([, projectConfig]) => {
|
|
2934
|
+
const tsConfigPath = projectConfig?.architect?.build?.options?.tsConfig;
|
|
2935
|
+
if (typeof tsConfigPath !== 'string') {
|
|
2936
|
+
return false;
|
|
2937
|
+
}
|
|
2938
|
+
return path.resolve(projectRoot, tsConfigPath) === normalizedConfigPath;
|
|
2939
|
+
});
|
|
2940
|
+
|
|
2941
|
+
const selectedEntries = targetEntries.length > 0 ? targetEntries : entries;
|
|
2942
|
+
const polyfillsPath = path
|
|
2943
|
+
.relative(projectRoot, filePath)
|
|
2944
|
+
.replace(/\\/g, '/');
|
|
2945
|
+
|
|
2946
|
+
let changed = false;
|
|
2947
|
+
const isRsxAotRegistrationEntry = (entry) =>
|
|
2948
|
+
typeof entry === 'string' &&
|
|
2949
|
+
entry.replace(/\\/g, '/').endsWith('rsx-aot-registration.generated.ts');
|
|
2950
|
+
|
|
2951
|
+
for (const [, projectConfig] of selectedEntries) {
|
|
2952
|
+
const buildOptions = projectConfig?.architect?.build?.options;
|
|
2953
|
+
if (!buildOptions || typeof buildOptions !== 'object') {
|
|
2954
|
+
continue;
|
|
2955
|
+
}
|
|
2956
|
+
|
|
2957
|
+
const currentPolyfills = buildOptions.polyfills;
|
|
2958
|
+
if (typeof currentPolyfills === 'string') {
|
|
2959
|
+
if (currentPolyfills === polyfillsPath) {
|
|
2960
|
+
continue;
|
|
2961
|
+
}
|
|
2962
|
+
if (isRsxAotRegistrationEntry(currentPolyfills)) {
|
|
2963
|
+
buildOptions.polyfills = [polyfillsPath];
|
|
2964
|
+
changed = true;
|
|
2965
|
+
continue;
|
|
2966
|
+
}
|
|
2967
|
+
buildOptions.polyfills = [currentPolyfills, polyfillsPath];
|
|
2968
|
+
changed = true;
|
|
2969
|
+
continue;
|
|
2970
|
+
}
|
|
2971
|
+
|
|
2972
|
+
if (Array.isArray(currentPolyfills)) {
|
|
2973
|
+
const filtered = currentPolyfills.filter(
|
|
2974
|
+
(entry) => !isRsxAotRegistrationEntry(entry),
|
|
2975
|
+
);
|
|
2976
|
+
const hasTarget = filtered.includes(polyfillsPath);
|
|
2977
|
+
const nextPolyfills = hasTarget ? filtered : [...filtered, polyfillsPath];
|
|
2978
|
+
|
|
2979
|
+
if (
|
|
2980
|
+
nextPolyfills.length !== currentPolyfills.length ||
|
|
2981
|
+
nextPolyfills.some((entry, index) => entry !== currentPolyfills[index])
|
|
2982
|
+
) {
|
|
2983
|
+
buildOptions.polyfills = nextPolyfills;
|
|
2984
|
+
changed = true;
|
|
2985
|
+
}
|
|
2986
|
+
continue;
|
|
2987
|
+
}
|
|
2988
|
+
|
|
2989
|
+
buildOptions.polyfills = [polyfillsPath];
|
|
2990
|
+
changed = true;
|
|
2991
|
+
}
|
|
2992
|
+
|
|
2993
|
+
if (!changed) {
|
|
2994
|
+
return;
|
|
2995
|
+
}
|
|
2996
|
+
|
|
2997
|
+
if (dryRun) {
|
|
2998
|
+
logInfo(
|
|
2999
|
+
`[dry-run] update angular.json build.options.polyfills with ${polyfillsPath}`,
|
|
3000
|
+
);
|
|
3001
|
+
return;
|
|
3002
|
+
}
|
|
3003
|
+
|
|
3004
|
+
fs.writeFileSync(
|
|
3005
|
+
angularJsonPath,
|
|
3006
|
+
`${JSON.stringify(angularJson, null, 2)}\n`,
|
|
3007
|
+
);
|
|
3008
|
+
logOk(`Updated angular.json to inject RS-X AOT runtime registration.`);
|
|
3009
|
+
}
|
|
3010
|
+
|
|
3011
|
+
function resolveRsxBuildConfig(projectRoot) {
|
|
3012
|
+
const packageJsonPath = path.join(projectRoot, 'package.json');
|
|
3013
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
3014
|
+
return {};
|
|
3015
|
+
}
|
|
3016
|
+
|
|
3017
|
+
try {
|
|
3018
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
3019
|
+
const rsxConfig = packageJson.rsx ?? {};
|
|
3020
|
+
const buildConfig = rsxConfig.build ?? {};
|
|
3021
|
+
return typeof buildConfig === 'object' && buildConfig ? buildConfig : {};
|
|
3022
|
+
} catch {
|
|
3023
|
+
return {};
|
|
3024
|
+
}
|
|
3025
|
+
}
|
|
3026
|
+
|
|
3027
|
+
function parseBooleanFlag(value, defaultValue) {
|
|
3028
|
+
if (value === undefined) {
|
|
3029
|
+
return defaultValue;
|
|
3030
|
+
}
|
|
3031
|
+
if (value === true) {
|
|
3032
|
+
return true;
|
|
3033
|
+
}
|
|
3034
|
+
if (typeof value !== 'string') {
|
|
3035
|
+
return defaultValue;
|
|
3036
|
+
}
|
|
3037
|
+
|
|
3038
|
+
const normalized = value.trim().toLowerCase();
|
|
3039
|
+
if (
|
|
3040
|
+
normalized === 'true' ||
|
|
3041
|
+
normalized === '1' ||
|
|
3042
|
+
normalized === 'yes' ||
|
|
3043
|
+
normalized === 'on'
|
|
3044
|
+
) {
|
|
3045
|
+
return true;
|
|
3046
|
+
}
|
|
3047
|
+
if (
|
|
3048
|
+
normalized === 'false' ||
|
|
3049
|
+
normalized === '0' ||
|
|
3050
|
+
normalized === 'no' ||
|
|
3051
|
+
normalized === 'off'
|
|
3052
|
+
) {
|
|
3053
|
+
return false;
|
|
3054
|
+
}
|
|
3055
|
+
return defaultValue;
|
|
3056
|
+
}
|
|
3057
|
+
|
|
3058
|
+
function printHelp() {
|
|
3059
|
+
printGeneralHelp();
|
|
3060
|
+
}
|
|
3061
|
+
|
|
3062
|
+
function printGeneralHelp() {
|
|
3063
|
+
console.log(`rsx v${CLI_VERSION}`);
|
|
3064
|
+
console.log('');
|
|
3065
|
+
console.log('Usage:');
|
|
3066
|
+
console.log(' rsx <command> [options]');
|
|
3067
|
+
console.log(' rsx help [command]');
|
|
3068
|
+
console.log('');
|
|
3069
|
+
console.log('Commands:');
|
|
3070
|
+
console.log(' doctor Run environment checks');
|
|
3071
|
+
console.log(' add | -a | -add Interactive expression scaffolder');
|
|
3072
|
+
console.log(' install vscode Install VS Code extension');
|
|
3073
|
+
console.log(' install compiler Install compiler tooling packages');
|
|
3074
|
+
console.log(
|
|
3075
|
+
' setup Install RS-X tooling (or setup framework integration)',
|
|
3076
|
+
);
|
|
3077
|
+
console.log(' init Setup packages and bootstrap wiring');
|
|
3078
|
+
console.log(
|
|
3079
|
+
' project Create RS-X starter project (angular/vuejs/react/nextjs/nodejs)',
|
|
3080
|
+
);
|
|
3081
|
+
console.log(' build Build project with RS-X transform');
|
|
3082
|
+
console.log(
|
|
3083
|
+
' typecheck Type-check project + RS-X semantic checks',
|
|
3084
|
+
);
|
|
3085
|
+
console.log(' version | -v Print CLI version');
|
|
3086
|
+
console.log('');
|
|
3087
|
+
console.log('Help Aliases:');
|
|
3088
|
+
console.log(' rsx -h');
|
|
3089
|
+
console.log(' rsx -help');
|
|
3090
|
+
console.log(' rsx --help');
|
|
3091
|
+
console.log('');
|
|
3092
|
+
console.log('Examples:');
|
|
3093
|
+
console.log(' rsx help init');
|
|
3094
|
+
console.log(' rsx help project');
|
|
3095
|
+
console.log(' rsx install vscode --help');
|
|
3096
|
+
console.log(' rsx add');
|
|
3097
|
+
}
|
|
3098
|
+
|
|
3099
|
+
function printDoctorHelp() {
|
|
3100
|
+
console.log('Usage:');
|
|
3101
|
+
console.log(' rsx doctor');
|
|
3102
|
+
console.log('');
|
|
3103
|
+
console.log('Checks:');
|
|
3104
|
+
console.log(' - Node.js >= 20');
|
|
3105
|
+
console.log(' - VS Code CLI (code)');
|
|
3106
|
+
console.log(' - Package manager (pnpm/npm/yarn/bun)');
|
|
3107
|
+
}
|
|
3108
|
+
|
|
3109
|
+
function printAddHelp() {
|
|
3110
|
+
console.log('Usage:');
|
|
3111
|
+
console.log(' rsx add');
|
|
3112
|
+
console.log(' rsx -a');
|
|
3113
|
+
console.log(' rsx -add');
|
|
3114
|
+
console.log('');
|
|
3115
|
+
console.log('What it does:');
|
|
3116
|
+
console.log(
|
|
3117
|
+
' - Prompts for expression export name (must be valid TS identifier)',
|
|
3118
|
+
);
|
|
3119
|
+
console.log(
|
|
3120
|
+
' - Prompts whether file name should be kebab-case (default: yes)',
|
|
3121
|
+
);
|
|
3122
|
+
console.log(' - Prompts for output directory (relative or absolute)');
|
|
3123
|
+
console.log(' - Prompts whether to reuse an existing model file');
|
|
3124
|
+
console.log(' - Creates <name>.ts and optionally creates <name>.model.ts');
|
|
3125
|
+
console.log(
|
|
3126
|
+
' - Expression file imports selected model and exports rsx expression',
|
|
3127
|
+
);
|
|
3128
|
+
}
|
|
3129
|
+
|
|
3130
|
+
function printInstallHelp(target) {
|
|
3131
|
+
if (target === 'vscode') {
|
|
3132
|
+
console.log('Usage:');
|
|
3133
|
+
console.log(' rsx install vscode [--force] [--local] [--dry-run]');
|
|
3134
|
+
console.log('');
|
|
3135
|
+
console.log('Options:');
|
|
3136
|
+
console.log(' --force Reinstall extension if already installed');
|
|
3137
|
+
console.log(' --local Build/install local VSIX from repo workspace');
|
|
3138
|
+
console.log(' --dry-run Print commands without executing them');
|
|
3139
|
+
return;
|
|
3140
|
+
}
|
|
3141
|
+
|
|
3142
|
+
if (target === 'compiler') {
|
|
3143
|
+
console.log('Usage:');
|
|
3144
|
+
console.log(
|
|
3145
|
+
' rsx install compiler [--pm <pnpm|npm|yarn|bun>] [--dry-run]',
|
|
3146
|
+
);
|
|
3147
|
+
console.log('');
|
|
3148
|
+
console.log('Options:');
|
|
3149
|
+
console.log(' --pm Explicit package manager');
|
|
3150
|
+
console.log(' --dry-run Print commands without executing them');
|
|
3151
|
+
return;
|
|
3152
|
+
}
|
|
3153
|
+
|
|
3154
|
+
console.log('Usage:');
|
|
3155
|
+
console.log(' rsx install vscode [--force] [--local] [--dry-run]');
|
|
3156
|
+
console.log(' rsx install compiler [--pm <pnpm|npm|yarn|bun>] [--dry-run]');
|
|
3157
|
+
}
|
|
3158
|
+
|
|
3159
|
+
function printSetupHelp() {
|
|
3160
|
+
console.log('Usage:');
|
|
3161
|
+
console.log(
|
|
3162
|
+
' rsx setup [--pm <pnpm|npm|yarn|bun>] [--force] [--local] [--dry-run]',
|
|
3163
|
+
);
|
|
3164
|
+
console.log('');
|
|
3165
|
+
console.log('What it does:');
|
|
3166
|
+
console.log(
|
|
3167
|
+
' - Auto-detects framework and applies matching setup flow (react/vuejs/next/angular)',
|
|
3168
|
+
);
|
|
3169
|
+
console.log(' - Installs runtime packages');
|
|
3170
|
+
console.log(' - Installs compiler tooling packages');
|
|
3171
|
+
console.log(' - Installs VS Code extension');
|
|
3172
|
+
console.log(' - Applies framework-specific transform/build integration');
|
|
3173
|
+
console.log('');
|
|
3174
|
+
console.log('Options:');
|
|
3175
|
+
console.log(' --pm Explicit package manager');
|
|
3176
|
+
console.log(' --force Reinstall extension if already installed');
|
|
3177
|
+
console.log(' --local Build/install local VSIX from repo workspace');
|
|
3178
|
+
console.log(' --dry-run Print commands without executing them');
|
|
3179
|
+
}
|
|
3180
|
+
|
|
3181
|
+
function printInitHelp() {
|
|
3182
|
+
console.log('Usage:');
|
|
3183
|
+
console.log(
|
|
3184
|
+
' rsx init [--pm <pnpm|npm|yarn|bun>] [--entry <path>] [--skip-install] [--skip-vscode] [--force] [--local] [--dry-run]',
|
|
3185
|
+
);
|
|
3186
|
+
console.log('');
|
|
3187
|
+
console.log('What it does:');
|
|
3188
|
+
console.log(
|
|
3189
|
+
' - Installs runtime and compiler tooling (unless --skip-install)',
|
|
3190
|
+
);
|
|
3191
|
+
console.log(
|
|
3192
|
+
' - Detects project context and wires RS-X bootstrap in entry file',
|
|
3193
|
+
);
|
|
3194
|
+
console.log(' - Installs VS Code extension (unless --skip-vscode)');
|
|
3195
|
+
console.log('');
|
|
3196
|
+
console.log('Options:');
|
|
3197
|
+
console.log(' --pm Explicit package manager');
|
|
3198
|
+
console.log(' --entry Explicit application entry file');
|
|
3199
|
+
console.log(' --skip-install Skip npm/pnpm/yarn/bun package installation');
|
|
3200
|
+
console.log(' --skip-vscode Skip VS Code extension installation');
|
|
3201
|
+
console.log(' --force Reinstall extension if already installed');
|
|
3202
|
+
console.log(' --local Build/install local VSIX from repo workspace');
|
|
3203
|
+
console.log(' --dry-run Print commands without executing them');
|
|
3204
|
+
}
|
|
3205
|
+
|
|
3206
|
+
function printProjectHelp() {
|
|
3207
|
+
console.log('Usage:');
|
|
3208
|
+
console.log(
|
|
3209
|
+
' rsx project [angular|vuejs|react|nextjs|nodejs] [--name <project-name>] [--pm <pnpm|npm|yarn|bun>] [--template <angular|vuejs|react|nextjs|nodejs>] [--tarballs-dir <path>] [--skip-install] [--skip-vscode] [--dry-run]',
|
|
3210
|
+
);
|
|
3211
|
+
console.log('');
|
|
3212
|
+
console.log('What it does:');
|
|
3213
|
+
console.log(' - Creates a new project folder');
|
|
3214
|
+
console.log(' - Supports templates: angular, vuejs, react, nextjs, nodejs');
|
|
3215
|
+
console.log(' - Scaffolds framework app and wires RS-X bootstrap/setup');
|
|
3216
|
+
console.log(' - Writes package.json with RS-X dependencies');
|
|
3217
|
+
console.log(
|
|
3218
|
+
' - Adds tsconfig + TypeScript plugin config for editor support',
|
|
3219
|
+
);
|
|
3220
|
+
console.log(' - For Angular template: also installs @rs-x/angular');
|
|
3221
|
+
console.log(' - For React/Next templates: also installs @rs-x/react');
|
|
3222
|
+
console.log(' - For Vue template: also installs @rs-x/vue');
|
|
3223
|
+
console.log(' - Installs dependencies (unless --skip-install)');
|
|
3224
|
+
console.log('');
|
|
3225
|
+
console.log('Options:');
|
|
3226
|
+
console.log(' --name Project folder/package name');
|
|
3227
|
+
console.log(
|
|
3228
|
+
' --template Project template (if omitted, asks interactively)',
|
|
3229
|
+
);
|
|
3230
|
+
console.log(' --pm Explicit package manager');
|
|
3231
|
+
console.log(
|
|
3232
|
+
' --tarballs-dir Directory containing local RS-X package tarballs (*.tgz)',
|
|
3233
|
+
);
|
|
3234
|
+
console.log(' (or set RSX_TARBALLS_DIR env var)');
|
|
3235
|
+
console.log(' --skip-install Skip dependency installation');
|
|
3236
|
+
console.log(' --skip-vscode Skip VS Code extension installation');
|
|
3237
|
+
console.log(' --dry-run Print actions without writing files');
|
|
3238
|
+
}
|
|
3239
|
+
|
|
3240
|
+
function printBuildHelp() {
|
|
3241
|
+
console.log('Usage:');
|
|
3242
|
+
console.log(
|
|
3243
|
+
' rsx build [--project <path-to-tsconfig>] [--out-dir <path>] [--prod] [--aot-preparse <true|false>] [--aot-preparse-file <path>] [--aot-compiled <true|false>] [--aot-compiled-file <path>] [--compiled-resolved-evaluator <true|false>] [--dry-run]',
|
|
3244
|
+
);
|
|
3245
|
+
console.log('');
|
|
3246
|
+
console.log('What it does:');
|
|
3247
|
+
console.log(' - Loads your TypeScript project config');
|
|
3248
|
+
console.log(
|
|
3249
|
+
' - Applies RS-X expression cache preload transform during compilation',
|
|
3250
|
+
);
|
|
3251
|
+
console.log(' - Emits JavaScript output to tsconfig outDir (or --out-dir)');
|
|
3252
|
+
console.log('');
|
|
3253
|
+
console.log('Options:');
|
|
3254
|
+
console.log(' --project Path to tsconfig file (default: tsconfig.json)');
|
|
3255
|
+
console.log(' --out-dir Override output directory');
|
|
3256
|
+
console.log(
|
|
3257
|
+
' --prod Production profile (enables configured AOT outputs)',
|
|
3258
|
+
);
|
|
3259
|
+
console.log(
|
|
3260
|
+
' --aot-preparse Generate RS-X preparse cache module (default: true for Angular projects)',
|
|
3261
|
+
);
|
|
3262
|
+
console.log(
|
|
3263
|
+
' --aot-preparse-file Output path for generated preparse cache module',
|
|
3264
|
+
);
|
|
3265
|
+
console.log(
|
|
3266
|
+
' --aot-compiled Generate RS-X compiled cache module (default: false, true in --prod)',
|
|
3267
|
+
);
|
|
3268
|
+
console.log(
|
|
3269
|
+
' --aot-compiled-file Output path for generated compiled cache module',
|
|
3270
|
+
);
|
|
3271
|
+
console.log(
|
|
3272
|
+
' --compiled-resolved-evaluator Include evaluateResolvedDependencies in compiled output',
|
|
3273
|
+
);
|
|
3274
|
+
console.log(' --no-emit Type-check only (skip JavaScript emit)');
|
|
3275
|
+
console.log(' --dry-run Print build plan without emitting');
|
|
3276
|
+
}
|
|
3277
|
+
|
|
3278
|
+
function printTypecheckHelp() {
|
|
3279
|
+
console.log('Usage:');
|
|
3280
|
+
console.log(' rsx typecheck [--project <path-to-tsconfig>] [--dry-run]');
|
|
3281
|
+
console.log('');
|
|
3282
|
+
console.log('What it does:');
|
|
3283
|
+
console.log(' - Loads your TypeScript project config');
|
|
3284
|
+
console.log(' - Fails on TypeScript compile errors');
|
|
3285
|
+
console.log(' - Fails on RS-X expression semantic errors');
|
|
3286
|
+
console.log(' - Does not emit build output');
|
|
3287
|
+
console.log('');
|
|
3288
|
+
console.log('Options:');
|
|
3289
|
+
console.log(' --project Path to tsconfig file (default: tsconfig.json)');
|
|
3290
|
+
console.log(' --dry-run Print typecheck plan without executing emit');
|
|
3291
|
+
}
|
|
3292
|
+
|
|
3293
|
+
function printVersionHelp() {
|
|
3294
|
+
console.log('Usage:');
|
|
3295
|
+
console.log(' rsx version');
|
|
3296
|
+
console.log(' rsx -v');
|
|
3297
|
+
console.log(' rsx -version');
|
|
3298
|
+
console.log(' rsx --version');
|
|
3299
|
+
}
|
|
3300
|
+
|
|
3301
|
+
function isHelpToken(value) {
|
|
3302
|
+
return value === '-h' || value === '--help' || value === '-help';
|
|
3303
|
+
}
|
|
3304
|
+
|
|
3305
|
+
function isVersionToken(value) {
|
|
3306
|
+
return (
|
|
3307
|
+
value === '-v' ||
|
|
3308
|
+
value === '--version' ||
|
|
3309
|
+
value === '-version' ||
|
|
3310
|
+
value === 'version'
|
|
3311
|
+
);
|
|
3312
|
+
}
|
|
3313
|
+
|
|
3314
|
+
function printHelpFor(command, target) {
|
|
3315
|
+
if (
|
|
3316
|
+
!command ||
|
|
3317
|
+
command === 'help' ||
|
|
3318
|
+
command === '--help' ||
|
|
3319
|
+
command === '-help' ||
|
|
3320
|
+
command === '-h'
|
|
3321
|
+
) {
|
|
3322
|
+
printGeneralHelp();
|
|
3323
|
+
return;
|
|
3324
|
+
}
|
|
3325
|
+
|
|
3326
|
+
if (command === 'doctor') {
|
|
3327
|
+
printDoctorHelp();
|
|
3328
|
+
return;
|
|
3329
|
+
}
|
|
3330
|
+
|
|
3331
|
+
if (command === 'add' || command === '-a' || command === '-add') {
|
|
3332
|
+
printAddHelp();
|
|
3333
|
+
return;
|
|
3334
|
+
}
|
|
3335
|
+
|
|
3336
|
+
if (command === 'install') {
|
|
3337
|
+
printInstallHelp(target);
|
|
3338
|
+
return;
|
|
3339
|
+
}
|
|
3340
|
+
|
|
3341
|
+
if (command === 'setup') {
|
|
3342
|
+
printSetupHelp();
|
|
3343
|
+
return;
|
|
3344
|
+
}
|
|
3345
|
+
|
|
3346
|
+
if (command === 'init') {
|
|
3347
|
+
printInitHelp();
|
|
3348
|
+
return;
|
|
3349
|
+
}
|
|
3350
|
+
|
|
3351
|
+
if (command === 'project') {
|
|
3352
|
+
printProjectHelp();
|
|
3353
|
+
return;
|
|
3354
|
+
}
|
|
3355
|
+
|
|
3356
|
+
if (command === 'build') {
|
|
3357
|
+
printBuildHelp();
|
|
3358
|
+
return;
|
|
3359
|
+
}
|
|
3360
|
+
|
|
3361
|
+
if (command === 'typecheck') {
|
|
3362
|
+
printTypecheckHelp();
|
|
3363
|
+
return;
|
|
3364
|
+
}
|
|
3365
|
+
|
|
3366
|
+
if (
|
|
3367
|
+
command === 'version' ||
|
|
3368
|
+
command === '-v' ||
|
|
3369
|
+
command === '--version' ||
|
|
3370
|
+
command === '-version'
|
|
3371
|
+
) {
|
|
3372
|
+
printVersionHelp();
|
|
3373
|
+
return;
|
|
3374
|
+
}
|
|
3375
|
+
|
|
3376
|
+
printGeneralHelp();
|
|
3377
|
+
}
|
|
3378
|
+
|
|
3379
|
+
function logInfo(message) {
|
|
3380
|
+
console.log(`[INFO] ${message}`);
|
|
3381
|
+
}
|
|
3382
|
+
|
|
3383
|
+
function logOk(message) {
|
|
3384
|
+
console.log(`[OK] ${message}`);
|
|
3385
|
+
}
|
|
3386
|
+
|
|
3387
|
+
function logWarn(message) {
|
|
3388
|
+
console.warn(`[WARN] ${message}`);
|
|
3389
|
+
}
|
|
3390
|
+
|
|
3391
|
+
function logError(message) {
|
|
3392
|
+
console.error(`[ERROR] ${message}`);
|
|
3393
|
+
}
|
|
3394
|
+
|
|
3395
|
+
function main() {
|
|
3396
|
+
const { positionals, flags } = parseArgs(process.argv);
|
|
3397
|
+
const [command, target, third] = positionals;
|
|
3398
|
+
const wantsVersion = isVersionToken(command) || flags.version === true;
|
|
3399
|
+
const wantsGeneralHelp =
|
|
3400
|
+
!command || command === 'help' || isHelpToken(command);
|
|
3401
|
+
const wantsCommandHelp = flags.help === true;
|
|
3402
|
+
const hasPositionalHelpToken = positionals.some((token, index) => {
|
|
3403
|
+
return index > 0 && isHelpToken(token);
|
|
3404
|
+
});
|
|
3405
|
+
const resolvedHelpTarget = isHelpToken(target) ? third : target;
|
|
3406
|
+
|
|
3407
|
+
if (command === 'help') {
|
|
3408
|
+
printHelpFor(target, positionals[2]);
|
|
3409
|
+
return;
|
|
3410
|
+
}
|
|
3411
|
+
|
|
3412
|
+
if (wantsGeneralHelp) {
|
|
3413
|
+
printHelpFor(command, target);
|
|
3414
|
+
return;
|
|
3415
|
+
}
|
|
3416
|
+
|
|
3417
|
+
if (wantsCommandHelp) {
|
|
3418
|
+
printHelpFor(command, target);
|
|
3419
|
+
return;
|
|
3420
|
+
}
|
|
3421
|
+
|
|
3422
|
+
if (hasPositionalHelpToken) {
|
|
3423
|
+
printHelpFor(command, resolvedHelpTarget);
|
|
3424
|
+
return;
|
|
3425
|
+
}
|
|
3426
|
+
|
|
3427
|
+
if (wantsVersion) {
|
|
3428
|
+
console.log(CLI_VERSION);
|
|
3429
|
+
return;
|
|
3430
|
+
}
|
|
3431
|
+
|
|
3432
|
+
if (command === 'doctor') {
|
|
3433
|
+
runDoctor();
|
|
3434
|
+
return;
|
|
3435
|
+
}
|
|
3436
|
+
|
|
3437
|
+
if (command === 'add' || command === '-a' || command === '-add') {
|
|
3438
|
+
runAdd().catch((error) => {
|
|
3439
|
+
logError(error instanceof Error ? error.message : String(error));
|
|
3440
|
+
process.exit(1);
|
|
3441
|
+
});
|
|
3442
|
+
return;
|
|
3443
|
+
}
|
|
3444
|
+
|
|
3445
|
+
if (command === 'install' && target === 'vscode') {
|
|
3446
|
+
installVsCodeExtension(flags);
|
|
3447
|
+
return;
|
|
3448
|
+
}
|
|
3449
|
+
|
|
3450
|
+
if (command === 'install' && target === 'compiler') {
|
|
3451
|
+
const pm = detectPackageManager(flags.pm);
|
|
3452
|
+
installCompilerPackages(pm, Boolean(flags['dry-run']));
|
|
3453
|
+
return;
|
|
3454
|
+
}
|
|
3455
|
+
|
|
3456
|
+
if (command === 'setup') {
|
|
3457
|
+
if (target) {
|
|
3458
|
+
logError(
|
|
3459
|
+
'Framework argument is not supported for `rsx setup`. The framework is auto-detected.',
|
|
3460
|
+
);
|
|
3461
|
+
logInfo('Use: `rsx setup`');
|
|
3462
|
+
process.exit(1);
|
|
3463
|
+
}
|
|
3464
|
+
runSetupAuto(flags);
|
|
3465
|
+
return;
|
|
3466
|
+
}
|
|
3467
|
+
|
|
3468
|
+
if (command === 'init') {
|
|
3469
|
+
runInit(flags);
|
|
3470
|
+
return;
|
|
3471
|
+
}
|
|
3472
|
+
|
|
3473
|
+
if (command === 'project') {
|
|
3474
|
+
const templateFromTarget = normalizeProjectTemplate(target);
|
|
3475
|
+
const templateFromFlag = normalizeProjectTemplate(flags.template);
|
|
3476
|
+
const nameHint =
|
|
3477
|
+
!templateFromTarget && typeof target === 'string' ? target : third;
|
|
3478
|
+
const effectiveTemplate = templateFromFlag ?? templateFromTarget ?? null;
|
|
3479
|
+
|
|
3480
|
+
(async () => {
|
|
3481
|
+
const chosenTemplate =
|
|
3482
|
+
effectiveTemplate ?? (await promptProjectTemplate());
|
|
3483
|
+
await runProjectWithTemplate(chosenTemplate, {
|
|
3484
|
+
...flags,
|
|
3485
|
+
_nameHint: nameHint,
|
|
3486
|
+
});
|
|
3487
|
+
})().catch((error) => {
|
|
3488
|
+
logError(error instanceof Error ? error.message : String(error));
|
|
3489
|
+
process.exit(1);
|
|
3490
|
+
});
|
|
3491
|
+
return;
|
|
3492
|
+
}
|
|
3493
|
+
|
|
3494
|
+
if (command === 'build') {
|
|
3495
|
+
runBuild(flags);
|
|
3496
|
+
return;
|
|
3497
|
+
}
|
|
3498
|
+
|
|
3499
|
+
if (command === 'typecheck') {
|
|
3500
|
+
runTypecheck(flags);
|
|
3501
|
+
return;
|
|
3502
|
+
}
|
|
3503
|
+
|
|
3504
|
+
logError(`Unknown command: ${positionals.join(' ')}`);
|
|
3505
|
+
printHelp();
|
|
3506
|
+
process.exit(1);
|
|
3507
|
+
}
|
|
3508
|
+
|
|
3509
|
+
main();
|