@clayroach/unplugin 0.1.0-source-trace.2 → 0.1.0-source-trace.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.
@@ -0,0 +1,140 @@
1
+ const PURE_ANNOTATION = "#__PURE__";
2
+ /**
3
+ * Checks if a node already has a PURE annotation.
4
+ */
5
+ function isPureAnnotated(node) {
6
+ const leadingComments = node.leadingComments;
7
+ if (!leadingComments) {
8
+ return false;
9
+ }
10
+ return leadingComments.some((comment) => /[@#]__PURE__/.test(comment.value));
11
+ }
12
+ /**
13
+ * Adds a PURE annotation comment to a node.
14
+ */
15
+ function annotateAsPure(path) {
16
+ if (isPureAnnotated(path.node)) {
17
+ return;
18
+ }
19
+ path.addComment("leading", PURE_ANNOTATION);
20
+ }
21
+ /**
22
+ * Checks if the parent is a CallExpression or NewExpression.
23
+ */
24
+ function hasCallableParent(path) {
25
+ const parentPath = path.parentPath;
26
+ if (!parentPath)
27
+ return false;
28
+ return parentPath.isCallExpression() || parentPath.isNewExpression();
29
+ }
30
+ /**
31
+ * Checks if this node is used as a callee (e.g., `foo()` where foo is the callee).
32
+ */
33
+ function isUsedAsCallee(path) {
34
+ if (!hasCallableParent(path)) {
35
+ return false;
36
+ }
37
+ const parentPath = path.parentPath;
38
+ return parentPath.get("callee") === path;
39
+ }
40
+ /**
41
+ * Checks if this node is inside a callee chain (e.g., `foo()()` or `foo.bar()`).
42
+ */
43
+ function isInCallee(path) {
44
+ let current = path;
45
+ do {
46
+ current = current.parentPath;
47
+ if (!current)
48
+ return false;
49
+ if (isUsedAsCallee(current)) {
50
+ return true;
51
+ }
52
+ } while (!current.isStatement() && !current.isFunction());
53
+ return false;
54
+ }
55
+ /**
56
+ * Checks if this expression is executed during module initialization
57
+ * (not inside a function that isn't immediately invoked).
58
+ */
59
+ function isExecutedDuringInitialization(path) {
60
+ let functionParent = path.getFunctionParent();
61
+ while (functionParent) {
62
+ if (!isUsedAsCallee(functionParent)) {
63
+ return false;
64
+ }
65
+ functionParent = functionParent.getFunctionParent();
66
+ }
67
+ return true;
68
+ }
69
+ /**
70
+ * Checks if this expression is in an assignment context
71
+ * (variable declaration, assignment expression, or class).
72
+ */
73
+ function isInAssignmentContext(path) {
74
+ const statement = path.getStatementParent();
75
+ if (!statement)
76
+ return false;
77
+ let currentPath = path;
78
+ do {
79
+ currentPath = currentPath.parentPath;
80
+ if (!currentPath)
81
+ return false;
82
+ if (currentPath.isVariableDeclaration() ||
83
+ currentPath.isAssignmentExpression() ||
84
+ currentPath.isClass()) {
85
+ return true;
86
+ }
87
+ } while (currentPath !== statement);
88
+ return false;
89
+ }
90
+ /**
91
+ * Visitor function for CallExpression and NewExpression nodes.
92
+ */
93
+ function callableExpressionVisitor(path, _state) {
94
+ // Skip if this is used as a callee (e.g., foo()())
95
+ if (isUsedAsCallee(path)) {
96
+ return;
97
+ }
98
+ // Skip if this is inside a callee chain
99
+ if (isInCallee(path)) {
100
+ return;
101
+ }
102
+ // Skip if not executed during initialization
103
+ if (!isExecutedDuringInitialization(path)) {
104
+ return;
105
+ }
106
+ // Must be in assignment context or export default
107
+ const statement = path.getStatementParent();
108
+ if (!isInAssignmentContext(path) && !statement?.isExportDefaultDeclaration()) {
109
+ return;
110
+ }
111
+ annotateAsPure(path);
112
+ }
113
+ /**
114
+ * Creates a Babel visitor that adds PURE annotations to call expressions.
115
+ */
116
+ export function createAnnotateEffectsVisitor(filename, _options) {
117
+ return {
118
+ Program: {
119
+ enter(_path, state) {
120
+ state.filename = filename;
121
+ }
122
+ },
123
+ CallExpression(path, state) {
124
+ callableExpressionVisitor(path, state);
125
+ },
126
+ NewExpression(path, state) {
127
+ callableExpressionVisitor(path, state);
128
+ }
129
+ };
130
+ }
131
+ /**
132
+ * Creates the annotate effects transformer plugin.
133
+ */
134
+ export function annotateEffectsTransformer(options) {
135
+ return {
136
+ name: "effect-annotate-pure-calls",
137
+ visitor: createAnnotateEffectsVisitor("", options)
138
+ };
139
+ }
140
+ //# sourceMappingURL=annotateEffects.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"annotateEffects.js","sourceRoot":"","sources":["../../../src/transformers/annotateEffects.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,160 @@
1
+ import * as t from "@babel/types";
2
+ import { createHoistingState } from "../utils/hoisting.js";
3
+ /**
4
+ * Checks if a CallExpression is a withSpan call.
5
+ * Matches: Effect.withSpan(...), _.withSpan(...), or standalone withSpan(...)
6
+ */
7
+ function isWithSpanCall(node) {
8
+ const callee = node.callee;
9
+ // Match Effect.withSpan(...) or _.withSpan(...)
10
+ if (callee.type === "MemberExpression" &&
11
+ callee.property.type === "Identifier" &&
12
+ callee.property.name === "withSpan") {
13
+ return true;
14
+ }
15
+ // Match standalone withSpan(...)
16
+ if (callee.type === "Identifier" && callee.name === "withSpan") {
17
+ return true;
18
+ }
19
+ return false;
20
+ }
21
+ /**
22
+ * Determines if this is a data-first call (effect as first arg) or data-last (name as first arg).
23
+ * Data-first: withSpan(effect, "name", options?)
24
+ * Data-last: withSpan("name", options?)
25
+ */
26
+ function isDataFirstCall(node) {
27
+ // If first argument is a string literal, it's data-last
28
+ if (node.arguments.length > 0 && t.isStringLiteral(node.arguments[0])) {
29
+ return false;
30
+ }
31
+ // If second argument exists and is a string literal, it's data-first
32
+ if (node.arguments.length > 1 && t.isStringLiteral(node.arguments[1])) {
33
+ return true;
34
+ }
35
+ // Default to data-last pattern
36
+ return false;
37
+ }
38
+ /**
39
+ * Creates the attributes object with source location.
40
+ */
41
+ function createSourceAttributes(filepath, line, column) {
42
+ return t.objectExpression([
43
+ t.objectProperty(t.stringLiteral("code.filepath"), t.stringLiteral(filepath)),
44
+ t.objectProperty(t.stringLiteral("code.lineno"), t.numericLiteral(line)),
45
+ t.objectProperty(t.stringLiteral("code.column"), t.numericLiteral(column))
46
+ ]);
47
+ }
48
+ /**
49
+ * Merges source attributes into an existing options object or creates a new one.
50
+ */
51
+ function mergeOrCreateOptions(existingOptions, sourceAttrs) {
52
+ if (!existingOptions || t.isSpreadElement(existingOptions) || t.isArgumentPlaceholder(existingOptions)) {
53
+ // No existing options, create new object with attributes
54
+ return t.objectExpression([
55
+ t.objectProperty(t.identifier("attributes"), sourceAttrs)
56
+ ]);
57
+ }
58
+ if (t.isObjectExpression(existingOptions)) {
59
+ // Check if there's an existing attributes property
60
+ const existingAttrsIndex = existingOptions.properties.findIndex((prop) => t.isObjectProperty(prop) &&
61
+ ((t.isIdentifier(prop.key) && prop.key.name === "attributes") ||
62
+ (t.isStringLiteral(prop.key) && prop.key.value === "attributes")));
63
+ if (existingAttrsIndex >= 0) {
64
+ // Merge with existing attributes using spread
65
+ const existingAttrsProp = existingOptions.properties[existingAttrsIndex];
66
+ const mergedAttrs = t.objectExpression([
67
+ t.spreadElement(existingAttrsProp.value),
68
+ ...sourceAttrs.properties
69
+ ]);
70
+ // Clone the options and replace the attributes property
71
+ const newProperties = [...existingOptions.properties];
72
+ newProperties[existingAttrsIndex] = t.objectProperty(t.identifier("attributes"), mergedAttrs);
73
+ return t.objectExpression(newProperties);
74
+ }
75
+ else {
76
+ // Add new attributes property to existing object
77
+ return t.objectExpression([
78
+ ...existingOptions.properties,
79
+ t.objectProperty(t.identifier("attributes"), sourceAttrs)
80
+ ]);
81
+ }
82
+ }
83
+ // If it's a variable reference, spread it and add attributes
84
+ return t.objectExpression([
85
+ t.spreadElement(existingOptions),
86
+ t.objectProperty(t.identifier("attributes"), sourceAttrs)
87
+ ]);
88
+ }
89
+ /**
90
+ * Creates a Babel visitor that injects source location attributes into Effect.withSpan calls.
91
+ */
92
+ export function createWithSpanTraceVisitor(filename, _options) {
93
+ return {
94
+ Program: {
95
+ enter(_path, state) {
96
+ state.filename = filename;
97
+ state.hoisting = createHoistingState();
98
+ },
99
+ exit(path, state) {
100
+ // Prepend all hoisted statements to the program body
101
+ if (state.hoisting.statements.length > 0) {
102
+ path.unshiftContainer("body", state.hoisting.statements);
103
+ }
104
+ }
105
+ },
106
+ CallExpression(path, state) {
107
+ const node = path.node;
108
+ if (!isWithSpanCall(node)) {
109
+ return;
110
+ }
111
+ // Get source location
112
+ const loc = node.loc;
113
+ if (!loc)
114
+ return;
115
+ const line = loc.start.line;
116
+ const column = loc.start.column;
117
+ // Create source attributes
118
+ const sourceAttrs = createSourceAttributes(state.filename, line, column);
119
+ const isDataFirst = isDataFirstCall(node);
120
+ if (isDataFirst) {
121
+ // Data-first: withSpan(effect, "name", options?)
122
+ // Options is at index 2
123
+ const optionsArg = node.arguments[2];
124
+ const newOptions = mergeOrCreateOptions(optionsArg, sourceAttrs);
125
+ if (node.arguments.length >= 3) {
126
+ // Replace existing options
127
+ node.arguments[2] = newOptions;
128
+ }
129
+ else {
130
+ // Add options as third argument
131
+ node.arguments.push(newOptions);
132
+ }
133
+ }
134
+ else {
135
+ // Data-last: withSpan("name", options?)
136
+ // Options is at index 1
137
+ const optionsArg = node.arguments[1];
138
+ const newOptions = mergeOrCreateOptions(optionsArg, sourceAttrs);
139
+ if (node.arguments.length >= 2) {
140
+ // Replace existing options
141
+ node.arguments[1] = newOptions;
142
+ }
143
+ else {
144
+ // Add options as second argument
145
+ node.arguments.push(newOptions);
146
+ }
147
+ }
148
+ }
149
+ };
150
+ }
151
+ /**
152
+ * Creates the withSpan trace transformer plugin.
153
+ */
154
+ export function withSpanTraceTransformer(options) {
155
+ return {
156
+ name: "effect-withspan-trace",
157
+ visitor: createWithSpanTraceVisitor("", options)
158
+ };
159
+ }
160
+ //# sourceMappingURL=withSpanTrace.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"withSpanTrace.js","sourceRoot":"","sources":["../../../src/transformers/withSpanTrace.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clayroach/unplugin",
3
- "version": "0.1.0-source-trace.2",
3
+ "version": "0.1.0-source-trace.4",
4
4
  "description": "Universal bundler plugin for Effect transformations",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -18,7 +18,7 @@
18
18
  "@babel/generator": "^7.26.0"
19
19
  },
20
20
  "peerDependencies": {
21
- "@clayroach/effect": "^3.19.14-source-trace.2"
21
+ "@clayroach/effect": "^3.19.14-source-trace.3"
22
22
  },
23
23
  "publishConfig": {
24
24
  "provenance": true
package/src/index.ts CHANGED
@@ -28,7 +28,9 @@ import type { Scope, TraverseOptions, Visitor } from "@babel/traverse"
28
28
  import _traverse from "@babel/traverse"
29
29
  import type { Node } from "@babel/types"
30
30
  import { createUnplugin, type UnpluginFactory, type UnpluginInstance } from "unplugin"
31
+ import { createAnnotateEffectsVisitor } from "./transformers/annotateEffects.js"
31
32
  import { createSourceTraceVisitor } from "./transformers/sourceTrace.js"
33
+ import { createWithSpanTraceVisitor } from "./transformers/withSpanTrace.js"
32
34
  import type { HoistingState } from "./utils/hoisting.js"
33
35
 
34
36
  // Define function types for Babel packages (they export differently in ESM vs CJS)
@@ -76,6 +78,14 @@ export interface EffectPluginOptions {
76
78
  */
77
79
  readonly sourceTrace?: boolean
78
80
 
81
+ /**
82
+ * Enable source location attributes injection into Effect.withSpan() calls.
83
+ * When enabled, spans will include code.filepath, code.lineno, and code.column attributes.
84
+ *
85
+ * @default true
86
+ */
87
+ readonly spanTrace?: boolean
88
+
79
89
  /**
80
90
  * Enable @__PURE__ annotations for tree-shaking.
81
91
  *
@@ -142,10 +152,11 @@ function shouldTransform(
142
152
  */
143
153
  const unpluginFactory: UnpluginFactory<EffectPluginOptions | undefined> = (options = {}) => {
144
154
  const {
145
- annotateEffects: _annotateEffects = false,
155
+ annotateEffects = false,
146
156
  exclude = DEFAULT_EXCLUDE,
147
157
  include = DEFAULT_INCLUDE,
148
- sourceTrace = true
158
+ sourceTrace = true,
159
+ spanTrace = true
149
160
  } = options
150
161
 
151
162
  return {
@@ -169,8 +180,18 @@ const unpluginFactory: UnpluginFactory<EffectPluginOptions | undefined> = (optio
169
180
  visitors.push(createSourceTraceVisitor(id))
170
181
  }
171
182
 
183
+ if (spanTrace) {
184
+ visitors.push(createWithSpanTraceVisitor(id))
185
+ }
186
+
187
+ // Run annotateEffects separately (it has a simpler state)
188
+ if (annotateEffects) {
189
+ const annotateState = { filename: id }
190
+ traverse(ast, createAnnotateEffectsVisitor(id), undefined, annotateState)
191
+ }
192
+
172
193
  // Combine visitors
173
- if (visitors.length === 0) {
194
+ if (visitors.length === 0 && !annotateEffects) {
174
195
  return null
175
196
  }
176
197
 
@@ -0,0 +1,197 @@
1
+ /**
2
+ * Pure call annotation transformer.
3
+ *
4
+ * This transformer adds `#__PURE__` comments to call expressions for better tree-shaking.
5
+ * It replicates the behavior of `babel-plugin-annotate-pure-calls` but integrated into
6
+ * the Effect unplugin.
7
+ *
8
+ * The `#__PURE__` annotation tells bundlers (Webpack, Rollup, esbuild) that a function call
9
+ * has no side effects and can be safely removed if the result is unused.
10
+ *
11
+ * @since 0.1.0
12
+ */
13
+ import type { NodePath, Visitor } from "@babel/traverse"
14
+ import * as t from "@babel/types"
15
+
16
+ /**
17
+ * Options for the annotate effects transformer.
18
+ */
19
+ export interface AnnotateEffectsOptions {
20
+ /**
21
+ * Filter function to determine if a file should be transformed.
22
+ */
23
+ readonly filter?: (filename: string) => boolean
24
+ }
25
+
26
+ /**
27
+ * State passed through the transformer.
28
+ */
29
+ interface TransformState {
30
+ filename: string
31
+ }
32
+
33
+ const PURE_ANNOTATION = "#__PURE__"
34
+
35
+ /**
36
+ * Checks if a node already has a PURE annotation.
37
+ */
38
+ function isPureAnnotated(node: t.Node): boolean {
39
+ const leadingComments = node.leadingComments
40
+ if (!leadingComments) {
41
+ return false
42
+ }
43
+ return leadingComments.some((comment) => /[@#]__PURE__/.test(comment.value))
44
+ }
45
+
46
+ /**
47
+ * Adds a PURE annotation comment to a node.
48
+ */
49
+ function annotateAsPure(path: NodePath<t.CallExpression | t.NewExpression>): void {
50
+ if (isPureAnnotated(path.node)) {
51
+ return
52
+ }
53
+ path.addComment("leading", PURE_ANNOTATION)
54
+ }
55
+
56
+ /**
57
+ * Checks if the parent is a CallExpression or NewExpression.
58
+ */
59
+ function hasCallableParent(path: NodePath): boolean {
60
+ const parentPath = path.parentPath
61
+ if (!parentPath) return false
62
+ return parentPath.isCallExpression() || parentPath.isNewExpression()
63
+ }
64
+
65
+ /**
66
+ * Checks if this node is used as a callee (e.g., `foo()` where foo is the callee).
67
+ */
68
+ function isUsedAsCallee(path: NodePath): boolean {
69
+ if (!hasCallableParent(path)) {
70
+ return false
71
+ }
72
+ const parentPath = path.parentPath as NodePath<t.CallExpression | t.NewExpression>
73
+ return parentPath.get("callee") === path
74
+ }
75
+
76
+ /**
77
+ * Checks if this node is inside a callee chain (e.g., `foo()()` or `foo.bar()`).
78
+ */
79
+ function isInCallee(path: NodePath): boolean {
80
+ let current: NodePath | null = path
81
+ do {
82
+ current = current.parentPath
83
+ if (!current) return false
84
+ if (isUsedAsCallee(current)) {
85
+ return true
86
+ }
87
+ } while (!current.isStatement() && !current.isFunction())
88
+ return false
89
+ }
90
+
91
+ /**
92
+ * Checks if this expression is executed during module initialization
93
+ * (not inside a function that isn't immediately invoked).
94
+ */
95
+ function isExecutedDuringInitialization(path: NodePath): boolean {
96
+ let functionParent = path.getFunctionParent()
97
+ while (functionParent) {
98
+ if (!isUsedAsCallee(functionParent)) {
99
+ return false
100
+ }
101
+ functionParent = functionParent.getFunctionParent()
102
+ }
103
+ return true
104
+ }
105
+
106
+ /**
107
+ * Checks if this expression is in an assignment context
108
+ * (variable declaration, assignment expression, or class).
109
+ */
110
+ function isInAssignmentContext(path: NodePath): boolean {
111
+ const statement = path.getStatementParent()
112
+ if (!statement) return false
113
+
114
+ let currentPath: NodePath | null = path
115
+ do {
116
+ currentPath = currentPath.parentPath
117
+ if (!currentPath) return false
118
+
119
+ if (
120
+ currentPath.isVariableDeclaration() ||
121
+ currentPath.isAssignmentExpression() ||
122
+ currentPath.isClass()
123
+ ) {
124
+ return true
125
+ }
126
+ } while (currentPath !== statement)
127
+
128
+ return false
129
+ }
130
+
131
+ /**
132
+ * Visitor function for CallExpression and NewExpression nodes.
133
+ */
134
+ function callableExpressionVisitor(
135
+ path: NodePath<t.CallExpression | t.NewExpression>,
136
+ _state: TransformState
137
+ ): void {
138
+ // Skip if this is used as a callee (e.g., foo()())
139
+ if (isUsedAsCallee(path)) {
140
+ return
141
+ }
142
+
143
+ // Skip if this is inside a callee chain
144
+ if (isInCallee(path)) {
145
+ return
146
+ }
147
+
148
+ // Skip if not executed during initialization
149
+ if (!isExecutedDuringInitialization(path)) {
150
+ return
151
+ }
152
+
153
+ // Must be in assignment context or export default
154
+ const statement = path.getStatementParent()
155
+ if (!isInAssignmentContext(path) && !statement?.isExportDefaultDeclaration()) {
156
+ return
157
+ }
158
+
159
+ annotateAsPure(path)
160
+ }
161
+
162
+ /**
163
+ * Creates a Babel visitor that adds PURE annotations to call expressions.
164
+ */
165
+ export function createAnnotateEffectsVisitor(
166
+ filename: string,
167
+ _options?: AnnotateEffectsOptions
168
+ ): Visitor<TransformState> {
169
+ return {
170
+ Program: {
171
+ enter(_path, state) {
172
+ state.filename = filename
173
+ }
174
+ },
175
+
176
+ CallExpression(path: NodePath<t.CallExpression>, state) {
177
+ callableExpressionVisitor(path, state)
178
+ },
179
+
180
+ NewExpression(path: NodePath<t.NewExpression>, state) {
181
+ callableExpressionVisitor(path, state)
182
+ }
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Creates the annotate effects transformer plugin.
188
+ */
189
+ export function annotateEffectsTransformer(options?: AnnotateEffectsOptions): {
190
+ visitor: Visitor<TransformState>
191
+ name: string
192
+ } {
193
+ return {
194
+ name: "effect-annotate-pure-calls",
195
+ visitor: createAnnotateEffectsVisitor("", options)
196
+ }
197
+ }