@kernlang/terminal 3.2.3 → 3.3.4
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 +27 -1
- package/dist/runtime/alternate-screen.d.ts +6 -0
- package/dist/runtime/alternate-screen.js +103 -0
- package/dist/runtime/alternate-screen.js.map +1 -0
- package/dist/runtime/index.d.ts +4 -0
- package/dist/runtime/index.js +3 -0
- package/dist/runtime/index.js.map +1 -0
- package/dist/runtime/scroll-box.d.ts +23 -0
- package/dist/runtime/scroll-box.js +186 -0
- package/dist/runtime/scroll-box.js.map +1 -0
- package/dist/runtime/terminal-mode.d.ts +10 -0
- package/dist/runtime/terminal-mode.js +59 -0
- package/dist/runtime/terminal-mode.js.map +1 -0
- package/dist/transpiler-ink.js +269 -30
- package/dist/transpiler-ink.js.map +1 -1
- package/package.json +22 -2
package/dist/transpiler-ink.js
CHANGED
|
@@ -3,7 +3,7 @@ import { accountNode, buildDiagnostics, countTokens, dedent, generateCoreNode, g
|
|
|
3
3
|
* Ink Transpiler — generates React (Ink) TSX components for terminal UIs
|
|
4
4
|
*
|
|
5
5
|
* Maps KERN terminal nodes to Ink components:
|
|
6
|
-
* screen → React function component (export default)
|
|
6
|
+
* screen → React function component (named export by default, default export when requested)
|
|
7
7
|
* text → <Text bold color="blue">...</Text>
|
|
8
8
|
* box → <Box borderStyle="round" borderColor="blue">...</Box>
|
|
9
9
|
* separator → <Text dimColor>{'─'.repeat(48)}</Text>
|
|
@@ -28,6 +28,16 @@ import { accountNode, buildDiagnostics, countTokens, dedent, generateCoreNode, g
|
|
|
28
28
|
function capitalize(s) {
|
|
29
29
|
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
30
30
|
}
|
|
31
|
+
function inkScreenExportKeyword(exportAttr) {
|
|
32
|
+
if (exportAttr === false || exportAttr === 'false')
|
|
33
|
+
return '';
|
|
34
|
+
return exportAttr === 'default' ? 'export default' : 'export';
|
|
35
|
+
}
|
|
36
|
+
function inkScreenExportStatement(exportKw, symbol) {
|
|
37
|
+
if (!exportKw)
|
|
38
|
+
return null;
|
|
39
|
+
return exportKw === 'export default' ? `export default ${symbol};` : `export { ${symbol} };`;
|
|
40
|
+
}
|
|
31
41
|
/** Check if a prop value is a {{ expression }} object from the parser. */
|
|
32
42
|
function isExpr(v) {
|
|
33
43
|
return typeof v === 'object' && v !== null && '__expr' in v;
|
|
@@ -118,6 +128,7 @@ class ImportTracker {
|
|
|
118
128
|
reactImports = new Set();
|
|
119
129
|
inkImports = new Set();
|
|
120
130
|
inkUIImports = new Set();
|
|
131
|
+
kernRuntimeImports = new Set();
|
|
121
132
|
addReact(name) {
|
|
122
133
|
this.reactImports.add(name);
|
|
123
134
|
}
|
|
@@ -128,6 +139,10 @@ class ImportTracker {
|
|
|
128
139
|
addInkUI(name) {
|
|
129
140
|
this.inkUIImports.add(name);
|
|
130
141
|
}
|
|
142
|
+
/** Add a component from @kernlang/terminal/runtime. */
|
|
143
|
+
addKernRuntime(name) {
|
|
144
|
+
this.kernRuntimeImports.add(name);
|
|
145
|
+
}
|
|
131
146
|
// Legacy convenience methods — now route to @inkjs/ui
|
|
132
147
|
needSpinner() {
|
|
133
148
|
this.inkUIImports.add('Spinner');
|
|
@@ -152,6 +167,9 @@ class ImportTracker {
|
|
|
152
167
|
if (this.inkUIImports.size > 0) {
|
|
153
168
|
lines.push(`import { ${[...this.inkUIImports].sort().join(', ')} } from '@inkjs/ui';`);
|
|
154
169
|
}
|
|
170
|
+
if (this.kernRuntimeImports.size > 0) {
|
|
171
|
+
lines.push(`import { ${[...this.kernRuntimeImports].sort().join(', ')} } from '@kernlang/terminal/runtime';`);
|
|
172
|
+
}
|
|
155
173
|
return lines;
|
|
156
174
|
}
|
|
157
175
|
}
|
|
@@ -189,6 +207,7 @@ function generateStateHook(stateNode, imports, ctx) {
|
|
|
189
207
|
const name = props.name;
|
|
190
208
|
const initialProp = props.initial;
|
|
191
209
|
const safe = props.safe !== 'false' && props.safe !== false; // default true
|
|
210
|
+
const external = props.external === 'true' || props.external === true;
|
|
192
211
|
if (name && initialProp !== undefined) {
|
|
193
212
|
imports.addReact('useState');
|
|
194
213
|
const initial = isExpr(initialProp) ? initialProp.code : String(initialProp);
|
|
@@ -217,6 +236,36 @@ function generateStateHook(stateNode, imports, ctx) {
|
|
|
217
236
|
const lazyInitVal = needsLazyInit(initVal, props.type) ? `() => ${initVal}` : initVal;
|
|
218
237
|
const throttle = props.throttle;
|
|
219
238
|
const debounce = props.debounce;
|
|
239
|
+
if (external) {
|
|
240
|
+
// External-state primitive: a stable reference whose internal mutations
|
|
241
|
+
// are tracked via a sibling version counter. Replaces the manual
|
|
242
|
+
// `state foo + state fooVersion + setFooVersion(v => v + 1)` pattern.
|
|
243
|
+
// The held value is emitted as a bare useState (no __inkSafe wrap — the
|
|
244
|
+
// user mutates the object in place; the rare full-replacement case is
|
|
245
|
+
// a sync setState that React 18 batches inside event handlers). The
|
|
246
|
+
// version counter is hidden; the user calls `bumpFoo()` after mutating
|
|
247
|
+
// the object, and any memo that references `foo` automatically gets
|
|
248
|
+
// `_fooVersion` injected into its dep array.
|
|
249
|
+
if (throttle !== undefined || debounce !== undefined) {
|
|
250
|
+
throw new Error(`state '${name}' uses external=true with throttle/debounce, which are mutually exclusive. ` +
|
|
251
|
+
`External state holds a stable reference; throttle/debounce apply to setter call rates and have no meaning ` +
|
|
252
|
+
`here. Drop one of the two, or split into a separate state node if you really need both.`);
|
|
253
|
+
}
|
|
254
|
+
if (props.safe === 'false' || props.safe === false) {
|
|
255
|
+
throw new Error(`state '${name}' uses external=true with safe=false. External state already emits a bare useState (the safe wrapper does not apply), so safe=false is redundant — and combining them suggests a misunderstanding. Drop safe=false.`);
|
|
256
|
+
}
|
|
257
|
+
imports.addReact('useMemo');
|
|
258
|
+
const cap = capitalize(name);
|
|
259
|
+
lines.push(` const [${name}, ${setter}] = useState${typeAnnotation}(${lazyInitVal});`);
|
|
260
|
+
lines.push(` const [_${name}Version, _set${cap}VersionRaw] = useState<number>(0);`);
|
|
261
|
+
lines.push(` const bump${cap} = useMemo(() => {`);
|
|
262
|
+
lines.push(` return () => setTimeout(() => _set${cap}VersionRaw((v: number) => v + 1), 0);`);
|
|
263
|
+
lines.push(` }, []);`);
|
|
264
|
+
// Touch the version in the closure so React picks it up if the user references
|
|
265
|
+
// it directly. The void cast keeps the lint quiet about an unused binding.
|
|
266
|
+
lines.push(` void _${name}Version;`);
|
|
267
|
+
return lines;
|
|
268
|
+
}
|
|
220
269
|
if (throttle) {
|
|
221
270
|
// Throttled setter — leading+trailing by default (lodash-style).
|
|
222
271
|
// trailing=false reverts to leading-only (drops intermediate + final values in window).
|
|
@@ -511,10 +560,101 @@ function collectNestedOnNodes(node) {
|
|
|
511
560
|
}
|
|
512
561
|
// ── Generate useInput from an on-node ───────────────────────────────────
|
|
513
562
|
let _onHookCounter = 0;
|
|
514
|
-
|
|
563
|
+
/**
|
|
564
|
+
* Rewrite a handler body so that every `setX(...)` call against a known safe state
|
|
565
|
+
* is replaced with the corresponding raw setter `_setXRaw(...)`. Used by batched
|
|
566
|
+
* handlers to bypass the per-setter __inkSafe macrotask deferral, so the whole
|
|
567
|
+
* batch can share a single deferred macrotask.
|
|
568
|
+
*
|
|
569
|
+
* The match uses a negative lookbehind so it only fires on bare setter calls.
|
|
570
|
+
* `form.setCount(...)` and any locally-shadowed `const setCount = ...; setCount(...)`
|
|
571
|
+
* preceded by a member access or word char are NOT rewritten.
|
|
572
|
+
*
|
|
573
|
+
* Limitation: substitution is text-based, so a setter name appearing as a bare
|
|
574
|
+
* call inside a string literal will also be rewritten. Document this in the
|
|
575
|
+
* language reference.
|
|
576
|
+
*/
|
|
577
|
+
function rewriteToRawSetters(code, stateNodes) {
|
|
578
|
+
let out = code;
|
|
579
|
+
for (const stateNode of stateNodes) {
|
|
580
|
+
const sp = getProps(stateNode);
|
|
581
|
+
// External-state setters use the bare useState form — there is no
|
|
582
|
+
// `_setXRaw` to rewrite to. Leave call sites alone.
|
|
583
|
+
if (sp.external === 'true' || sp.external === true)
|
|
584
|
+
continue;
|
|
585
|
+
const safe = sp.safe !== 'false' && sp.safe !== false;
|
|
586
|
+
if (!safe)
|
|
587
|
+
continue;
|
|
588
|
+
if (sp.throttle !== undefined || sp.debounce !== undefined)
|
|
589
|
+
continue;
|
|
590
|
+
const name = sp.name;
|
|
591
|
+
if (!name)
|
|
592
|
+
continue;
|
|
593
|
+
const setter = `set${capitalize(name)}`;
|
|
594
|
+
const raw = `_${setter}Raw`;
|
|
595
|
+
// Negative lookbehind: setter must not be preceded by `.` (member access)
|
|
596
|
+
// or `\w` (substring of a longer identifier).
|
|
597
|
+
const pattern = new RegExp(`(?<![\\w.])${setter}\\s*\\(`, 'g');
|
|
598
|
+
out = out.replace(pattern, `${raw}(`);
|
|
599
|
+
}
|
|
600
|
+
return out;
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* Refuse to batch handlers that contain async or deferred constructs. The whole
|
|
604
|
+
* point of batch=true is "collapse N synchronous setter calls into one shared
|
|
605
|
+
* macrotask." If the handler defers work into a nested timer or promise, those
|
|
606
|
+
* inner setter calls would land in their own task AFTER the batch's setTimeout
|
|
607
|
+
* has already flushed, with no __inkSafe wrapper to bridge them — exactly the
|
|
608
|
+
* missed-repaint failure mode __inkSafe exists to prevent. Better to surface
|
|
609
|
+
* the misuse at compile time than ship code that silently regresses on a
|
|
610
|
+
* subset of paths.
|
|
611
|
+
*/
|
|
612
|
+
const BATCH_FORBIDDEN_PATTERNS = [
|
|
613
|
+
{ name: 'setTimeout(', pattern: /\bsetTimeout\s*\(/ },
|
|
614
|
+
{ name: 'setInterval(', pattern: /\bsetInterval\s*\(/ },
|
|
615
|
+
{ name: 'setImmediate(', pattern: /\bsetImmediate\s*\(/ },
|
|
616
|
+
{ name: 'queueMicrotask(', pattern: /\bqueueMicrotask\s*\(/ },
|
|
617
|
+
{ name: 'await', pattern: /\bawait\b/ },
|
|
618
|
+
{ name: '.then(', pattern: /\.then\s*\(/ },
|
|
619
|
+
];
|
|
620
|
+
/**
|
|
621
|
+
* Append `_${name}Version` to a memo's dep list for every external state name
|
|
622
|
+
* the memo already references. The user writes `deps="registry"` and the codegen
|
|
623
|
+
* produces `[registry, _registryVersion]`, so the memo invalidates when the user
|
|
624
|
+
* calls `bumpRegistry()` after mutating the held object in place. Idempotent —
|
|
625
|
+
* if the user already listed the version manually, nothing is duplicated.
|
|
626
|
+
*/
|
|
627
|
+
function injectExternalVersionDeps(depsRaw, externalStateNames) {
|
|
628
|
+
if (externalStateNames.length === 0)
|
|
629
|
+
return depsRaw;
|
|
630
|
+
const tokens = depsRaw
|
|
631
|
+
.split(',')
|
|
632
|
+
.map((t) => t.trim())
|
|
633
|
+
.filter(Boolean);
|
|
634
|
+
const present = new Set(tokens);
|
|
635
|
+
for (const name of externalStateNames) {
|
|
636
|
+
if (!present.has(name))
|
|
637
|
+
continue;
|
|
638
|
+
const versionTok = `_${name}Version`;
|
|
639
|
+
if (!present.has(versionTok)) {
|
|
640
|
+
tokens.push(versionTok);
|
|
641
|
+
present.add(versionTok);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
return tokens.join(', ');
|
|
645
|
+
}
|
|
646
|
+
function checkBatchBodyIsSync(code, onNode) {
|
|
647
|
+
for (const { name, pattern } of BATCH_FORBIDDEN_PATTERNS) {
|
|
648
|
+
if (pattern.test(code)) {
|
|
649
|
+
throw new Error(`batch=true handler at on-node '${getProps(onNode).key || getProps(onNode).event || 'unknown'}' contains '${name}'. Batched handlers must be fully synchronous — deferred work would bypass __inkSafe and cause missed repaints. Either remove batch=true or move the deferred work to a separate non-batched on-node.`);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
function generateOnHook(onNode, imports, stateNodes) {
|
|
515
654
|
const lines = [];
|
|
516
655
|
const onProps = getProps(onNode);
|
|
517
656
|
const event = (onProps.event || onProps.name);
|
|
657
|
+
const batch = onProps.batch === 'true' || onProps.batch === true;
|
|
518
658
|
if (event === 'key' || event === 'input') {
|
|
519
659
|
imports.addInk('useInput');
|
|
520
660
|
imports.addReact('useRef');
|
|
@@ -532,8 +672,20 @@ function generateOnHook(onNode, imports) {
|
|
|
532
672
|
}
|
|
533
673
|
if (code) {
|
|
534
674
|
const dedented = dedent(code);
|
|
535
|
-
|
|
536
|
-
|
|
675
|
+
if (batch) {
|
|
676
|
+
checkBatchBodyIsSync(dedented, onNode);
|
|
677
|
+
const body = rewriteToRawSetters(dedented, stateNodes);
|
|
678
|
+
// Single deferred macrotask: collapse N __inkSafe wrappers into one paint cycle.
|
|
679
|
+
lines.push(` setTimeout(() => {`);
|
|
680
|
+
for (const line of body.split('\n')) {
|
|
681
|
+
lines.push(` ${line}`);
|
|
682
|
+
}
|
|
683
|
+
lines.push(` }, 0);`);
|
|
684
|
+
}
|
|
685
|
+
else {
|
|
686
|
+
for (const line of dedented.split('\n')) {
|
|
687
|
+
lines.push(` ${line}`);
|
|
688
|
+
}
|
|
537
689
|
}
|
|
538
690
|
}
|
|
539
691
|
lines.push(` };`);
|
|
@@ -607,6 +759,58 @@ function renderInkBox(node, p, indent, imports) {
|
|
|
607
759
|
lines.push(`${indent}</Box>`);
|
|
608
760
|
return lines;
|
|
609
761
|
}
|
|
762
|
+
function renderInkAlternateScreen(node, p, indent, imports) {
|
|
763
|
+
imports.addKernRuntime('AlternateScreen');
|
|
764
|
+
const mouseTracking = p.mouseTracking === 'true' ||
|
|
765
|
+
p.mouseTracking === true ||
|
|
766
|
+
p['mouse-tracking'] === 'true' ||
|
|
767
|
+
p['mouse-tracking'] === true;
|
|
768
|
+
const attrs = [];
|
|
769
|
+
if (mouseTracking)
|
|
770
|
+
attrs.push('mouseTracking');
|
|
771
|
+
const propsStr = attrs.length > 0 ? ` ${attrs.join(' ')}` : '';
|
|
772
|
+
const lines = [];
|
|
773
|
+
lines.push(`${indent}<AlternateScreen${propsStr}>`);
|
|
774
|
+
for (const child of node.children || []) {
|
|
775
|
+
if (child.type === 'on')
|
|
776
|
+
continue;
|
|
777
|
+
lines.push(...renderInkNode(child, `${indent} `, imports));
|
|
778
|
+
}
|
|
779
|
+
lines.push(`${indent}</AlternateScreen>`);
|
|
780
|
+
return lines;
|
|
781
|
+
}
|
|
782
|
+
function renderInkScrollBox(node, p, indent, imports) {
|
|
783
|
+
imports.addKernRuntime('ScrollBox');
|
|
784
|
+
const stickyScroll = p.stickyScroll === 'true' ||
|
|
785
|
+
p.stickyScroll === true ||
|
|
786
|
+
p['sticky-scroll'] === 'true' ||
|
|
787
|
+
p['sticky-scroll'] === true;
|
|
788
|
+
const flexGrow = p.flexGrow ?? p['flex-grow'];
|
|
789
|
+
const flexShrink = p.flexShrink ?? p['flex-shrink'];
|
|
790
|
+
const height = p.height;
|
|
791
|
+
const rowHeight = p.rowHeight ?? p['row-height'];
|
|
792
|
+
const attrs = [];
|
|
793
|
+
if (stickyScroll)
|
|
794
|
+
attrs.push('stickyScroll');
|
|
795
|
+
if (flexGrow !== undefined)
|
|
796
|
+
attrs.push(`flexGrow={${unwrapProp(flexGrow)}}`);
|
|
797
|
+
if (flexShrink !== undefined)
|
|
798
|
+
attrs.push(`flexShrink={${unwrapProp(flexShrink)}}`);
|
|
799
|
+
if (height !== undefined)
|
|
800
|
+
attrs.push(`height={${unwrapProp(height)}}`);
|
|
801
|
+
if (rowHeight !== undefined)
|
|
802
|
+
attrs.push(`rowHeight={${unwrapProp(rowHeight)}}`);
|
|
803
|
+
const propsStr = attrs.length > 0 ? ` ${attrs.join(' ')}` : '';
|
|
804
|
+
const lines = [];
|
|
805
|
+
lines.push(`${indent}<ScrollBox${propsStr}>`);
|
|
806
|
+
for (const child of node.children || []) {
|
|
807
|
+
if (child.type === 'on')
|
|
808
|
+
continue;
|
|
809
|
+
lines.push(...renderInkNode(child, `${indent} `, imports));
|
|
810
|
+
}
|
|
811
|
+
lines.push(`${indent}</ScrollBox>`);
|
|
812
|
+
return lines;
|
|
813
|
+
}
|
|
610
814
|
function renderInkTable(node, p, indent, imports) {
|
|
611
815
|
imports.addInk('Box');
|
|
612
816
|
imports.addInk('Text');
|
|
@@ -1044,6 +1248,10 @@ function renderInkNode(node, indent, imports) {
|
|
|
1044
1248
|
return renderInkSeparator(p, indent, imports);
|
|
1045
1249
|
case 'box':
|
|
1046
1250
|
return renderInkBox(node, p, indent, imports);
|
|
1251
|
+
case 'alternate-screen':
|
|
1252
|
+
return renderInkAlternateScreen(node, p, indent, imports);
|
|
1253
|
+
case 'scroll-box':
|
|
1254
|
+
return renderInkScrollBox(node, p, indent, imports);
|
|
1047
1255
|
case 'table':
|
|
1048
1256
|
return renderInkTable(node, p, indent, imports);
|
|
1049
1257
|
case 'scoreboard':
|
|
@@ -1209,11 +1417,23 @@ function compileScreenBody(screenNode, imports) {
|
|
|
1209
1417
|
}
|
|
1210
1418
|
if (appExitNodes.length > 0)
|
|
1211
1419
|
bodyLines.push('');
|
|
1420
|
+
// Names of state nodes declared with external=true. Memos that reference
|
|
1421
|
+
// any of these names auto-receive the corresponding `_${name}Version` token
|
|
1422
|
+
// in their dep array, so the user does not have to remember to list both.
|
|
1423
|
+
const externalStateNames = stateNodes
|
|
1424
|
+
.filter((s) => {
|
|
1425
|
+
const sp = getProps(s);
|
|
1426
|
+
return sp.external === 'true' || sp.external === true;
|
|
1427
|
+
})
|
|
1428
|
+
.map((s) => getProps(s).name)
|
|
1429
|
+
.filter(Boolean);
|
|
1212
1430
|
// Memo hooks
|
|
1213
1431
|
for (const memoNode of memoNodes) {
|
|
1214
1432
|
const mp = getProps(memoNode);
|
|
1215
1433
|
const mName = mp.name;
|
|
1216
|
-
const
|
|
1434
|
+
const mDepsRaw = mp.deps || '';
|
|
1435
|
+
// Auto-inject `_${name}Version` for every external state referenced in deps.
|
|
1436
|
+
const mDeps = injectExternalVersionDeps(mDepsRaw, externalStateNames);
|
|
1217
1437
|
const mDepsArr = mDeps ? `[${mDeps}]` : '[]';
|
|
1218
1438
|
const handlerChild = (memoNode.children || []).find((c) => c.type === 'handler');
|
|
1219
1439
|
const code = handlerChild ? getProps(handlerChild).code || '' : '';
|
|
@@ -1235,7 +1455,7 @@ function compileScreenBody(screenNode, imports) {
|
|
|
1235
1455
|
}
|
|
1236
1456
|
// on event=key → useInput() hooks
|
|
1237
1457
|
for (const onNode of allOnNodes) {
|
|
1238
|
-
bodyLines.push(...generateOnHook(onNode, imports));
|
|
1458
|
+
bodyLines.push(...generateOnHook(onNode, imports, stateNodes));
|
|
1239
1459
|
}
|
|
1240
1460
|
// Stream effects
|
|
1241
1461
|
for (const streamNode of streamNodes) {
|
|
@@ -1264,7 +1484,9 @@ function compileScreenBody(screenNode, imports) {
|
|
|
1264
1484
|
const typeAnnotation = dType ? `<${dType}>` : '';
|
|
1265
1485
|
let depsStr;
|
|
1266
1486
|
if (dDeps) {
|
|
1267
|
-
|
|
1487
|
+
// Explicit deps — same auto-injection path as memo nodes.
|
|
1488
|
+
const injected = injectExternalVersionDeps(dDeps, externalStateNames);
|
|
1489
|
+
depsStr = `[${injected}]`;
|
|
1268
1490
|
}
|
|
1269
1491
|
else {
|
|
1270
1492
|
const sNames = stateNodes.map((s) => getProps(s).name).filter(Boolean);
|
|
@@ -1276,7 +1498,19 @@ function compileScreenBody(screenNode, imports) {
|
|
|
1276
1498
|
.filter(Boolean);
|
|
1277
1499
|
const allNames = [...sNames, ...rNames];
|
|
1278
1500
|
const autoDeps = allNames.filter((n) => new RegExp(`\\b${n}\\b`).test(dExpr));
|
|
1279
|
-
|
|
1501
|
+
// After auto-detect, append `_${name}Version` for any external state that
|
|
1502
|
+
// showed up in the expression — otherwise bumpRegistry() never invalidates.
|
|
1503
|
+
const autoDepsWithVersions = [];
|
|
1504
|
+
for (const dep of autoDeps) {
|
|
1505
|
+
autoDepsWithVersions.push(dep);
|
|
1506
|
+
if (externalStateNames.includes(dep)) {
|
|
1507
|
+
const versionTok = `_${dep}Version`;
|
|
1508
|
+
if (!autoDepsWithVersions.includes(versionTok)) {
|
|
1509
|
+
autoDepsWithVersions.push(versionTok);
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
depsStr = `[${autoDepsWithVersions.join(', ')}]`;
|
|
1280
1514
|
}
|
|
1281
1515
|
bodyLines.push(` const ${dName} = useMemo${typeAnnotation}(() => ${dExpr}, ${depsStr});`);
|
|
1282
1516
|
bodyLines.push('');
|
|
@@ -1457,8 +1691,7 @@ export function transpileInk(root, _config) {
|
|
|
1457
1691
|
: '';
|
|
1458
1692
|
// Full body compilation — same pipeline as primary screen
|
|
1459
1693
|
const { bodyLines: secBodyLines, stateCtx: secCtx } = compileScreenBody(secScreen, imports);
|
|
1460
|
-
const
|
|
1461
|
-
const secExportKw = secExportAttr === 'default' ? 'export default' : 'export';
|
|
1694
|
+
const secExportKw = inkScreenExportKeyword(secProps.export);
|
|
1462
1695
|
const secMemoAttr = secProps.memo;
|
|
1463
1696
|
const secUseMemo = secMemoAttr === 'true' || secMemoAttr === true || (typeof secMemoAttr === 'string' && secMemoAttr !== 'false');
|
|
1464
1697
|
const secMemoComp = secUseMemo && typeof secMemoAttr === 'string' && secMemoAttr !== 'true' ? secMemoAttr : null;
|
|
@@ -1469,10 +1702,13 @@ export function transpileInk(root, _config) {
|
|
|
1469
1702
|
componentLines.push(...emitInkSafePreamble());
|
|
1470
1703
|
componentLines.push(...secBodyLines);
|
|
1471
1704
|
componentLines.push(secMemoExpr ? `}, ${secMemoExpr});` : '});');
|
|
1472
|
-
|
|
1705
|
+
const secExportStatement = inkScreenExportStatement(secExportKw, secName);
|
|
1706
|
+
if (secExportStatement) {
|
|
1707
|
+
componentLines.push(secExportStatement);
|
|
1708
|
+
}
|
|
1473
1709
|
}
|
|
1474
1710
|
else {
|
|
1475
|
-
componentLines.push(`${secExportKw} function ${secName}(${secParam}) {`);
|
|
1711
|
+
componentLines.push(`${secExportKw ? `${secExportKw} ` : ''}function ${secName}(${secParam}) {`);
|
|
1476
1712
|
if (secCtx.needsInkSafe)
|
|
1477
1713
|
componentLines.push(...emitInkSafePreamble());
|
|
1478
1714
|
componentLines.push(...secBodyLines);
|
|
@@ -1481,7 +1717,7 @@ export function transpileInk(root, _config) {
|
|
|
1481
1717
|
componentLines.push('');
|
|
1482
1718
|
}
|
|
1483
1719
|
// Component (Feature #9: with props) — respect export= and memo= attributes
|
|
1484
|
-
const
|
|
1720
|
+
const screenExportKw = inkScreenExportKeyword(screenProps.export);
|
|
1485
1721
|
const screenMemoAttr = screenProps.memo;
|
|
1486
1722
|
const useMemo = screenMemoAttr === 'true' ||
|
|
1487
1723
|
screenMemoAttr === true ||
|
|
@@ -1490,7 +1726,6 @@ export function transpileInk(root, _config) {
|
|
|
1490
1726
|
const memoComparatorExpr = memoComparator && isExpr(screenProps.memo) ? screenProps.memo.code : memoComparator;
|
|
1491
1727
|
if (useMemo) {
|
|
1492
1728
|
// React.memo wrapper: const Name = React.memo(function Name(props) { ... }, comparator?);
|
|
1493
|
-
const exportKw = screenExportAttr === 'default' ? 'export default' : 'export';
|
|
1494
1729
|
componentLines.push(`const ${screenName} = React.memo(function ${screenName}(${propsParam}) {`);
|
|
1495
1730
|
if (stateCtx.needsInkSafe) {
|
|
1496
1731
|
componentLines.push(...emitInkSafePreamble());
|
|
@@ -1502,11 +1737,13 @@ export function transpileInk(root, _config) {
|
|
|
1502
1737
|
else {
|
|
1503
1738
|
componentLines.push('});');
|
|
1504
1739
|
}
|
|
1505
|
-
|
|
1740
|
+
const exportStatement = inkScreenExportStatement(screenExportKw, screenName);
|
|
1741
|
+
if (exportStatement) {
|
|
1742
|
+
componentLines.push(exportStatement);
|
|
1743
|
+
}
|
|
1506
1744
|
}
|
|
1507
1745
|
else {
|
|
1508
|
-
|
|
1509
|
-
componentLines.push(`${exportKw} function ${screenName}(${propsParam}) {`);
|
|
1746
|
+
componentLines.push(`${screenExportKw ? `${screenExportKw} ` : ''}function ${screenName}(${propsParam}) {`);
|
|
1510
1747
|
if (stateCtx.needsInkSafe) {
|
|
1511
1748
|
componentLines.push(...emitInkSafePreamble());
|
|
1512
1749
|
}
|
|
@@ -1545,20 +1782,22 @@ export function transpileInk(root, _config) {
|
|
|
1545
1782
|
// Generate artifacts: entry point + per-screen component files for multi-screen
|
|
1546
1783
|
const artifacts = [];
|
|
1547
1784
|
// Entry-point artifact: render(<App />) + waitUntilExit()
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1785
|
+
if (screenExportKw) {
|
|
1786
|
+
const entryLines = [];
|
|
1787
|
+
entryLines.push(`#!/usr/bin/env node`);
|
|
1788
|
+
entryLines.push(`import React from 'react';`);
|
|
1789
|
+
entryLines.push(`import { render } from 'ink';`);
|
|
1790
|
+
if (screenExportKw === 'export default') {
|
|
1791
|
+
entryLines.push(`import ${screenName} from './${screenName}.js';`);
|
|
1792
|
+
}
|
|
1793
|
+
else {
|
|
1794
|
+
entryLines.push(`import { ${screenName} } from './${screenName}.js';`);
|
|
1795
|
+
}
|
|
1796
|
+
entryLines.push('');
|
|
1797
|
+
entryLines.push(`const app = render(<${screenName} />);`);
|
|
1798
|
+
entryLines.push(`await app.waitUntilExit();`);
|
|
1799
|
+
artifacts.push({ path: 'index.tsx', content: entryLines.join('\n'), type: 'entry' });
|
|
1557
1800
|
}
|
|
1558
|
-
entryLines.push('');
|
|
1559
|
-
entryLines.push(`const app = render(<${screenName} />);`);
|
|
1560
|
-
entryLines.push(`await app.waitUntilExit();`);
|
|
1561
|
-
artifacts.push({ path: 'index.tsx', content: entryLines.join('\n'), type: 'entry' });
|
|
1562
1801
|
// Main component artifact (always emitted so entry-point import resolves)
|
|
1563
1802
|
artifacts.push({ path: `${screenName}.tsx`, content: code, type: 'component' });
|
|
1564
1803
|
// Per-screen component artifacts for secondary screens
|