@salesforce/vite-plugin-webapp-experimental 1.47.0 → 1.48.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/dist/babel/reactDesignTimeLocatorBabelPlugin.d.ts +26 -0
- package/dist/babel/reactDesignTimeLocatorBabelPlugin.d.ts.map +1 -0
- package/dist/babel/reactDesignTimeLocatorBabelPlugin.js +112 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +80 -0
- package/package.json +7 -3
|
@@ -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
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;
|
|
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.
|
|
4
|
+
"version": "1.48.1",
|
|
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
|
-
"@
|
|
29
|
+
"@babel/core": "^7.28.4",
|
|
30
|
+
"@babel/helper-plugin-utils": "^7.28.3",
|
|
31
|
+
"@salesforce/webapp-experimental": "^1.48.1"
|
|
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": "
|
|
48
|
+
"gitHead": "3a6ed78f0171a5106e8716c8154ef20e2962bf59"
|
|
45
49
|
}
|