@salesforce/vite-plugin-webapp-experimental 1.47.0 → 1.48.0

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.
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Copyright (c) 2026, Salesforce, Inc.,
3
+ * All rights reserved.
4
+ * For full license text, see the LICENSE.txt file
5
+ */
6
+ /**
7
+ * React design-time Babel plugin.
8
+ *
9
+ * Injects DOM-locating and text metadata attributes into HTML JSX elements at compile-time.
10
+ *
11
+ * Attributes injected (when location is available):
12
+ * - data-source-file="<file_path>:<line>:<col>"
13
+ *
14
+ * Optional text metadata:
15
+ * - data-text-type: none | static | dynamic | mixed | element
16
+ *
17
+ * NOTE: This is a Babel plugin. In this repo it's run via a Vite `transform` hook.
18
+ */
19
+ declare const reactDesignTimeLocatorBabelPlugin: (api: object, options: Record<string, any> | null | undefined, dirname: string) => {
20
+ name: string;
21
+ visitor: {
22
+ JSXElement(this: import("@babel/core").PluginPass, path: any, state: any): void;
23
+ };
24
+ };
25
+ export default reactDesignTimeLocatorBabelPlugin;
26
+ //# sourceMappingURL=reactDesignTimeLocatorBabelPlugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reactDesignTimeLocatorBabelPlugin.d.ts","sourceRoot":"","sources":["../../src/babel/reactDesignTimeLocatorBabelPlugin.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAMH;;;;;;;;;;;;GAYG;AACH,QAAA,MAAM,iCAAiC;;;iEAiCnB,GAAG,SAAS,GAAG;;CAmEjC,CAAC;AAEH,eAAe,iCAAiC,CAAC"}
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Copyright (c) 2026, Salesforce, Inc.,
3
+ * All rights reserved.
4
+ * For full license text, see the LICENSE.txt file
5
+ */
6
+ /* eslint-disable @typescript-eslint/no-explicit-any */
7
+ import { declare } from "@babel/helper-plugin-utils";
8
+ /**
9
+ * React design-time Babel plugin.
10
+ *
11
+ * Injects DOM-locating and text metadata attributes into HTML JSX elements at compile-time.
12
+ *
13
+ * Attributes injected (when location is available):
14
+ * - data-source-file="<file_path>:<line>:<col>"
15
+ *
16
+ * Optional text metadata:
17
+ * - data-text-type: none | static | dynamic | mixed | element
18
+ *
19
+ * NOTE: This is a Babel plugin. In this repo it's run via a Vite `transform` hook.
20
+ */
21
+ const reactDesignTimeLocatorBabelPlugin = declare((api) => {
22
+ api.assertVersion(7);
23
+ const t = api.types;
24
+ function isReactFragmentName(nameNode) {
25
+ // <Fragment> or <React.Fragment>
26
+ if (t.isJSXIdentifier(nameNode) && nameNode.name === "Fragment")
27
+ return true;
28
+ if (t.isJSXMemberExpression(nameNode) &&
29
+ t.isJSXIdentifier(nameNode.object, { name: "React" }) &&
30
+ t.isJSXIdentifier(nameNode.property, { name: "Fragment" })) {
31
+ return true;
32
+ }
33
+ return false;
34
+ }
35
+ function hasAttr(openingElement, name) {
36
+ return openingElement.attributes.some((attr) => {
37
+ return t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name) && attr.name.name === name;
38
+ });
39
+ }
40
+ function addAttr(openingElement, name, value) {
41
+ if (hasAttr(openingElement, name))
42
+ return;
43
+ openingElement.attributes.push(t.jsxAttribute(t.jsxIdentifier(name), t.stringLiteral(String(value))));
44
+ }
45
+ return {
46
+ name: "babel-plugin-react-design-time-locator",
47
+ visitor: {
48
+ JSXElement(path, state) {
49
+ const openingElement = path.node.openingElement;
50
+ // Ignore fragments (they don't render a DOM node).
51
+ if (isReactFragmentName(openingElement.name))
52
+ return;
53
+ // Only support common JSX name node types:
54
+ // - <div> (JSXIdentifier)
55
+ // - <Button> (JSXIdentifier)
56
+ // - <React.Suspense> / <Mui.Button> (JSXMemberExpression)
57
+ if (!t.isJSXIdentifier(openingElement.name) &&
58
+ !t.isJSXMemberExpression(openingElement.name)) {
59
+ return;
60
+ }
61
+ // Decide whether this is a native DOM element or a component usage.
62
+ // We tag BOTH:
63
+ // - DOM elements are tagged directly (always works)
64
+ // - Components are tagged via props (works when the component forwards props, e.g. many MUI components)
65
+ let tagHead = "";
66
+ if (t.isJSXIdentifier(openingElement.name)) {
67
+ tagHead = openingElement.name.name;
68
+ }
69
+ else if (t.isJSXMemberExpression(openingElement.name)) {
70
+ // Member expressions are always components in React (e.g. <Foo.Bar>)
71
+ tagHead = openingElement.name.property?.name ?? "";
72
+ }
73
+ if (!tagHead)
74
+ return;
75
+ // Ignore node_modules
76
+ const filename = state?.file?.opts?.filename;
77
+ if (!filename || filename.includes("node_modules"))
78
+ return;
79
+ // Analyze children for text metadata
80
+ const children = path.node.children ?? [];
81
+ const relevantChildren = children.filter((child) => {
82
+ return !(t.isJSXText(child) && child.value.trim() === "");
83
+ });
84
+ let textType = "none"; // none | static | dynamic | mixed | element
85
+ if (relevantChildren.length === 1) {
86
+ const child = relevantChildren[0];
87
+ if (t.isJSXText(child)) {
88
+ textType = "static";
89
+ }
90
+ else if (t.isJSXExpressionContainer(child)) {
91
+ textType = "dynamic";
92
+ }
93
+ else {
94
+ textType = "element";
95
+ }
96
+ }
97
+ else if (relevantChildren.length > 1) {
98
+ textType = "mixed";
99
+ }
100
+ // Inject location info (best-effort)
101
+ const loc = path.node.loc;
102
+ if (loc?.start) {
103
+ const source = `${filename}:${loc.start.line}:${loc.start.column}`;
104
+ addAttr(openingElement, "data-source-file", source);
105
+ }
106
+ // Inject text metadata
107
+ addAttr(openingElement, "data-text-type", textType);
108
+ },
109
+ },
110
+ };
111
+ });
112
+ export default reactDesignTimeLocatorBabelPlugin;
package/dist/index.d.ts CHANGED
@@ -9,6 +9,13 @@ export interface PluginOptions {
9
9
  orgAlias?: string;
10
10
  /** Enable verbose logging */
11
11
  debug?: boolean;
12
+ /**
13
+ * Enable design-time features (inject `data-source-file`, serve & inject design interactions script).
14
+ *
15
+ * Defaults to:
16
+ * - enabled when Vite `mode` is `"design"` (e.g. `vite --mode design`)
17
+ */
18
+ designMode?: boolean;
12
19
  }
13
20
  export default function webappsPlugin(options?: PluginOptions): Plugin;
14
21
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAMH,OAAO,KAAK,EAAE,MAAM,EAAiB,MAAM,MAAM,CAAC;AAUlD,MAAM,WAAW,aAAa;IAC7B,2BAA2B;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,6BAA6B;IAC7B,KAAK,CAAC,EAAE,OAAO,CAAC;CAChB;AAID,MAAM,CAAC,OAAO,UAAU,aAAa,CAAC,OAAO,GAAE,aAAkB,GAAG,MAAM,CAiIzE"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAOH,OAAO,KAAK,EAAE,MAAM,EAAiB,MAAM,MAAM,CAAC;AAWlD,MAAM,WAAW,aAAa;IAC7B,2BAA2B;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,6BAA6B;IAC7B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB;;;;;OAKG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;CACrB;AAID,MAAM,CAAC,OAAO,UAAU,aAAa,CAAC,OAAO,GAAE,aAAkB,GAAG,MAAM,CAiNzE"}
package/dist/index.js CHANGED
@@ -4,7 +4,9 @@
4
4
  * For full license text, see the LICENSE.txt file
5
5
  */
6
6
  import { loadManifest, getOrgInfo } from "@salesforce/webapp-experimental/app";
7
+ import { getDesignModeScriptContent } from "@salesforce/webapp-experimental/design";
7
8
  import { createProxyHandler } from "@salesforce/webapp-experimental/proxy";
9
+ import reactDesignTimeLocatorBabelPlugin from "./babel/reactDesignTimeLocatorBabelPlugin.js";
8
10
  import { getBasePath, getCodeBuilderBasePath, getPort, getDevServerTarget, DEFAULT_API_VERSION, DEFAULT_PORT, } from "./utils.js";
9
11
  const codeBuilderProxyUrl = process.env.CODE_BUILDER_FRAMEWORK_PROXY_URI;
10
12
  export default function webappsPlugin(options = {}) {
@@ -14,14 +16,20 @@ export default function webappsPlugin(options = {}) {
14
16
  let orgInfo;
15
17
  let manifest;
16
18
  let proxyHandler;
19
+ // Allow tests or callers to force design mode without needing to run the `config` hook.
20
+ let designModeEnabled = options.designMode ?? false;
17
21
  return {
18
22
  name: "@salesforce/vite-plugin-webapp-experimental",
23
+ enforce: "pre",
19
24
  async config(config, env) {
20
25
  const rootPath = config.root ?? process.cwd();
26
+ // If caller explicitly set it, keep that value. Otherwise derive from Vite mode.
27
+ designModeEnabled = options.designMode ?? env.mode === "design";
21
28
  // Note: At this stage we may not have the correct manifest path yet,
22
29
  // so we only load the org info to get the API version
23
30
  // Development server configuration
24
31
  let version;
32
+ version = DEFAULT_API_VERSION;
25
33
  try {
26
34
  orgInfo = await getOrgInfo(options.orgAlias);
27
35
  version = orgInfo?.apiVersion || DEFAULT_API_VERSION;
@@ -71,6 +79,33 @@ export default function webappsPlugin(options = {}) {
71
79
  // Add middleware to handle all requests (API, rewrites, redirects, and dev server forwarding)
72
80
  // Must run BEFORE Vite's internal middlewares (hence not returning a post-hook function)
73
81
  server.middlewares.use(async (req, res, next) => {
82
+ // Serve design mode interactions script
83
+ // NOTE: Live Preview and other proxies may host the dev server behind a path prefix,
84
+ // so we intentionally match by suffix rather than strict equality.
85
+ const urlPath = stripUrlQuery(req.url);
86
+ if (designModeEnabled &&
87
+ (urlPath === "/design-mode-interactions.js" ||
88
+ urlPath.endsWith("/design-mode-interactions.js"))) {
89
+ try {
90
+ const script = getDesignModeScriptContent();
91
+ if (script !== null) {
92
+ res.setHeader("Content-Type", "application/javascript");
93
+ res.setHeader("Cache-Control", "no-cache");
94
+ res.end(script);
95
+ return;
96
+ }
97
+ console.error(`[webapps-plugin] Design mode script not found. Run 'npm run build:design' in @salesforce/webapp-experimental.`);
98
+ res.writeHead(404);
99
+ res.end("Design mode script not found");
100
+ return;
101
+ }
102
+ catch (error) {
103
+ console.error("[webapps-plugin] Error serving design mode script:", error);
104
+ res.writeHead(500);
105
+ res.end("Error loading design mode script");
106
+ return;
107
+ }
108
+ }
74
109
  // Let proxy handler handle all requests
75
110
  if (proxyHandler) {
76
111
  try {
@@ -95,6 +130,35 @@ export default function webappsPlugin(options = {}) {
95
130
  }
96
131
  });
97
132
  },
133
+ async transform(code, id) {
134
+ // Only inject design-time locator attributes when design mode is enabled
135
+ if (!designModeEnabled)
136
+ return null;
137
+ // Strip Vite query string (e.g. ?import, ?v=hash)
138
+ const filepath = id.split("?", 1)[0] ?? id;
139
+ // Best-effort: only handle React source files where JSX loc info matters.
140
+ const isTsx = filepath.endsWith(".tsx");
141
+ const isJsx = filepath.endsWith(".jsx");
142
+ if (!isTsx && !isJsx)
143
+ return null;
144
+ if (filepath.includes("node_modules"))
145
+ return null;
146
+ const { transformAsync } = await import("@babel/core");
147
+ const result = await transformAsync(code, {
148
+ filename: filepath,
149
+ babelrc: false,
150
+ configFile: false,
151
+ sourceMaps: true,
152
+ parserOpts: {
153
+ sourceType: "module",
154
+ plugins: isTsx ? ["jsx", "typescript"] : ["jsx"],
155
+ },
156
+ plugins: [reactDesignTimeLocatorBabelPlugin],
157
+ });
158
+ if (!result?.code)
159
+ return null;
160
+ return { code: result.code, map: result.map };
161
+ },
98
162
  async handleHotUpdate({ file, server }) {
99
163
  // Watch for manifest changes and reload
100
164
  if (file.endsWith("webapplication.json")) {
@@ -114,5 +178,21 @@ export default function webappsPlugin(options = {}) {
114
178
  }
115
179
  }
116
180
  },
181
+ transformIndexHtml(html) {
182
+ // Only inject design mode interactions script when design mode is enabled in dev
183
+ if (designModeEnabled) {
184
+ // Use a relative URL so it works when the app is hosted under a path prefix
185
+ // (e.g. VS Code Live Preview or Code Builder reverse proxies).
186
+ const designScriptTag = '<script src="design-mode-interactions.js"></script>';
187
+ return html.replace("</body>", `${designScriptTag}\n</body>`);
188
+ }
189
+ return html;
190
+ },
117
191
  };
118
192
  }
193
+ function stripUrlQuery(url) {
194
+ if (!url)
195
+ return "";
196
+ const queryIndex = url.indexOf("?");
197
+ return queryIndex === -1 ? url : url.slice(0, queryIndex);
198
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@salesforce/vite-plugin-webapp-experimental",
3
3
  "description": "[experimental] Vite plugin for Salesforce Web Applications",
4
- "version": "1.47.0",
4
+ "version": "1.48.0",
5
5
  "license": "SEE LICENSE IN LICENSE.txt",
6
6
  "type": "module",
7
7
  "main": "./dist/index.js",
@@ -26,9 +26,13 @@
26
26
  "test:coverage": "vitest run --coverage"
27
27
  },
28
28
  "dependencies": {
29
- "@salesforce/webapp-experimental": "^1.47.0"
29
+ "@babel/core": "^7.28.4",
30
+ "@babel/helper-plugin-utils": "^7.28.3",
31
+ "@salesforce/webapp-experimental": "^1.48.0"
30
32
  },
31
33
  "devDependencies": {
34
+ "@types/babel__core": "^7.20.5",
35
+ "@types/babel__helper-plugin-utils": "^7.10.3",
32
36
  "vite": "^7.0.0",
33
37
  "vitest": "^4.0.6"
34
38
  },
@@ -41,5 +45,5 @@
41
45
  "publishConfig": {
42
46
  "access": "public"
43
47
  },
44
- "gitHead": "664bfe805c8d96ec2c7b8d3ce403295f420bf42c"
48
+ "gitHead": "5ccfffec66505f70bdf4e6679e886c7bdabfe0ff"
45
49
  }