jsx-loc-plugin 0.1.22
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/dist/index.cjs +131 -0
- package/dist/index.d.cts +22 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +95 -0
- package/dist/loader.cjs +239 -0
- package/dist/loader.d.cts +10 -0
- package/dist/loader.d.ts +10 -0
- package/dist/loader.js +206 -0
- package/package.json +55 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
default: () => index_default,
|
|
34
|
+
withJsxLoc: () => withJsxLoc
|
|
35
|
+
});
|
|
36
|
+
module.exports = __toCommonJS(index_exports);
|
|
37
|
+
var import_path = __toESM(require("path"), 1);
|
|
38
|
+
var import_url = require("url");
|
|
39
|
+
var import_meta = {};
|
|
40
|
+
var computedDirname;
|
|
41
|
+
try {
|
|
42
|
+
computedDirname = import_path.default.dirname((0, import_url.fileURLToPath)(import_meta.url));
|
|
43
|
+
} catch {
|
|
44
|
+
computedDirname = typeof __dirname !== "undefined" ? __dirname : "";
|
|
45
|
+
}
|
|
46
|
+
function withJsxLoc(nextConfig = {}, pluginOptions = {}) {
|
|
47
|
+
const loaderPath = import_path.default.join(computedDirname, "loader.cjs");
|
|
48
|
+
console.log(`[jsx-loc-plugin] Plugin loaded, loader path: ${loaderPath}`);
|
|
49
|
+
const loaderObj = {
|
|
50
|
+
loader: loaderPath,
|
|
51
|
+
options: { ...pluginOptions }
|
|
52
|
+
};
|
|
53
|
+
const jsxRule = {
|
|
54
|
+
test: /\.(jsx|tsx)$/,
|
|
55
|
+
enforce: "pre",
|
|
56
|
+
use: [loaderObj],
|
|
57
|
+
exclude: pluginOptions.exclude || /node_modules/
|
|
58
|
+
};
|
|
59
|
+
if (pluginOptions.include) {
|
|
60
|
+
jsxRule.include = pluginOptions.include;
|
|
61
|
+
}
|
|
62
|
+
const tsxLoaderConfig = {
|
|
63
|
+
condition: {
|
|
64
|
+
all: [
|
|
65
|
+
{ not: "foreign" },
|
|
66
|
+
// Only match paths that end with .tsx (not directories)
|
|
67
|
+
{ path: /\/[^/]+\.tsx$/ }
|
|
68
|
+
]
|
|
69
|
+
},
|
|
70
|
+
loaders: [
|
|
71
|
+
{
|
|
72
|
+
loader: loaderPath,
|
|
73
|
+
options: { ...pluginOptions }
|
|
74
|
+
}
|
|
75
|
+
]
|
|
76
|
+
// as: "*.tsx",
|
|
77
|
+
};
|
|
78
|
+
const jsxLoaderConfig = {
|
|
79
|
+
condition: {
|
|
80
|
+
all: [
|
|
81
|
+
{ not: "foreign" },
|
|
82
|
+
// Only match paths that end with .jsx (not directories)
|
|
83
|
+
{ path: /\/[^/]+\.jsx$/ }
|
|
84
|
+
]
|
|
85
|
+
},
|
|
86
|
+
loaders: [
|
|
87
|
+
{
|
|
88
|
+
loader: loaderPath,
|
|
89
|
+
options: { ...pluginOptions }
|
|
90
|
+
}
|
|
91
|
+
]
|
|
92
|
+
// as: "*.jsx",
|
|
93
|
+
};
|
|
94
|
+
const turbopackRules = {
|
|
95
|
+
"*.tsx": tsxLoaderConfig,
|
|
96
|
+
"*.jsx": jsxLoaderConfig
|
|
97
|
+
};
|
|
98
|
+
const existingTurbopack = nextConfig.turbopack || {};
|
|
99
|
+
const existingRules = existingTurbopack.rules || {};
|
|
100
|
+
const enhancedConfig = {
|
|
101
|
+
...nextConfig,
|
|
102
|
+
// Configure webpack
|
|
103
|
+
webpack(config, options) {
|
|
104
|
+
if (!config.module) {
|
|
105
|
+
config.module = { rules: [] };
|
|
106
|
+
}
|
|
107
|
+
if (!config.module.rules) {
|
|
108
|
+
config.module.rules = [];
|
|
109
|
+
}
|
|
110
|
+
config.module.rules.unshift(jsxRule);
|
|
111
|
+
if (typeof nextConfig.webpack === "function") {
|
|
112
|
+
return nextConfig.webpack(config, options);
|
|
113
|
+
}
|
|
114
|
+
return config;
|
|
115
|
+
},
|
|
116
|
+
// Configure Turbopack (Next.js 16+ top-level turbopack key)
|
|
117
|
+
turbopack: {
|
|
118
|
+
...existingTurbopack,
|
|
119
|
+
rules: {
|
|
120
|
+
...existingRules,
|
|
121
|
+
...turbopackRules
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
return enhancedConfig;
|
|
126
|
+
}
|
|
127
|
+
var index_default = withJsxLoc;
|
|
128
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
129
|
+
0 && (module.exports = {
|
|
130
|
+
withJsxLoc
|
|
131
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { NextConfig } from 'next';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Next.js plugin that adds data-loc attributes to JSX elements.
|
|
5
|
+
* Compatible with Next.js 16+ (supports both webpack and Turbopack)
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
interface JsxLocPluginOptions {
|
|
9
|
+
/** RegExp patterns to include (default: all .jsx/.tsx files) */
|
|
10
|
+
include?: RegExp | RegExp[];
|
|
11
|
+
/** RegExp patterns to exclude (default: /node_modules/) */
|
|
12
|
+
exclude?: RegExp | RegExp[];
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Next.js plugin that adds data-loc attributes to JSX elements
|
|
16
|
+
* to help with component tracking and debugging.
|
|
17
|
+
*
|
|
18
|
+
* Supports both webpack and Turbopack configurations.
|
|
19
|
+
*/
|
|
20
|
+
declare function withJsxLoc(nextConfig?: NextConfig, pluginOptions?: JsxLocPluginOptions): NextConfig;
|
|
21
|
+
|
|
22
|
+
export { type JsxLocPluginOptions, withJsxLoc as default, withJsxLoc };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { NextConfig } from 'next';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Next.js plugin that adds data-loc attributes to JSX elements.
|
|
5
|
+
* Compatible with Next.js 16+ (supports both webpack and Turbopack)
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
interface JsxLocPluginOptions {
|
|
9
|
+
/** RegExp patterns to include (default: all .jsx/.tsx files) */
|
|
10
|
+
include?: RegExp | RegExp[];
|
|
11
|
+
/** RegExp patterns to exclude (default: /node_modules/) */
|
|
12
|
+
exclude?: RegExp | RegExp[];
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Next.js plugin that adds data-loc attributes to JSX elements
|
|
16
|
+
* to help with component tracking and debugging.
|
|
17
|
+
*
|
|
18
|
+
* Supports both webpack and Turbopack configurations.
|
|
19
|
+
*/
|
|
20
|
+
declare function withJsxLoc(nextConfig?: NextConfig, pluginOptions?: JsxLocPluginOptions): NextConfig;
|
|
21
|
+
|
|
22
|
+
export { type JsxLocPluginOptions, withJsxLoc as default, withJsxLoc };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
var computedDirname;
|
|
5
|
+
try {
|
|
6
|
+
computedDirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
} catch {
|
|
8
|
+
computedDirname = typeof __dirname !== "undefined" ? __dirname : "";
|
|
9
|
+
}
|
|
10
|
+
function withJsxLoc(nextConfig = {}, pluginOptions = {}) {
|
|
11
|
+
const loaderPath = path.join(computedDirname, "loader.cjs");
|
|
12
|
+
console.log(`[jsx-loc-plugin] Plugin loaded, loader path: ${loaderPath}`);
|
|
13
|
+
const loaderObj = {
|
|
14
|
+
loader: loaderPath,
|
|
15
|
+
options: { ...pluginOptions }
|
|
16
|
+
};
|
|
17
|
+
const jsxRule = {
|
|
18
|
+
test: /\.(jsx|tsx)$/,
|
|
19
|
+
enforce: "pre",
|
|
20
|
+
use: [loaderObj],
|
|
21
|
+
exclude: pluginOptions.exclude || /node_modules/
|
|
22
|
+
};
|
|
23
|
+
if (pluginOptions.include) {
|
|
24
|
+
jsxRule.include = pluginOptions.include;
|
|
25
|
+
}
|
|
26
|
+
const tsxLoaderConfig = {
|
|
27
|
+
condition: {
|
|
28
|
+
all: [
|
|
29
|
+
{ not: "foreign" },
|
|
30
|
+
// Only match paths that end with .tsx (not directories)
|
|
31
|
+
{ path: /\/[^/]+\.tsx$/ }
|
|
32
|
+
]
|
|
33
|
+
},
|
|
34
|
+
loaders: [
|
|
35
|
+
{
|
|
36
|
+
loader: loaderPath,
|
|
37
|
+
options: { ...pluginOptions }
|
|
38
|
+
}
|
|
39
|
+
]
|
|
40
|
+
// as: "*.tsx",
|
|
41
|
+
};
|
|
42
|
+
const jsxLoaderConfig = {
|
|
43
|
+
condition: {
|
|
44
|
+
all: [
|
|
45
|
+
{ not: "foreign" },
|
|
46
|
+
// Only match paths that end with .jsx (not directories)
|
|
47
|
+
{ path: /\/[^/]+\.jsx$/ }
|
|
48
|
+
]
|
|
49
|
+
},
|
|
50
|
+
loaders: [
|
|
51
|
+
{
|
|
52
|
+
loader: loaderPath,
|
|
53
|
+
options: { ...pluginOptions }
|
|
54
|
+
}
|
|
55
|
+
]
|
|
56
|
+
// as: "*.jsx",
|
|
57
|
+
};
|
|
58
|
+
const turbopackRules = {
|
|
59
|
+
"*.tsx": tsxLoaderConfig,
|
|
60
|
+
"*.jsx": jsxLoaderConfig
|
|
61
|
+
};
|
|
62
|
+
const existingTurbopack = nextConfig.turbopack || {};
|
|
63
|
+
const existingRules = existingTurbopack.rules || {};
|
|
64
|
+
const enhancedConfig = {
|
|
65
|
+
...nextConfig,
|
|
66
|
+
// Configure webpack
|
|
67
|
+
webpack(config, options) {
|
|
68
|
+
if (!config.module) {
|
|
69
|
+
config.module = { rules: [] };
|
|
70
|
+
}
|
|
71
|
+
if (!config.module.rules) {
|
|
72
|
+
config.module.rules = [];
|
|
73
|
+
}
|
|
74
|
+
config.module.rules.unshift(jsxRule);
|
|
75
|
+
if (typeof nextConfig.webpack === "function") {
|
|
76
|
+
return nextConfig.webpack(config, options);
|
|
77
|
+
}
|
|
78
|
+
return config;
|
|
79
|
+
},
|
|
80
|
+
// Configure Turbopack (Next.js 16+ top-level turbopack key)
|
|
81
|
+
turbopack: {
|
|
82
|
+
...existingTurbopack,
|
|
83
|
+
rules: {
|
|
84
|
+
...existingRules,
|
|
85
|
+
...turbopackRules
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
return enhancedConfig;
|
|
90
|
+
}
|
|
91
|
+
var index_default = withJsxLoc;
|
|
92
|
+
export {
|
|
93
|
+
index_default as default,
|
|
94
|
+
withJsxLoc
|
|
95
|
+
};
|
package/dist/loader.cjs
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/loader.ts
|
|
31
|
+
var loader_exports = {};
|
|
32
|
+
__export(loader_exports, {
|
|
33
|
+
default: () => loader_default
|
|
34
|
+
});
|
|
35
|
+
module.exports = __toCommonJS(loader_exports);
|
|
36
|
+
|
|
37
|
+
// src/transform.ts
|
|
38
|
+
var import_parser = require("@babel/parser");
|
|
39
|
+
var import_magic_string = __toESM(require("magic-string"), 1);
|
|
40
|
+
var VALID_EXTENSIONS = /* @__PURE__ */ new Set([".jsx", ".tsx"]);
|
|
41
|
+
var DEFAULT_PARSER_OPTIONS = {
|
|
42
|
+
sourceType: "module",
|
|
43
|
+
plugins: [
|
|
44
|
+
"jsx",
|
|
45
|
+
"typescript",
|
|
46
|
+
"decorators-legacy",
|
|
47
|
+
"classProperties",
|
|
48
|
+
"classPrivateProperties",
|
|
49
|
+
"classPrivateMethods",
|
|
50
|
+
"exportDefaultFrom",
|
|
51
|
+
"exportNamespaceFrom",
|
|
52
|
+
"asyncGenerators",
|
|
53
|
+
"functionBind",
|
|
54
|
+
"functionSent",
|
|
55
|
+
"dynamicImport",
|
|
56
|
+
"numericSeparator",
|
|
57
|
+
"optionalChaining",
|
|
58
|
+
"importMeta",
|
|
59
|
+
"bigInt",
|
|
60
|
+
"optionalCatchBinding",
|
|
61
|
+
"throwExpressions",
|
|
62
|
+
"nullishCoalescingOperator",
|
|
63
|
+
"topLevelAwait"
|
|
64
|
+
],
|
|
65
|
+
errorRecovery: true
|
|
66
|
+
};
|
|
67
|
+
var SKIP_ELEMENTS = /* @__PURE__ */ new Set([
|
|
68
|
+
"Fragment",
|
|
69
|
+
"React.Fragment",
|
|
70
|
+
"Suspense",
|
|
71
|
+
"React.Suspense",
|
|
72
|
+
"StrictMode",
|
|
73
|
+
"React.StrictMode",
|
|
74
|
+
"Profiler",
|
|
75
|
+
"React.Profiler"
|
|
76
|
+
]);
|
|
77
|
+
function shouldProcessFile(filePath) {
|
|
78
|
+
const ext = filePath.slice(filePath.lastIndexOf("."));
|
|
79
|
+
return VALID_EXTENSIONS.has(ext);
|
|
80
|
+
}
|
|
81
|
+
function getElementName(node) {
|
|
82
|
+
if (!node || !node.name) return null;
|
|
83
|
+
const name = node.name;
|
|
84
|
+
if (name.type === "JSXIdentifier") {
|
|
85
|
+
return name.name;
|
|
86
|
+
}
|
|
87
|
+
if (name.type === "JSXMemberExpression") {
|
|
88
|
+
const parts = [];
|
|
89
|
+
let current = name;
|
|
90
|
+
while (current) {
|
|
91
|
+
if (current.type === "JSXMemberExpression") {
|
|
92
|
+
parts.unshift(current.property.name);
|
|
93
|
+
current = current.object;
|
|
94
|
+
} else if (current.type === "JSXIdentifier") {
|
|
95
|
+
parts.unshift(current.name);
|
|
96
|
+
break;
|
|
97
|
+
} else {
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return parts.join(".");
|
|
102
|
+
}
|
|
103
|
+
if (name.type === "JSXNamespacedName") {
|
|
104
|
+
return `${name.namespace.name}:${name.name.name}`;
|
|
105
|
+
}
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
function shouldSkipElement(elementName) {
|
|
109
|
+
if (!elementName) return true;
|
|
110
|
+
return SKIP_ELEMENTS.has(elementName);
|
|
111
|
+
}
|
|
112
|
+
function hasDataLocAttribute(node) {
|
|
113
|
+
if (!node.attributes) return false;
|
|
114
|
+
return node.attributes.some(
|
|
115
|
+
(attr) => attr.type === "JSXAttribute" && attr.name?.type === "JSXIdentifier" && attr.name.name === "data-loc"
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
function findInsertionPoint(node, source) {
|
|
119
|
+
let insertPos = node.name.end;
|
|
120
|
+
if (node.typeParameters) {
|
|
121
|
+
insertPos = node.typeParameters.end;
|
|
122
|
+
}
|
|
123
|
+
if (node.attributes && node.attributes.length > 0) {
|
|
124
|
+
const firstAttr = node.attributes[0];
|
|
125
|
+
const nameEnd = node.typeParameters ? node.typeParameters.end : node.name.end;
|
|
126
|
+
let pos = nameEnd;
|
|
127
|
+
while (pos < firstAttr.start && /\s/.test(source[pos])) {
|
|
128
|
+
pos++;
|
|
129
|
+
}
|
|
130
|
+
insertPos = pos;
|
|
131
|
+
}
|
|
132
|
+
return insertPos;
|
|
133
|
+
}
|
|
134
|
+
function normalizeFilePath(filePath) {
|
|
135
|
+
return filePath.replace(/\\/g, "/");
|
|
136
|
+
}
|
|
137
|
+
function transformJsxCode(source, filePath, options = {}) {
|
|
138
|
+
const parserOptions = {
|
|
139
|
+
...DEFAULT_PARSER_OPTIONS,
|
|
140
|
+
...options.parserOptions
|
|
141
|
+
};
|
|
142
|
+
let ast;
|
|
143
|
+
try {
|
|
144
|
+
ast = (0, import_parser.parse)(source, parserOptions);
|
|
145
|
+
} catch (error) {
|
|
146
|
+
console.error(`[jsx-loc] Failed to parse ${filePath}:`, error);
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
const magicString = new import_magic_string.default(source);
|
|
150
|
+
const normalizedPath = normalizeFilePath(filePath);
|
|
151
|
+
let hasChanges = false;
|
|
152
|
+
function walk(node) {
|
|
153
|
+
if (!node || typeof node !== "object") return;
|
|
154
|
+
if (node.type === "JSXOpeningElement") {
|
|
155
|
+
const elementName = getElementName(node);
|
|
156
|
+
if (!shouldSkipElement(elementName) && !hasDataLocAttribute(node)) {
|
|
157
|
+
const loc = node.loc;
|
|
158
|
+
if (loc && loc.start) {
|
|
159
|
+
const line = loc.start.line;
|
|
160
|
+
const column = loc.start.column;
|
|
161
|
+
const locValue = `${normalizedPath}:${line}:${column}`;
|
|
162
|
+
const insertPos = findInsertionPoint(node, source);
|
|
163
|
+
magicString.appendLeft(insertPos, ` data-loc="${locValue}"`);
|
|
164
|
+
hasChanges = true;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
for (const key of Object.keys(node)) {
|
|
169
|
+
const child = node[key];
|
|
170
|
+
if (Array.isArray(child)) {
|
|
171
|
+
for (const item of child) {
|
|
172
|
+
walk(item);
|
|
173
|
+
}
|
|
174
|
+
} else if (child && typeof child === "object" && child.type) {
|
|
175
|
+
walk(child);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (ast.program && ast.program.body) {
|
|
180
|
+
for (const node of ast.program.body) {
|
|
181
|
+
walk(node);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (!hasChanges) {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
return {
|
|
188
|
+
code: magicString.toString(),
|
|
189
|
+
map: magicString.generateMap({
|
|
190
|
+
source: filePath,
|
|
191
|
+
file: filePath,
|
|
192
|
+
includeContent: true
|
|
193
|
+
})
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// src/loader.ts
|
|
198
|
+
var import_fs = __toESM(require("fs"), 1);
|
|
199
|
+
function isFile(filePath) {
|
|
200
|
+
try {
|
|
201
|
+
const stat = import_fs.default.statSync(filePath);
|
|
202
|
+
return stat.isFile();
|
|
203
|
+
} catch {
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
async function jsxLocLoader(source) {
|
|
208
|
+
const callback = this.async();
|
|
209
|
+
console.log(`[jsx-loc-plugin] Processing: ${this.resourcePath}`);
|
|
210
|
+
if (!isFile(this.resourcePath)) {
|
|
211
|
+
console.log(`[jsx-loc-plugin] Skipping (not a file): ${this.resourcePath}`);
|
|
212
|
+
return callback(null, source);
|
|
213
|
+
}
|
|
214
|
+
if (!shouldProcessFile(this.resourcePath)) {
|
|
215
|
+
console.log(
|
|
216
|
+
`[jsx-loc-plugin] Skipping (shouldProcessFile=false): ${this.resourcePath}`
|
|
217
|
+
);
|
|
218
|
+
return callback(null, source);
|
|
219
|
+
}
|
|
220
|
+
try {
|
|
221
|
+
console.log(`[jsx-loc-plugin] resourcePath: ${this.resourcePath}`);
|
|
222
|
+
const result = transformJsxCode(source, this.resourcePath);
|
|
223
|
+
if (!result) {
|
|
224
|
+
console.log(
|
|
225
|
+
`[jsx-loc-plugin] No transform result for: ${this.resourcePath}`
|
|
226
|
+
);
|
|
227
|
+
return callback(null, source);
|
|
228
|
+
}
|
|
229
|
+
console.log(`[jsx-loc-plugin] Transformed: ${this.resourcePath}`);
|
|
230
|
+
callback(null, result.code, result.map);
|
|
231
|
+
} catch (error) {
|
|
232
|
+
console.error(
|
|
233
|
+
`[jsx-loc-plugin] Error processing file ${this.resourcePath}:`,
|
|
234
|
+
error
|
|
235
|
+
);
|
|
236
|
+
callback(null, source);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
var loader_default = jsxLocLoader;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webpack/Turbopack loader that transforms JSX to add data-loc attributes
|
|
3
|
+
*/
|
|
4
|
+
interface LoaderContext {
|
|
5
|
+
async(): (err: Error | null, content?: string, map?: any) => void;
|
|
6
|
+
resourcePath: string;
|
|
7
|
+
}
|
|
8
|
+
declare function jsxLocLoader(this: LoaderContext, source: string): Promise<void>;
|
|
9
|
+
|
|
10
|
+
export { jsxLocLoader as default };
|
package/dist/loader.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webpack/Turbopack loader that transforms JSX to add data-loc attributes
|
|
3
|
+
*/
|
|
4
|
+
interface LoaderContext {
|
|
5
|
+
async(): (err: Error | null, content?: string, map?: any) => void;
|
|
6
|
+
resourcePath: string;
|
|
7
|
+
}
|
|
8
|
+
declare function jsxLocLoader(this: LoaderContext, source: string): Promise<void>;
|
|
9
|
+
|
|
10
|
+
export { jsxLocLoader as default };
|
package/dist/loader.js
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
// src/transform.ts
|
|
2
|
+
import { parse } from "@babel/parser";
|
|
3
|
+
import MagicString from "magic-string";
|
|
4
|
+
var VALID_EXTENSIONS = /* @__PURE__ */ new Set([".jsx", ".tsx"]);
|
|
5
|
+
var DEFAULT_PARSER_OPTIONS = {
|
|
6
|
+
sourceType: "module",
|
|
7
|
+
plugins: [
|
|
8
|
+
"jsx",
|
|
9
|
+
"typescript",
|
|
10
|
+
"decorators-legacy",
|
|
11
|
+
"classProperties",
|
|
12
|
+
"classPrivateProperties",
|
|
13
|
+
"classPrivateMethods",
|
|
14
|
+
"exportDefaultFrom",
|
|
15
|
+
"exportNamespaceFrom",
|
|
16
|
+
"asyncGenerators",
|
|
17
|
+
"functionBind",
|
|
18
|
+
"functionSent",
|
|
19
|
+
"dynamicImport",
|
|
20
|
+
"numericSeparator",
|
|
21
|
+
"optionalChaining",
|
|
22
|
+
"importMeta",
|
|
23
|
+
"bigInt",
|
|
24
|
+
"optionalCatchBinding",
|
|
25
|
+
"throwExpressions",
|
|
26
|
+
"nullishCoalescingOperator",
|
|
27
|
+
"topLevelAwait"
|
|
28
|
+
],
|
|
29
|
+
errorRecovery: true
|
|
30
|
+
};
|
|
31
|
+
var SKIP_ELEMENTS = /* @__PURE__ */ new Set([
|
|
32
|
+
"Fragment",
|
|
33
|
+
"React.Fragment",
|
|
34
|
+
"Suspense",
|
|
35
|
+
"React.Suspense",
|
|
36
|
+
"StrictMode",
|
|
37
|
+
"React.StrictMode",
|
|
38
|
+
"Profiler",
|
|
39
|
+
"React.Profiler"
|
|
40
|
+
]);
|
|
41
|
+
function shouldProcessFile(filePath) {
|
|
42
|
+
const ext = filePath.slice(filePath.lastIndexOf("."));
|
|
43
|
+
return VALID_EXTENSIONS.has(ext);
|
|
44
|
+
}
|
|
45
|
+
function getElementName(node) {
|
|
46
|
+
if (!node || !node.name) return null;
|
|
47
|
+
const name = node.name;
|
|
48
|
+
if (name.type === "JSXIdentifier") {
|
|
49
|
+
return name.name;
|
|
50
|
+
}
|
|
51
|
+
if (name.type === "JSXMemberExpression") {
|
|
52
|
+
const parts = [];
|
|
53
|
+
let current = name;
|
|
54
|
+
while (current) {
|
|
55
|
+
if (current.type === "JSXMemberExpression") {
|
|
56
|
+
parts.unshift(current.property.name);
|
|
57
|
+
current = current.object;
|
|
58
|
+
} else if (current.type === "JSXIdentifier") {
|
|
59
|
+
parts.unshift(current.name);
|
|
60
|
+
break;
|
|
61
|
+
} else {
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return parts.join(".");
|
|
66
|
+
}
|
|
67
|
+
if (name.type === "JSXNamespacedName") {
|
|
68
|
+
return `${name.namespace.name}:${name.name.name}`;
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
function shouldSkipElement(elementName) {
|
|
73
|
+
if (!elementName) return true;
|
|
74
|
+
return SKIP_ELEMENTS.has(elementName);
|
|
75
|
+
}
|
|
76
|
+
function hasDataLocAttribute(node) {
|
|
77
|
+
if (!node.attributes) return false;
|
|
78
|
+
return node.attributes.some(
|
|
79
|
+
(attr) => attr.type === "JSXAttribute" && attr.name?.type === "JSXIdentifier" && attr.name.name === "data-loc"
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
function findInsertionPoint(node, source) {
|
|
83
|
+
let insertPos = node.name.end;
|
|
84
|
+
if (node.typeParameters) {
|
|
85
|
+
insertPos = node.typeParameters.end;
|
|
86
|
+
}
|
|
87
|
+
if (node.attributes && node.attributes.length > 0) {
|
|
88
|
+
const firstAttr = node.attributes[0];
|
|
89
|
+
const nameEnd = node.typeParameters ? node.typeParameters.end : node.name.end;
|
|
90
|
+
let pos = nameEnd;
|
|
91
|
+
while (pos < firstAttr.start && /\s/.test(source[pos])) {
|
|
92
|
+
pos++;
|
|
93
|
+
}
|
|
94
|
+
insertPos = pos;
|
|
95
|
+
}
|
|
96
|
+
return insertPos;
|
|
97
|
+
}
|
|
98
|
+
function normalizeFilePath(filePath) {
|
|
99
|
+
return filePath.replace(/\\/g, "/");
|
|
100
|
+
}
|
|
101
|
+
function transformJsxCode(source, filePath, options = {}) {
|
|
102
|
+
const parserOptions = {
|
|
103
|
+
...DEFAULT_PARSER_OPTIONS,
|
|
104
|
+
...options.parserOptions
|
|
105
|
+
};
|
|
106
|
+
let ast;
|
|
107
|
+
try {
|
|
108
|
+
ast = parse(source, parserOptions);
|
|
109
|
+
} catch (error) {
|
|
110
|
+
console.error(`[jsx-loc] Failed to parse ${filePath}:`, error);
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
const magicString = new MagicString(source);
|
|
114
|
+
const normalizedPath = normalizeFilePath(filePath);
|
|
115
|
+
let hasChanges = false;
|
|
116
|
+
function walk(node) {
|
|
117
|
+
if (!node || typeof node !== "object") return;
|
|
118
|
+
if (node.type === "JSXOpeningElement") {
|
|
119
|
+
const elementName = getElementName(node);
|
|
120
|
+
if (!shouldSkipElement(elementName) && !hasDataLocAttribute(node)) {
|
|
121
|
+
const loc = node.loc;
|
|
122
|
+
if (loc && loc.start) {
|
|
123
|
+
const line = loc.start.line;
|
|
124
|
+
const column = loc.start.column;
|
|
125
|
+
const locValue = `${normalizedPath}:${line}:${column}`;
|
|
126
|
+
const insertPos = findInsertionPoint(node, source);
|
|
127
|
+
magicString.appendLeft(insertPos, ` data-loc="${locValue}"`);
|
|
128
|
+
hasChanges = true;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
for (const key of Object.keys(node)) {
|
|
133
|
+
const child = node[key];
|
|
134
|
+
if (Array.isArray(child)) {
|
|
135
|
+
for (const item of child) {
|
|
136
|
+
walk(item);
|
|
137
|
+
}
|
|
138
|
+
} else if (child && typeof child === "object" && child.type) {
|
|
139
|
+
walk(child);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (ast.program && ast.program.body) {
|
|
144
|
+
for (const node of ast.program.body) {
|
|
145
|
+
walk(node);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (!hasChanges) {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
code: magicString.toString(),
|
|
153
|
+
map: magicString.generateMap({
|
|
154
|
+
source: filePath,
|
|
155
|
+
file: filePath,
|
|
156
|
+
includeContent: true
|
|
157
|
+
})
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// src/loader.ts
|
|
162
|
+
import fs from "fs";
|
|
163
|
+
function isFile(filePath) {
|
|
164
|
+
try {
|
|
165
|
+
const stat = fs.statSync(filePath);
|
|
166
|
+
return stat.isFile();
|
|
167
|
+
} catch {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
async function jsxLocLoader(source) {
|
|
172
|
+
const callback = this.async();
|
|
173
|
+
console.log(`[jsx-loc-plugin] Processing: ${this.resourcePath}`);
|
|
174
|
+
if (!isFile(this.resourcePath)) {
|
|
175
|
+
console.log(`[jsx-loc-plugin] Skipping (not a file): ${this.resourcePath}`);
|
|
176
|
+
return callback(null, source);
|
|
177
|
+
}
|
|
178
|
+
if (!shouldProcessFile(this.resourcePath)) {
|
|
179
|
+
console.log(
|
|
180
|
+
`[jsx-loc-plugin] Skipping (shouldProcessFile=false): ${this.resourcePath}`
|
|
181
|
+
);
|
|
182
|
+
return callback(null, source);
|
|
183
|
+
}
|
|
184
|
+
try {
|
|
185
|
+
console.log(`[jsx-loc-plugin] resourcePath: ${this.resourcePath}`);
|
|
186
|
+
const result = transformJsxCode(source, this.resourcePath);
|
|
187
|
+
if (!result) {
|
|
188
|
+
console.log(
|
|
189
|
+
`[jsx-loc-plugin] No transform result for: ${this.resourcePath}`
|
|
190
|
+
);
|
|
191
|
+
return callback(null, source);
|
|
192
|
+
}
|
|
193
|
+
console.log(`[jsx-loc-plugin] Transformed: ${this.resourcePath}`);
|
|
194
|
+
callback(null, result.code, result.map);
|
|
195
|
+
} catch (error) {
|
|
196
|
+
console.error(
|
|
197
|
+
`[jsx-loc-plugin] Error processing file ${this.resourcePath}:`,
|
|
198
|
+
error
|
|
199
|
+
);
|
|
200
|
+
callback(null, source);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
var loader_default = jsxLocLoader;
|
|
204
|
+
export {
|
|
205
|
+
loader_default as default
|
|
206
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "jsx-loc-plugin",
|
|
3
|
+
"version": "0.1.22",
|
|
4
|
+
"description": "Next.js plugin that adds data-loc attributes to JSX elements for UILint inspection",
|
|
5
|
+
"author": "Peter Suggate",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/peter-suggate/uilint.git",
|
|
10
|
+
"directory": "packages/jsx-loc-plugin"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/peter-suggate/uilint#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/peter-suggate/uilint/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"next",
|
|
18
|
+
"nextjs",
|
|
19
|
+
"jsx",
|
|
20
|
+
"uilint",
|
|
21
|
+
"plugin",
|
|
22
|
+
"webpack",
|
|
23
|
+
"turbopack"
|
|
24
|
+
],
|
|
25
|
+
"type": "module",
|
|
26
|
+
"main": "./dist/index.cjs",
|
|
27
|
+
"module": "./dist/index.js",
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"types": "./dist/index.d.ts",
|
|
32
|
+
"import": "./dist/index.js",
|
|
33
|
+
"require": "./dist/index.cjs"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"dist"
|
|
38
|
+
],
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@babel/parser": "^7.24.0",
|
|
41
|
+
"magic-string": "^0.30.10"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"next": "^16.1.1",
|
|
45
|
+
"tsup": "^8.0.0",
|
|
46
|
+
"typescript": "^5.0.0"
|
|
47
|
+
},
|
|
48
|
+
"peerDependencies": {
|
|
49
|
+
"next": ">=14.0.0"
|
|
50
|
+
},
|
|
51
|
+
"scripts": {
|
|
52
|
+
"build": "tsup",
|
|
53
|
+
"dev": "tsup --watch"
|
|
54
|
+
}
|
|
55
|
+
}
|