imxc 0.6.2 → 0.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -5
- package/dist/cli.d.ts +19 -0
- package/dist/cli.js +256 -0
- package/dist/compile.js +28 -22
- package/dist/index.js +3 -82
- package/dist/lowering.js +35 -28
- package/dist/parser.d.ts +1 -0
- package/dist/parser.js +17 -4
- package/dist/templates/custom.js +1 -1
- package/dist/templates/hotreload.js +1 -1
- package/dist/templates/index.js +1 -1
- package/dist/validator.js +37 -17
- package/dist/watch.d.ts +10 -0
- package/dist/watch.js +28 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,23 +5,58 @@ Compiler for IMX. Compiles React-like `.tsx` to native Dear ImGui C++ apps.
|
|
|
5
5
|
## Usage
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
|
|
8
|
+
npx imxc --help
|
|
9
|
+
npx imxc --version
|
|
10
|
+
|
|
9
11
|
npx imxc init myapp
|
|
10
12
|
cd myapp
|
|
11
13
|
cmake -B build
|
|
12
14
|
cmake --build build --config Release
|
|
13
15
|
|
|
14
|
-
# Add to existing CMake project
|
|
15
16
|
npx imxc add
|
|
16
17
|
|
|
17
|
-
# Compile TSX manually
|
|
18
18
|
imxc App.tsx -o build/generated
|
|
19
19
|
|
|
20
|
-
# Watch mode
|
|
21
20
|
imxc watch src -o build/generated
|
|
21
|
+
|
|
22
|
+
npx imxc templates
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Common Commands
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
# Help
|
|
29
|
+
npx imxc --help
|
|
30
|
+
npx imxc init --help
|
|
31
|
+
npx imxc watch --help
|
|
32
|
+
|
|
33
|
+
# Scaffold a project
|
|
34
|
+
npx imxc init myapp --template=minimal
|
|
35
|
+
npx imxc init myapp --template=async,persistence
|
|
36
|
+
|
|
37
|
+
# Add IMX to an existing project
|
|
38
|
+
npx imxc add
|
|
39
|
+
|
|
40
|
+
# Compile TSX manually
|
|
41
|
+
imxc src/App.tsx src/Counter.tsx -o build/generated
|
|
42
|
+
|
|
43
|
+
# Watch and optionally rebuild
|
|
44
|
+
imxc watch src -o build/generated
|
|
45
|
+
imxc watch src -o build/generated --build "cmake --build build"
|
|
46
|
+
|
|
47
|
+
# List built-in templates
|
|
48
|
+
npx imxc templates
|
|
22
49
|
```
|
|
23
50
|
|
|
24
|
-
|
|
51
|
+
## Notes
|
|
52
|
+
|
|
53
|
+
- `imxc watch` prefers `src/App.tsx` as the app entrypoint when present.
|
|
54
|
+
- TypeScript `number` props generate C++ `float`.
|
|
55
|
+
- Declare custom native widgets in `src/imx.d.ts` for type checking.
|
|
56
|
+
|
|
57
|
+
## Features
|
|
58
|
+
|
|
59
|
+
~98 components, 5-prop theme system, custom C++ widgets, canvas drawing, drag-drop.
|
|
25
60
|
|
|
26
61
|
Requires: Node.js, CMake 3.25+, C++20 compiler.
|
|
27
62
|
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { compile } from './compile.js';
|
|
2
|
+
import { addToProject, initProject } from './init.js';
|
|
3
|
+
import { TEMPLATES, promptProjectName, promptTemplateName } from './templates/index.js';
|
|
4
|
+
import { startWatch } from './watch.js';
|
|
5
|
+
type WriteFn = (text: string) => void;
|
|
6
|
+
export interface CliDeps {
|
|
7
|
+
compile: typeof compile;
|
|
8
|
+
initProject: typeof initProject;
|
|
9
|
+
addToProject: typeof addToProject;
|
|
10
|
+
startWatch: typeof startWatch;
|
|
11
|
+
promptProjectName: typeof promptProjectName;
|
|
12
|
+
promptTemplateName: typeof promptTemplateName;
|
|
13
|
+
templates: typeof TEMPLATES;
|
|
14
|
+
version: string;
|
|
15
|
+
stdout: WriteFn;
|
|
16
|
+
stderr: WriteFn;
|
|
17
|
+
}
|
|
18
|
+
export declare function runCli(argv: string[], deps?: CliDeps): Promise<number>;
|
|
19
|
+
export {};
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { parseArgs } from 'node:util';
|
|
4
|
+
import { compile } from './compile.js';
|
|
5
|
+
import { addToProject, initProject } from './init.js';
|
|
6
|
+
import { TEMPLATES, promptProjectName, promptTemplateName } from './templates/index.js';
|
|
7
|
+
import { startWatch } from './watch.js';
|
|
8
|
+
const defaultDeps = {
|
|
9
|
+
compile,
|
|
10
|
+
initProject,
|
|
11
|
+
addToProject,
|
|
12
|
+
startWatch,
|
|
13
|
+
promptProjectName,
|
|
14
|
+
promptTemplateName,
|
|
15
|
+
templates: TEMPLATES,
|
|
16
|
+
version: JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf-8')).version,
|
|
17
|
+
stdout: text => process.stdout.write(text),
|
|
18
|
+
stderr: text => process.stderr.write(text),
|
|
19
|
+
};
|
|
20
|
+
function isHelpFlag(arg) {
|
|
21
|
+
return arg === '--help' || arg === '-h';
|
|
22
|
+
}
|
|
23
|
+
function isVersionFlag(arg) {
|
|
24
|
+
return arg === '--version' || arg === '-v';
|
|
25
|
+
}
|
|
26
|
+
function templateList(templates) {
|
|
27
|
+
return templates.map(t => ` ${t.name} - ${t.description}`).join('\n');
|
|
28
|
+
}
|
|
29
|
+
function mainHelp(templates) {
|
|
30
|
+
return [
|
|
31
|
+
'imxc - compile IMX .tsx into native Dear ImGui C++.',
|
|
32
|
+
'',
|
|
33
|
+
'Usage:',
|
|
34
|
+
' imxc <input.tsx ...> -o <output-dir>',
|
|
35
|
+
' imxc init [project-dir] [--template=<name[,name,...]>]',
|
|
36
|
+
' imxc add [project-dir]',
|
|
37
|
+
' imxc watch <dir> -o <output-dir> [--build <cmd>]',
|
|
38
|
+
' imxc templates',
|
|
39
|
+
' imxc help [command]',
|
|
40
|
+
' imxc --version',
|
|
41
|
+
'',
|
|
42
|
+
'Notes:',
|
|
43
|
+
' Root component in explicit builds is the first .tsx input you pass.',
|
|
44
|
+
' Watch mode chooses the root in this order: src/App.tsx, App.tsx, then first file alphabetically.',
|
|
45
|
+
' Declare native widgets in src/imx.d.ts to enable typing and suppress unknown-component warnings.',
|
|
46
|
+
'',
|
|
47
|
+
'Templates:',
|
|
48
|
+
templateList(templates),
|
|
49
|
+
'',
|
|
50
|
+
'First project:',
|
|
51
|
+
' npx imxc init myapp --template=minimal',
|
|
52
|
+
' cd myapp',
|
|
53
|
+
' cmake -B build',
|
|
54
|
+
' cmake --build build',
|
|
55
|
+
'',
|
|
56
|
+
].join('\n');
|
|
57
|
+
}
|
|
58
|
+
function initHelp(templates) {
|
|
59
|
+
return [
|
|
60
|
+
'Usage: imxc init [project-dir] [--template=<name[,name,...]>]',
|
|
61
|
+
'',
|
|
62
|
+
'Creates a new IMX project scaffold.',
|
|
63
|
+
'If no project dir is provided, imxc prompts for one.',
|
|
64
|
+
'If no template is provided, imxc opens the interactive template selector.',
|
|
65
|
+
'',
|
|
66
|
+
'Available templates:',
|
|
67
|
+
templateList(templates),
|
|
68
|
+
'',
|
|
69
|
+
'Examples:',
|
|
70
|
+
' imxc init myapp',
|
|
71
|
+
' imxc init myapp --template=minimal',
|
|
72
|
+
' imxc init myapp --template=async,persistence',
|
|
73
|
+
'',
|
|
74
|
+
].join('\n');
|
|
75
|
+
}
|
|
76
|
+
function addHelp() {
|
|
77
|
+
return [
|
|
78
|
+
'Usage: imxc add [project-dir]',
|
|
79
|
+
'',
|
|
80
|
+
'Adds IMX compiler integration to an existing CMake project.',
|
|
81
|
+
'Defaults to the current directory when no project dir is provided.',
|
|
82
|
+
'',
|
|
83
|
+
].join('\n');
|
|
84
|
+
}
|
|
85
|
+
function watchHelp() {
|
|
86
|
+
return [
|
|
87
|
+
'Usage: imxc watch <dir> -o <output-dir> [--build <cmd>]',
|
|
88
|
+
'',
|
|
89
|
+
'Watches a directory recursively for .tsx changes and recompiles on change.',
|
|
90
|
+
'Root selection order is: src/App.tsx, App.tsx, then first file alphabetically.',
|
|
91
|
+
'If --build is provided, imxc runs the build command after each successful compile.',
|
|
92
|
+
'',
|
|
93
|
+
'Examples:',
|
|
94
|
+
' imxc watch src -o build/generated',
|
|
95
|
+
' imxc watch src -o build/generated --build "cmake --build build"',
|
|
96
|
+
'',
|
|
97
|
+
].join('\n');
|
|
98
|
+
}
|
|
99
|
+
function templatesHelp(templates) {
|
|
100
|
+
return [
|
|
101
|
+
'Usage: imxc templates',
|
|
102
|
+
'',
|
|
103
|
+
'Lists all built-in project templates.',
|
|
104
|
+
'',
|
|
105
|
+
'Templates:',
|
|
106
|
+
templateList(templates),
|
|
107
|
+
'',
|
|
108
|
+
].join('\n');
|
|
109
|
+
}
|
|
110
|
+
function helpText(command, templates) {
|
|
111
|
+
switch (command) {
|
|
112
|
+
case 'init': return initHelp(templates);
|
|
113
|
+
case 'add': return addHelp();
|
|
114
|
+
case 'watch': return watchHelp();
|
|
115
|
+
case 'templates': return templatesHelp(templates);
|
|
116
|
+
default: return mainHelp(templates);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function formatParseError(error) {
|
|
120
|
+
if (error && typeof error === 'object' && 'message' in error && typeof error.message === 'string') {
|
|
121
|
+
return error.message;
|
|
122
|
+
}
|
|
123
|
+
return 'Invalid command line arguments.';
|
|
124
|
+
}
|
|
125
|
+
function asOptionalString(value) {
|
|
126
|
+
return typeof value === 'string' ? value : undefined;
|
|
127
|
+
}
|
|
128
|
+
function safeParseArgs(config, deps, help) {
|
|
129
|
+
try {
|
|
130
|
+
return parseArgs(config);
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
deps.stderr(`imxc: ${formatParseError(error)}\n\n${help}`);
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
export async function runCli(argv, deps = defaultDeps) {
|
|
138
|
+
const [command, ...rest] = argv;
|
|
139
|
+
if (!command) {
|
|
140
|
+
deps.stdout(mainHelp(deps.templates));
|
|
141
|
+
return 0;
|
|
142
|
+
}
|
|
143
|
+
if (isHelpFlag(command)) {
|
|
144
|
+
deps.stdout(mainHelp(deps.templates));
|
|
145
|
+
return 0;
|
|
146
|
+
}
|
|
147
|
+
if (isVersionFlag(command)) {
|
|
148
|
+
deps.stdout(`${deps.version}\n`);
|
|
149
|
+
return 0;
|
|
150
|
+
}
|
|
151
|
+
if (command === 'help') {
|
|
152
|
+
deps.stdout(helpText(rest[0], deps.templates));
|
|
153
|
+
return 0;
|
|
154
|
+
}
|
|
155
|
+
if (command === 'templates') {
|
|
156
|
+
if (isHelpFlag(rest[0])) {
|
|
157
|
+
deps.stdout(templatesHelp(deps.templates));
|
|
158
|
+
}
|
|
159
|
+
else if (rest.length > 0) {
|
|
160
|
+
deps.stderr(`imxc: templates does not accept additional arguments.\n\n${templatesHelp(deps.templates)}`);
|
|
161
|
+
return 1;
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
deps.stdout(`Available templates:\n\n${templateList(deps.templates)}\n\nUse "imxc init <project-dir> --template=<name[,name,...]>" to scaffold one.\n`);
|
|
165
|
+
}
|
|
166
|
+
return 0;
|
|
167
|
+
}
|
|
168
|
+
if (command === 'init') {
|
|
169
|
+
if (isHelpFlag(rest[0])) {
|
|
170
|
+
deps.stdout(initHelp(deps.templates));
|
|
171
|
+
return 0;
|
|
172
|
+
}
|
|
173
|
+
const parsed = safeParseArgs({
|
|
174
|
+
args: rest,
|
|
175
|
+
allowPositionals: true,
|
|
176
|
+
options: { template: { type: 'string', short: 't' } },
|
|
177
|
+
}, deps, initHelp(deps.templates));
|
|
178
|
+
if (!parsed)
|
|
179
|
+
return 1;
|
|
180
|
+
let dir = parsed.positionals[0];
|
|
181
|
+
if (!dir) {
|
|
182
|
+
dir = await deps.promptProjectName();
|
|
183
|
+
}
|
|
184
|
+
const absDir = path.resolve(dir);
|
|
185
|
+
const templateName = asOptionalString(parsed.values.template) ?? await deps.promptTemplateName();
|
|
186
|
+
deps.initProject(absDir, path.basename(absDir), templateName);
|
|
187
|
+
return 0;
|
|
188
|
+
}
|
|
189
|
+
if (command === 'add') {
|
|
190
|
+
if (isHelpFlag(rest[0])) {
|
|
191
|
+
deps.stdout(addHelp());
|
|
192
|
+
return 0;
|
|
193
|
+
}
|
|
194
|
+
const parsed = safeParseArgs({
|
|
195
|
+
args: rest,
|
|
196
|
+
allowPositionals: true,
|
|
197
|
+
options: {},
|
|
198
|
+
}, deps, addHelp());
|
|
199
|
+
if (!parsed)
|
|
200
|
+
return 1;
|
|
201
|
+
const dir = parsed.positionals[0] ?? '.';
|
|
202
|
+
deps.addToProject(path.resolve(dir));
|
|
203
|
+
return 0;
|
|
204
|
+
}
|
|
205
|
+
if (command === 'watch') {
|
|
206
|
+
if (isHelpFlag(rest[0])) {
|
|
207
|
+
deps.stdout(watchHelp());
|
|
208
|
+
return 0;
|
|
209
|
+
}
|
|
210
|
+
if (!rest[0]) {
|
|
211
|
+
deps.stderr(`imxc: missing watch directory.\n\n${watchHelp()}`);
|
|
212
|
+
return 1;
|
|
213
|
+
}
|
|
214
|
+
const watchDir = rest[0];
|
|
215
|
+
const parsed = safeParseArgs({
|
|
216
|
+
args: rest.slice(1),
|
|
217
|
+
allowPositionals: false,
|
|
218
|
+
options: {
|
|
219
|
+
output: { type: 'string', short: 'o' },
|
|
220
|
+
build: { type: 'string', short: 'b' },
|
|
221
|
+
},
|
|
222
|
+
}, deps, watchHelp());
|
|
223
|
+
if (!parsed)
|
|
224
|
+
return 1;
|
|
225
|
+
const outputDir = asOptionalString(parsed.values.output) ?? '.';
|
|
226
|
+
const buildCmd = asOptionalString(parsed.values.build);
|
|
227
|
+
deps.startWatch(path.resolve(watchDir), path.resolve(outputDir), buildCmd);
|
|
228
|
+
return 0;
|
|
229
|
+
}
|
|
230
|
+
if (command.startsWith('-')) {
|
|
231
|
+
deps.stderr(`imxc: unknown option '${command}'.\n\n${mainHelp(deps.templates)}`);
|
|
232
|
+
return 1;
|
|
233
|
+
}
|
|
234
|
+
const parsed = safeParseArgs({
|
|
235
|
+
args: argv,
|
|
236
|
+
allowPositionals: true,
|
|
237
|
+
options: { output: { type: 'string', short: 'o' } },
|
|
238
|
+
}, deps, mainHelp(deps.templates));
|
|
239
|
+
if (!parsed)
|
|
240
|
+
return 1;
|
|
241
|
+
if (parsed.positionals.length === 0) {
|
|
242
|
+
deps.stderr(`imxc: no input .tsx files provided.\n\n${mainHelp(deps.templates)}`);
|
|
243
|
+
return 1;
|
|
244
|
+
}
|
|
245
|
+
const outputDir = asOptionalString(parsed.values.output) ?? '.';
|
|
246
|
+
const result = deps.compile(parsed.positionals, outputDir);
|
|
247
|
+
if (result.warnings.length > 0) {
|
|
248
|
+
result.warnings.forEach(w => deps.stderr(w + '\n'));
|
|
249
|
+
}
|
|
250
|
+
if (!result.success) {
|
|
251
|
+
result.errors.forEach(e => deps.stderr(e + '\n'));
|
|
252
|
+
return 1;
|
|
253
|
+
}
|
|
254
|
+
deps.stdout(`imxc: ${result.componentCount} component(s) compiled successfully.\n`);
|
|
255
|
+
return 0;
|
|
256
|
+
}
|
package/dist/compile.js
CHANGED
|
@@ -49,7 +49,7 @@ export function compile(files, outputDir) {
|
|
|
49
49
|
const imports = extractImports(parsed.sourceFile);
|
|
50
50
|
compiled.push({
|
|
51
51
|
name: ir.name,
|
|
52
|
-
sourceFile:
|
|
52
|
+
sourceFile: parsed.sourceFile.fileName,
|
|
53
53
|
sourcePath: file,
|
|
54
54
|
stateCount: ir.stateSlots.length,
|
|
55
55
|
bufferCount: ir.bufferCount,
|
|
@@ -99,7 +99,7 @@ export function compile(files, outputDir) {
|
|
|
99
99
|
}
|
|
100
100
|
const sharedPropsType = compiled.find(c => c.ir.namedPropsType)?.ir.namedPropsType;
|
|
101
101
|
// Resolve actual C++ types for bound props by tracing through parent interfaces.
|
|
102
|
-
// When a child declares `speed: number
|
|
102
|
+
// When a child declares `speed: number`, but the parent struct has `float speed`,
|
|
103
103
|
// the bound prop pointer must use the parent's actual type.
|
|
104
104
|
const resolvedBoundPropTypes = new Map();
|
|
105
105
|
for (const comp of compiled) {
|
|
@@ -478,7 +478,7 @@ function extractNestedPropAccess(expr, bound) {
|
|
|
478
478
|
}
|
|
479
479
|
/**
|
|
480
480
|
* Walk IR nodes in a parent component, resolving actual C++ types for child bound props.
|
|
481
|
-
* When a child has `speed: number`
|
|
481
|
+
* When a child has `speed: number` but the parent passes props.speed from a struct
|
|
482
482
|
* where speed is float, the resolved type overrides the child's inferred type.
|
|
483
483
|
*/
|
|
484
484
|
function resolveChildBoundTypes(nodes, parentFieldTypes, componentMap, extIfaces, result) {
|
|
@@ -489,25 +489,7 @@ function resolveChildBoundTypes(nodes, parentFieldTypes, componentMap, extIfaces
|
|
|
489
489
|
for (const [propName, valueExpr] of Object.entries(node.props)) {
|
|
490
490
|
if (!child.boundProps.has(propName) || !valueExpr.startsWith('props.'))
|
|
491
491
|
continue;
|
|
492
|
-
const
|
|
493
|
-
const topField = parts[0].split('[')[0];
|
|
494
|
-
const topType = parentFieldTypes.get(topField);
|
|
495
|
-
if (!topType || topType === 'callback')
|
|
496
|
-
continue;
|
|
497
|
-
let resolvedType;
|
|
498
|
-
if (parts.length === 1) {
|
|
499
|
-
// Direct scalar: props.speed → parent's type for speed
|
|
500
|
-
resolvedType = topType;
|
|
501
|
-
}
|
|
502
|
-
else if (parts.length === 2) {
|
|
503
|
-
// Nested: props.data.speed → look up speed in data's interface
|
|
504
|
-
const iface = extIfaces.get(topType);
|
|
505
|
-
if (iface) {
|
|
506
|
-
const ft = iface.get(parts[1].split('[')[0]);
|
|
507
|
-
if (ft && ft !== 'callback')
|
|
508
|
-
resolvedType = ft;
|
|
509
|
-
}
|
|
510
|
-
}
|
|
492
|
+
const resolvedType = resolveBoundPropType(valueExpr, parentFieldTypes, extIfaces);
|
|
511
493
|
if (resolvedType) {
|
|
512
494
|
if (!result.has(node.name))
|
|
513
495
|
result.set(node.name, new Map());
|
|
@@ -526,6 +508,30 @@ function resolveChildBoundTypes(nodes, parentFieldTypes, componentMap, extIfaces
|
|
|
526
508
|
}
|
|
527
509
|
}
|
|
528
510
|
}
|
|
511
|
+
function resolveBoundPropType(valueExpr, rootFieldTypes, extIfaces) {
|
|
512
|
+
if (!valueExpr.startsWith('props.'))
|
|
513
|
+
return undefined;
|
|
514
|
+
const parts = valueExpr
|
|
515
|
+
.slice(6)
|
|
516
|
+
.split('.')
|
|
517
|
+
.map(part => part.split('[')[0])
|
|
518
|
+
.filter(Boolean);
|
|
519
|
+
if (parts.length === 0)
|
|
520
|
+
return undefined;
|
|
521
|
+
let currentType = rootFieldTypes.get(parts[0]);
|
|
522
|
+
if (!currentType || currentType === 'callback')
|
|
523
|
+
return undefined;
|
|
524
|
+
for (let i = 1; i < parts.length; i++) {
|
|
525
|
+
const iface = extIfaces.get(currentType);
|
|
526
|
+
if (!iface)
|
|
527
|
+
return undefined;
|
|
528
|
+
const nextType = iface.get(parts[i]);
|
|
529
|
+
if (!nextType || nextType === 'callback')
|
|
530
|
+
return undefined;
|
|
531
|
+
currentType = nextType;
|
|
532
|
+
}
|
|
533
|
+
return currentType;
|
|
534
|
+
}
|
|
529
535
|
/**
|
|
530
536
|
* Parse the imx.d.ts in the given directory (if present) and extract
|
|
531
537
|
* all interface declarations as a map from interface name -> field name -> type.
|
package/dist/index.js
CHANGED
|
@@ -1,83 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
import { compile } from './compile.js';
|
|
6
|
-
import { startWatch } from './watch.js';
|
|
7
|
-
import { promptProjectName, promptTemplateName, TEMPLATES } from './templates/index.js';
|
|
8
|
-
// Handle `imxc templates` — list available templates
|
|
9
|
-
if (process.argv[2] === 'templates') {
|
|
10
|
-
console.log('Available templates:\n');
|
|
11
|
-
TEMPLATES.forEach(t => {
|
|
12
|
-
console.log(` ${t.name} — ${t.description}`);
|
|
13
|
-
});
|
|
14
|
-
console.log('\nUsage: imxc init <project-name> --template=<name>');
|
|
15
|
-
console.log('Combine: imxc init <project-name> --template=async,persistence');
|
|
16
|
-
process.exit(0);
|
|
17
|
-
}
|
|
18
|
-
// Handle `imxc init [dir] [--template=<name>]` subcommand
|
|
19
|
-
if (process.argv[2] === 'init') {
|
|
20
|
-
const { values, positionals } = parseArgs({
|
|
21
|
-
args: process.argv.slice(3),
|
|
22
|
-
allowPositionals: true,
|
|
23
|
-
options: { template: { type: 'string', short: 't' } },
|
|
24
|
-
});
|
|
25
|
-
let dir = positionals[0];
|
|
26
|
-
if (!dir) {
|
|
27
|
-
dir = await promptProjectName();
|
|
28
|
-
}
|
|
29
|
-
const absDir = path.resolve(dir);
|
|
30
|
-
const templateName = values.template ?? await promptTemplateName();
|
|
31
|
-
initProject(absDir, path.basename(absDir), templateName);
|
|
32
|
-
process.exit(0);
|
|
33
|
-
}
|
|
34
|
-
// Handle `imxc add [dir]` subcommand
|
|
35
|
-
if (process.argv[2] === 'add') {
|
|
36
|
-
const dir = process.argv[3] ?? '.';
|
|
37
|
-
const absDir = path.resolve(dir);
|
|
38
|
-
addToProject(absDir);
|
|
39
|
-
process.exit(0);
|
|
40
|
-
}
|
|
41
|
-
// Handle `imxc watch <dir> -o <output-dir>` subcommand
|
|
42
|
-
if (process.argv[2] === 'watch') {
|
|
43
|
-
const watchDir = process.argv[3];
|
|
44
|
-
if (!watchDir) {
|
|
45
|
-
console.error('Usage: imxc watch <dir> -o <output-dir>');
|
|
46
|
-
process.exit(1);
|
|
47
|
-
}
|
|
48
|
-
const { values } = parseArgs({
|
|
49
|
-
args: process.argv.slice(4),
|
|
50
|
-
allowPositionals: false,
|
|
51
|
-
options: {
|
|
52
|
-
output: { type: 'string', short: 'o' },
|
|
53
|
-
build: { type: 'string', short: 'b' },
|
|
54
|
-
},
|
|
55
|
-
});
|
|
56
|
-
const outputDir = values.output ?? '.';
|
|
57
|
-
startWatch(path.resolve(watchDir), path.resolve(outputDir), values.build);
|
|
58
|
-
}
|
|
59
|
-
else {
|
|
60
|
-
// Default: build command
|
|
61
|
-
const { values, positionals } = parseArgs({
|
|
62
|
-
allowPositionals: true,
|
|
63
|
-
options: { output: { type: 'string', short: 'o' } },
|
|
64
|
-
});
|
|
65
|
-
if (positionals.length === 0) {
|
|
66
|
-
console.error('Usage: imxc <input.tsx ...> -o <output-dir>');
|
|
67
|
-
console.error(' imxc init [project-dir] [--template=<name[,name,...]>]');
|
|
68
|
-
console.error(' imxc templates');
|
|
69
|
-
console.error(' imxc add [project-dir]');
|
|
70
|
-
console.error(' imxc watch <dir> -o <output-dir> [--build <cmd>]');
|
|
71
|
-
process.exit(1);
|
|
72
|
-
}
|
|
73
|
-
const outputDir = values.output ?? '.';
|
|
74
|
-
const result = compile(positionals, outputDir);
|
|
75
|
-
if (result.warnings.length > 0) {
|
|
76
|
-
result.warnings.forEach(w => console.warn(w));
|
|
77
|
-
}
|
|
78
|
-
if (!result.success) {
|
|
79
|
-
result.errors.forEach(e => console.error(e));
|
|
80
|
-
process.exit(1);
|
|
81
|
-
}
|
|
82
|
-
console.log(`imxc: ${result.componentCount} component(s) compiled successfully.`);
|
|
83
|
-
}
|
|
2
|
+
import { runCli } from './cli.js';
|
|
3
|
+
const exitCode = await runCli(process.argv.slice(2));
|
|
4
|
+
process.exit(exitCode);
|
package/dist/lowering.js
CHANGED
|
@@ -90,7 +90,7 @@ export function lowerComponent(parsed, validation, externalInterfaces) {
|
|
|
90
90
|
function normalizePropTypeText(typeText) {
|
|
91
91
|
const trimmed = typeText.trim().replace(/\s*\|\s*undefined$/, '');
|
|
92
92
|
if (trimmed === 'number')
|
|
93
|
-
return '
|
|
93
|
+
return 'float';
|
|
94
94
|
if (trimmed === 'boolean')
|
|
95
95
|
return 'bool';
|
|
96
96
|
if (trimmed === 'string')
|
|
@@ -1215,33 +1215,9 @@ function inferExprType(expr, ctx) {
|
|
|
1215
1215
|
// .length maps to .size() in C++ — always int
|
|
1216
1216
|
if (prop === 'length')
|
|
1217
1217
|
return 'int';
|
|
1218
|
-
|
|
1219
|
-
if (
|
|
1220
|
-
|
|
1221
|
-
if (ft && ft !== 'callback') {
|
|
1222
|
-
if (ft === 'int' || ft === 'float' || ft === 'bool' || ft === 'string' || ft === 'color' || ft === 'int_array') {
|
|
1223
|
-
return ft;
|
|
1224
|
-
}
|
|
1225
|
-
return 'string';
|
|
1226
|
-
}
|
|
1227
|
-
}
|
|
1228
|
-
// Nested access: props.data.field — resolve through external interfaces
|
|
1229
|
-
if (ctx.propsParam && ctx.externalInterfaces && ts.isPropertyAccessExpression(expr.expression)) {
|
|
1230
|
-
const mid = expr.expression;
|
|
1231
|
-
if (ts.isIdentifier(mid.expression) && mid.expression.text === ctx.propsParam) {
|
|
1232
|
-
const midType = ctx.propsFieldTypes.get(mid.name.text);
|
|
1233
|
-
if (midType && midType !== 'callback') {
|
|
1234
|
-
const iface = ctx.externalInterfaces.get(midType);
|
|
1235
|
-
if (iface) {
|
|
1236
|
-
const fieldType = iface.get(prop);
|
|
1237
|
-
if (fieldType && fieldType !== 'callback') {
|
|
1238
|
-
if (fieldType === 'int' || fieldType === 'float' || fieldType === 'bool' || fieldType === 'string' || fieldType === 'color' || fieldType === 'int_array') {
|
|
1239
|
-
return fieldType;
|
|
1240
|
-
}
|
|
1241
|
-
}
|
|
1242
|
-
}
|
|
1243
|
-
}
|
|
1244
|
-
}
|
|
1218
|
+
const resolvedType = resolvePropertyAccessType(expr, ctx);
|
|
1219
|
+
if (resolvedType) {
|
|
1220
|
+
return resolvedType;
|
|
1245
1221
|
}
|
|
1246
1222
|
return 'string';
|
|
1247
1223
|
}
|
|
@@ -1275,6 +1251,37 @@ function inferExprType(expr, ctx) {
|
|
|
1275
1251
|
}
|
|
1276
1252
|
return 'int'; // default
|
|
1277
1253
|
}
|
|
1254
|
+
function resolvePropertyAccessType(expr, ctx) {
|
|
1255
|
+
if (!ctx.propsParam)
|
|
1256
|
+
return null;
|
|
1257
|
+
const parts = [];
|
|
1258
|
+
let current = expr;
|
|
1259
|
+
while (ts.isPropertyAccessExpression(current)) {
|
|
1260
|
+
parts.unshift(current.name.text);
|
|
1261
|
+
current = current.expression;
|
|
1262
|
+
}
|
|
1263
|
+
if (!ts.isIdentifier(current) || current.text !== ctx.propsParam || parts.length === 0) {
|
|
1264
|
+
return null;
|
|
1265
|
+
}
|
|
1266
|
+
let currentType = ctx.propsFieldTypes.get(parts[0]);
|
|
1267
|
+
if (!currentType || currentType === 'callback')
|
|
1268
|
+
return null;
|
|
1269
|
+
for (let i = 1; i < parts.length; i++) {
|
|
1270
|
+
if (!ctx.externalInterfaces)
|
|
1271
|
+
return null;
|
|
1272
|
+
const iface = ctx.externalInterfaces.get(currentType);
|
|
1273
|
+
if (!iface)
|
|
1274
|
+
return null;
|
|
1275
|
+
const nextType = iface.get(parts[i]);
|
|
1276
|
+
if (!nextType || nextType === 'callback')
|
|
1277
|
+
return null;
|
|
1278
|
+
currentType = nextType;
|
|
1279
|
+
}
|
|
1280
|
+
if (currentType === 'int' || currentType === 'float' || currentType === 'bool' || currentType === 'string' || currentType === 'color' || currentType === 'int_array') {
|
|
1281
|
+
return currentType;
|
|
1282
|
+
}
|
|
1283
|
+
return 'string';
|
|
1284
|
+
}
|
|
1278
1285
|
function lowerListMap(node, body, ctx, loc) {
|
|
1279
1286
|
const propAccess = node.expression;
|
|
1280
1287
|
const array = exprToCpp(propAccess.expression, ctx);
|
package/dist/parser.d.ts
CHANGED
|
@@ -12,5 +12,6 @@ export interface ParseError {
|
|
|
12
12
|
message: string;
|
|
13
13
|
severity?: 'error' | 'warning';
|
|
14
14
|
}
|
|
15
|
+
export declare function normalizeDisplayPath(filePath: string): string;
|
|
15
16
|
export declare function parseFile(filePath: string, source: string): ParsedFile;
|
|
16
17
|
export declare function extractImports(sourceFile: ts.SourceFile): Map<string, string>;
|
package/dist/parser.js
CHANGED
|
@@ -4,9 +4,19 @@ function formatError(sourceFile, node, message) {
|
|
|
4
4
|
const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
|
|
5
5
|
return { file: sourceFile.fileName, line: line + 1, col: character + 1, message };
|
|
6
6
|
}
|
|
7
|
+
export function normalizeDisplayPath(filePath) {
|
|
8
|
+
const absolutePath = path.resolve(filePath);
|
|
9
|
+
const relativePath = path.relative(process.cwd(), absolutePath);
|
|
10
|
+
const displayPath = relativePath &&
|
|
11
|
+
!relativePath.startsWith('..') &&
|
|
12
|
+
!path.isAbsolute(relativePath)
|
|
13
|
+
? relativePath
|
|
14
|
+
: absolutePath;
|
|
15
|
+
return displayPath.replace(/\\/g, '/');
|
|
16
|
+
}
|
|
7
17
|
export function parseFile(filePath, source) {
|
|
8
|
-
const
|
|
9
|
-
const sourceFile = ts.createSourceFile(
|
|
18
|
+
const displayPath = normalizeDisplayPath(filePath);
|
|
19
|
+
const sourceFile = ts.createSourceFile(displayPath, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
|
|
10
20
|
const errors = [];
|
|
11
21
|
let component = null;
|
|
12
22
|
for (const stmt of sourceFile.statements) {
|
|
@@ -18,6 +28,9 @@ export function parseFile(filePath, source) {
|
|
|
18
28
|
component = stmt;
|
|
19
29
|
}
|
|
20
30
|
}
|
|
31
|
+
else if (ts.isInterfaceDeclaration(stmt)) {
|
|
32
|
+
// allowed
|
|
33
|
+
}
|
|
21
34
|
else if (ts.isImportDeclaration(stmt)) {
|
|
22
35
|
// allowed
|
|
23
36
|
}
|
|
@@ -26,9 +39,9 @@ export function parseFile(filePath, source) {
|
|
|
26
39
|
}
|
|
27
40
|
}
|
|
28
41
|
if (!component && errors.length === 0) {
|
|
29
|
-
errors.push({ file:
|
|
42
|
+
errors.push({ file: displayPath, line: 1, col: 1, message: 'No component function found in file' });
|
|
30
43
|
}
|
|
31
|
-
return { sourceFile, filePath, component, errors };
|
|
44
|
+
return { sourceFile, filePath: path.resolve(filePath), component, errors };
|
|
32
45
|
}
|
|
33
46
|
export function extractImports(sourceFile) {
|
|
34
47
|
const imports = new Map();
|
package/dist/templates/custom.js
CHANGED
package/dist/templates/index.js
CHANGED
package/dist/validator.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
1
3
|
import ts from 'typescript';
|
|
2
4
|
import { HOST_COMPONENTS, isHostComponent } from './components.js';
|
|
3
5
|
import { extractImports } from './parser.js';
|
|
@@ -15,6 +17,7 @@ export function validate(parsed) {
|
|
|
15
17
|
const sf = parsed.sourceFile;
|
|
16
18
|
const func = parsed.component;
|
|
17
19
|
const customComponents = extractImports(sf);
|
|
20
|
+
const nativeWidgets = loadDeclaredNativeWidgets(path.dirname(parsed.filePath));
|
|
18
21
|
const useStateCalls = [];
|
|
19
22
|
if (!func || !func.body)
|
|
20
23
|
return { errors, warnings, customComponents, useStateCalls };
|
|
@@ -34,7 +37,7 @@ export function validate(parsed) {
|
|
|
34
37
|
}
|
|
35
38
|
const returnStmt = func.body.statements.find(ts.isReturnStatement);
|
|
36
39
|
if (returnStmt && returnStmt.expression) {
|
|
37
|
-
validateExpression(returnStmt.expression, sf, customComponents, errors, warnings);
|
|
40
|
+
validateExpression(returnStmt.expression, sf, customComponents, nativeWidgets, errors, warnings);
|
|
38
41
|
}
|
|
39
42
|
return { errors, warnings, customComponents, useStateCalls };
|
|
40
43
|
}
|
|
@@ -70,30 +73,30 @@ function extractUseState(decl, index, sf, errors) {
|
|
|
70
73
|
}
|
|
71
74
|
return { name: nameEl.name.text, setter: setterEl.name.text, initializer: call.arguments[0], index };
|
|
72
75
|
}
|
|
73
|
-
function validateExpression(node, sf, customComponents, errors, warnings) {
|
|
76
|
+
function validateExpression(node, sf, customComponents, nativeWidgets, errors, warnings) {
|
|
74
77
|
if (ts.isJsxElement(node)) {
|
|
75
|
-
validateJsxElement(node, sf, customComponents, errors, warnings);
|
|
78
|
+
validateJsxElement(node, sf, customComponents, nativeWidgets, errors, warnings);
|
|
76
79
|
}
|
|
77
80
|
else if (ts.isJsxSelfClosingElement(node)) {
|
|
78
|
-
validateJsxTag(node.tagName, node, sf, customComponents, warnings);
|
|
81
|
+
validateJsxTag(node.tagName, node, sf, customComponents, nativeWidgets, warnings);
|
|
79
82
|
validateJsxAttributes(node.attributes, node.tagName, sf, errors);
|
|
80
83
|
}
|
|
81
84
|
else if (ts.isJsxFragment(node)) {
|
|
82
85
|
for (const child of node.children)
|
|
83
|
-
validateExpression(child, sf, customComponents, errors, warnings);
|
|
86
|
+
validateExpression(child, sf, customComponents, nativeWidgets, errors, warnings);
|
|
84
87
|
}
|
|
85
88
|
else if (ts.isParenthesizedExpression(node)) {
|
|
86
|
-
validateExpression(node.expression, sf, customComponents, errors, warnings);
|
|
89
|
+
validateExpression(node.expression, sf, customComponents, nativeWidgets, errors, warnings);
|
|
87
90
|
}
|
|
88
91
|
else if (ts.isConditionalExpression(node)) {
|
|
89
|
-
validateExpression(node.whenTrue, sf, customComponents, errors, warnings);
|
|
90
|
-
validateExpression(node.whenFalse, sf, customComponents, errors, warnings);
|
|
92
|
+
validateExpression(node.whenTrue, sf, customComponents, nativeWidgets, errors, warnings);
|
|
93
|
+
validateExpression(node.whenFalse, sf, customComponents, nativeWidgets, errors, warnings);
|
|
91
94
|
}
|
|
92
95
|
else if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) {
|
|
93
|
-
validateExpression(node.right, sf, customComponents, errors, warnings);
|
|
96
|
+
validateExpression(node.right, sf, customComponents, nativeWidgets, errors, warnings);
|
|
94
97
|
}
|
|
95
98
|
else if (ts.isJsxExpression(node) && node.expression) {
|
|
96
|
-
validateExpression(node.expression, sf, customComponents, errors, warnings);
|
|
99
|
+
validateExpression(node.expression, sf, customComponents, nativeWidgets, errors, warnings);
|
|
97
100
|
}
|
|
98
101
|
else if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === 'map') {
|
|
99
102
|
const callback = node.arguments[0];
|
|
@@ -107,7 +110,7 @@ function validateExpression(node, sf, customComponents, errors, warnings) {
|
|
|
107
110
|
mapBody = callback.body;
|
|
108
111
|
}
|
|
109
112
|
if (mapBody) {
|
|
110
|
-
validateExpression(mapBody, sf, customComponents, errors, warnings);
|
|
113
|
+
validateExpression(mapBody, sf, customComponents, nativeWidgets, errors, warnings);
|
|
111
114
|
if (!hasIDWrapper(mapBody)) {
|
|
112
115
|
warnings.push(warn(sf, node, 'Items in .map() should be wrapped in <ID scope={i}> to avoid ImGui ID conflicts'));
|
|
113
116
|
}
|
|
@@ -126,13 +129,13 @@ function hasIDWrapper(expr) {
|
|
|
126
129
|
return true;
|
|
127
130
|
return false;
|
|
128
131
|
}
|
|
129
|
-
function validateJsxElement(node, sf, customComponents, errors, warnings) {
|
|
130
|
-
validateJsxTag(node.openingElement.tagName, node, sf, customComponents, warnings);
|
|
132
|
+
function validateJsxElement(node, sf, customComponents, nativeWidgets, errors, warnings) {
|
|
133
|
+
validateJsxTag(node.openingElement.tagName, node, sf, customComponents, nativeWidgets, warnings);
|
|
131
134
|
validateJsxAttributes(node.openingElement.attributes, node.openingElement.tagName, sf, errors);
|
|
132
135
|
for (const child of node.children)
|
|
133
|
-
validateExpression(child, sf, customComponents, errors, warnings);
|
|
136
|
+
validateExpression(child, sf, customComponents, nativeWidgets, errors, warnings);
|
|
134
137
|
}
|
|
135
|
-
function validateJsxTag(tagName, node, sf, customComponents, warnings) {
|
|
138
|
+
function validateJsxTag(tagName, node, sf, customComponents, nativeWidgets, warnings) {
|
|
136
139
|
if (!ts.isIdentifier(tagName))
|
|
137
140
|
return;
|
|
138
141
|
const name = tagName.text;
|
|
@@ -140,10 +143,10 @@ function validateJsxTag(tagName, node, sf, customComponents, warnings) {
|
|
|
140
143
|
if (name[0] === name[0].toLowerCase())
|
|
141
144
|
return;
|
|
142
145
|
// Known host component or imported custom component — fine
|
|
143
|
-
if (isHostComponent(name) || customComponents.has(name))
|
|
146
|
+
if (isHostComponent(name) || customComponents.has(name) || nativeWidgets.has(name))
|
|
144
147
|
return;
|
|
145
148
|
// Unknown uppercase component — warn (may be a native C++ widget)
|
|
146
|
-
warnings.push(warn(sf, node, `Unknown component '<${name}>' -- will be treated as a native C++ widget. If this is
|
|
149
|
+
warnings.push(warn(sf, node, `Unknown component '<${name}>' -- will be treated as a native C++ widget. If this is a registered widget, declare it in imx.d.ts to suppress this warning.`));
|
|
147
150
|
}
|
|
148
151
|
function validateJsxAttributes(attrs, tagName, sf, errors) {
|
|
149
152
|
if (!ts.isIdentifier(tagName))
|
|
@@ -163,3 +166,20 @@ function validateJsxAttributes(attrs, tagName, sf, errors) {
|
|
|
163
166
|
}
|
|
164
167
|
}
|
|
165
168
|
}
|
|
169
|
+
function loadDeclaredNativeWidgets(dir) {
|
|
170
|
+
const widgets = new Set();
|
|
171
|
+
const dtsPath = path.join(dir, 'imx.d.ts');
|
|
172
|
+
if (!fs.existsSync(dtsPath))
|
|
173
|
+
return widgets;
|
|
174
|
+
const source = fs.readFileSync(dtsPath, 'utf-8');
|
|
175
|
+
const sf = ts.createSourceFile('imx.d.ts', source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
176
|
+
for (const stmt of sf.statements) {
|
|
177
|
+
if (!ts.isFunctionDeclaration(stmt) || !stmt.name)
|
|
178
|
+
continue;
|
|
179
|
+
const name = stmt.name.text;
|
|
180
|
+
if (name && name[0] !== name[0].toLowerCase()) {
|
|
181
|
+
widgets.add(name);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return widgets;
|
|
185
|
+
}
|
package/dist/watch.d.ts
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discover all .tsx files in a directory (recursive).
|
|
3
|
+
*/
|
|
4
|
+
export declare function discoverTsxFiles(dir: string): string[];
|
|
5
|
+
export interface WatchCompileSelection {
|
|
6
|
+
files: string[];
|
|
7
|
+
rootFile: string;
|
|
8
|
+
appCandidates: string[];
|
|
9
|
+
}
|
|
10
|
+
export declare function selectWatchCompileFiles(watchDir: string, discoveredFiles: string[]): WatchCompileSelection;
|
|
1
11
|
/**
|
|
2
12
|
* Start watching a directory for .tsx changes and recompile on change.
|
|
3
13
|
* If buildCmd is provided, runs it after each successful compile (for hot reload).
|
package/dist/watch.js
CHANGED
|
@@ -5,7 +5,7 @@ import { compile } from './compile.js';
|
|
|
5
5
|
/**
|
|
6
6
|
* Discover all .tsx files in a directory (recursive).
|
|
7
7
|
*/
|
|
8
|
-
function discoverTsxFiles(dir) {
|
|
8
|
+
export function discoverTsxFiles(dir) {
|
|
9
9
|
const results = [];
|
|
10
10
|
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
11
11
|
for (const entry of entries) {
|
|
@@ -19,14 +19,38 @@ function discoverTsxFiles(dir) {
|
|
|
19
19
|
}
|
|
20
20
|
return results;
|
|
21
21
|
}
|
|
22
|
+
export function selectWatchCompileFiles(watchDir, discoveredFiles) {
|
|
23
|
+
const files = [...discoveredFiles].sort((a, b) => a.localeCompare(b));
|
|
24
|
+
const normalizedDir = path.resolve(watchDir);
|
|
25
|
+
const preferredCandidates = [
|
|
26
|
+
path.join(normalizedDir, 'src', 'App.tsx'),
|
|
27
|
+
path.join(normalizedDir, 'App.tsx'),
|
|
28
|
+
].map(candidate => path.resolve(candidate));
|
|
29
|
+
const appCandidates = files.filter(file => path.basename(file) === 'App.tsx');
|
|
30
|
+
const rootFile = preferredCandidates.find(candidate => files.includes(candidate)) ??
|
|
31
|
+
files[0];
|
|
32
|
+
if (rootFile !== files[0]) {
|
|
33
|
+
const remaining = files.filter(file => file !== rootFile);
|
|
34
|
+
return { files: [rootFile, ...remaining], rootFile, appCandidates };
|
|
35
|
+
}
|
|
36
|
+
return { files, rootFile, appCandidates };
|
|
37
|
+
}
|
|
22
38
|
function runCompile(watchDir, outputDir, buildCmd) {
|
|
23
|
-
const
|
|
24
|
-
if (
|
|
39
|
+
const discoveredFiles = discoverTsxFiles(watchDir);
|
|
40
|
+
if (discoveredFiles.length === 0) {
|
|
25
41
|
console.log('[watch] No .tsx files found in ' + watchDir);
|
|
26
42
|
return;
|
|
27
43
|
}
|
|
44
|
+
const selection = selectWatchCompileFiles(watchDir, discoveredFiles);
|
|
45
|
+
const rootRelative = path.relative(process.cwd(), selection.rootFile).replace(/\\/g, '/');
|
|
46
|
+
if (selection.appCandidates.length > 1) {
|
|
47
|
+
console.warn(`[watch] multiple App.tsx candidates found; using ${rootRelative} as root`);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
console.log(`[watch] root: ${rootRelative}`);
|
|
51
|
+
}
|
|
28
52
|
const start = performance.now();
|
|
29
|
-
const result = compile(files, outputDir);
|
|
53
|
+
const result = compile(selection.files, outputDir);
|
|
30
54
|
const elapsed = Math.round(performance.now() - start);
|
|
31
55
|
if (result.warnings.length > 0) {
|
|
32
56
|
result.warnings.forEach(w => console.warn(w));
|