@lego-build/plugins 0.0.1 → 0.0.3
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.d.ts +30 -4
- package/dist/index.js +289 -21
- package/dist/webpack-loader.d.ts +25 -0
- package/dist/webpack-loader.js +198 -0
- package/package.json +34 -6
package/dist/index.d.ts
CHANGED
|
@@ -3,10 +3,36 @@ declare function setupIframeBridge(): void;
|
|
|
3
3
|
|
|
4
4
|
declare function setupElementSelector(): void;
|
|
5
5
|
|
|
6
|
+
interface SourceLocatorOptions {
|
|
7
|
+
/** Files to include */
|
|
8
|
+
include?: string | string[];
|
|
9
|
+
/** Files to exclude */
|
|
10
|
+
exclude?: string | string[];
|
|
11
|
+
/** Prefix for data attributes (default: 'locator') */
|
|
12
|
+
prefix?: string;
|
|
13
|
+
/** Enable/disable the plugin (default: true) */
|
|
14
|
+
enable?: boolean;
|
|
15
|
+
/** Custom filter for tags */
|
|
16
|
+
filterTag?: string[] | ((tag: string) => boolean);
|
|
17
|
+
/** Auto inject iframe bridge import (default: true) */
|
|
18
|
+
autoInjectBridge?: boolean;
|
|
19
|
+
/** Entry files to inject bridge import (default: auto-detect main entry) */
|
|
20
|
+
entryFiles?: string[];
|
|
21
|
+
}
|
|
22
|
+
interface TransformResult {
|
|
23
|
+
code: string;
|
|
24
|
+
map: any;
|
|
25
|
+
}
|
|
6
26
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
27
|
+
* Vite plugin for adding source locator attributes to JSX elements.
|
|
28
|
+
* This enables click-to-source functionality in the preview iframe.
|
|
9
29
|
*/
|
|
10
|
-
declare function
|
|
30
|
+
declare function sourceLocator(options?: SourceLocatorOptions): {
|
|
31
|
+
name: string;
|
|
32
|
+
enforce: "pre";
|
|
33
|
+
apply: "serve";
|
|
34
|
+
version: string;
|
|
35
|
+
transform(code: string, id: string): Promise<TransformResult | null | undefined>;
|
|
36
|
+
};
|
|
11
37
|
|
|
12
|
-
export {
|
|
38
|
+
export { type SourceLocatorOptions, sendToParent, setupElementSelector, setupIframeBridge, sourceLocator };
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
// src/bridge.ts
|
|
2
|
+
function getParentOrigin() {
|
|
3
|
+
if (document.referrer) {
|
|
4
|
+
try {
|
|
5
|
+
return new URL(document.referrer).origin;
|
|
6
|
+
} catch {
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
if (typeof window !== "undefined" && window.location.hostname === "localhost") {
|
|
10
|
+
return "http://localhost:4000";
|
|
11
|
+
}
|
|
12
|
+
return "*";
|
|
13
|
+
}
|
|
14
|
+
var PARENT_ORIGIN = getParentOrigin();
|
|
2
15
|
function sendToParent(type, payload) {
|
|
3
|
-
window.parent.postMessage({ type, payload },
|
|
16
|
+
window.parent.postMessage({ type, payload }, PARENT_ORIGIN);
|
|
4
17
|
}
|
|
5
18
|
function setupIframeBridge() {
|
|
6
19
|
const pushState = window.history.pushState.bind(window.history);
|
|
@@ -90,16 +103,37 @@ function createTooltip() {
|
|
|
90
103
|
document.body.appendChild(tooltip);
|
|
91
104
|
return tooltip;
|
|
92
105
|
}
|
|
106
|
+
var EDITOR_MIN_WIDTH = 200;
|
|
107
|
+
var EDITOR_APPROX_HEIGHT = 180;
|
|
108
|
+
var EDITOR_OFFSET = 8;
|
|
109
|
+
var VIEWPORT_PADDING = 16;
|
|
93
110
|
function createInlineEditor(state, element, onSave, onCancel) {
|
|
94
111
|
const rect = element.getBoundingClientRect();
|
|
95
112
|
const elementText = getElementTextContent(element);
|
|
113
|
+
const viewportWidth = window.innerWidth;
|
|
114
|
+
const viewportHeight = window.innerHeight;
|
|
115
|
+
const editorWidth = Math.max(rect.width, EDITOR_MIN_WIDTH);
|
|
116
|
+
let left = rect.left;
|
|
117
|
+
let top = rect.bottom + EDITOR_OFFSET;
|
|
118
|
+
if (left + editorWidth > viewportWidth - VIEWPORT_PADDING) {
|
|
119
|
+
left = viewportWidth - editorWidth - VIEWPORT_PADDING;
|
|
120
|
+
}
|
|
121
|
+
if (left < VIEWPORT_PADDING) {
|
|
122
|
+
left = VIEWPORT_PADDING;
|
|
123
|
+
}
|
|
124
|
+
if (top + EDITOR_APPROX_HEIGHT > viewportHeight - VIEWPORT_PADDING) {
|
|
125
|
+
top = rect.top - EDITOR_APPROX_HEIGHT - EDITOR_OFFSET;
|
|
126
|
+
if (top < VIEWPORT_PADDING) {
|
|
127
|
+
top = VIEWPORT_PADDING;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
96
130
|
const editor = document.createElement("div");
|
|
97
131
|
editor.id = "lego-inline-editor";
|
|
98
132
|
editor.style.cssText = `
|
|
99
133
|
position: fixed;
|
|
100
|
-
top: ${
|
|
101
|
-
left: ${
|
|
102
|
-
width: ${
|
|
134
|
+
top: ${top}px;
|
|
135
|
+
left: ${left}px;
|
|
136
|
+
width: ${editorWidth}px;
|
|
103
137
|
z-index: 2147483647;
|
|
104
138
|
background: white;
|
|
105
139
|
border: 2px solid #3b82f6;
|
|
@@ -107,6 +141,7 @@ function createInlineEditor(state, element, onSave, onCancel) {
|
|
|
107
141
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
|
|
108
142
|
padding: 8px;
|
|
109
143
|
font-family: ui-sans-serif, system-ui, sans-serif;
|
|
144
|
+
box-sizing: border-box;
|
|
110
145
|
`;
|
|
111
146
|
const header = document.createElement("div");
|
|
112
147
|
header.style.cssText = `
|
|
@@ -216,16 +251,43 @@ function createInlineEditor(state, element, onSave, onCancel) {
|
|
|
216
251
|
return editor;
|
|
217
252
|
}
|
|
218
253
|
function getElementTextContent(element) {
|
|
219
|
-
|
|
254
|
+
const MAX_TEXT_LENGTH = 200;
|
|
255
|
+
const truncateText = (text) => {
|
|
256
|
+
const trimmed = text.trim();
|
|
257
|
+
return trimmed.length > MAX_TEXT_LENGTH ? trimmed.slice(0, MAX_TEXT_LENGTH) + "..." : trimmed;
|
|
258
|
+
};
|
|
259
|
+
let directText = "";
|
|
220
260
|
for (const node of element.childNodes) {
|
|
221
261
|
if (node.nodeType === Node.TEXT_NODE) {
|
|
222
|
-
|
|
262
|
+
directText += node.textContent || "";
|
|
223
263
|
}
|
|
224
264
|
}
|
|
225
|
-
|
|
226
|
-
|
|
265
|
+
directText = directText.trim();
|
|
266
|
+
if (directText) {
|
|
267
|
+
return truncateText(directText);
|
|
227
268
|
}
|
|
228
|
-
|
|
269
|
+
if (element.children.length === 1) {
|
|
270
|
+
const child = element.children[0];
|
|
271
|
+
const simpleWrapperTags = ["span", "strong", "em", "b", "i", "small", "mark", "del", "ins", "sub", "sup"];
|
|
272
|
+
if (simpleWrapperTags.includes(child.tagName.toLowerCase())) {
|
|
273
|
+
const childText = child.textContent?.trim() || "";
|
|
274
|
+
if (childText) {
|
|
275
|
+
return truncateText(childText);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
const hasComplexChildren = Array.from(element.children).some((child) => {
|
|
280
|
+
const tagName = child.tagName.toLowerCase();
|
|
281
|
+
const complexTags = ["div", "section", "article", "ul", "ol", "table", "form", "nav", "header", "footer", "main"];
|
|
282
|
+
return complexTags.includes(tagName) || child.children.length > 0;
|
|
283
|
+
});
|
|
284
|
+
if (!hasComplexChildren && element.children.length <= 2) {
|
|
285
|
+
const fullText = element.textContent?.trim() || "";
|
|
286
|
+
if (fullText) {
|
|
287
|
+
return truncateText(fullText);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return "";
|
|
229
291
|
}
|
|
230
292
|
function getElementDepth(element) {
|
|
231
293
|
let depth = 0;
|
|
@@ -465,6 +527,7 @@ function setupElementSelector() {
|
|
|
465
527
|
};
|
|
466
528
|
const handleClick = (e) => {
|
|
467
529
|
if (!state.isActive) return;
|
|
530
|
+
e._legoSelectorHandled = true;
|
|
468
531
|
if (state.isEditing) {
|
|
469
532
|
const target = e.target;
|
|
470
533
|
if (state.inlineEditor?.contains(target)) {
|
|
@@ -481,20 +544,23 @@ function setupElementSelector() {
|
|
|
481
544
|
}
|
|
482
545
|
e.preventDefault();
|
|
483
546
|
e.stopPropagation();
|
|
484
|
-
e.stopImmediatePropagation();
|
|
485
547
|
cancelInlineEdit();
|
|
486
548
|
return;
|
|
487
549
|
}
|
|
488
550
|
e.preventDefault();
|
|
489
551
|
e.stopPropagation();
|
|
490
|
-
e.stopImmediatePropagation();
|
|
491
552
|
selectElement();
|
|
492
553
|
};
|
|
493
554
|
const handleDoubleClick = (e) => {
|
|
494
555
|
if (!state.isActive || state.isEditing) return;
|
|
556
|
+
const target = e.target;
|
|
557
|
+
if (target.id === "lego-element-selector-overlay" || target.id === "lego-element-selector-tooltip" || target.id === "lego-inline-editor") {
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
e._legoSelectorHandled = true;
|
|
495
561
|
e.preventDefault();
|
|
496
562
|
e.stopPropagation();
|
|
497
|
-
|
|
563
|
+
state.hoveredElement = target;
|
|
498
564
|
startEditing();
|
|
499
565
|
};
|
|
500
566
|
const activateSelector = () => {
|
|
@@ -548,19 +614,221 @@ function setupElementSelector() {
|
|
|
548
614
|
});
|
|
549
615
|
}
|
|
550
616
|
|
|
551
|
-
// src/
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
617
|
+
// src/source-locator.ts
|
|
618
|
+
import path from "path";
|
|
619
|
+
import * as parser from "@babel/parser";
|
|
620
|
+
import traverse from "@babel/traverse";
|
|
621
|
+
import generate from "@babel/generator";
|
|
622
|
+
import * as t from "@babel/types";
|
|
623
|
+
import micromatch from "micromatch";
|
|
624
|
+
import { createHash } from "crypto";
|
|
625
|
+
var HTML_TAGS = [
|
|
626
|
+
"div",
|
|
627
|
+
"span",
|
|
628
|
+
"p",
|
|
629
|
+
"h1",
|
|
630
|
+
"h2",
|
|
631
|
+
"h3",
|
|
632
|
+
"h4",
|
|
633
|
+
"h5",
|
|
634
|
+
"h6",
|
|
635
|
+
"button",
|
|
636
|
+
"a",
|
|
637
|
+
"img",
|
|
638
|
+
"input",
|
|
639
|
+
"textarea",
|
|
640
|
+
"select",
|
|
641
|
+
"form",
|
|
642
|
+
"ul",
|
|
643
|
+
"ol",
|
|
644
|
+
"li",
|
|
645
|
+
"table",
|
|
646
|
+
"thead",
|
|
647
|
+
"tbody",
|
|
648
|
+
"tr",
|
|
649
|
+
"td",
|
|
650
|
+
"th",
|
|
651
|
+
"header",
|
|
652
|
+
"footer",
|
|
653
|
+
"nav",
|
|
654
|
+
"main",
|
|
655
|
+
"section",
|
|
656
|
+
"article",
|
|
657
|
+
"aside",
|
|
658
|
+
"label",
|
|
659
|
+
"strong",
|
|
660
|
+
"em",
|
|
661
|
+
"code",
|
|
662
|
+
"pre",
|
|
663
|
+
"blockquote"
|
|
664
|
+
];
|
|
665
|
+
var TEXT_TAGS = ["label", "span", "button", "p", "div", "h1", "h2", "h3", "h4", "h5", "h6"];
|
|
666
|
+
function getWorkspaceRoot(filePath) {
|
|
667
|
+
let dir = path.resolve(filePath);
|
|
668
|
+
const { root } = path.parse(dir);
|
|
669
|
+
while (dir !== root) {
|
|
670
|
+
if (path.basename(dir) === "workspace") return dir;
|
|
671
|
+
dir = path.dirname(dir);
|
|
672
|
+
}
|
|
673
|
+
return path.resolve(process.cwd(), "..");
|
|
674
|
+
}
|
|
675
|
+
function shouldProcessTag(tagName, options) {
|
|
676
|
+
if (options.filterTag) {
|
|
677
|
+
if (Array.isArray(options.filterTag)) {
|
|
678
|
+
return options.filterTag.includes(tagName);
|
|
557
679
|
}
|
|
558
|
-
|
|
680
|
+
return options.filterTag(tagName);
|
|
681
|
+
}
|
|
682
|
+
return HTML_TAGS.includes(tagName.toLowerCase());
|
|
683
|
+
}
|
|
684
|
+
function getElementTextContent2(nodePath) {
|
|
685
|
+
const parent = nodePath.parent;
|
|
686
|
+
if (!t.isJSXElement(parent)) return "";
|
|
687
|
+
let text = "";
|
|
688
|
+
let hasExpression = false;
|
|
689
|
+
for (const child of parent.children) {
|
|
690
|
+
if (t.isJSXText(child)) {
|
|
691
|
+
text += child.value;
|
|
692
|
+
} else if (t.isJSXExpressionContainer(child)) {
|
|
693
|
+
hasExpression = true;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
return hasExpression ? "" : text.trim();
|
|
697
|
+
}
|
|
698
|
+
function transformJSX(code, filePath, prefix, options) {
|
|
699
|
+
const ast = parser.parse(code, {
|
|
700
|
+
sourceType: "module",
|
|
701
|
+
plugins: ["jsx", "typescript", "classProperties", "decorators-legacy"]
|
|
702
|
+
});
|
|
703
|
+
const workspaceRoot = getWorkspaceRoot(filePath);
|
|
704
|
+
const relativePath = path.relative(workspaceRoot, filePath);
|
|
705
|
+
let hasChanges = false;
|
|
706
|
+
traverse(ast, {
|
|
707
|
+
JSXOpeningElement(nodePath) {
|
|
708
|
+
const node = nodePath.node;
|
|
709
|
+
const hasLocatorAttr = node.attributes.some(
|
|
710
|
+
(attr) => t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name) && attr.name.name.startsWith(`data-${prefix}`)
|
|
711
|
+
);
|
|
712
|
+
if (hasLocatorAttr) return;
|
|
713
|
+
if (t.isJSXIdentifier(node.name) && node.name.name === "Fragment") return;
|
|
714
|
+
if (t.isJSXMemberExpression(node.name) && t.isJSXIdentifier(node.name.object) && node.name.object.name === "React" && t.isJSXIdentifier(node.name.property) && node.name.property.name === "Fragment")
|
|
715
|
+
return;
|
|
716
|
+
let tagName = "";
|
|
717
|
+
if (t.isJSXIdentifier(node.name)) {
|
|
718
|
+
tagName = node.name.name;
|
|
719
|
+
}
|
|
720
|
+
if (!shouldProcessTag(tagName, options)) return;
|
|
721
|
+
const line = node.loc?.start.line;
|
|
722
|
+
const column = node.loc?.start.column;
|
|
723
|
+
if (!line || column === void 0) return;
|
|
724
|
+
const textContent = TEXT_TAGS.includes(tagName.toLowerCase()) ? getElementTextContent2(nodePath) : "";
|
|
725
|
+
const attributes = [
|
|
726
|
+
t.jsxAttribute(t.jsxIdentifier(`data-${prefix}-path`), t.stringLiteral(relativePath)),
|
|
727
|
+
t.jsxAttribute(t.jsxIdentifier(`data-${prefix}-line`), t.stringLiteral(String(line))),
|
|
728
|
+
t.jsxAttribute(t.jsxIdentifier(`data-${prefix}-column`), t.stringLiteral(String(column))),
|
|
729
|
+
t.jsxAttribute(t.jsxIdentifier(`data-${prefix}-tag`), t.stringLiteral(tagName))
|
|
730
|
+
];
|
|
731
|
+
if (textContent) {
|
|
732
|
+
attributes.push(
|
|
733
|
+
t.jsxAttribute(
|
|
734
|
+
t.jsxIdentifier(`data-${prefix}-text`),
|
|
735
|
+
t.stringLiteral(textContent)
|
|
736
|
+
)
|
|
737
|
+
);
|
|
738
|
+
}
|
|
739
|
+
const spreadIndex = node.attributes.findIndex((attr) => t.isJSXSpreadAttribute(attr));
|
|
740
|
+
if (spreadIndex === -1) {
|
|
741
|
+
node.attributes.push(...attributes);
|
|
742
|
+
} else {
|
|
743
|
+
node.attributes.splice(spreadIndex, 0, ...attributes);
|
|
744
|
+
}
|
|
745
|
+
hasChanges = true;
|
|
746
|
+
}
|
|
747
|
+
});
|
|
748
|
+
if (!hasChanges) return null;
|
|
749
|
+
const result = generate(ast, {}, code);
|
|
750
|
+
return {
|
|
751
|
+
code: result.code,
|
|
752
|
+
map: result.map
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
var transformCache = /* @__PURE__ */ new Map();
|
|
756
|
+
function isEntryFile(filePath, entryFiles) {
|
|
757
|
+
if (entryFiles && entryFiles.length > 0) {
|
|
758
|
+
return entryFiles.some((entry) => filePath.endsWith(entry));
|
|
759
|
+
}
|
|
760
|
+
const fileName = path.basename(filePath);
|
|
761
|
+
const entryPatterns = [
|
|
762
|
+
"main.tsx",
|
|
763
|
+
"main.ts",
|
|
764
|
+
"main.jsx",
|
|
765
|
+
"main.js",
|
|
766
|
+
"index.tsx",
|
|
767
|
+
"index.ts",
|
|
768
|
+
"index.jsx",
|
|
769
|
+
"index.js",
|
|
770
|
+
"App.tsx",
|
|
771
|
+
"App.jsx"
|
|
772
|
+
];
|
|
773
|
+
return entryPatterns.includes(fileName);
|
|
774
|
+
}
|
|
775
|
+
function sourceLocator(options = {}) {
|
|
776
|
+
const {
|
|
777
|
+
include = ["src/**/*.{jsx,tsx,js,ts}"],
|
|
778
|
+
exclude = [],
|
|
779
|
+
prefix = "locator",
|
|
780
|
+
enable = true,
|
|
781
|
+
autoInjectBridge = true,
|
|
782
|
+
entryFiles
|
|
783
|
+
} = options;
|
|
784
|
+
const includePatterns = Array.isArray(include) ? include : [include];
|
|
785
|
+
const excludePatterns = Array.isArray(exclude) ? exclude : [exclude];
|
|
786
|
+
return {
|
|
787
|
+
name: "@lego/plugin-source-locator",
|
|
788
|
+
enforce: "pre",
|
|
789
|
+
apply: "serve",
|
|
790
|
+
version: "0.0.1",
|
|
791
|
+
async transform(code, id) {
|
|
792
|
+
if (!enable) return;
|
|
793
|
+
if (autoInjectBridge && isEntryFile(id, entryFiles)) {
|
|
794
|
+
const bridgeImport = `import "@lego-build/plugins";`;
|
|
795
|
+
if (!code.includes(bridgeImport) && !code.includes("@lego-build/plugins")) {
|
|
796
|
+
code = `${bridgeImport}
|
|
797
|
+
${code}`;
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
const relativePath = path.relative(process.cwd(), id);
|
|
801
|
+
const isIncluded = micromatch.isMatch(relativePath, includePatterns);
|
|
802
|
+
const isExcluded = excludePatterns.length > 0 && micromatch.isMatch(relativePath, excludePatterns);
|
|
803
|
+
if (!isIncluded || isExcluded) return code !== code ? { code, map: null } : null;
|
|
804
|
+
const ext = path.extname(id);
|
|
805
|
+
if (![".jsx", ".tsx", ".js", ".ts"].includes(ext)) return null;
|
|
806
|
+
const hash = createHash("md5").update(code).digest("hex");
|
|
807
|
+
const cacheKey = `${id}:${hash}`;
|
|
808
|
+
if (transformCache.has(cacheKey)) {
|
|
809
|
+
return transformCache.get(cacheKey);
|
|
810
|
+
}
|
|
811
|
+
const result = transformJSX(code, id, prefix, options);
|
|
812
|
+
if (result) {
|
|
813
|
+
transformCache.set(cacheKey, result);
|
|
814
|
+
return result;
|
|
815
|
+
}
|
|
816
|
+
return null;
|
|
817
|
+
}
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// src/index.ts
|
|
822
|
+
try {
|
|
823
|
+
if (typeof window !== "undefined" && window.parent !== window) {
|
|
824
|
+
setupIframeBridge();
|
|
825
|
+
setupElementSelector();
|
|
559
826
|
}
|
|
827
|
+
} catch {
|
|
560
828
|
}
|
|
561
829
|
export {
|
|
562
|
-
initIframeBridge,
|
|
563
830
|
sendToParent,
|
|
564
831
|
setupElementSelector,
|
|
565
|
-
setupIframeBridge
|
|
832
|
+
setupIframeBridge,
|
|
833
|
+
sourceLocator
|
|
566
834
|
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
interface SourceLocatorLoaderOptions {
|
|
2
|
+
/** Prefix for data attributes (default: 'locator') */
|
|
3
|
+
prefix?: string;
|
|
4
|
+
/** Custom filter for tags */
|
|
5
|
+
filterTag?: string[] | ((tag: string) => boolean);
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Webpack loader for adding source locator attributes to JSX elements.
|
|
9
|
+
* Use in next.config.js with webpack config.
|
|
10
|
+
*/
|
|
11
|
+
declare function sourceLocatorLoader(this: any, source: string): string;
|
|
12
|
+
/**
|
|
13
|
+
* Helper to add source locator loader to Next.js webpack config.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* // next.config.mjs
|
|
17
|
+
* import { withSourceLocator } from '@lego-build/plugins/next';
|
|
18
|
+
*
|
|
19
|
+
* export default withSourceLocator({
|
|
20
|
+
* // your next config
|
|
21
|
+
* });
|
|
22
|
+
*/
|
|
23
|
+
declare function withSourceLocator(nextConfig?: any, loaderOptions?: SourceLocatorLoaderOptions): any;
|
|
24
|
+
|
|
25
|
+
export { type SourceLocatorLoaderOptions, sourceLocatorLoader as default, withSourceLocator };
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
// src/webpack-loader.ts
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { createRequire } from "module";
|
|
4
|
+
import * as parser from "@babel/parser";
|
|
5
|
+
import traverse from "@babel/traverse";
|
|
6
|
+
import generate from "@babel/generator";
|
|
7
|
+
import * as t from "@babel/types";
|
|
8
|
+
var require2 = createRequire(import.meta.url);
|
|
9
|
+
var HTML_TAGS = [
|
|
10
|
+
"div",
|
|
11
|
+
"span",
|
|
12
|
+
"p",
|
|
13
|
+
"h1",
|
|
14
|
+
"h2",
|
|
15
|
+
"h3",
|
|
16
|
+
"h4",
|
|
17
|
+
"h5",
|
|
18
|
+
"h6",
|
|
19
|
+
"button",
|
|
20
|
+
"a",
|
|
21
|
+
"img",
|
|
22
|
+
"input",
|
|
23
|
+
"textarea",
|
|
24
|
+
"select",
|
|
25
|
+
"form",
|
|
26
|
+
"ul",
|
|
27
|
+
"ol",
|
|
28
|
+
"li",
|
|
29
|
+
"table",
|
|
30
|
+
"thead",
|
|
31
|
+
"tbody",
|
|
32
|
+
"tr",
|
|
33
|
+
"td",
|
|
34
|
+
"th",
|
|
35
|
+
"header",
|
|
36
|
+
"footer",
|
|
37
|
+
"nav",
|
|
38
|
+
"main",
|
|
39
|
+
"section",
|
|
40
|
+
"article",
|
|
41
|
+
"aside",
|
|
42
|
+
"label",
|
|
43
|
+
"strong",
|
|
44
|
+
"em",
|
|
45
|
+
"code",
|
|
46
|
+
"pre",
|
|
47
|
+
"blockquote"
|
|
48
|
+
];
|
|
49
|
+
var TEXT_TAGS = ["label", "span", "button", "p", "div", "h1", "h2", "h3", "h4", "h5", "h6"];
|
|
50
|
+
function getWorkspaceRoot(filePath) {
|
|
51
|
+
let dir = path.resolve(filePath);
|
|
52
|
+
const { root } = path.parse(dir);
|
|
53
|
+
while (dir !== root) {
|
|
54
|
+
if (path.basename(dir) === "workspace") return dir;
|
|
55
|
+
dir = path.dirname(dir);
|
|
56
|
+
}
|
|
57
|
+
return path.resolve(process.cwd(), "..");
|
|
58
|
+
}
|
|
59
|
+
function shouldProcessTag(tagName, options) {
|
|
60
|
+
if (options.filterTag) {
|
|
61
|
+
if (Array.isArray(options.filterTag)) {
|
|
62
|
+
return options.filterTag.includes(tagName);
|
|
63
|
+
}
|
|
64
|
+
return options.filterTag(tagName);
|
|
65
|
+
}
|
|
66
|
+
return HTML_TAGS.includes(tagName.toLowerCase());
|
|
67
|
+
}
|
|
68
|
+
function getElementTextContent(nodePath) {
|
|
69
|
+
const parent = nodePath.parent;
|
|
70
|
+
if (!t.isJSXElement(parent)) return "";
|
|
71
|
+
let text = "";
|
|
72
|
+
let hasExpression = false;
|
|
73
|
+
for (const child of parent.children) {
|
|
74
|
+
if (t.isJSXText(child)) {
|
|
75
|
+
text += child.value;
|
|
76
|
+
} else if (t.isJSXExpressionContainer(child)) {
|
|
77
|
+
hasExpression = true;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return hasExpression ? "" : text.trim();
|
|
81
|
+
}
|
|
82
|
+
function transformJSX(code, filePath, prefix, options) {
|
|
83
|
+
const ast = parser.parse(code, {
|
|
84
|
+
sourceType: "module",
|
|
85
|
+
plugins: ["jsx", "typescript", "classProperties", "decorators-legacy"]
|
|
86
|
+
});
|
|
87
|
+
const workspaceRoot = getWorkspaceRoot(filePath);
|
|
88
|
+
const relativePath = path.relative(workspaceRoot, filePath);
|
|
89
|
+
let hasChanges = false;
|
|
90
|
+
traverse(ast, {
|
|
91
|
+
JSXOpeningElement(nodePath) {
|
|
92
|
+
const node = nodePath.node;
|
|
93
|
+
const hasLocatorAttr = node.attributes.some(
|
|
94
|
+
(attr) => t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name) && attr.name.name.startsWith(`data-${prefix}`)
|
|
95
|
+
);
|
|
96
|
+
if (hasLocatorAttr) return;
|
|
97
|
+
if (t.isJSXIdentifier(node.name) && node.name.name === "Fragment") return;
|
|
98
|
+
if (t.isJSXMemberExpression(node.name) && t.isJSXIdentifier(node.name.object) && node.name.object.name === "React" && t.isJSXIdentifier(node.name.property) && node.name.property.name === "Fragment")
|
|
99
|
+
return;
|
|
100
|
+
let tagName = "";
|
|
101
|
+
if (t.isJSXIdentifier(node.name)) {
|
|
102
|
+
tagName = node.name.name;
|
|
103
|
+
}
|
|
104
|
+
if (!shouldProcessTag(tagName, options)) return;
|
|
105
|
+
const line = node.loc?.start.line;
|
|
106
|
+
const column = node.loc?.start.column;
|
|
107
|
+
if (!line || column === void 0) return;
|
|
108
|
+
const textContent = TEXT_TAGS.includes(tagName.toLowerCase()) ? getElementTextContent(nodePath) : "";
|
|
109
|
+
const attributes = [
|
|
110
|
+
t.jsxAttribute(t.jsxIdentifier(`data-${prefix}-path`), t.stringLiteral(relativePath)),
|
|
111
|
+
t.jsxAttribute(t.jsxIdentifier(`data-${prefix}-line`), t.stringLiteral(String(line))),
|
|
112
|
+
t.jsxAttribute(t.jsxIdentifier(`data-${prefix}-column`), t.stringLiteral(String(column))),
|
|
113
|
+
t.jsxAttribute(t.jsxIdentifier(`data-${prefix}-tag`), t.stringLiteral(tagName))
|
|
114
|
+
];
|
|
115
|
+
if (textContent) {
|
|
116
|
+
attributes.push(
|
|
117
|
+
t.jsxAttribute(
|
|
118
|
+
t.jsxIdentifier(`data-${prefix}-text`),
|
|
119
|
+
t.stringLiteral(textContent)
|
|
120
|
+
)
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
const spreadIndex = node.attributes.findIndex((attr) => t.isJSXSpreadAttribute(attr));
|
|
124
|
+
if (spreadIndex === -1) {
|
|
125
|
+
node.attributes.push(...attributes);
|
|
126
|
+
} else {
|
|
127
|
+
node.attributes.splice(spreadIndex, 0, ...attributes);
|
|
128
|
+
}
|
|
129
|
+
hasChanges = true;
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
if (!hasChanges) return null;
|
|
133
|
+
const result = generate(ast, {}, code);
|
|
134
|
+
return result.code;
|
|
135
|
+
}
|
|
136
|
+
function sourceLocatorLoader(source) {
|
|
137
|
+
const options = this.getOptions() || {};
|
|
138
|
+
const { prefix = "locator" } = options;
|
|
139
|
+
if (process.env.NODE_ENV !== "development") {
|
|
140
|
+
return source;
|
|
141
|
+
}
|
|
142
|
+
const ext = path.extname(this.resourcePath);
|
|
143
|
+
if (![".jsx", ".tsx", ".js", ".ts"].includes(ext)) {
|
|
144
|
+
return source;
|
|
145
|
+
}
|
|
146
|
+
if (this.resourcePath.includes("node_modules")) {
|
|
147
|
+
return source;
|
|
148
|
+
}
|
|
149
|
+
try {
|
|
150
|
+
const result = transformJSX(source, this.resourcePath, prefix, options);
|
|
151
|
+
return result || source;
|
|
152
|
+
} catch (error) {
|
|
153
|
+
console.error(`[source-locator] Error transforming ${this.resourcePath}:`, error);
|
|
154
|
+
return source;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
function withSourceLocator(nextConfig = {}, loaderOptions = {}) {
|
|
158
|
+
return {
|
|
159
|
+
...nextConfig,
|
|
160
|
+
webpack(config, options) {
|
|
161
|
+
if (options.dev) {
|
|
162
|
+
config.module.rules.push({
|
|
163
|
+
test: /\.(jsx|tsx|js|ts)$/,
|
|
164
|
+
exclude: /node_modules/,
|
|
165
|
+
use: [
|
|
166
|
+
{
|
|
167
|
+
loader: require2.resolve("@lego-build/plugins/webpack-loader"),
|
|
168
|
+
options: loaderOptions
|
|
169
|
+
}
|
|
170
|
+
]
|
|
171
|
+
});
|
|
172
|
+
if (!options.isServer) {
|
|
173
|
+
const originalEntry = config.entry;
|
|
174
|
+
config.entry = async () => {
|
|
175
|
+
const entries = await (typeof originalEntry === "function" ? originalEntry() : originalEntry);
|
|
176
|
+
for (const entryName of Object.keys(entries)) {
|
|
177
|
+
if (Array.isArray(entries[entryName])) {
|
|
178
|
+
const bridgePath = require2.resolve("@lego-build/plugins");
|
|
179
|
+
if (!entries[entryName].includes(bridgePath)) {
|
|
180
|
+
entries[entryName].unshift(bridgePath);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return entries;
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (typeof nextConfig.webpack === "function") {
|
|
189
|
+
return nextConfig.webpack(config, options);
|
|
190
|
+
}
|
|
191
|
+
return config;
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
export {
|
|
196
|
+
sourceLocatorLoader as default,
|
|
197
|
+
withSourceLocator
|
|
198
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lego-build/plugins",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -9,19 +9,47 @@
|
|
|
9
9
|
"types": "./dist/index.d.ts",
|
|
10
10
|
"import": "./dist/index.js"
|
|
11
11
|
},
|
|
12
|
-
"./
|
|
13
|
-
"import": "./dist/
|
|
12
|
+
"./webpack-loader": {
|
|
13
|
+
"import": "./dist/webpack-loader.js"
|
|
14
|
+
},
|
|
15
|
+
"./next": {
|
|
16
|
+
"import": "./dist/webpack-loader.js"
|
|
14
17
|
}
|
|
15
18
|
},
|
|
16
19
|
"files": [
|
|
17
20
|
"dist"
|
|
18
21
|
],
|
|
19
22
|
"scripts": {
|
|
20
|
-
"build": "tsup src/index.ts src/
|
|
21
|
-
"dev": "tsup src/index.ts src/
|
|
23
|
+
"build": "tsup src/index.ts src/webpack-loader.ts --format esm --dts",
|
|
24
|
+
"dev": "tsup src/index.ts src/webpack-loader.ts --format esm --dts --watch"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@babel/generator": "^7.27.0",
|
|
28
|
+
"@babel/parser": "^7.27.0",
|
|
29
|
+
"@babel/traverse": "^7.27.0",
|
|
30
|
+
"@babel/types": "^7.27.0",
|
|
31
|
+
"micromatch": "^4.0.5"
|
|
22
32
|
},
|
|
23
33
|
"devDependencies": {
|
|
34
|
+
"@types/babel__generator": "^7.27.0",
|
|
35
|
+
"@types/babel__traverse": "^7.20.7",
|
|
36
|
+
"@types/micromatch": "^4.0.9",
|
|
37
|
+
"@types/node": "^22",
|
|
38
|
+
"@types/webpack": "^5.28.5",
|
|
24
39
|
"tsup": "^8.0.0",
|
|
25
|
-
"typescript": "^5.0.0"
|
|
40
|
+
"typescript": "^5.0.0",
|
|
41
|
+
"webpack": "^5.98.0"
|
|
42
|
+
},
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0",
|
|
45
|
+
"webpack": "^5.0.0"
|
|
46
|
+
},
|
|
47
|
+
"peerDependenciesMeta": {
|
|
48
|
+
"vite": {
|
|
49
|
+
"optional": true
|
|
50
|
+
},
|
|
51
|
+
"webpack": {
|
|
52
|
+
"optional": true
|
|
53
|
+
}
|
|
26
54
|
}
|
|
27
55
|
}
|