@acebuilder/component-tagger 0.0.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.
package/index.d.ts ADDED
@@ -0,0 +1,15 @@
1
+ import type { NextConfig } from 'next'
2
+
3
+ export interface AceBuilderComponentTaggerOptions {
4
+ /** Test regex for files to process. If provided, takes precedence over `extensions`. */
5
+ test?: RegExp
6
+ }
7
+
8
+ export type WithAceBuilderComponentTagger = (config: NextConfig) => NextConfig
9
+
10
+ /**
11
+ * Next.js plugin that injects the AceBuilder component tagger loader into the webpack pipeline.
12
+ */
13
+ declare function withAceBuilderComponentTagger(options?: AceBuilderComponentTaggerOptions): WithAceBuilderComponentTagger
14
+
15
+ export default withAceBuilderComponentTagger
package/index.js ADDED
@@ -0,0 +1,66 @@
1
+ 'use strict'
2
+
3
+ /**
4
+ * Next.js plugin that injects the AceBuilder component tagger loader into the webpack pipeline.
5
+ *
6
+ * Purpose:
7
+ * - Automatically adds a pre-loader that parses JSX/TSX modules and injects data attributes
8
+ * used by AceBuilder for component discovery and visualization.
9
+ *
10
+ * Options:
11
+ * - pluginOptions.test (RegExp): Which files to process. Defaults to `/\.((j|t)sx)$/i`.
12
+ *
13
+ * Usage (CommonJS in next.config.js / next.config.mjs):
14
+ *
15
+ * const withComponentTagger = require('@acebuilder/component-tagger')({
16
+ * // Optionally override which files to process
17
+ * // test: /\.tsx?$/i,
18
+ * })
19
+ *
20
+ * module.exports = withComponentTagger({
21
+ * reactStrictMode: true,
22
+ * // ...any other Next.js config
23
+ * })
24
+ *
25
+ * How it works:
26
+ * - Adds a webpack rule (enforce: 'pre') that applies to files matching `test`, excluding node_modules.
27
+ * - The rule uses the local loader at `./lib/loader`.
28
+ * - If the user already defines a `webpack` function in their Next config, we call it afterwards
29
+ * to preserve user customizations.
30
+ */
31
+ module.exports = (pluginOptions = {}) => (inputConfig = {}) => {
32
+ if (
33
+ process.env.ACEBUILDER_TAGGER_ENABLED === '0' ||
34
+ process.env.ACEBUILDER_TAGGER_ENABLED === 'false' ||
35
+ !process.env.ACEBUILDER_TAGGER_ENABLED
36
+ ) {
37
+ console.log('AceBuilder Component Tagger is disabled', process.env.ACEBUILDER_TAGGER_ENABLED)
38
+ return inputConfig
39
+ }
40
+ const test = pluginOptions.test || /\.((j|t)sx)$/i
41
+
42
+ /** @type {import('next').NextConfig} */
43
+ const nextConfig = {
44
+ ...inputConfig,
45
+ webpack(config, options) {
46
+ config.module.rules.push({
47
+ test,
48
+ exclude: /node_modules/,
49
+ enforce: 'pre',
50
+ use: {
51
+ loader: require.resolve('./lib/loader'),
52
+ },
53
+ })
54
+ if (typeof inputConfig.webpack === 'function') {
55
+ return inputConfig.webpack(config, options)
56
+ }
57
+
58
+ return config
59
+ },
60
+ }
61
+
62
+ return nextConfig
63
+ }
64
+
65
+ // Provide a .default for ESM interop so `import x from` works reliably
66
+ module.exports.default = module.exports
@@ -0,0 +1,296 @@
1
+ /*
2
+ * Blacklists for components that should not be tagged by the AceBuilder component tagger.
3
+ *
4
+ * These lists mirror the ones that were embedded in the app's loader.
5
+ * They are kept here so the loader can import them and so they can be extended/tested independently.
6
+ *
7
+ * Why blacklist?
8
+ * - Some libraries (e.g., react-three-fiber and @react-three/drei) expose many JSX elements that map to low-level 3D primitives.
9
+ * Tagging those would create excessive noise and large HTML payloads with little benefit for UI inspection.
10
+ * - By excluding these known elements, we keep tagging focused on meaningful app-level components.
11
+ */
12
+ 'use strict'
13
+
14
+ /**
15
+ * List of JSX element names originating from react-three-fiber that we intentionally skip tagging.
16
+ *
17
+ * Notes:
18
+ * - The names here match the JSX tag identifiers (lower/upper case as used in code) that the tagger detects at parse time.
19
+ * - If a new react-three-fiber element starts appearing in your app and shouldn't be tagged, add it here.
20
+ */
21
+ const threeFiberElems = [
22
+ 'object3D',
23
+ 'audioListener',
24
+ 'positionalAudio',
25
+ 'mesh',
26
+ 'batchedMesh',
27
+ 'instancedMesh',
28
+ 'scene',
29
+ 'sprite',
30
+ 'lOD',
31
+ 'skinnedMesh',
32
+ 'skeleton',
33
+ 'bone',
34
+ 'lineSegments',
35
+ 'lineLoop',
36
+ 'points',
37
+ 'group',
38
+ 'camera',
39
+ 'perspectiveCamera',
40
+ 'orthographicCamera',
41
+ 'cubeCamera',
42
+ 'arrayCamera',
43
+ 'instancedBufferGeometry',
44
+ 'bufferGeometry',
45
+ 'boxBufferGeometry',
46
+ 'circleBufferGeometry',
47
+ 'coneBufferGeometry',
48
+ 'cylinderBufferGeometry',
49
+ 'dodecahedronBufferGeometry',
50
+ 'extrudeBufferGeometry',
51
+ 'icosahedronBufferGeometry',
52
+ 'latheBufferGeometry',
53
+ 'octahedronBufferGeometry',
54
+ 'planeBufferGeometry',
55
+ 'polyhedronBufferGeometry',
56
+ 'ringBufferGeometry',
57
+ 'shapeBufferGeometry',
58
+ 'sphereBufferGeometry',
59
+ 'tetrahedronBufferGeometry',
60
+ 'torusBufferGeometry',
61
+ 'torusKnotBufferGeometry',
62
+ 'tubeBufferGeometry',
63
+ 'wireframeGeometry',
64
+ 'tetrahedronGeometry',
65
+ 'octahedronGeometry',
66
+ 'icosahedronGeometry',
67
+ 'dodecahedronGeometry',
68
+ 'polyhedronGeometry',
69
+ 'tubeGeometry',
70
+ 'torusKnotGeometry',
71
+ 'torusGeometry',
72
+ 'sphereGeometry',
73
+ 'ringGeometry',
74
+ 'planeGeometry',
75
+ 'latheGeometry',
76
+ 'shapeGeometry',
77
+ 'extrudeGeometry',
78
+ 'edgesGeometry',
79
+ 'coneGeometry',
80
+ 'cylinderGeometry',
81
+ 'circleGeometry',
82
+ 'boxGeometry',
83
+ 'capsuleGeometry',
84
+ 'material',
85
+ 'shadowMaterial',
86
+ 'spriteMaterial',
87
+ 'rawShaderMaterial',
88
+ 'shaderMaterial',
89
+ 'pointsMaterial',
90
+ 'meshPhysicalMaterial',
91
+ 'meshStandardMaterial',
92
+ 'meshPhongMaterial',
93
+ 'meshToonMaterial',
94
+ 'meshNormalMaterial',
95
+ 'meshLambertMaterial',
96
+ 'meshDepthMaterial',
97
+ 'meshDistanceMaterial',
98
+ 'meshBasicMaterial',
99
+ 'meshMatcapMaterial',
100
+ 'lineDashedMaterial',
101
+ 'lineBasicMaterial',
102
+ 'primitive',
103
+ 'light',
104
+ 'spotLightShadow',
105
+ 'spotLight',
106
+ 'pointLight',
107
+ 'rectAreaLight',
108
+ 'hemisphereLight',
109
+ 'directionalLightShadow',
110
+ 'directionalLight',
111
+ 'ambientLight',
112
+ 'lightShadow',
113
+ 'ambientLightProbe',
114
+ 'hemisphereLightProbe',
115
+ 'lightProbe',
116
+ 'spotLightHelper',
117
+ 'skeletonHelper',
118
+ 'pointLightHelper',
119
+ 'hemisphereLightHelper',
120
+ 'gridHelper',
121
+ 'polarGridHelper',
122
+ 'directionalLightHelper',
123
+ 'cameraHelper',
124
+ 'boxHelper',
125
+ 'box3Helper',
126
+ 'planeHelper',
127
+ 'arrowHelper',
128
+ 'axesHelper',
129
+ 'texture',
130
+ 'videoTexture',
131
+ 'dataTexture',
132
+ 'dataTexture3D',
133
+ 'compressedTexture',
134
+ 'cubeTexture',
135
+ 'canvasTexture',
136
+ 'depthTexture',
137
+ 'raycaster',
138
+ 'vector2',
139
+ 'vector3',
140
+ 'vector4',
141
+ 'euler',
142
+ 'matrix3',
143
+ 'matrix4',
144
+ 'quaternion',
145
+ 'bufferAttribute',
146
+ 'float16BufferAttribute',
147
+ 'float32BufferAttribute',
148
+ 'float64BufferAttribute',
149
+ 'int8BufferAttribute',
150
+ 'int16BufferAttribute',
151
+ 'int32BufferAttribute',
152
+ 'uint8BufferAttribute',
153
+ 'uint16BufferAttribute',
154
+ 'uint32BufferAttribute',
155
+ 'instancedBufferAttribute',
156
+ 'color',
157
+ 'fog',
158
+ 'fogExp2',
159
+ 'shape',
160
+ 'colorShiftMaterial',
161
+ ]
162
+
163
+ /**
164
+ * List of JSX element names from @react-three/drei we do not tag.
165
+ *
166
+ * Drei provides many helpers and higher-level constructs around react-three-fiber.
167
+ * These are still generally low-level from a UI tagging perspective, so we exclude them by default.
168
+ */
169
+ const dreiElems = [
170
+ 'AsciiRenderer',
171
+ 'Billboard',
172
+ 'Clone',
173
+ 'ComputedAttribute',
174
+ 'Decal',
175
+ 'Edges',
176
+ 'Effects',
177
+ 'GradientTexture',
178
+ 'MarchingCubes',
179
+ 'Outlines',
180
+ 'PositionalAudio',
181
+ 'Sampler',
182
+ 'ScreenSizer',
183
+ 'ScreenSpace',
184
+ 'Splat',
185
+ 'Svg',
186
+ 'Text',
187
+ 'Text3D',
188
+ 'Trail',
189
+ 'CubeCamera',
190
+ 'OrthographicCamera',
191
+ 'PerspectiveCamera',
192
+ 'CameraControls',
193
+ 'FaceControls',
194
+ 'KeyboardControls',
195
+ 'MotionPathControls',
196
+ 'PresentationControls',
197
+ 'ScrollControls',
198
+ 'DragControls',
199
+ 'GizmoHelper',
200
+ 'Grid',
201
+ 'Helper',
202
+ 'PivotControls',
203
+ 'TransformControls',
204
+ 'CubeTexture',
205
+ 'Fbx',
206
+ 'Gltf',
207
+ 'Ktx2',
208
+ 'Loader',
209
+ 'Progress',
210
+ 'ScreenVideoTexture',
211
+ 'Texture',
212
+ 'TrailTexture',
213
+ 'VideoTexture',
214
+ 'WebcamVideoTexture',
215
+ 'CycleRaycast',
216
+ 'DetectGPU',
217
+ 'Example',
218
+ 'FaceLandmarker',
219
+ 'Fbo',
220
+ 'Html',
221
+ 'Select',
222
+ 'SpriteAnimator',
223
+ 'StatsGl',
224
+ 'Stats',
225
+ 'Trail',
226
+ 'Wireframe',
227
+ 'CurveModifier',
228
+ 'AdaptiveDpr',
229
+ 'AdaptiveEvents',
230
+ 'BakeShadows',
231
+ 'Bvh',
232
+ 'Detailed',
233
+ 'Instances',
234
+ 'Merged',
235
+ 'meshBounds',
236
+ 'PerformanceMonitor',
237
+ 'Points',
238
+ 'Preload',
239
+ 'Segments',
240
+ 'Fisheye',
241
+ 'Hud',
242
+ 'Mask',
243
+ 'MeshPortalMaterial',
244
+ 'RenderCubeTexture',
245
+ 'RenderTexture',
246
+ 'View',
247
+ 'MeshDiscardMaterial',
248
+ 'MeshDistortMaterial',
249
+ 'MeshReflectorMaterial',
250
+ 'MeshRefractionMaterial',
251
+ 'MeshTransmissionMaterial',
252
+ 'MeshWobbleMaterial',
253
+ 'PointMaterial',
254
+ 'shaderMaterial',
255
+ 'SoftShadows',
256
+ 'CatmullRomLine',
257
+ 'CubicBezierLine',
258
+ 'Facemesh',
259
+ 'Line',
260
+ 'Mesh',
261
+ 'QuadraticBezierLine',
262
+ 'RoundedBox',
263
+ 'ScreenQuad',
264
+ 'AccumulativeShadows',
265
+ 'Backdrop',
266
+ 'BBAnchor',
267
+ 'Bounds',
268
+ 'CameraShake',
269
+ 'Caustics',
270
+ 'Center',
271
+ 'Cloud',
272
+ 'ContactShadows',
273
+ 'Environment',
274
+ 'Float',
275
+ 'Lightformer',
276
+ 'MatcapTexture',
277
+ 'NormalTexture',
278
+ 'RandomizedLight',
279
+ 'Resize',
280
+ 'ShadowAlpha',
281
+ 'Shadow',
282
+ 'Sky',
283
+ 'Sparkles',
284
+ 'SpotLightShadow',
285
+ 'SpotLight',
286
+ 'Stage',
287
+ 'Stars',
288
+ 'OrbitControls',
289
+ ]
290
+
291
+ /**
292
+ * Exports the blacklists for components that should not be tagged by the AceBuilder component tagger.
293
+ *
294
+ * @module blacklists
295
+ */
296
+ module.exports = { threeFiberElems, dreiElems }
package/lib/loader.js ADDED
@@ -0,0 +1,167 @@
1
+ 'use strict'
2
+
3
+ const { parse } = require('@babel/parser')
4
+ const MagicString = require('magic-string')
5
+ const { walk } = require('estree-walker')
6
+ const path = require('path')
7
+ const {
8
+ shouldTag,
9
+ isNextImageAlias,
10
+ findVariableDeclarations,
11
+ findMapContext,
12
+ getSemanticName,
13
+ } = require('./utils')
14
+
15
+ /**
16
+ * AceBuilder Component Tagger Loader
17
+ *
18
+ * This webpack-style loader parses source files and injects data attributes
19
+ * onto JSX elements so the AceBuilder app can locate, highlight, and map
20
+ * UI components in the rendered DOM.
21
+ *
22
+ * Key behaviors:
23
+ * - Skips files under `node_modules`.
24
+ * - Parses with Babel (JSX + TypeScript) and traverses the AST.
25
+ * - Injects `data-acebuilder-id` and `data-acebuilder-name` onto selected JSX elements.
26
+ * - Adds `data-map-index={index}` when inside an Array.map callback with an index parameter.
27
+ * - Respects blacklists for react-three-fiber / @react-three/drei via `shouldTag`.
28
+ * - Treats aliases of `next/image` as semantic `img` for tagging purposes.
29
+ * - Normalizes and preserves source maps for compatibility with webpack/Turbopack.
30
+ *
31
+ * The `data-acebuilder-id` contains a stable reference in the format:
32
+ * "<relativeFilePath>:<line>:<column>[@<context>...@<refName>]"
33
+ * Where optional context can include the mapped array name and referenced variable names.
34
+ *
35
+ * @this {import('webpack').LoaderContext<unknown>}
36
+ * @param {string} code - The module source code.
37
+ * @param {any} map - An optional incoming sourcemap (object or stringified JSON).
38
+ * @returns {void} - Uses the async callback to return results to webpack.
39
+ */
40
+ function componentTagger(code, map) {
41
+
42
+ const done = this.async()
43
+ try {
44
+ // Normalize incoming sourcemap: Turbopack expects an object, not a JSON string
45
+ let inMap = map
46
+ if (typeof inMap === 'string') {
47
+ try {
48
+ inMap = JSON.parse(inMap)
49
+ } catch (_) {
50
+ // ignore, treat as no map
51
+ inMap = undefined
52
+ }
53
+ }
54
+
55
+ // Do not process vendor code
56
+ if (/node_modules/.test(this.resourcePath)) return done(null, code, inMap)
57
+
58
+ // Parse with Babel (supports JSX and TypeScript)
59
+ const ast = parse(code, {
60
+ sourceType: 'module',
61
+ plugins: ['jsx', 'typescript'],
62
+ sourceFilename: this.resourcePath,
63
+ })
64
+
65
+ // MagicString allows non-destructive code edits and sourcemap generation
66
+ const ms = new MagicString(code)
67
+ const fileRelative = path.relative(this.rootContext, this.resourcePath)
68
+
69
+
70
+ let mutated = false
71
+
72
+ // Add parent references to AST nodes for upward traversal (non-enumerable)
73
+ walk(ast, {
74
+ enter(node, parent) {
75
+ if (parent && !Object.prototype.hasOwnProperty.call(node, 'parent')) {
76
+ Object.defineProperty(node, 'parent', { value: parent, enumerable: false })
77
+ }
78
+ },
79
+ })
80
+
81
+ // Collect variable declarations first for later contextual tagging
82
+ const variables = findVariableDeclarations(ast)
83
+
84
+ // Gather local identifiers that reference `next/image` imports
85
+ const imageAliases = new Set()
86
+ walk(ast, {
87
+ enter(node) {
88
+ if (node.type === 'ImportDeclaration' && node.source.value === 'next/image') {
89
+ for (const spec of node.specifiers) imageAliases.add(spec.local.name)
90
+ }
91
+ },
92
+ })
93
+
94
+ // Inject attributes with enhanced semantic context
95
+ walk(ast, {
96
+ enter(node) {
97
+
98
+ if (node.type !== 'JSXOpeningElement') return
99
+ const mapContext = findMapContext(node, variables)
100
+ const semanticName = getSemanticName(node, mapContext, imageAliases)
101
+
102
+
103
+ // Respect blacklist and fragments
104
+ if (
105
+ !semanticName ||
106
+ ['Fragment', 'React.Fragment'].includes(semanticName) ||
107
+ (!isNextImageAlias(imageAliases, semanticName.split('-')[0]) && !shouldTag(semanticName))
108
+ )
109
+ return
110
+
111
+ // Use source location to build a stable identifier
112
+ const locStart = node.loc?.start
113
+ const line = locStart?.line ?? 0
114
+ const column = locStart?.column ?? 0
115
+ let acebuilderId = `${fileRelative}:${line}:${column}`
116
+
117
+ // Enhance the ID with context if we have map information
118
+ if (mapContext) acebuilderId += `@${mapContext.arrayName}`
119
+
120
+ // Append referenced variable names for simple identifier references in props
121
+ node.attributes?.forEach((attr) => {
122
+ if (
123
+ attr.type === 'JSXAttribute' &&
124
+ attr.value?.type === 'JSXExpressionContainer' &&
125
+ attr.value.expression?.type === 'Identifier'
126
+ ) {
127
+ const refName = attr.value.expression.name
128
+ const varInfo = variables.get(refName)
129
+ if (varInfo) acebuilderId += `@${refName}`
130
+ }
131
+ })
132
+
133
+ // If inside a map context and we have an index variable, inject data-map-index
134
+ if (mapContext?.indexVarName) {
135
+ ms.appendLeft(node.name.end, ` data-map-index={${mapContext.indexVarName}}`)
136
+ }
137
+
138
+ // Inject the AceBuilder data attributes
139
+ ms.appendLeft(
140
+ node.name.end,
141
+ ` data-acebuilder-id="${acebuilderId}" data-acebuilder-name="${semanticName}"`
142
+ )
143
+ mutated = true
144
+ },
145
+ })
146
+
147
+ // If we did not modify the code, reuse original code and incoming map
148
+ if (!mutated) return done(null, code, inMap)
149
+
150
+ const out = ms.toString()
151
+ // Create a plain RawSourceMap object for webpack/Turbopack
152
+ const mapStr = ms.generateMap({
153
+ hires: true,
154
+ source: this.resourcePath,
155
+ includeContent: true,
156
+ file: this.resourcePath,
157
+ }).toString()
158
+
159
+ const outMap = JSON.parse(mapStr)
160
+
161
+ done(null, out, outMap)
162
+ } catch (err) {
163
+ done(err)
164
+ }
165
+ }
166
+
167
+ module.exports = componentTagger
package/lib/utils.js ADDED
@@ -0,0 +1,210 @@
1
+ 'use strict'
2
+
3
+ const { walk } = require('estree-walker')
4
+ const { threeFiberElems, dreiElems } = require('./blacklists')
5
+
6
+ /**
7
+ * Utilities used by the AceBuilder component tagger loader.
8
+ *
9
+ * This module provides:
10
+ * - Blacklist-aware checks to decide if a JSX element should be tagged.
11
+ * - Helpers to extract literal values from Babel AST nodes.
12
+ * - AST walkers to collect variable declarations and infer contextual information
13
+ * like Array.map() usage to enrich tag metadata.
14
+ */
15
+
16
+ /**
17
+ * Decide whether a given JSX tag name should be tagged based on known blacklists.
18
+ *
19
+ * @param {string} name - The JSX tag name (e.g., 'Button', 'div', 'mesh').
20
+ * @returns {boolean} True if the element should be tagged; false if it is blacklisted.
21
+ */
22
+ const shouldTag = (name) => !threeFiberElems.includes(name) && !dreiElems.includes(name)
23
+
24
+ /**
25
+ * Check whether a tag name corresponds to an alias imported from `next/image`.
26
+ *
27
+ * Example: `import Img from 'next/image'` then `Img` is an alias that should be
28
+ * treated as semantic `img` for tagging.
29
+ *
30
+ * @param {Set<string>} aliases - Local identifiers bound to `next/image` imports.
31
+ * @param {string} name - The tag name to test.
32
+ * @returns {boolean} True if the name is a `next/image` alias.
33
+ */
34
+ const isNextImageAlias = (aliases, name) => aliases.has(name)
35
+
36
+ /**
37
+ * Attempt to resolve a static literal value from a Babel AST expression node.
38
+ *
39
+ * Supported nodes:
40
+ * - StringLiteral, NumericLiteral, BooleanLiteral
41
+ * - TemplateLiteral (only when all parts resolve to literals)
42
+ * - ObjectExpression (non-computed keys, recursively resolves values)
43
+ * - ArrayExpression (maps each element; holes become undefined)
44
+ *
45
+ * Returns `undefined` when the value cannot be safely determined at build time.
46
+ *
47
+ * @param {import('@babel/types').Node | undefined} node
48
+ * @returns {unknown} The extracted literal value, or undefined.
49
+ */
50
+ function extractLiteralValue(node) {
51
+ if (!node) return undefined
52
+ switch (node.type) {
53
+ case 'StringLiteral':
54
+ return node.value
55
+ case 'NumericLiteral':
56
+ return node.value
57
+ case 'BooleanLiteral':
58
+ return node.value
59
+ case 'TemplateLiteral': {
60
+ // Best-effort: only resolve when all quasis/expressions are static literals
61
+ try {
62
+ const parts = []
63
+ const { quasis, expressions } = node
64
+ for (let i = 0; i < quasis.length; i++) {
65
+ parts.push(quasis[i]?.value?.cooked ?? '')
66
+ if (i < expressions.length) {
67
+ const v = extractLiteralValue(expressions[i])
68
+ if (v === undefined) return undefined
69
+ parts.push(String(v))
70
+ }
71
+ }
72
+ return parts.join('')
73
+ } catch (_) {
74
+ return undefined
75
+ }
76
+ }
77
+ case 'ObjectExpression': {
78
+ const obj = {}
79
+ for (const prop of node.properties) {
80
+ if (prop.type === 'ObjectProperty' && !prop.computed) {
81
+ const key = prop.key.type === 'Identifier' ? prop.key.name : prop.key.value
82
+ obj[key] = extractLiteralValue(prop.value)
83
+ }
84
+ }
85
+ return obj
86
+ }
87
+ case 'ArrayExpression':
88
+ // Handle sparse arrays (holes) and map each element safely
89
+ return node.elements.map((el) => (el ? extractLiteralValue(el) : undefined))
90
+ default:
91
+ return undefined
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Collect variable declarations from the AST and retain basic metadata
97
+ * that can be used later when building semantic IDs.
98
+ *
99
+ * For array literals, this also stores the array items for potential context.
100
+ *
101
+ * @param {import('@babel/types').File} ast - The parsed Babel AST.
102
+ * @returns {Map<string, { name: string, type: 'array'|'object'|'primitive', value: unknown, arrayItems?: unknown[], loc?: { line: number, column: number } }>} A map of variable info by name.
103
+ */
104
+ function findVariableDeclarations(ast) {
105
+ const variables = new Map()
106
+ walk(ast, {
107
+ enter(node) {
108
+ if (node.type === 'VariableDeclaration') {
109
+ for (const declarator of node.declarations) {
110
+ if (declarator.id.type === 'Identifier' && declarator.init) {
111
+ const varName = declarator.id.name
112
+ const value = extractLiteralValue(declarator.init)
113
+ variables.set(varName, {
114
+ name: varName,
115
+ type: Array.isArray(value)
116
+ ? 'array'
117
+ : typeof value === 'object'
118
+ ? 'object'
119
+ : 'primitive',
120
+ value,
121
+ arrayItems: Array.isArray(value) ? value : undefined,
122
+ loc: declarator.loc?.start,
123
+ })
124
+ }
125
+ }
126
+ }
127
+ },
128
+ })
129
+ return variables
130
+ }
131
+
132
+ /**
133
+ * Ascend from a JSX node to detect whether it resides within an Array.map call.
134
+ * If found, extracts the source array name, callback parameter names, and related metadata.
135
+ *
136
+ * This information is used to add `data-map-index` and to enrich `data-acebuilder-id`.
137
+ *
138
+ * @param {import('@babel/types').Node & { parent?: any }} node - Current JSXOpeningElement node (with parent links).
139
+ * @param {Map<string, any>} variables - Variable metadata collected by findVariableDeclarations.
140
+ * @returns {null | { arrayName: string, itemVarName: string, arrayItems?: unknown[], arrayLoc?: { line: number, column: number }, indexVarName?: string }}
141
+ */
142
+ function findMapContext(node, variables) {
143
+ // Walk up the tree to find if this JSX element is inside a map call
144
+ let current = node
145
+ let depth = 0
146
+ const maxDepth = 10
147
+ while (current && depth < maxDepth) {
148
+ if (
149
+ current.type === 'CallExpression' &&
150
+ current.callee?.type === 'MemberExpression' &&
151
+ current.callee?.property?.name === 'map'
152
+ ) {
153
+ const arrayName = current.callee.object?.name
154
+ const mapCallback = current.arguments?.[0]
155
+ if (
156
+ arrayName &&
157
+ (mapCallback?.type === 'ArrowFunctionExpression' || mapCallback?.type === 'FunctionExpression')
158
+ ) {
159
+ const itemParam = mapCallback.params?.[0]
160
+ const indexParam = mapCallback.params?.[1]
161
+ if (itemParam?.type === 'Identifier') {
162
+ const varInfo = variables.get(arrayName)
163
+ return {
164
+ arrayName,
165
+ itemVarName: itemParam.name,
166
+ arrayItems: varInfo?.arrayItems,
167
+ arrayLoc: varInfo?.loc,
168
+ indexVarName: indexParam?.type === 'Identifier' ? indexParam.name : undefined,
169
+ }
170
+ }
171
+ }
172
+ }
173
+ current = current.parent
174
+ depth++
175
+ }
176
+ return null
177
+ }
178
+
179
+ /**
180
+ * Derive a semantic name for a JSX element that will be used as `data-acebuilder-name`.
181
+ *
182
+ * - Converts `next/image` aliases to a standard 'img' tag for consistency.
183
+ * - Supports identifiers (e.g., `Button`) and member expressions (e.g., `UI.Card`).
184
+ *
185
+ * @param {import('@babel/types').JSXOpeningElement} node - JSX opening element.
186
+ * @param {ReturnType<typeof findMapContext>} _mapContext - Optional map context (unused here but reserved for future rules).
187
+ * @param {Set<string>} imageAliases - Local names that alias `next/image`.
188
+ * @returns {string | null} The semantic tag name or null if it cannot be determined.
189
+ */
190
+ function getSemanticName(node, _mapContext, imageAliases) {
191
+ const getName = () => {
192
+ if (node.name.type === 'JSXIdentifier') return node.name.name
193
+ if (node.name.type === 'JSXMemberExpression')
194
+ return `${node.name.object.name}.${node.name.property.name}`
195
+ return null
196
+ }
197
+ const tagName = getName()
198
+ if (!tagName) return null
199
+ if (isNextImageAlias(imageAliases, tagName)) return 'img'
200
+ return tagName
201
+ }
202
+
203
+ module.exports = {
204
+ shouldTag,
205
+ isNextImageAlias,
206
+ extractLiteralValue,
207
+ findVariableDeclarations,
208
+ findMapContext,
209
+ getSemanticName,
210
+ }
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@acebuilder/component-tagger",
3
+ "version": "0.0.1",
4
+ "description": "Next.js webpack pre-loader to tag React components with AceBuilder metadata during development.",
5
+ "main": "index.js",
6
+ "types": "index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "import": "./index.js",
10
+ "require": "./index.js",
11
+ "default": "./index.js",
12
+ "types": "./index.d.ts"
13
+ }
14
+ },
15
+ "files": [
16
+ "index.js",
17
+ "index.d.ts",
18
+ "lib/**/*"
19
+ ],
20
+ "sideEffects": false,
21
+ "publishConfig": {
22
+ "access": "public"
23
+ },
24
+ "engines": {
25
+ "node": ">=18"
26
+ },
27
+ "keywords": [
28
+ "acebuilder"
29
+ ],
30
+ "dependencies": {
31
+ "@babel/parser": "^7.28.4",
32
+ "estree-walker": "2.0.2",
33
+ "magic-string": "^0.30.19",
34
+ "source-map": "^0.7.0"
35
+ }
36
+ }