0x-lang 0.1.12 → 0.1.14

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/dist/ast.d.ts CHANGED
@@ -79,6 +79,7 @@ export interface OnDestroy extends BaseNode {
79
79
  export interface WatchBlock extends BaseNode {
80
80
  type: 'WatchBlock';
81
81
  variable: string;
82
+ variables: string[];
82
83
  body: Statement[];
83
84
  }
84
85
  export interface CheckDecl extends BaseNode {
@@ -188,6 +189,12 @@ export interface JsBlock extends BaseNode {
188
189
  type: 'JsBlock';
189
190
  code: string;
190
191
  }
192
+ export interface TopLevelVarDecl extends BaseNode {
193
+ type: 'TopLevelVarDecl';
194
+ keyword: 'const' | 'let';
195
+ name: string;
196
+ value: Expression;
197
+ }
191
198
  export interface CommentNode extends BaseNode {
192
199
  type: 'Comment';
193
200
  text: string;
@@ -903,7 +910,7 @@ export interface RtlNode extends BaseNode {
903
910
  props: Record<string, Expression>;
904
911
  }
905
912
  export type UINode = LayoutNode | TextNode | ButtonNode | InputNode | ImageNode | LinkNode | ToggleNode | SelectNode | IfBlock | ForBlock | ShowBlock | HideBlock | ComponentCall | CommentNode | TableNode | ChartNode | StatNode | NavNode | UploadNode | ModalNode | ToastNode | CrudNode | ListNode | LayoutShellNode | SlideOverNode | DrawerNode | CommandNode | ConfirmNode | PayNode | CartNode | MediaNode | NotificationNode | SearchNode | FilterNode | SocialNode | ProfileNode | HeroNode | FeaturesNode | PricingNode | FaqNode | TestimonialNode | FooterNode | AdminNode | SeoNode | A11yNode | AnimateNode | GestureNode | AiNode | AutomationNode | DevNode | EmitNode | ResponsiveNode | BreadcrumbNode | StatsGridNode | DividerNode | ProgressNode | ErrorNode | LoadingNode | OfflineNode | RetryNode | LogNode;
906
- export type ASTNode = AppNode | PageNode | ComponentNode | StateDecl | DerivedDecl | PropDecl | TypeDecl | StoreDecl | ApiDecl | FnDecl | OnMount | OnDestroy | WatchBlock | CheckDecl | StyleDecl | JsImport | UseImport | JsBlock | ModelNode | DataDecl | FormDecl | AuthDecl | RealtimeDecl | RouteDecl | RoleDecl | AutomationNode | DevNode | DeployNode | EnvNode | DockerNode | CiNode | DomainNode | CdnNode | MonitorNode | BackupNode | EndpointNode | MiddlewareNode | QueueNode | CronNode | CacheNode | MigrateNode | SeedNode | WebhookNode | StorageNode | TestNode | E2eNode | MockNode | FixtureNode | I18nNode | LocaleNode | RtlNode | UINode;
913
+ export type ASTNode = AppNode | PageNode | ComponentNode | StateDecl | DerivedDecl | PropDecl | TypeDecl | StoreDecl | ApiDecl | FnDecl | OnMount | OnDestroy | WatchBlock | CheckDecl | StyleDecl | JsImport | UseImport | JsBlock | TopLevelVarDecl | ModelNode | DataDecl | FormDecl | AuthDecl | RealtimeDecl | RouteDecl | RoleDecl | AutomationNode | DevNode | DeployNode | EnvNode | DockerNode | CiNode | DomainNode | CdnNode | MonitorNode | BackupNode | EndpointNode | MiddlewareNode | QueueNode | CronNode | CacheNode | MigrateNode | SeedNode | WebhookNode | StorageNode | TestNode | E2eNode | MockNode | FixtureNode | I18nNode | LocaleNode | RtlNode | UINode;
907
914
  export interface CodeGenerator {
908
915
  target: string;
909
916
  generate(ast: PageNode | ComponentNode | AppNode): GeneratedCode;
@@ -914,4 +921,5 @@ export interface GeneratedCode {
914
921
  imports: string[];
915
922
  lineCount: number;
916
923
  tokenCount: number;
924
+ sourceMap?: string;
917
925
  }
@@ -2,5 +2,7 @@ import type { GeneratedCode } from './ast.js';
2
2
  export interface CompileOptions {
3
3
  target: 'react' | 'vue' | 'svelte';
4
4
  validate?: boolean;
5
+ sourceMap?: boolean;
6
+ useClient?: boolean;
5
7
  }
6
8
  export declare function compile(source: string, options: CompileOptions): GeneratedCode;
package/dist/compiler.js CHANGED
@@ -14,15 +14,33 @@ export function compile(source, options) {
14
14
  throw new Error(`Validation errors:\n${errMsg}`);
15
15
  }
16
16
  }
17
+ let result;
17
18
  switch (options.target) {
18
19
  case 'react':
19
- return generateReact(ast);
20
+ result = generateReact(ast);
21
+ break;
20
22
  case 'vue':
21
- return generateVue(ast);
23
+ result = generateVue(ast);
24
+ break;
22
25
  case 'svelte':
23
- return generateSvelte(ast);
26
+ result = generateSvelte(ast);
27
+ break;
24
28
  default:
25
29
  throw new Error(`Unknown target: ${options.target}`);
26
30
  }
31
+ // Post-process: strip source map comments and V3 source map if disabled
32
+ if (options.sourceMap === false) {
33
+ result = { ...result, code: result.code.replace(/\{\/\* 0x:L\d+ \*\/\}/g, '').replace(/<!-- 0x:L\d+ -->/g, ''), sourceMap: undefined };
34
+ }
35
+ else if (result.sourceMap) {
36
+ // Append inline sourceMappingURL as base64 data URL
37
+ const b64 = Buffer.from(result.sourceMap).toString('base64');
38
+ result = { ...result, code: result.code + `\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,${b64}\n` };
39
+ }
40
+ // Post-process: strip 'use client' if explicitly disabled
41
+ if (options.useClient === false && options.target === 'react') {
42
+ result = { ...result, code: result.code.replace(/^'use client';\n\n/gm, '') };
43
+ }
44
+ return result;
27
45
  }
28
46
  //# sourceMappingURL=compiler.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"compiler.js","sourceRoot":"","sources":["../src/compiler.ts"],"names":[],"mappings":"AAAA,uBAAuB;AAEvB,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AACpC,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AACtD,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAClD,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAQ1C,MAAM,UAAU,OAAO,CAAC,MAAc,EAAE,OAAuB;IAC7D,MAAM,GAAG,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC;IAE1B,8BAA8B;IAC9B,IAAI,OAAO,CAAC,QAAQ,KAAK,KAAK,EAAE,CAAC;QAC/B,MAAM,MAAM,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC;QAC7B,IAAI,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC7B,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,QAAQ,CAAC,CAAC,IAAI,SAAS,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAClG,MAAM,IAAI,KAAK,CAAC,uBAAuB,MAAM,EAAE,CAAC,CAAC;QACnD,CAAC;IACH,CAAC;IAED,QAAQ,OAAO,CAAC,MAAM,EAAE,CAAC;QACvB,KAAK,OAAO;YACV,OAAO,aAAa,CAAC,GAAG,CAAC,CAAC;QAC5B,KAAK,KAAK;YACR,OAAO,WAAW,CAAC,GAAG,CAAC,CAAC;QAC1B,KAAK,QAAQ;YACX,OAAO,cAAc,CAAC,GAAG,CAAC,CAAC;QAC7B;YACE,MAAM,IAAI,KAAK,CAAC,mBAAmB,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACzD,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"compiler.js","sourceRoot":"","sources":["../src/compiler.ts"],"names":[],"mappings":"AAAA,uBAAuB;AAEvB,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AACpC,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AACtD,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAClD,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAY1C,MAAM,UAAU,OAAO,CAAC,MAAc,EAAE,OAAuB;IAC7D,MAAM,GAAG,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC;IAE1B,8BAA8B;IAC9B,IAAI,OAAO,CAAC,QAAQ,KAAK,KAAK,EAAE,CAAC;QAC/B,MAAM,MAAM,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC;QAC7B,IAAI,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC7B,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,QAAQ,CAAC,CAAC,IAAI,SAAS,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAClG,MAAM,IAAI,KAAK,CAAC,uBAAuB,MAAM,EAAE,CAAC,CAAC;QACnD,CAAC;IACH,CAAC;IAED,IAAI,MAAqB,CAAC;IAC1B,QAAQ,OAAO,CAAC,MAAM,EAAE,CAAC;QACvB,KAAK,OAAO;YACV,MAAM,GAAG,aAAa,CAAC,GAAG,CAAC,CAAC;YAC5B,MAAM;QACR,KAAK,KAAK;YACR,MAAM,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC;YAC1B,MAAM;QACR,KAAK,QAAQ;YACX,MAAM,GAAG,cAAc,CAAC,GAAG,CAAC,CAAC;YAC7B,MAAM;QACR;YACE,MAAM,IAAI,KAAK,CAAC,mBAAmB,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACzD,CAAC;IAED,wEAAwE;IACxE,IAAI,OAAO,CAAC,SAAS,KAAK,KAAK,EAAE,CAAC;QAChC,MAAM,GAAG,EAAE,GAAG,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,wBAAwB,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,mBAAmB,EAAE,EAAE,CAAC,EAAE,SAAS,EAAE,SAAS,EAAE,CAAC;IACzI,CAAC;SAAM,IAAI,MAAM,CAAC,SAAS,EAAE,CAAC;QAC5B,oDAAoD;QACpD,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;QAC7D,MAAM,GAAG,EAAE,GAAG,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,GAAG,qEAAqE,GAAG,IAAI,EAAE,CAAC;IAC3H,CAAC;IAED,0DAA0D;IAC1D,IAAI,OAAO,CAAC,SAAS,KAAK,KAAK,IAAI,OAAO,CAAC,MAAM,KAAK,OAAO,EAAE,CAAC;QAC9D,MAAM,GAAG,EAAE,GAAG,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,sBAAsB,EAAE,EAAE,CAAC,EAAE,CAAC;IAChF,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC"}
@@ -1,13 +1,16 @@
1
1
  // 0x → React Code Generator
2
2
  import { SIZE_MAP, unquote, capitalize, parseGradient, addPx } from './shared.js';
3
+ import { SourceMapBuilder } from './source-map.js';
3
4
  function ctx() {
4
5
  return {
5
6
  imports: new Set(),
6
7
  states: new Map(),
7
8
  derivedNames: new Set(),
9
+ propNames: new Set(),
8
10
  styles: new Map(),
9
11
  indent: 1,
10
12
  readOnly: false,
13
+ smb: null,
11
14
  };
12
15
  }
13
16
  function ind(c) {
@@ -116,12 +119,25 @@ export function generateReact(ast) {
116
119
  }
117
120
  const code = parts.join('\n\n');
118
121
  const importLines = code.match(/^import .+$/gm) || [];
122
+ // Build V3 source map from 0x:L### comments in generated code
123
+ const sourceFile = ast.find(n => n.type === 'Page' || n.type === 'Component' || n.type === 'App');
124
+ const srcName = sourceFile?.name ? `${sourceFile.name}.0x` : 'source.0x';
125
+ const smb = new SourceMapBuilder(srcName, 'Component.jsx');
126
+ const lines = code.split('\n');
127
+ for (let i = 0; i < lines.length; i++) {
128
+ const match = lines[i].match(/\{\/\* 0x:L(\d+) \*\/\}/);
129
+ if (match) {
130
+ smb.addMapping(parseInt(match[1], 10), 0);
131
+ }
132
+ smb.advance(lines[i] + (i < lines.length - 1 ? '\n' : ''));
133
+ }
119
134
  return {
120
135
  code,
121
136
  filename: 'Component.jsx',
122
137
  imports: importLines,
123
- lineCount: code.split('\n').length,
138
+ lineCount: lines.length,
124
139
  tokenCount: code.split(/\s+/).length,
140
+ sourceMap: smb.toJSON(),
125
141
  };
126
142
  }
127
143
  /** Generate framework-agnostic backend/infrastructure code for a non-UI node. Returns null if unrecognized. */
@@ -162,12 +178,14 @@ export function generateBackendCode(node) {
162
178
  }
163
179
  function generateTopLevel(node) {
164
180
  const c = ctx();
165
- // First pass: collect states, derived, styles
181
+ // First pass: collect states, derived, props, styles
166
182
  for (const child of node.body) {
167
183
  if (child.type === 'StateDecl')
168
184
  c.states.set(child.name, child);
169
185
  if (child.type === 'DerivedDecl')
170
186
  c.derivedNames.add(child.name);
187
+ if (child.type === 'PropDecl')
188
+ c.propNames.add(child.name);
171
189
  if (child.type === 'StyleDecl')
172
190
  c.styles.set(child.name, child);
173
191
  }
@@ -259,6 +277,11 @@ function generateTopLevel(node) {
259
277
  case 'JsBlock':
260
278
  hookLines.push(child.code);
261
279
  break;
280
+ case 'TopLevelVarDecl': {
281
+ const tlv = child;
282
+ hookLines.push(`${tlv.keyword} ${tlv.name} = ${genExpr(tlv.value, c)};`);
283
+ break;
284
+ }
262
285
  case 'StyleDecl':
263
286
  // Collected already, used by reference
264
287
  break;
@@ -276,13 +299,17 @@ function generateTopLevel(node) {
276
299
  const propsArg = props.length > 0
277
300
  ? `{ ${props.map(p => p.defaultValue ? `${p.name} = ${genExpr(p.defaultValue, c)}` : p.name).join(', ')} }`
278
301
  : '';
279
- // Build import line
280
- const reactImports = ['React'];
302
+ // Build import line — only import hooks that are actually used
281
303
  const hookNames = Array.from(c.imports).sort();
282
- if (hookNames.length > 0) {
283
- reactImports.push(...hookNames);
284
- }
285
- const importLine = `import ${reactImports[0]}, { ${hookNames.join(', ')} } from 'react';`;
304
+ // Check if React.Fragment is used in JSX (for loops, show/hide with multiple children)
305
+ const needsReact = jsxParts.some(p => p.includes('React.Fragment'));
306
+ const importLine = needsReact && hookNames.length > 0
307
+ ? `import React, { ${hookNames.join(', ')} } from 'react';`
308
+ : needsReact
309
+ ? `import React from 'react';`
310
+ : hookNames.length > 0
311
+ ? `import { ${hookNames.join(', ')} } from 'react';`
312
+ : `import React from 'react';`;
286
313
  // Collect user imports (js import / import / use)
287
314
  const userImports = [];
288
315
  for (const child of node.body) {
@@ -303,9 +330,12 @@ function generateTopLevel(node) {
303
330
  // Build component
304
331
  const isComponent = node.type === 'Component';
305
332
  const exportKw = isComponent ? '' : 'export default ';
333
+ // Add "use client" for Next.js SSR when client-side hooks are used
334
+ const needsClient = c.imports.has('useState') || c.imports.has('useEffect') || c.imports.has('useRef') || c.imports.has('useCallback');
306
335
  const lines = [
336
+ ...(needsClient ? [`'use client';`, ''] : []),
307
337
  `// Generated by 0x`,
308
- hookNames.length > 0 ? importLine : `import React from 'react';`,
338
+ importLine,
309
339
  ...userImports,
310
340
  '',
311
341
  `${exportKw}function ${node.name}(${propsArg}) {`,
@@ -337,8 +367,9 @@ function genDerived(node, c) {
337
367
  c.readOnly = true;
338
368
  const expr = genExpr(node.expression, c);
339
369
  c.readOnly = prevReadOnly;
340
- const deps = extractDeps(node.expression, c);
341
- return `const ${node.name} = useMemo(() => ${expr}, [${deps.join(', ')}]);`;
370
+ const { deps, warning } = extractDepsWithWarning(node.expression, c);
371
+ const warnComment = warning ? ` ${warning}` : '';
372
+ return `${warnComment ? warnComment + '\n ' : ''}const ${node.name} = useMemo(() => ${expr}, [${deps.join(', ')}]);`;
342
373
  }
343
374
  function genCheck(node, c) {
344
375
  const cond = genExpr(node.condition, c);
@@ -389,13 +420,17 @@ function genOnDestroy(node, c) {
389
420
  function genWatch(node, c) {
390
421
  c.imports.add('useEffect');
391
422
  const body = node.body.map(s => genStatement(s, c)).join('\n ');
423
+ const vars = (node.variables || [node.variable]).join(', ');
392
424
  const hasAwait = bodyContainsAwait(node.body);
393
425
  if (hasAwait) {
394
- return `useEffect(() => {\n (async () => {\n ${body}\n })();\n }, [${node.variable}]);`;
426
+ return `useEffect(() => {\n (async () => {\n ${body}\n })();\n }, [${vars}]);`;
395
427
  }
396
- return `useEffect(() => {\n ${body}\n }, [${node.variable}]);`;
428
+ return `useEffect(() => {\n ${body}\n }, [${vars}]);`;
397
429
  }
398
430
  // ── UI Nodes ────────────────────────────────────────
431
+ function srcComment(node) {
432
+ return node.loc?.line ? `{/* 0x:L${node.loc.line} */}` : '';
433
+ }
399
434
  function genUINode(node, c) {
400
435
  switch (node.type) {
401
436
  case 'Layout': return genLayout(node, c);
@@ -566,7 +601,8 @@ function genLayout(node, c) {
566
601
  }
567
602
  const styleStr = genStyleObj(style, dynamicKeys);
568
603
  const children = node.children.map(ch => genUINode(ch, c)).join('\n');
569
- return `<div style={${styleStr}}>\n${children}\n</div>`;
604
+ const sc = srcComment(node);
605
+ return `${sc}<div style={${styleStr}}>\n${children}\n</div>`;
570
606
  }
571
607
  function genText(node, c) {
572
608
  const style = {};
@@ -626,7 +662,8 @@ function genText(node, c) {
626
662
  // Badge prop: render a badge indicator next to content
627
663
  const badgeExpr = node.props['badge'];
628
664
  const tooltipExpr = node.props['tooltip'];
629
- let result = `<span${styleStr}>${content}</span>`;
665
+ const sc = srcComment(node);
666
+ let result = `${sc}<span${styleStr}>${content}</span>`;
630
667
  if (badgeExpr) {
631
668
  const badge = genExpr(badgeExpr, c);
632
669
  result = `<span style={{ position: 'relative', display: 'inline-flex', alignItems: 'center' }}>\n<span${styleStr}>${content}</span>\n<span style={{ marginLeft: '6px', padding: '2px 6px', fontSize: '12px', fontWeight: 'bold', borderRadius: '9999px', backgroundColor: '#ef4444', color: '#fff', minWidth: '20px', textAlign: 'center' }}>{${badge}}</span>\n</span>`;
@@ -654,7 +691,8 @@ function genButton(node, c) {
654
691
  }
655
692
  }
656
693
  const propsStr = styleProps.join(' ');
657
- return `<button onClick={() => ${actionCode}}${propsStr ? ' ' + propsStr : ''}>${label}</button>`;
694
+ const sc = srcComment(node);
695
+ return `${sc}<button onClick={() => ${actionCode}}${propsStr ? ' ' + propsStr : ''}>${label}</button>`;
658
696
  }
659
697
  function genInput(node, c) {
660
698
  const setter = 'set' + capitalize(node.binding);
@@ -680,7 +718,8 @@ function genInput(node, c) {
680
718
  props.push(`onKeyPress={e => ${handler}(e.key)}`);
681
719
  }
682
720
  }
683
- return `<input ${props.join(' ')} />`;
721
+ const sc = srcComment(node);
722
+ return `${sc}<input ${props.join(' ')} />`;
684
723
  }
685
724
  function genImage(node, c) {
686
725
  const src = genExpr(node.src, c);
@@ -2445,14 +2484,32 @@ function buildSpreadUpdate(prevVar, path, value) {
2445
2484
  const inner = buildSpreadUpdate(`${prevVar}.${head}`, rest, value);
2446
2485
  return `({...${prevVar}, ${head}: ${inner}})`;
2447
2486
  }
2448
- function extractDeps(expr, c) {
2487
+ const KNOWN_GLOBALS = new Set([
2488
+ 'console', 'Math', 'JSON', 'parseInt', 'parseFloat', 'Number', 'String', 'Boolean',
2489
+ 'Array', 'Object', 'Date', 'RegExp', 'Error', 'Promise', 'Map', 'Set', 'Symbol',
2490
+ 'window', 'document', 'navigator', 'localStorage', 'sessionStorage', 'fetch',
2491
+ 'setTimeout', 'setInterval', 'clearTimeout', 'clearInterval', 'requestAnimationFrame',
2492
+ 'undefined', 'NaN', 'Infinity', 'globalThis', 'alert', 'confirm', 'prompt',
2493
+ 'encodeURIComponent', 'decodeURIComponent', 'encodeURI', 'decodeURI', 'btoa', 'atob',
2494
+ 'e', 'event', 'prev', 'item', 'index', 'key', 'value', 'i', 'j', 'k', 'v', 'x', 'el',
2495
+ ]);
2496
+ function extractDepsWithWarning(expr, c) {
2449
2497
  const deps = new Set();
2498
+ const unknowns = new Set();
2450
2499
  walkExpr(expr, e => {
2451
- if (e.kind === 'identifier' && (c.states.has(e.name) || c.derivedNames.has(e.name))) {
2452
- deps.add(e.name);
2500
+ if (e.kind === 'identifier') {
2501
+ if (c.states.has(e.name) || c.derivedNames.has(e.name) || c.propNames.has(e.name)) {
2502
+ deps.add(e.name);
2503
+ }
2504
+ else if (!KNOWN_GLOBALS.has(e.name)) {
2505
+ unknowns.add(e.name);
2506
+ }
2453
2507
  }
2454
2508
  });
2455
- return Array.from(deps);
2509
+ const warning = unknowns.size > 0
2510
+ ? `/* 0x-warn: untracked deps: ${Array.from(unknowns).join(', ')} */`
2511
+ : null;
2512
+ return { deps: Array.from(deps), warning };
2456
2513
  }
2457
2514
  function walkExpr(expr, fn) {
2458
2515
  fn(expr);
@@ -2487,8 +2544,12 @@ function walkExpr(expr, fn) {
2487
2544
  expr.properties.forEach(p => walkExpr(p.value, fn));
2488
2545
  break;
2489
2546
  case 'arrow':
2490
- if (!Array.isArray(expr.body))
2547
+ if (Array.isArray(expr.body)) {
2548
+ walkStmts(expr.body, fn);
2549
+ }
2550
+ else {
2491
2551
  walkExpr(expr.body, fn);
2552
+ }
2492
2553
  break;
2493
2554
  case 'template':
2494
2555
  expr.parts.forEach(p => { if (typeof p !== 'string')
@@ -2503,6 +2564,37 @@ function walkExpr(expr, fn) {
2503
2564
  break;
2504
2565
  }
2505
2566
  }
2567
+ function walkStmts(stmts, fn) {
2568
+ for (const stmt of stmts) {
2569
+ switch (stmt.kind) {
2570
+ case 'expr_stmt':
2571
+ walkExpr(stmt.expression, fn);
2572
+ break;
2573
+ case 'return':
2574
+ if (stmt.value)
2575
+ walkExpr(stmt.value, fn);
2576
+ break;
2577
+ case 'var_decl':
2578
+ walkExpr(stmt.value, fn);
2579
+ break;
2580
+ case 'assignment_stmt':
2581
+ walkExpr(stmt.target, fn);
2582
+ walkExpr(stmt.value, fn);
2583
+ break;
2584
+ case 'if_stmt':
2585
+ walkExpr(stmt.condition, fn);
2586
+ walkStmts(stmt.body, fn);
2587
+ stmt.elifs.forEach(ei => { walkExpr(ei.condition, fn); walkStmts(ei.body, fn); });
2588
+ if (stmt.elseBody)
2589
+ walkStmts(stmt.elseBody, fn);
2590
+ break;
2591
+ case 'for_stmt':
2592
+ walkExpr(stmt.iterable, fn);
2593
+ walkStmts(stmt.body, fn);
2594
+ break;
2595
+ }
2596
+ }
2597
+ }
2506
2598
  function genStyleObj(style, dynamicKeys) {
2507
2599
  if (Object.keys(style).length === 0)
2508
2600
  return '{}';