@alloy-js/core 0.22.0 → 0.23.0-dev.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,222 @@
1
+ import pc from "picocolors";
2
+ import { contextsByKey } from "./context.js";
3
+ import { SourceDirectoryContext } from "./context/source-directory.js";
4
+ import { SourceFileContext } from "./context/source-file.js";
5
+ import { Context, getContext } from "./reactivity.js";
6
+ import { Component, Props, SourceLocation } from "./runtime/component.js";
7
+
8
+ // Store render stack for error diagnostics
9
+ const renderStack: Array<{
10
+ component: Component<any>;
11
+ props: Props;
12
+ context: Context | null;
13
+ source?: SourceLocation;
14
+ }> = [];
15
+
16
+ export function pushStack(
17
+ component: Component<any>,
18
+ props: Props,
19
+ source?: SourceLocation,
20
+ ) {
21
+ renderStack.push({ component, props, context: getContext(), source });
22
+ }
23
+
24
+ export function popStack() {
25
+ renderStack.pop();
26
+ }
27
+
28
+ export function clearRenderStack() {
29
+ renderStack.length = 0;
30
+ }
31
+
32
+ // Helper functions
33
+ function getComponentDisplayName(
34
+ component: Component<any>,
35
+ props: Props,
36
+ ): string {
37
+ // Check if this is a Provider and if we can find its context name
38
+ if (component.name === "Provider") {
39
+ // Try to find the context this provider is associated with
40
+ for (const ctx of contextsByKey.values()) {
41
+ if (ctx.Provider === component && ctx.name) {
42
+ return ctx.name;
43
+ }
44
+ }
45
+ }
46
+ return component.name;
47
+ }
48
+
49
+ function inspectProps(props: Props) {
50
+ const entries = Object.entries(props)
51
+ .filter(([key]) => key !== "children") // Exclude children prop
52
+ .map(([key, value]) => {
53
+ const formattedValue = formatValue(value);
54
+ return `${pc.dim(key)}: ${formattedValue}`;
55
+ });
56
+
57
+ return entries.length > 0 ? entries.join(pc.dim(", ")) : "";
58
+ }
59
+
60
+ function formatValue(value: unknown): string {
61
+ switch (typeof value) {
62
+ case "string":
63
+ return pc.blue(`"${value}"`);
64
+ case "number":
65
+ case "boolean":
66
+ return pc.blue(String(value));
67
+ case "undefined":
68
+ return pc.gray("undefined");
69
+ case "object":
70
+ return value ? pc.gray("{...}") : pc.gray("null");
71
+ case "function":
72
+ return pc.gray("function");
73
+ default:
74
+ return pc.gray(String(value));
75
+ }
76
+ }
77
+
78
+ function formatSourceLocation(source: SourceLocation): string {
79
+ const cwd = process.cwd();
80
+ let filePath = source.fileName;
81
+
82
+ // Convert to relative path if under cwd
83
+ if (filePath.startsWith(cwd)) {
84
+ filePath = filePath.slice(cwd.length + 1); // +1 to remove leading slash
85
+ }
86
+
87
+ return `${filePath}:${source.lineNumber}:${source.columnNumber}`;
88
+ }
89
+
90
+ /**
91
+ * Print the current render stack to the console for debugging.
92
+ *
93
+ * This differs from debug.component.stack in that this uses a purpose-built
94
+ * stack rather than walking the context chain. When this is called, the context
95
+ * chain has been restored. In the future this can probably be unified nicely.
96
+ */
97
+ export function printRenderStack() {
98
+ // Find the nearest SourceFileContext or SourceDirectoryContext from the render stack
99
+ let currentPath: string | undefined;
100
+ for (let i = renderStack.length - 1; i >= 0; i--) {
101
+ const { context } = renderStack[i];
102
+ // Prefer SourceFileContext over SourceDirectoryContext
103
+ if (context?.context?.[SourceFileContext.id]) {
104
+ const fileContext = context.context[
105
+ SourceFileContext.id
106
+ ] as SourceFileContext;
107
+ currentPath = fileContext.path;
108
+ break;
109
+ }
110
+ if (!currentPath && context?.context?.[SourceDirectoryContext.id]) {
111
+ const dirContext = context.context[
112
+ SourceDirectoryContext.id
113
+ ] as SourceDirectoryContext;
114
+ currentPath = dirContext.path;
115
+ // Don't break - keep looking for a SourceFileContext
116
+ }
117
+ }
118
+
119
+ if (currentPath) {
120
+ // eslint-disable-next-line no-console
121
+ console.error(pc.red(`Error rendering in file ${currentPath}`));
122
+ } else {
123
+ // eslint-disable-next-line no-console
124
+ console.error(pc.red("Error rendering:"));
125
+ }
126
+
127
+ // First pass: determine which providers should be nested vs standalone
128
+ // A provider should be nested under its parent if it's from a different file
129
+ // (i.e., it's part of the component's implementation, not user-provided)
130
+ const nestedProviderIndices = new Set<number>();
131
+
132
+ for (let i = renderStack.length - 1; i >= 0; i--) {
133
+ const { component, source } = renderStack[i];
134
+
135
+ // Skip anonymous components and providers
136
+ if (!component.name || component.name === "Provider") {
137
+ continue;
138
+ }
139
+
140
+ // Look for providers that come immediately after this component
141
+ for (let j = i + 1; j < renderStack.length; j++) {
142
+ const providerEntry = renderStack[j];
143
+ if (!providerEntry.component.name) continue;
144
+ if (providerEntry.component.name !== "Provider") break;
145
+
146
+ // Nest provider if it's from a different file (component-internal)
147
+ const shouldNest =
148
+ !source ||
149
+ !providerEntry.source ||
150
+ source.fileName !== providerEntry.source.fileName;
151
+
152
+ if (shouldNest) {
153
+ nestedProviderIndices.add(j);
154
+ } else {
155
+ // Stop looking - this provider and all after should be standalone
156
+ break;
157
+ }
158
+ }
159
+ }
160
+
161
+ // Second pass: print stack entries in order, nesting providers where appropriate
162
+ for (let i = renderStack.length - 1; i >= 0; i--) {
163
+ const { component, props, source } = renderStack[i];
164
+
165
+ // Skip anonymous components
166
+ if (!component.name) {
167
+ continue;
168
+ }
169
+
170
+ // Skip providers that will be nested under their parent
171
+ if (nestedProviderIndices.has(i)) {
172
+ continue;
173
+ }
174
+
175
+ const displayName = getComponentDisplayName(component, props);
176
+ const sourceStr =
177
+ source ? pc.gray(` (${formatSourceLocation(source)})`) : "";
178
+
179
+ // eslint-disable-next-line no-console
180
+ console.error(` ${pc.cyan("at")} ${pc.bold(displayName)}${sourceStr}`);
181
+
182
+ // Print props on next line if there are any
183
+ const propsStr = inspectProps(props);
184
+ if (propsStr) {
185
+ // eslint-disable-next-line no-console
186
+ console.error(` ${propsStr}`);
187
+ }
188
+
189
+ // For non-Provider components, print any nested providers
190
+ if (component.name !== "Provider") {
191
+ for (let j = i + 1; j < renderStack.length; j++) {
192
+ if (!nestedProviderIndices.has(j)) break;
193
+
194
+ const providerEntry = renderStack[j];
195
+ if (!providerEntry.component.name) continue;
196
+
197
+ const providerName = getComponentDisplayName(
198
+ providerEntry.component,
199
+ providerEntry.props,
200
+ );
201
+ const providerSourceStr =
202
+ providerEntry.source ?
203
+ pc.gray(` (${formatSourceLocation(providerEntry.source)})`)
204
+ : "";
205
+
206
+ // eslint-disable-next-line no-console
207
+ console.error(
208
+ ` ${pc.magenta("provides")} ${pc.bold(providerName)}${providerSourceStr}`,
209
+ );
210
+
211
+ const providerPropsStr = inspectProps(providerEntry.props);
212
+ if (providerPropsStr) {
213
+ // eslint-disable-next-line no-console
214
+ console.error(` ${providerPropsStr}`);
215
+ }
216
+ }
217
+ }
218
+ }
219
+
220
+ // Clear the stack after printing to avoid stale data on subsequent render errors
221
+ clearRenderStack();
222
+ }
package/src/render.ts CHANGED
@@ -3,7 +3,6 @@ import { Doc, doc } from "prettier";
3
3
  import prettier from "prettier/doc.js";
4
4
  import { useContext } from "./context.js";
5
5
  import { SourceFileContext } from "./context/source-file.js";
6
- import { shouldDebug } from "./debug.js";
7
6
  import {
8
7
  Context,
9
8
  CustomContext,
@@ -15,13 +14,12 @@ import {
15
14
  untrack,
16
15
  } from "./reactivity.js";
17
16
  import { isRefkeyable, toRefkey } from "./refkey.js";
17
+ import { popStack, printRenderStack, pushStack } from "./render-stack.js";
18
18
  import {
19
19
  Child,
20
20
  Children,
21
- Component,
22
21
  isComponentCreator,
23
22
  isRenderableObject,
24
- Props,
25
23
  RENDERABLE,
26
24
  } from "./runtime/component.js";
27
25
  import { IntrinsicElement, isIntrinsicElement } from "./runtime/intrinsic.js";
@@ -585,7 +583,8 @@ function appendChild(node: RenderedTextTree, rawChild: Child) {
585
583
 
586
584
  if (context) context.componentOwner = child;
587
585
  const componentRoot: RenderedTextTree = [];
588
- pushStack(child.component, child.props);
586
+
587
+ pushStack(child.component, child.props, child.source);
589
588
  renderWorker(componentRoot, untrack(child));
590
589
  popStack();
591
590
  node.push(componentRoot);
@@ -768,57 +767,3 @@ function printTreeWorker(tree: RenderedTextTree): Doc {
768
767
 
769
768
  return doc;
770
769
  }
771
- // debugging utilities
772
- const renderStack: {
773
- component: Component<any>;
774
- props: Props;
775
- }[] = [];
776
-
777
- export function pushStack(component: Component<any>, props: Props) {
778
- if (!shouldDebug()) return;
779
- renderStack.push({ component, props });
780
- }
781
-
782
- export function popStack() {
783
- if (!shouldDebug()) return;
784
- renderStack.pop();
785
- }
786
-
787
- export function printRenderStack() {
788
- if (!shouldDebug()) return;
789
-
790
- // eslint-disable-next-line no-console
791
- console.error("Error rendering:");
792
- for (let i = renderStack.length - 1; i >= 0; i--) {
793
- const { component, props } = renderStack[i];
794
- // eslint-disable-next-line no-console
795
- console.error(` at ${component.name}(${inspectProps(props)})`);
796
- }
797
- }
798
-
799
- function inspectProps(props: Props) {
800
- return JSON.stringify(
801
- Object.fromEntries(
802
- Object.entries(props).map(([key, value]) => {
803
- let safeValue;
804
- switch (typeof value) {
805
- case "string":
806
- case "number":
807
- case "boolean":
808
- safeValue = value;
809
- break;
810
- case "undefined":
811
- safeValue = "undefined";
812
- break;
813
- case "object":
814
- safeValue = value ? "{...}" : null;
815
- break;
816
- case "function":
817
- safeValue = "function";
818
- break;
819
- }
820
- return [key, safeValue];
821
- }),
822
- ),
823
- );
824
- }
@@ -51,6 +51,12 @@ export type Child =
51
51
  export type Children = Child | Children[];
52
52
  export type Props = Record<string, any>;
53
53
 
54
+ export interface SourceLocation {
55
+ fileName: string;
56
+ lineNumber: number;
57
+ columnNumber: number;
58
+ }
59
+
54
60
  export interface ComponentDefinition<TProps = Props> {
55
61
  (props: TProps): Children;
56
62
  }
@@ -64,6 +70,7 @@ export interface ComponentCreator<TProps = Props> {
64
70
  (): Children;
65
71
  props: TProps;
66
72
  tag?: symbol;
73
+ source?: SourceLocation;
67
74
  }
68
75
 
69
76
  export function isComponentCreator<TProps = any>(
@@ -79,6 +86,7 @@ export function isComponentCreator<TProps = any>(
79
86
  export function createComponent<TProps extends Props = Props>(
80
87
  C: Component<TProps>,
81
88
  props: TProps,
89
+ source?: SourceLocation,
82
90
  ): ComponentCreator<TProps> {
83
91
  const creator: ComponentCreator<TProps> = () => /* */ C(props);
84
92
  creator.props = props;
@@ -86,6 +94,9 @@ export function createComponent<TProps extends Props = Props>(
86
94
  if (C.tag) {
87
95
  creator.tag = C.tag;
88
96
  }
97
+ if (source) {
98
+ creator.source = source;
99
+ }
89
100
 
90
101
  return creator;
91
102
  }
package/temp/api.json CHANGED
@@ -3061,6 +3061,34 @@
3061
3061
  "endIndex": 2
3062
3062
  }
3063
3063
  },
3064
+ {
3065
+ "kind": "PropertySignature",
3066
+ "canonicalReference": "@alloy-js/core!ComponentCreator#source:member",
3067
+ "docComment": "",
3068
+ "excerptTokens": [
3069
+ {
3070
+ "kind": "Content",
3071
+ "text": "source?: "
3072
+ },
3073
+ {
3074
+ "kind": "Reference",
3075
+ "text": "SourceLocation",
3076
+ "canonicalReference": "@alloy-js/core!SourceLocation:interface"
3077
+ },
3078
+ {
3079
+ "kind": "Content",
3080
+ "text": ";"
3081
+ }
3082
+ ],
3083
+ "isReadonly": false,
3084
+ "isOptional": true,
3085
+ "releaseTag": "Public",
3086
+ "name": "source",
3087
+ "propertyTypeTokenRange": {
3088
+ "startIndex": 1,
3089
+ "endIndex": 2
3090
+ }
3091
+ },
3064
3092
  {
3065
3093
  "kind": "PropertySignature",
3066
3094
  "canonicalReference": "@alloy-js/core!ComponentCreator#tag:member",
@@ -4322,6 +4350,15 @@
4322
4350
  "kind": "Content",
4323
4351
  "text": "TProps"
4324
4352
  },
4353
+ {
4354
+ "kind": "Content",
4355
+ "text": ", source?: "
4356
+ },
4357
+ {
4358
+ "kind": "Reference",
4359
+ "text": "SourceLocation",
4360
+ "canonicalReference": "@alloy-js/core!SourceLocation:interface"
4361
+ },
4325
4362
  {
4326
4363
  "kind": "Content",
4327
4364
  "text": "): "
@@ -4342,8 +4379,8 @@
4342
4379
  ],
4343
4380
  "fileUrlPath": "src/runtime/component.ts",
4344
4381
  "returnTypeTokenRange": {
4345
- "startIndex": 10,
4346
- "endIndex": 12
4382
+ "startIndex": 12,
4383
+ "endIndex": 14
4347
4384
  },
4348
4385
  "releaseTag": "Public",
4349
4386
  "overloadIndex": 1,
@@ -4363,6 +4400,14 @@
4363
4400
  "endIndex": 9
4364
4401
  },
4365
4402
  "isOptional": false
4403
+ },
4404
+ {
4405
+ "parameterName": "source",
4406
+ "parameterTypeTokenRange": {
4407
+ "startIndex": 10,
4408
+ "endIndex": 11
4409
+ },
4410
+ "isOptional": true
4366
4411
  }
4367
4412
  ],
4368
4413
  "typeParameters": [
@@ -17135,34 +17180,6 @@
17135
17180
  ],
17136
17181
  "extendsTokenRanges": []
17137
17182
  },
17138
- {
17139
- "kind": "Function",
17140
- "canonicalReference": "@alloy-js/core!popStack:function(1)",
17141
- "docComment": "",
17142
- "excerptTokens": [
17143
- {
17144
- "kind": "Content",
17145
- "text": "export declare function popStack(): "
17146
- },
17147
- {
17148
- "kind": "Content",
17149
- "text": "void"
17150
- },
17151
- {
17152
- "kind": "Content",
17153
- "text": ";"
17154
- }
17155
- ],
17156
- "fileUrlPath": "src/render.ts",
17157
- "returnTypeTokenRange": {
17158
- "startIndex": 1,
17159
- "endIndex": 2
17160
- },
17161
- "releaseTag": "Public",
17162
- "overloadIndex": 1,
17163
- "parameters": [],
17164
- "name": "popStack"
17165
- },
17166
17183
  {
17167
17184
  "kind": "Interface",
17168
17185
  "canonicalReference": "@alloy-js/core!PrintHook:interface",
@@ -17392,34 +17409,6 @@
17392
17409
  "endIndex": 2
17393
17410
  }
17394
17411
  },
17395
- {
17396
- "kind": "Function",
17397
- "canonicalReference": "@alloy-js/core!printRenderStack:function(1)",
17398
- "docComment": "",
17399
- "excerptTokens": [
17400
- {
17401
- "kind": "Content",
17402
- "text": "export declare function printRenderStack(): "
17403
- },
17404
- {
17405
- "kind": "Content",
17406
- "text": "void"
17407
- },
17408
- {
17409
- "kind": "Content",
17410
- "text": ";"
17411
- }
17412
- ],
17413
- "fileUrlPath": "src/render.ts",
17414
- "returnTypeTokenRange": {
17415
- "startIndex": 1,
17416
- "endIndex": 2
17417
- },
17418
- "releaseTag": "Public",
17419
- "overloadIndex": 1,
17420
- "parameters": [],
17421
- "name": "printRenderStack"
17422
- },
17423
17412
  {
17424
17413
  "kind": "Function",
17425
17414
  "canonicalReference": "@alloy-js/core!printTree:function(1)",
@@ -17733,73 +17722,6 @@
17733
17722
  ],
17734
17723
  "extendsTokenRanges": []
17735
17724
  },
17736
- {
17737
- "kind": "Function",
17738
- "canonicalReference": "@alloy-js/core!pushStack:function(1)",
17739
- "docComment": "",
17740
- "excerptTokens": [
17741
- {
17742
- "kind": "Content",
17743
- "text": "export declare function pushStack(component: "
17744
- },
17745
- {
17746
- "kind": "Reference",
17747
- "text": "Component",
17748
- "canonicalReference": "@alloy-js/core!Component:interface"
17749
- },
17750
- {
17751
- "kind": "Content",
17752
- "text": "<any>"
17753
- },
17754
- {
17755
- "kind": "Content",
17756
- "text": ", props: "
17757
- },
17758
- {
17759
- "kind": "Reference",
17760
- "text": "Props",
17761
- "canonicalReference": "@alloy-js/core!Props:type"
17762
- },
17763
- {
17764
- "kind": "Content",
17765
- "text": "): "
17766
- },
17767
- {
17768
- "kind": "Content",
17769
- "text": "void"
17770
- },
17771
- {
17772
- "kind": "Content",
17773
- "text": ";"
17774
- }
17775
- ],
17776
- "fileUrlPath": "src/render.ts",
17777
- "returnTypeTokenRange": {
17778
- "startIndex": 6,
17779
- "endIndex": 7
17780
- },
17781
- "releaseTag": "Public",
17782
- "overloadIndex": 1,
17783
- "parameters": [
17784
- {
17785
- "parameterName": "component",
17786
- "parameterTypeTokenRange": {
17787
- "startIndex": 1,
17788
- "endIndex": 3
17789
- },
17790
- "isOptional": false
17791
- },
17792
- {
17793
- "parameterName": "props",
17794
- "parameterTypeTokenRange": {
17795
- "startIndex": 4,
17796
- "endIndex": 5
17797
- },
17798
- "isOptional": false
17799
- }
17800
- ],
17801
- "name": "pushStack"
17802
- },
17803
17725
  {
17804
17726
  "kind": "Class",
17805
17727
  "canonicalReference": "@alloy-js/core!ReactiveUnionSet:class",
@@ -21565,6 +21487,105 @@
21565
21487
  ],
21566
21488
  "name": "sourceFilesForTreeAsync"
21567
21489
  },
21490
+ {
21491
+ "kind": "Interface",
21492
+ "canonicalReference": "@alloy-js/core!SourceLocation:interface",
21493
+ "docComment": "",
21494
+ "excerptTokens": [
21495
+ {
21496
+ "kind": "Content",
21497
+ "text": "export interface SourceLocation "
21498
+ }
21499
+ ],
21500
+ "fileUrlPath": "src/runtime/component.ts",
21501
+ "releaseTag": "Public",
21502
+ "name": "SourceLocation",
21503
+ "preserveMemberOrder": false,
21504
+ "members": [
21505
+ {
21506
+ "kind": "PropertySignature",
21507
+ "canonicalReference": "@alloy-js/core!SourceLocation#columnNumber:member",
21508
+ "docComment": "",
21509
+ "excerptTokens": [
21510
+ {
21511
+ "kind": "Content",
21512
+ "text": "columnNumber: "
21513
+ },
21514
+ {
21515
+ "kind": "Content",
21516
+ "text": "number"
21517
+ },
21518
+ {
21519
+ "kind": "Content",
21520
+ "text": ";"
21521
+ }
21522
+ ],
21523
+ "isReadonly": false,
21524
+ "isOptional": false,
21525
+ "releaseTag": "Public",
21526
+ "name": "columnNumber",
21527
+ "propertyTypeTokenRange": {
21528
+ "startIndex": 1,
21529
+ "endIndex": 2
21530
+ }
21531
+ },
21532
+ {
21533
+ "kind": "PropertySignature",
21534
+ "canonicalReference": "@alloy-js/core!SourceLocation#fileName:member",
21535
+ "docComment": "",
21536
+ "excerptTokens": [
21537
+ {
21538
+ "kind": "Content",
21539
+ "text": "fileName: "
21540
+ },
21541
+ {
21542
+ "kind": "Content",
21543
+ "text": "string"
21544
+ },
21545
+ {
21546
+ "kind": "Content",
21547
+ "text": ";"
21548
+ }
21549
+ ],
21550
+ "isReadonly": false,
21551
+ "isOptional": false,
21552
+ "releaseTag": "Public",
21553
+ "name": "fileName",
21554
+ "propertyTypeTokenRange": {
21555
+ "startIndex": 1,
21556
+ "endIndex": 2
21557
+ }
21558
+ },
21559
+ {
21560
+ "kind": "PropertySignature",
21561
+ "canonicalReference": "@alloy-js/core!SourceLocation#lineNumber:member",
21562
+ "docComment": "",
21563
+ "excerptTokens": [
21564
+ {
21565
+ "kind": "Content",
21566
+ "text": "lineNumber: "
21567
+ },
21568
+ {
21569
+ "kind": "Content",
21570
+ "text": "number"
21571
+ },
21572
+ {
21573
+ "kind": "Content",
21574
+ "text": ";"
21575
+ }
21576
+ ],
21577
+ "isReadonly": false,
21578
+ "isOptional": false,
21579
+ "releaseTag": "Public",
21580
+ "name": "lineNumber",
21581
+ "propertyTypeTokenRange": {
21582
+ "startIndex": 1,
21583
+ "endIndex": 2
21584
+ }
21585
+ }
21586
+ ],
21587
+ "extendsTokenRanges": []
21588
+ },
21568
21589
  {
21569
21590
  "kind": "Function",
21570
21591
  "canonicalReference": "@alloy-js/core!splitProps:function(1)",