@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 +15 -0
- package/index.js +66 -0
- package/lib/blacklists.js +296 -0
- package/lib/loader.js +167 -0
- package/lib/utils.js +210 -0
- package/package.json +36 -0
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
|
+
}
|