@lego-build/plugins 0.0.2 → 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 CHANGED
@@ -14,6 +14,10 @@ interface SourceLocatorOptions {
14
14
  enable?: boolean;
15
15
  /** Custom filter for tags */
16
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[];
17
21
  }
18
22
  interface TransformResult {
19
23
  code: string;
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: ${rect.top}px;
101
- left: ${rect.left}px;
102
- width: ${Math.max(rect.width, 200)}px;
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
- let text = "";
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
- text += node.textContent?.trim() || "";
262
+ directText += node.textContent || "";
223
263
  }
224
264
  }
225
- if (!text && element.children.length === 0) {
226
- text = element.textContent?.trim() || "";
265
+ directText = directText.trim();
266
+ if (directText) {
267
+ return truncateText(directText);
227
268
  }
228
- return text;
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
- e.stopImmediatePropagation();
563
+ state.hoveredElement = target;
498
564
  startEditing();
499
565
  };
500
566
  const activateSelector = () => {
@@ -687,12 +753,33 @@ function transformJSX(code, filePath, prefix, options) {
687
753
  };
688
754
  }
689
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
+ }
690
775
  function sourceLocator(options = {}) {
691
776
  const {
692
777
  include = ["src/**/*.{jsx,tsx,js,ts}"],
693
778
  exclude = [],
694
779
  prefix = "locator",
695
- enable = true
780
+ enable = true,
781
+ autoInjectBridge = true,
782
+ entryFiles
696
783
  } = options;
697
784
  const includePatterns = Array.isArray(include) ? include : [include];
698
785
  const excludePatterns = Array.isArray(exclude) ? exclude : [exclude];
@@ -703,12 +790,19 @@ function sourceLocator(options = {}) {
703
790
  version: "0.0.1",
704
791
  async transform(code, id) {
705
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
+ }
706
800
  const relativePath = path.relative(process.cwd(), id);
707
801
  const isIncluded = micromatch.isMatch(relativePath, includePatterns);
708
802
  const isExcluded = excludePatterns.length > 0 && micromatch.isMatch(relativePath, excludePatterns);
709
- if (!isIncluded || isExcluded) return;
803
+ if (!isIncluded || isExcluded) return code !== code ? { code, map: null } : null;
710
804
  const ext = path.extname(id);
711
- if (![".jsx", ".tsx", ".js", ".ts"].includes(ext)) return;
805
+ if (![".jsx", ".tsx", ".js", ".ts"].includes(ext)) return null;
712
806
  const hash = createHash("md5").update(code).digest("hex");
713
807
  const cacheKey = `${id}:${hash}`;
714
808
  if (transformCache.has(cacheKey)) {
@@ -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.2",
3
+ "version": "0.0.3",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -8,14 +8,20 @@
8
8
  ".": {
9
9
  "types": "./dist/index.d.ts",
10
10
  "import": "./dist/index.js"
11
+ },
12
+ "./webpack-loader": {
13
+ "import": "./dist/webpack-loader.js"
14
+ },
15
+ "./next": {
16
+ "import": "./dist/webpack-loader.js"
11
17
  }
12
18
  },
13
19
  "files": [
14
20
  "dist"
15
21
  ],
16
22
  "scripts": {
17
- "build": "tsup src/index.ts --format esm --dts",
18
- "dev": "tsup src/index.ts --format esm --dts --watch"
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"
19
25
  },
20
26
  "dependencies": {
21
27
  "@babel/generator": "^7.27.0",
@@ -29,15 +35,21 @@
29
35
  "@types/babel__traverse": "^7.20.7",
30
36
  "@types/micromatch": "^4.0.9",
31
37
  "@types/node": "^22",
38
+ "@types/webpack": "^5.28.5",
32
39
  "tsup": "^8.0.0",
33
- "typescript": "^5.0.0"
40
+ "typescript": "^5.0.0",
41
+ "webpack": "^5.98.0"
34
42
  },
35
43
  "peerDependencies": {
36
- "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0"
44
+ "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0",
45
+ "webpack": "^5.0.0"
37
46
  },
38
47
  "peerDependenciesMeta": {
39
48
  "vite": {
40
49
  "optional": true
50
+ },
51
+ "webpack": {
52
+ "optional": true
41
53
  }
42
54
  }
43
55
  }