@clayroach/effect-unplugin 4.0.0-source-tracing.0

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,201 @@
1
+ /**
2
+ * Effect tree-shaking annotations transformer.
3
+ *
4
+ * Adds `\/* @__PURE__ *\/` comments to Effect calls for bundler tree-shaking.
5
+ *
6
+ * @since 0.0.1
7
+ */
8
+ import { parse } from "@babel/parser"
9
+ import * as _traverse from "@babel/traverse"
10
+ import * as _generate from "@babel/generator"
11
+ import * as t from "@babel/types"
12
+
13
+ type NodePath<T = t.Node> = _traverse.NodePath<T>
14
+
15
+ type GenerateFn = (
16
+ ast: t.Node,
17
+ opts: _generate.GeneratorOptions,
18
+ code?: string | { [filename: string]: string }
19
+ ) => _generate.GeneratorResult
20
+
21
+ // Handle CommonJS default exports - runtime interop
22
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
23
+ const traverse: (ast: t.Node, opts: _traverse.TraverseOptions) => void = (_traverse as any).default ?? _traverse
24
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
25
+ const generate: GenerateFn = (_generate as any).default ?? _generate
26
+
27
+ /**
28
+ * Effect module names that should have pure annotations.
29
+ */
30
+ const EFFECT_MODULES = new Set([
31
+ "Effect",
32
+ "Option",
33
+ "Either",
34
+ "Data",
35
+ "Schema",
36
+ "Array",
37
+ "Chunk",
38
+ "HashMap",
39
+ "HashSet",
40
+ "List",
41
+ "Queue",
42
+ "Stream",
43
+ "Layer",
44
+ "Scope",
45
+ "Ref",
46
+ "SynchronizedRef",
47
+ "SubscriptionRef",
48
+ "Duration",
49
+ "Schedule",
50
+ "Cause",
51
+ "Exit",
52
+ "Match",
53
+ "Boolean",
54
+ "Number",
55
+ "String",
56
+ "Struct",
57
+ "Tuple",
58
+ "Function",
59
+ "Predicate",
60
+ "Order",
61
+ "Equivalence",
62
+ "Context",
63
+ "Brand",
64
+ "Types"
65
+ ])
66
+
67
+ /**
68
+ * @since 0.0.1
69
+ * @category models
70
+ */
71
+ export interface AnnotateResult {
72
+ readonly code: string
73
+ readonly map?: unknown
74
+ readonly transformed: boolean
75
+ }
76
+
77
+ /**
78
+ * Checks if a call expression already has a pure annotation.
79
+ */
80
+ function hasPureAnnotation(node: t.CallExpression): boolean {
81
+ const comments = node.leadingComments
82
+ if (!comments) return false
83
+ return comments.some(
84
+ (comment) =>
85
+ comment.type === "CommentBlock" &&
86
+ (comment.value.includes("@__PURE__") || comment.value.includes("#__PURE__"))
87
+ )
88
+ }
89
+
90
+ /**
91
+ * Checks if a call is to an Effect module method.
92
+ */
93
+ function isEffectModuleCall(node: t.CallExpression): boolean {
94
+ const callee = node.callee
95
+
96
+ // Effect.succeed(...) or Option.some(...)
97
+ if (
98
+ t.isMemberExpression(callee) &&
99
+ t.isIdentifier(callee.object) &&
100
+ EFFECT_MODULES.has(callee.object.name)
101
+ ) {
102
+ return true
103
+ }
104
+
105
+ return false
106
+ }
107
+
108
+ /**
109
+ * Checks if a CallExpression is in a context where pure annotation is useful.
110
+ * Only annotate calls in variable declarations or export declarations.
111
+ */
112
+ function isInAnnotatableContext(path: NodePath<t.CallExpression>): boolean {
113
+ let parent: NodePath | null = path.parentPath
114
+
115
+ while (parent !== null) {
116
+ const node = parent.node
117
+
118
+ // Variable declaration: const x = Effect.succeed(...)
119
+ if (t.isVariableDeclarator(node)) {
120
+ return true
121
+ }
122
+
123
+ // Export: export const x = Effect.succeed(...)
124
+ if (t.isExportDefaultDeclaration(node) || t.isExportNamedDeclaration(node)) {
125
+ return true
126
+ }
127
+
128
+ // Return statement: return Effect.succeed(...)
129
+ if (t.isReturnStatement(node)) {
130
+ return true
131
+ }
132
+
133
+ // Arrow function body: () => Effect.succeed(...)
134
+ if (t.isArrowFunctionExpression(node)) {
135
+ return true
136
+ }
137
+
138
+ // Stop at block statements
139
+ if (t.isBlockStatement(node) || t.isProgram(node)) {
140
+ break
141
+ }
142
+
143
+ parent = parent.parentPath
144
+ }
145
+
146
+ return false
147
+ }
148
+
149
+ /**
150
+ * Transforms source code to add pure annotations to Effect calls.
151
+ *
152
+ * @since 0.0.1
153
+ * @category transform
154
+ */
155
+ export function annotateEffects(code: string, id: string): AnnotateResult {
156
+ let ast: t.File
157
+ try {
158
+ ast = parse(code, {
159
+ sourceType: "module",
160
+ plugins: ["typescript", "jsx"],
161
+ sourceFilename: id
162
+ })
163
+ } catch {
164
+ return { code, transformed: false }
165
+ }
166
+
167
+ let hasTransformed = false
168
+
169
+ traverse(ast, {
170
+ CallExpression(path: NodePath<t.CallExpression>) {
171
+ // Skip if already has pure annotation
172
+ if (hasPureAnnotation(path.node)) return
173
+
174
+ // Only annotate Effect module calls
175
+ if (!isEffectModuleCall(path.node)) return
176
+
177
+ // Only annotate in useful contexts
178
+ if (!isInAnnotatableContext(path)) return
179
+
180
+ // Add @__PURE__ annotation
181
+ t.addComment(path.node, "leading", "#__PURE__", false)
182
+ hasTransformed = true
183
+ }
184
+ })
185
+
186
+ if (!hasTransformed) {
187
+ return { code, transformed: false }
188
+ }
189
+
190
+ const result = generate(ast, {
191
+ sourceMaps: true,
192
+ sourceFileName: id,
193
+ comments: true
194
+ }, code)
195
+
196
+ return {
197
+ code: result.code,
198
+ map: result.map,
199
+ transformed: true
200
+ }
201
+ }
package/src/esbuild.ts ADDED
@@ -0,0 +1,18 @@
1
+ /**
2
+ * esbuild plugin for Effect source location tracing.
3
+ *
4
+ * @example
5
+ * ```ts
6
+ * import esbuild from "esbuild"
7
+ * import effectSourceTrace from "@effect/unplugin/esbuild"
8
+ *
9
+ * esbuild.build({
10
+ * plugins: [effectSourceTrace()]
11
+ * })
12
+ * ```
13
+ *
14
+ * @since 0.0.1
15
+ */
16
+ import unplugin from "./index.ts"
17
+
18
+ export default unplugin.esbuild
package/src/index.ts ADDED
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Build-time AST transformer for Effect source location tracing and auto-instrumentation.
3
+ *
4
+ * This plugin provides two features:
5
+ * 1. **Source Tracing**: Transforms `yield*` expressions inside `Effect.gen()` to
6
+ * inject source location information via `CurrentStackFrame`.
7
+ * 2. **Span Instrumentation**: Wraps Effect combinators with `withSpan()` for
8
+ * automatic distributed tracing.
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * // vite.config.ts
13
+ * import { defineConfig } from "vite"
14
+ * import effectSourceTrace from "@effect/unplugin/vite"
15
+ *
16
+ * export default defineConfig({
17
+ * plugins: [effectSourceTrace({
18
+ * // Enable source tracing (default: true)
19
+ * sourceTrace: true,
20
+ * // Enable span instrumentation
21
+ * spans: {
22
+ * enabled: true,
23
+ * include: ["gen", "fork", "all", "forEach"]
24
+ * }
25
+ * })]
26
+ * })
27
+ * ```
28
+ *
29
+ * @since 0.0.1
30
+ */
31
+ import { createUnplugin, type TransformResult as UnpluginTransformResult } from "unplugin"
32
+ import { annotateEffects } from "./annotateEffects.ts"
33
+ import { transform } from "./sourceTrace.ts"
34
+ import type { FilterPattern, SourceTraceOptions } from "./types.ts"
35
+
36
+ export { type AnnotateResult, annotateEffects } from "./annotateEffects.ts"
37
+ export { type TransformResult } from "./sourceTrace.ts"
38
+ export type { FilterPattern, InstrumentableEffect, SourceTraceOptions, SpanInstrumentationOptions } from "./types.ts"
39
+
40
+ const defaultInclude: ReadonlyArray<string | RegExp> = [/\.[jt]sx?$/]
41
+ const defaultExclude: ReadonlyArray<string | RegExp> = [/node_modules/]
42
+
43
+ function toArray(value: FilterPattern | undefined): ReadonlyArray<string | RegExp> {
44
+ if (value === undefined) return []
45
+ if (Array.isArray(value)) return value
46
+ return [value as string | RegExp]
47
+ }
48
+
49
+ function createFilter(
50
+ include: FilterPattern | undefined,
51
+ exclude: FilterPattern | undefined
52
+ ): (id: string) => boolean {
53
+ const includePatterns = toArray(include ?? defaultInclude)
54
+ const excludePatterns = toArray(exclude ?? defaultExclude)
55
+
56
+ return (id: string): boolean => {
57
+ for (const pattern of excludePatterns) {
58
+ if (typeof pattern === "string" ? id.includes(pattern) : pattern.test(id)) {
59
+ return false
60
+ }
61
+ }
62
+ for (const pattern of includePatterns) {
63
+ if (typeof pattern === "string" ? id.includes(pattern) : pattern.test(id)) {
64
+ return true
65
+ }
66
+ }
67
+ return false
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Creates the Effect source trace unplugin.
73
+ *
74
+ * @since 0.0.1
75
+ * @category unplugin
76
+ */
77
+ export const unplugin = createUnplugin<SourceTraceOptions | undefined>((options = {}) => {
78
+ const filter = createFilter(options.include, options.exclude)
79
+
80
+ return {
81
+ name: "effect-source-trace",
82
+ enforce: "pre",
83
+
84
+ transformInclude(id) {
85
+ return filter(id)
86
+ },
87
+
88
+ transform(code, id) {
89
+ let currentCode = code
90
+ let hasTransformed = false
91
+ let finalMap: unknown
92
+
93
+ // Run source trace and span instrumentation
94
+ const traceResult = transform(currentCode, id, options)
95
+ if (traceResult.transformed) {
96
+ currentCode = traceResult.code
97
+ finalMap = traceResult.map
98
+ hasTransformed = true
99
+ }
100
+
101
+ // Run annotateEffects if enabled
102
+ if (options.annotateEffects) {
103
+ const annotateResult = annotateEffects(currentCode, id)
104
+ if (annotateResult.transformed) {
105
+ currentCode = annotateResult.code
106
+ finalMap = annotateResult.map
107
+ hasTransformed = true
108
+ }
109
+ }
110
+
111
+ if (!hasTransformed) {
112
+ return null
113
+ }
114
+ return { code: currentCode, map: finalMap } as UnpluginTransformResult
115
+ }
116
+ }
117
+ })
118
+
119
+ export default unplugin
package/src/rollup.ts ADDED
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Rollup plugin for Effect source location tracing.
3
+ *
4
+ * @example
5
+ * ```ts
6
+ * // rollup.config.js
7
+ * import effectSourceTrace from "@effect/unplugin/rollup"
8
+ *
9
+ * export default {
10
+ * plugins: [effectSourceTrace()]
11
+ * }
12
+ * ```
13
+ *
14
+ * @since 0.0.1
15
+ */
16
+ import unplugin from "./index.ts"
17
+
18
+ export default unplugin.rollup