@salesforce/vite-plugin-webapp-experimental 1.81.0 → 1.83.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.
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
declare const reactDesignTimeLocatorBabelPlugin: (api: object, options: Record<string, any> | null | undefined, dirname: string) => {
|
|
20
20
|
name: string;
|
|
21
21
|
visitor: {
|
|
22
|
-
JSXElement(this: import(
|
|
22
|
+
JSXElement(this: import('@babel/core').PluginPass, path: any, state: any): void;
|
|
23
23
|
};
|
|
24
24
|
};
|
|
25
25
|
export default reactDesignTimeLocatorBabelPlugin;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* Copyright (c) 2026, Salesforce, Inc.,
|
|
3
|
-
* All rights reserved.
|
|
4
|
-
* For full license text, see the LICENSE.txt file
|
|
5
|
-
*/
|
|
6
|
-
import type { Plugin } from "vite";
|
|
1
|
+
import { Plugin } from 'vite';
|
|
7
2
|
export interface PluginOptions {
|
|
8
3
|
/** Salesforce org alias */
|
|
9
4
|
orgAlias?: string;
|
package/dist/index.js
CHANGED
|
@@ -1,203 +1,312 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Copyright (c) 2026, Salesforce, Inc.,
|
|
3
|
-
* All rights reserved.
|
|
4
|
-
* For full license text, see the LICENSE.txt file
|
|
5
|
-
*/
|
|
6
1
|
import { loadManifest, getOrgInfo } from "@salesforce/webapp-experimental/app";
|
|
7
2
|
import { getDesignModeScriptContent } from "@salesforce/webapp-experimental/design";
|
|
8
3
|
import { createProxyHandler, injectLivePreviewScript } from "@salesforce/webapp-experimental/proxy";
|
|
9
|
-
import
|
|
10
|
-
import {
|
|
4
|
+
import { declare } from "@babel/helper-plugin-utils";
|
|
5
|
+
import { readdirSync } from "node:fs";
|
|
6
|
+
import { basename } from "node:path";
|
|
7
|
+
const reactDesignTimeLocatorBabelPlugin = declare((api) => {
|
|
8
|
+
api.assertVersion(7);
|
|
9
|
+
const t = api.types;
|
|
10
|
+
function isReactFragmentName(nameNode) {
|
|
11
|
+
if (t.isJSXIdentifier(nameNode) && nameNode.name === "Fragment") return true;
|
|
12
|
+
if (t.isJSXMemberExpression(nameNode) && t.isJSXIdentifier(nameNode.object, { name: "React" }) && t.isJSXIdentifier(nameNode.property, { name: "Fragment" })) {
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
function hasAttr(openingElement, name) {
|
|
18
|
+
return openingElement.attributes.some((attr) => {
|
|
19
|
+
return t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name) && attr.name.name === name;
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
function addAttr(openingElement, name, value) {
|
|
23
|
+
if (hasAttr(openingElement, name)) return;
|
|
24
|
+
openingElement.attributes.push(
|
|
25
|
+
t.jsxAttribute(t.jsxIdentifier(name), t.stringLiteral(String(value)))
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
name: "babel-plugin-react-design-time-locator",
|
|
30
|
+
visitor: {
|
|
31
|
+
JSXElement(path, state) {
|
|
32
|
+
const openingElement = path.node.openingElement;
|
|
33
|
+
if (isReactFragmentName(openingElement.name)) return;
|
|
34
|
+
if (!t.isJSXIdentifier(openingElement.name) && !t.isJSXMemberExpression(openingElement.name)) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
let tagHead = "";
|
|
38
|
+
if (t.isJSXIdentifier(openingElement.name)) {
|
|
39
|
+
tagHead = openingElement.name.name;
|
|
40
|
+
} else if (t.isJSXMemberExpression(openingElement.name)) {
|
|
41
|
+
tagHead = openingElement.name.property?.name ?? "";
|
|
42
|
+
}
|
|
43
|
+
if (!tagHead) return;
|
|
44
|
+
const filename = state?.file?.opts?.filename;
|
|
45
|
+
if (!filename || filename.includes("node_modules")) return;
|
|
46
|
+
const excludePaths = state.opts?.excludePaths ?? [];
|
|
47
|
+
if (excludePaths.some((p) => filename.includes(p))) return;
|
|
48
|
+
const children = path.node.children ?? [];
|
|
49
|
+
const relevantChildren = children.filter((child) => {
|
|
50
|
+
return !(t.isJSXText(child) && child.value.trim() === "");
|
|
51
|
+
});
|
|
52
|
+
let textType = "none";
|
|
53
|
+
if (relevantChildren.length === 1) {
|
|
54
|
+
const child = relevantChildren[0];
|
|
55
|
+
if (t.isJSXText(child)) {
|
|
56
|
+
textType = "static";
|
|
57
|
+
} else if (t.isJSXExpressionContainer(child)) {
|
|
58
|
+
textType = "dynamic";
|
|
59
|
+
} else {
|
|
60
|
+
textType = "element";
|
|
61
|
+
}
|
|
62
|
+
} else if (relevantChildren.length > 1) {
|
|
63
|
+
textType = "mixed";
|
|
64
|
+
}
|
|
65
|
+
const loc = path.node.loc;
|
|
66
|
+
if (loc?.start) {
|
|
67
|
+
const source = `${filename}:${loc.start.line}:${loc.start.column}`;
|
|
68
|
+
addAttr(openingElement, "data-source-file", source);
|
|
69
|
+
}
|
|
70
|
+
addAttr(openingElement, "data-text-type", textType);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
});
|
|
75
|
+
const DEFAULT_PORT = 5173;
|
|
76
|
+
const DEFAULT_API_VERSION = "65.0";
|
|
77
|
+
const DEFAULT_NAMESPACE = "c";
|
|
78
|
+
function getAppName(rootPath) {
|
|
79
|
+
try {
|
|
80
|
+
const files = readdirSync(rootPath);
|
|
81
|
+
const metaFile = files.find((f) => f.endsWith(".webapplication-meta.xml"));
|
|
82
|
+
if (metaFile) {
|
|
83
|
+
return metaFile.replace(".webapplication-meta.xml", "");
|
|
84
|
+
}
|
|
85
|
+
} catch {
|
|
86
|
+
}
|
|
87
|
+
return basename(rootPath);
|
|
88
|
+
}
|
|
89
|
+
function getNamespace() {
|
|
90
|
+
return DEFAULT_NAMESPACE;
|
|
91
|
+
}
|
|
92
|
+
function buildProdBasePath(namespace, appName) {
|
|
93
|
+
return `/lwr/application/ai/${namespace}-${appName}`;
|
|
94
|
+
}
|
|
95
|
+
function getCodeBuilderBasePath(proxyUri, port) {
|
|
96
|
+
try {
|
|
97
|
+
const url = new URL(proxyUri.replace("{{port}}", port.toString()));
|
|
98
|
+
return url.pathname;
|
|
99
|
+
} catch (error) {
|
|
100
|
+
console.error("Failed to parse CODE_BUILDER_FRAMEWORK_PROXY_URI:", error);
|
|
101
|
+
return `/absproxy/${port}`;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function getBasePath(mode, codeBuilderProxyUrl2, port, rootPath) {
|
|
105
|
+
const isProd = mode === "production";
|
|
106
|
+
if (isProd) {
|
|
107
|
+
const appName = getAppName(rootPath);
|
|
108
|
+
const namespace = getNamespace();
|
|
109
|
+
return buildProdBasePath(namespace, appName);
|
|
110
|
+
}
|
|
111
|
+
if (!codeBuilderProxyUrl2) {
|
|
112
|
+
return "";
|
|
113
|
+
}
|
|
114
|
+
return getCodeBuilderBasePath(codeBuilderProxyUrl2, port);
|
|
115
|
+
}
|
|
116
|
+
function getDevServerTarget(codeBuilderProxyUrl2, port) {
|
|
117
|
+
if (codeBuilderProxyUrl2) {
|
|
118
|
+
return getCodeBuilderBasePath(codeBuilderProxyUrl2, port);
|
|
119
|
+
}
|
|
120
|
+
return `http://localhost:${port}`;
|
|
121
|
+
}
|
|
122
|
+
function getPort() {
|
|
123
|
+
return parseInt(process.env.SF_WEBAPP_PORT || DEFAULT_PORT.toString(), 10);
|
|
124
|
+
}
|
|
11
125
|
const codeBuilderProxyUrl = process.env.CODE_BUILDER_FRAMEWORK_PROXY_URI;
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
console.error(`[webapps-plugin] Design mode script not found. Run 'npm run build:design' in @salesforce/webapp-experimental.`);
|
|
139
|
-
res.writeHead(404);
|
|
140
|
-
res.end("Design mode script not found");
|
|
141
|
-
return;
|
|
142
|
-
}
|
|
143
|
-
catch (error) {
|
|
144
|
-
console.error("[webapps-plugin] Error serving design mode script:", error);
|
|
145
|
-
res.writeHead(500);
|
|
146
|
-
res.end("Error loading design mode script");
|
|
147
|
-
return;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
next();
|
|
151
|
-
});
|
|
152
|
-
},
|
|
153
|
-
async transform(code, id) {
|
|
154
|
-
if (!designModeEnabled)
|
|
155
|
-
return null;
|
|
156
|
-
// Strip Vite query string (e.g. ?import, ?v=hash)
|
|
157
|
-
const filepath = id.split("?", 1)[0] ?? id;
|
|
158
|
-
// Best-effort: only handle React source files where JSX loc info matters.
|
|
159
|
-
const isTsx = filepath.endsWith(".tsx");
|
|
160
|
-
const isJsx = filepath.endsWith(".jsx");
|
|
161
|
-
if (!isTsx && !isJsx)
|
|
162
|
-
return null;
|
|
163
|
-
if (filepath.includes("node_modules"))
|
|
164
|
-
return null;
|
|
165
|
-
const excludePaths = options.designModeExcludePaths ?? ["/components/ui/"];
|
|
166
|
-
if (excludePaths.some((p) => filepath.includes(p)))
|
|
167
|
-
return null;
|
|
168
|
-
const { transformAsync } = await import("@babel/core");
|
|
169
|
-
const result = await transformAsync(code, {
|
|
170
|
-
filename: filepath,
|
|
171
|
-
babelrc: false,
|
|
172
|
-
configFile: false,
|
|
173
|
-
sourceMaps: true,
|
|
174
|
-
parserOpts: {
|
|
175
|
-
sourceType: "module",
|
|
176
|
-
plugins: isTsx ? ["jsx", "typescript"] : ["jsx"],
|
|
177
|
-
},
|
|
178
|
-
plugins: [[reactDesignTimeLocatorBabelPlugin, { excludePaths }]],
|
|
179
|
-
});
|
|
180
|
-
if (!result?.code)
|
|
181
|
-
return null;
|
|
182
|
-
return { code: result.code, map: result.map };
|
|
183
|
-
},
|
|
184
|
-
transformIndexHtml(html) {
|
|
185
|
-
// Inject Live Preview script (error handling, copy/paste, postMessage bridge)
|
|
186
|
-
html = injectLivePreviewScript(html);
|
|
187
|
-
if (designModeEnabled) {
|
|
188
|
-
const designScriptTag = '<script src="/_sfdc/design-mode-interactions.js"></script>';
|
|
189
|
-
return html.replace("</body>", `${designScriptTag}\n</body>`);
|
|
126
|
+
function webappsPlugin(options = {}) {
|
|
127
|
+
const proxyOptions = {
|
|
128
|
+
debug: options.debug ?? false
|
|
129
|
+
};
|
|
130
|
+
let orgInfo;
|
|
131
|
+
let manifest;
|
|
132
|
+
let proxyHandler;
|
|
133
|
+
let designModeEnabled = options.designMode ?? false;
|
|
134
|
+
const corePlugin = {
|
|
135
|
+
name: "@salesforce/vite-plugin-webapp-experimental:core",
|
|
136
|
+
enforce: "pre",
|
|
137
|
+
async config(config, env) {
|
|
138
|
+
const rootPath = config.root ?? process.cwd();
|
|
139
|
+
let version = DEFAULT_API_VERSION;
|
|
140
|
+
try {
|
|
141
|
+
orgInfo = await getOrgInfo(options.orgAlias);
|
|
142
|
+
version = orgInfo?.apiVersion || DEFAULT_API_VERSION;
|
|
143
|
+
if (options.debug) {
|
|
144
|
+
console.log(`[webapps-plugin] Using Salesforce API version: ${version}`);
|
|
145
|
+
}
|
|
146
|
+
} catch {
|
|
147
|
+
version = DEFAULT_API_VERSION;
|
|
148
|
+
}
|
|
149
|
+
const isCodeBuilder = !!codeBuilderProxyUrl;
|
|
150
|
+
const define = {
|
|
151
|
+
__SF_API_VERSION__: JSON.stringify(version),
|
|
152
|
+
__SF_SERVER_BASE_PATH__: JSON.stringify("")
|
|
153
|
+
};
|
|
154
|
+
if (isCodeBuilder && env.mode !== "production") {
|
|
155
|
+
const basePath = getCodeBuilderBasePath(codeBuilderProxyUrl, getPort());
|
|
156
|
+
define["__SF_SERVER_BASE_PATH__"] = JSON.stringify(basePath);
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
define,
|
|
160
|
+
base: getBasePath(env.mode, codeBuilderProxyUrl, getPort(), rootPath),
|
|
161
|
+
server: {
|
|
162
|
+
port: getPort(),
|
|
163
|
+
// Code Builder specific configuration
|
|
164
|
+
...isCodeBuilder && {
|
|
165
|
+
allowedHosts: true,
|
|
166
|
+
strictPort: true
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
},
|
|
171
|
+
async configResolved(config) {
|
|
172
|
+
try {
|
|
173
|
+
const rootPath = config.root ?? process.cwd();
|
|
174
|
+
manifest = await loadManifest(`${rootPath}/webapplication.json`);
|
|
175
|
+
const target = getDevServerTarget(codeBuilderProxyUrl, config.server.port ?? DEFAULT_PORT);
|
|
176
|
+
const basePath = getBasePath(config.mode, codeBuilderProxyUrl, getPort(), rootPath);
|
|
177
|
+
proxyHandler = createProxyHandler(manifest, orgInfo, target, basePath, proxyOptions);
|
|
178
|
+
} catch (error) {
|
|
179
|
+
console.error(`[webapps-plugin] Initialization failed:`, error);
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
configureServer(server) {
|
|
183
|
+
server.middlewares.use(async (req, res, next) => {
|
|
184
|
+
if (proxyHandler) {
|
|
185
|
+
try {
|
|
186
|
+
await proxyHandler(req, res, next);
|
|
187
|
+
} catch (error) {
|
|
188
|
+
console.error("[webapps-plugin] Proxy handler error:", error);
|
|
189
|
+
next();
|
|
190
|
+
}
|
|
191
|
+
} else {
|
|
192
|
+
if (req.url?.startsWith("/services")) {
|
|
193
|
+
res.writeHead(503, { "Content-Type": "application/json" });
|
|
194
|
+
res.end(
|
|
195
|
+
JSON.stringify({
|
|
196
|
+
error: "SERVICE_UNAVAILABLE",
|
|
197
|
+
message: "Proxy not initialized."
|
|
198
|
+
})
|
|
199
|
+
);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
next();
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
},
|
|
206
|
+
async handleHotUpdate({ file, server }) {
|
|
207
|
+
if (file.endsWith("webapplication.json")) {
|
|
208
|
+
const updatedManifest = await loadManifest(file);
|
|
209
|
+
if (updatedManifest) {
|
|
210
|
+
manifest = updatedManifest;
|
|
211
|
+
const rootPath = server.config.root ?? process.cwd();
|
|
212
|
+
const target = getDevServerTarget(
|
|
213
|
+
codeBuilderProxyUrl,
|
|
214
|
+
server.config.server.port ?? DEFAULT_PORT
|
|
215
|
+
);
|
|
216
|
+
const basePath = getBasePath(
|
|
217
|
+
server.config.mode,
|
|
218
|
+
codeBuilderProxyUrl,
|
|
219
|
+
getPort(),
|
|
220
|
+
rootPath
|
|
221
|
+
);
|
|
222
|
+
proxyHandler = createProxyHandler(manifest, orgInfo, target, basePath, proxyOptions);
|
|
223
|
+
server.ws.send({
|
|
224
|
+
type: "full-reload",
|
|
225
|
+
path: "*"
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
const designPlugin = {
|
|
232
|
+
name: "@salesforce/vite-plugin-webapp-experimental:design",
|
|
233
|
+
enforce: "pre",
|
|
234
|
+
config(_config, env) {
|
|
235
|
+
designModeEnabled = options.designMode ?? env.mode === "design";
|
|
236
|
+
},
|
|
237
|
+
configureServer(server) {
|
|
238
|
+
server.middlewares.use(async (req, res, next) => {
|
|
239
|
+
if (!designModeEnabled) {
|
|
240
|
+
next();
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
const urlPath = stripUrlQuery(req.url);
|
|
244
|
+
if (urlPath === "/_sfdc/design-mode-interactions.js") {
|
|
245
|
+
try {
|
|
246
|
+
const script = getDesignModeScriptContent();
|
|
247
|
+
if (script !== null) {
|
|
248
|
+
res.setHeader("Content-Type", "application/javascript");
|
|
249
|
+
res.setHeader("Cache-Control", "no-cache");
|
|
250
|
+
res.end(script);
|
|
251
|
+
return;
|
|
190
252
|
}
|
|
191
|
-
|
|
253
|
+
console.error(
|
|
254
|
+
`[webapps-plugin] Design mode script not found. Run 'npm run build:design' in @salesforce/webapp-experimental.`
|
|
255
|
+
);
|
|
256
|
+
res.writeHead(404);
|
|
257
|
+
res.end("Design mode script not found");
|
|
258
|
+
return;
|
|
259
|
+
} catch (error) {
|
|
260
|
+
console.error("[webapps-plugin] Error serving design mode script:", error);
|
|
261
|
+
res.writeHead(500);
|
|
262
|
+
res.end("Error loading design mode script");
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
next();
|
|
267
|
+
});
|
|
268
|
+
},
|
|
269
|
+
async transform(code, id) {
|
|
270
|
+
if (!designModeEnabled) return null;
|
|
271
|
+
const filepath = id.split("?", 1)[0] ?? id;
|
|
272
|
+
const isTsx = filepath.endsWith(".tsx");
|
|
273
|
+
const isJsx = filepath.endsWith(".jsx");
|
|
274
|
+
if (!isTsx && !isJsx) return null;
|
|
275
|
+
if (filepath.includes("node_modules")) return null;
|
|
276
|
+
const excludePaths = options.designModeExcludePaths ?? ["/components/ui/"];
|
|
277
|
+
if (excludePaths.some((p) => filepath.includes(p))) return null;
|
|
278
|
+
const { transformAsync } = await import("@babel/core");
|
|
279
|
+
const result = await transformAsync(code, {
|
|
280
|
+
filename: filepath,
|
|
281
|
+
babelrc: false,
|
|
282
|
+
configFile: false,
|
|
283
|
+
sourceMaps: true,
|
|
284
|
+
parserOpts: {
|
|
285
|
+
sourceType: "module",
|
|
286
|
+
plugins: isTsx ? ["jsx", "typescript"] : ["jsx"]
|
|
192
287
|
},
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
288
|
+
plugins: [[reactDesignTimeLocatorBabelPlugin, { excludePaths }]]
|
|
289
|
+
});
|
|
290
|
+
if (!result?.code) return null;
|
|
291
|
+
return { code: result.code, map: result.map };
|
|
292
|
+
},
|
|
293
|
+
transformIndexHtml(html) {
|
|
294
|
+
html = injectLivePreviewScript(html);
|
|
295
|
+
if (designModeEnabled) {
|
|
296
|
+
const designScriptTag = '<script src="/_sfdc/design-mode-interactions.js"><\/script>';
|
|
297
|
+
return html.replace("</body>", `${designScriptTag}
|
|
298
|
+
</body>`);
|
|
299
|
+
}
|
|
300
|
+
return html;
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
return [designPlugin, corePlugin];
|
|
197
304
|
}
|
|
198
305
|
function stripUrlQuery(url) {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
return queryIndex === -1 ? url : url.slice(0, queryIndex);
|
|
306
|
+
if (!url) return "";
|
|
307
|
+
const queryIndex = url.indexOf("?");
|
|
308
|
+
return queryIndex === -1 ? url : url.slice(0, queryIndex);
|
|
203
309
|
}
|
|
310
|
+
export {
|
|
311
|
+
webappsPlugin as default
|
|
312
|
+
};
|
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.83.0",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.txt",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./dist/index.js",
|
|
@@ -18,9 +18,9 @@
|
|
|
18
18
|
"dist"
|
|
19
19
|
],
|
|
20
20
|
"scripts": {
|
|
21
|
-
"build": "
|
|
22
|
-
"clean": "rm -rf dist
|
|
23
|
-
"dev": "
|
|
21
|
+
"build": "vite build",
|
|
22
|
+
"clean": "rm -rf dist",
|
|
23
|
+
"dev": "vite build --watch",
|
|
24
24
|
"test": "vitest run",
|
|
25
25
|
"test:watch": "vitest",
|
|
26
26
|
"test:coverage": "vitest run --coverage"
|
|
@@ -28,12 +28,13 @@
|
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"@babel/core": "^7.28.4",
|
|
30
30
|
"@babel/helper-plugin-utils": "^7.28.3",
|
|
31
|
-
"@salesforce/webapp-experimental": "^1.
|
|
31
|
+
"@salesforce/webapp-experimental": "^1.83.0"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"@types/babel__core": "^7.20.5",
|
|
35
35
|
"@types/babel__helper-plugin-utils": "^7.10.3",
|
|
36
36
|
"vite": "^7.0.0",
|
|
37
|
+
"vite-plugin-dts": "^4.5.4",
|
|
37
38
|
"vitest": "^4.0.6"
|
|
38
39
|
},
|
|
39
40
|
"peerDependencies": {
|
|
@@ -1,117 +0,0 @@
|
|
|
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
|
-
const filename = state?.file?.opts?.filename;
|
|
76
|
-
if (!filename || filename.includes("node_modules"))
|
|
77
|
-
return;
|
|
78
|
-
// Skip files matching excludePaths (e.g. shadcn components/ui/) so their
|
|
79
|
-
// internal elements don't get data-source-file. The root element still
|
|
80
|
-
// receives it from the usage site via props spread.
|
|
81
|
-
const excludePaths = state.opts?.excludePaths ?? [];
|
|
82
|
-
if (excludePaths.some((p) => filename.includes(p)))
|
|
83
|
-
return;
|
|
84
|
-
// Analyze children for text metadata
|
|
85
|
-
const children = path.node.children ?? [];
|
|
86
|
-
const relevantChildren = children.filter((child) => {
|
|
87
|
-
return !(t.isJSXText(child) && child.value.trim() === "");
|
|
88
|
-
});
|
|
89
|
-
let textType = "none"; // none | static | dynamic | mixed | element
|
|
90
|
-
if (relevantChildren.length === 1) {
|
|
91
|
-
const child = relevantChildren[0];
|
|
92
|
-
if (t.isJSXText(child)) {
|
|
93
|
-
textType = "static";
|
|
94
|
-
}
|
|
95
|
-
else if (t.isJSXExpressionContainer(child)) {
|
|
96
|
-
textType = "dynamic";
|
|
97
|
-
}
|
|
98
|
-
else {
|
|
99
|
-
textType = "element";
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
else if (relevantChildren.length > 1) {
|
|
103
|
-
textType = "mixed";
|
|
104
|
-
}
|
|
105
|
-
// Inject location info (best-effort)
|
|
106
|
-
const loc = path.node.loc;
|
|
107
|
-
if (loc?.start) {
|
|
108
|
-
const source = `${filename}:${loc.start.line}:${loc.start.column}`;
|
|
109
|
-
addAttr(openingElement, "data-source-file", source);
|
|
110
|
-
}
|
|
111
|
-
// Inject text metadata
|
|
112
|
-
addAttr(openingElement, "data-text-type", textType);
|
|
113
|
-
},
|
|
114
|
-
},
|
|
115
|
-
};
|
|
116
|
-
});
|
|
117
|
-
export default reactDesignTimeLocatorBabelPlugin;
|
package/dist/utils.js
DELETED
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Copyright (c) 2026, Salesforce, Inc.,
|
|
3
|
-
* All rights reserved.
|
|
4
|
-
* For full license text, see the LICENSE.txt file
|
|
5
|
-
*/
|
|
6
|
-
import { readdirSync } from "node:fs";
|
|
7
|
-
import { basename } from "node:path";
|
|
8
|
-
export const DEFAULT_PORT = 5173;
|
|
9
|
-
export const DEFAULT_API_VERSION = "65.0";
|
|
10
|
-
export const DEFAULT_NAMESPACE = "c";
|
|
11
|
-
/**
|
|
12
|
-
* Get the app name from a *.webapplication-meta.xml file in the root path.
|
|
13
|
-
* Falls back to the directory name if no matching file is found.
|
|
14
|
-
*
|
|
15
|
-
* @param rootPath - The root directory to search for the webapplication-meta.xml file
|
|
16
|
-
* @returns The app name
|
|
17
|
-
*/
|
|
18
|
-
export function getAppName(rootPath) {
|
|
19
|
-
try {
|
|
20
|
-
const files = readdirSync(rootPath);
|
|
21
|
-
const metaFile = files.find((f) => f.endsWith(".webapplication-meta.xml"));
|
|
22
|
-
if (metaFile) {
|
|
23
|
-
// Extract name from "myapp.webapplication-meta.xml" -> "myapp"
|
|
24
|
-
return metaFile.replace(".webapplication-meta.xml", "");
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
catch {
|
|
28
|
-
// Fall through to directory name
|
|
29
|
-
}
|
|
30
|
-
return basename(rootPath);
|
|
31
|
-
}
|
|
32
|
-
/**
|
|
33
|
-
* Get the namespace for the app.
|
|
34
|
-
* Currently returns the default namespace ("c") since rootPath points to the
|
|
35
|
-
* webapp directory (where vite.config lives), not the sfdx project root.
|
|
36
|
-
*
|
|
37
|
-
* @returns The namespace (currently always "c")
|
|
38
|
-
*/
|
|
39
|
-
export function getNamespace() {
|
|
40
|
-
// TODO: In the future, we could traverse up from rootPath to find sfdx-project.json
|
|
41
|
-
return DEFAULT_NAMESPACE;
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* Build the production base path from namespace and app name.
|
|
45
|
-
*
|
|
46
|
-
* @param namespace - The Salesforce namespace (e.g., "c")
|
|
47
|
-
* @param appName - The app name (e.g., "myapp")
|
|
48
|
-
* @returns The production base path (e.g., "/lwr/application/ai/c-myapp")
|
|
49
|
-
*/
|
|
50
|
-
export function buildProdBasePath(namespace, appName) {
|
|
51
|
-
return `/lwr/application/ai/${namespace}-${appName}`;
|
|
52
|
-
}
|
|
53
|
-
/**
|
|
54
|
-
* Calculate the code builder base path from the proxy URI (CODE_BUILDER_FRAMEWORK_PROXY_URI) and dev server port
|
|
55
|
-
* @param proxyUri - The full proxy URI (e.g., https://name.iad.001.sf.code-builder.platform.salesforce.com/absproxy/{{port}})
|
|
56
|
-
* @param port - The port number to replace {{port}} with (e.g., "5173")
|
|
57
|
-
* @returns The parsed path with port (e.g., /absproxy/5173/)
|
|
58
|
-
*/
|
|
59
|
-
export function getCodeBuilderBasePath(proxyUri, port) {
|
|
60
|
-
try {
|
|
61
|
-
const url = new URL(proxyUri.replace("{{port}}", port.toString()));
|
|
62
|
-
return url.pathname;
|
|
63
|
-
}
|
|
64
|
-
catch (error) {
|
|
65
|
-
console.error("Failed to parse CODE_BUILDER_FRAMEWORK_PROXY_URI:", error);
|
|
66
|
-
return `/absproxy/${port}`; // Default code builder proxy path
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
/**
|
|
70
|
-
* Get the base path for the webapp based on mode and environment.
|
|
71
|
-
*
|
|
72
|
-
* For production mode, resolves the app name and namespace from the rootPath
|
|
73
|
-
* and returns a path like "/lwr/application/ai/{namespace}-{appName}".
|
|
74
|
-
*
|
|
75
|
-
* For development mode with Code Builder, returns the proxy path.
|
|
76
|
-
* For local development, returns an empty string.
|
|
77
|
-
*
|
|
78
|
-
* @param mode - The Vite mode ("production", "development", etc.)
|
|
79
|
-
* @param codeBuilderProxyUrl - The Code Builder proxy URL (if running in Code Builder)
|
|
80
|
-
* @param port - The dev server port
|
|
81
|
-
* @param rootPath - The project root path (used to resolve app name and namespace)
|
|
82
|
-
* @returns The base path
|
|
83
|
-
*/
|
|
84
|
-
export function getBasePath(mode, codeBuilderProxyUrl, port, rootPath) {
|
|
85
|
-
const isProd = mode === "production";
|
|
86
|
-
if (isProd) {
|
|
87
|
-
const appName = getAppName(rootPath);
|
|
88
|
-
const namespace = getNamespace();
|
|
89
|
-
return buildProdBasePath(namespace, appName);
|
|
90
|
-
}
|
|
91
|
-
// Development mode
|
|
92
|
-
if (!codeBuilderProxyUrl) {
|
|
93
|
-
return "";
|
|
94
|
-
}
|
|
95
|
-
// A4V / Code Builder: extract path from proxy URI and include port
|
|
96
|
-
return getCodeBuilderBasePath(codeBuilderProxyUrl, port);
|
|
97
|
-
}
|
|
98
|
-
export function getDevServerTarget(codeBuilderProxyUrl, port) {
|
|
99
|
-
if (codeBuilderProxyUrl) {
|
|
100
|
-
return getCodeBuilderBasePath(codeBuilderProxyUrl, port);
|
|
101
|
-
}
|
|
102
|
-
return `http://localhost:${port}`;
|
|
103
|
-
}
|
|
104
|
-
export function getPort() {
|
|
105
|
-
return parseInt(process.env.SF_WEBAPP_PORT || DEFAULT_PORT.toString(), 10);
|
|
106
|
-
}
|