@salesforce/webapp-experimental 0.2.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.
- package/README.md +3 -0
- package/dist/api/apex.d.ts +9 -0
- package/dist/api/apex.d.ts.map +1 -0
- package/dist/api/apex.js +18 -0
- package/dist/api/apex.test.d.ts +2 -0
- package/dist/api/apex.test.d.ts.map +1 -0
- package/dist/api/apex.test.js +61 -0
- package/dist/api/clients.d.ts +27 -0
- package/dist/api/clients.d.ts.map +1 -0
- package/dist/api/clients.js +116 -0
- package/dist/api/clients.test.d.ts +2 -0
- package/dist/api/clients.test.d.ts.map +1 -0
- package/dist/api/clients.test.js +237 -0
- package/dist/api/graphql.d.ts +20 -0
- package/dist/api/graphql.d.ts.map +1 -0
- package/dist/api/graphql.js +20 -0
- package/dist/api/graphql.test.d.ts +2 -0
- package/dist/api/graphql.test.d.ts.map +1 -0
- package/dist/api/graphql.test.js +70 -0
- package/dist/api/index.d.ts +7 -0
- package/dist/api/index.d.ts.map +1 -0
- package/dist/api/index.js +10 -0
- package/dist/api/utils/accounts.d.ts +32 -0
- package/dist/api/utils/accounts.d.ts.map +1 -0
- package/dist/api/utils/accounts.js +45 -0
- package/dist/api/utils/records.d.ts +11 -0
- package/dist/api/utils/records.d.ts.map +1 -0
- package/dist/api/utils/records.js +20 -0
- package/dist/api/utils/records.test.d.ts +2 -0
- package/dist/api/utils/records.test.d.ts.map +1 -0
- package/dist/api/utils/records.test.js +185 -0
- package/dist/api/utils/user.d.ts +12 -0
- package/dist/api/utils/user.d.ts.map +1 -0
- package/dist/api/utils/user.js +23 -0
- package/dist/api/utils/user.test.d.ts +2 -0
- package/dist/api/utils/user.test.d.ts.map +1 -0
- package/dist/api/utils/user.test.js +156 -0
- package/dist/app/index.d.ts +5 -0
- package/dist/app/index.d.ts.map +1 -0
- package/dist/app/index.js +2 -0
- package/dist/app/manifest.d.ts +32 -0
- package/dist/app/manifest.d.ts.map +1 -0
- package/dist/app/manifest.js +46 -0
- package/dist/app/org.d.ts +22 -0
- package/dist/app/org.d.ts.map +1 -0
- package/dist/app/org.js +62 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/proxy/handler.d.ts +23 -0
- package/dist/proxy/handler.d.ts.map +1 -0
- package/dist/proxy/handler.js +210 -0
- package/dist/proxy/index.d.ts +3 -0
- package/dist/proxy/index.d.ts.map +1 -0
- package/dist/proxy/index.js +1 -0
- package/dist/proxy/routing.d.ts +34 -0
- package/dist/proxy/routing.d.ts.map +1 -0
- package/dist/proxy/routing.js +100 -0
- package/package.json +50 -0
package/dist/app/org.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Org } from "@salesforce/core";
|
|
2
|
+
/**
|
|
3
|
+
* Get Salesforce org info and authentication details
|
|
4
|
+
*
|
|
5
|
+
* @param orgAlias - Optional org alias or username, uses default org if not provided
|
|
6
|
+
* @returns Promise resolving to org info or null if authentication fails
|
|
7
|
+
*/
|
|
8
|
+
export async function getOrgInfo(orgAlias) {
|
|
9
|
+
const org = await createOrg(orgAlias);
|
|
10
|
+
if (!org) {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
const connection = org.getConnection();
|
|
14
|
+
const authInfo = connection.getAuthInfo();
|
|
15
|
+
const authFields = authInfo.getFields();
|
|
16
|
+
return {
|
|
17
|
+
apiVersion: connection.getApiVersion(),
|
|
18
|
+
orgId: authFields.orgId ?? "",
|
|
19
|
+
instanceUrl: toLightningDomain(connection.instanceUrl),
|
|
20
|
+
username: authFields.username ?? "",
|
|
21
|
+
accessToken: connection.accessToken ?? "",
|
|
22
|
+
orgAlias,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
async function createOrg(orgAlias) {
|
|
26
|
+
try {
|
|
27
|
+
if (!orgAlias) {
|
|
28
|
+
return await Org.create({});
|
|
29
|
+
}
|
|
30
|
+
return await Org.create({ aliasOrUsername: orgAlias });
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
console.error("Failed to get SF org info:", error);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Refresh Salesforce org authentication
|
|
38
|
+
*
|
|
39
|
+
* @param orgAlias
|
|
40
|
+
*/
|
|
41
|
+
export async function refreshOrgAuth(orgAlias) {
|
|
42
|
+
const org = await Org.create({ aliasOrUsername: orgAlias });
|
|
43
|
+
await org.refreshAuth();
|
|
44
|
+
return getOrgInfo(orgAlias);
|
|
45
|
+
}
|
|
46
|
+
function replaceLast(originalString, searchString, replacementString) {
|
|
47
|
+
const lastIndex = originalString.lastIndexOf(searchString);
|
|
48
|
+
if (lastIndex === -1) {
|
|
49
|
+
return originalString;
|
|
50
|
+
}
|
|
51
|
+
const before = originalString.slice(0, lastIndex);
|
|
52
|
+
const after = originalString.slice(lastIndex + searchString.length);
|
|
53
|
+
return before + replacementString + after;
|
|
54
|
+
}
|
|
55
|
+
function toLightningDomain(instanceUrl) {
|
|
56
|
+
if (!instanceUrl.includes(".my.")) {
|
|
57
|
+
return instanceUrl;
|
|
58
|
+
}
|
|
59
|
+
let url = replaceLast(instanceUrl, ".my.", ".lightning.");
|
|
60
|
+
url = replaceLast(url, ".salesforce", ".force");
|
|
61
|
+
return url;
|
|
62
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,kBAAkB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
import type { OrgInfo, WebAppManifest } from "../app/index.js";
|
|
3
|
+
/**
|
|
4
|
+
* Configuration options for the WebApp proxy handler
|
|
5
|
+
*/
|
|
6
|
+
export interface ProxyOptions {
|
|
7
|
+
debug?: boolean;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Proxy handler function type
|
|
11
|
+
*/
|
|
12
|
+
export type ProxyHandler = (req: IncomingMessage, res: ServerResponse, next?: () => void) => Promise<void>;
|
|
13
|
+
/**
|
|
14
|
+
* Create proxy request handler
|
|
15
|
+
*
|
|
16
|
+
* @param manifest - WebApp manifest configuration
|
|
17
|
+
* @param orgInfo - Salesforce org information
|
|
18
|
+
* @param target - Target URL for dev server forwarding
|
|
19
|
+
* @param options - Proxy configuration options
|
|
20
|
+
* @returns Async request handler function for Node.js HTTP server
|
|
21
|
+
*/
|
|
22
|
+
export declare function createProxyHandler(manifest: WebAppManifest, orgInfo?: OrgInfo, target?: string, basePath?: string, options?: ProxyOptions): ProxyHandler;
|
|
23
|
+
//# sourceMappingURL=handler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../src/proxy/handler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAEjE,OAAO,KAAK,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAG/D;;GAEG;AACH,MAAM,WAAW,YAAY;IAE5B,KAAK,CAAC,EAAE,OAAO,CAAC;CAChB;AAED;;GAEG;AACH,MAAM,MAAM,YAAY,GAAG,CAC1B,GAAG,EAAE,eAAe,EACpB,GAAG,EAAE,cAAc,EACnB,IAAI,CAAC,EAAE,MAAM,IAAI,KACb,OAAO,CAAC,IAAI,CAAC,CAAC;AA8MnB;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CACjC,QAAQ,EAAE,cAAc,EACxB,OAAO,CAAC,EAAE,OAAO,EACjB,MAAM,CAAC,EAAE,MAAM,EACf,QAAQ,CAAC,EAAE,MAAM,EACjB,OAAO,CAAC,EAAE,YAAY,GACpB,YAAY,CAGd"}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { applyTrailingSlash, matchRoute } from "./routing.js";
|
|
2
|
+
import { refreshOrgAuth } from "../app/index.js";
|
|
3
|
+
/**
|
|
4
|
+
* Handles all proxy routing and forwarding for WebApps
|
|
5
|
+
*/
|
|
6
|
+
class WebAppProxyHandler {
|
|
7
|
+
manifest;
|
|
8
|
+
orgInfo;
|
|
9
|
+
target;
|
|
10
|
+
basePath;
|
|
11
|
+
options;
|
|
12
|
+
constructor(manifest, orgInfo, target, basePath, options) {
|
|
13
|
+
this.manifest = manifest;
|
|
14
|
+
this.orgInfo = orgInfo;
|
|
15
|
+
this.target = target;
|
|
16
|
+
this.basePath = basePath;
|
|
17
|
+
this.options = options;
|
|
18
|
+
}
|
|
19
|
+
async handle(req, res, next) {
|
|
20
|
+
const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
|
|
21
|
+
let pathname = url.pathname;
|
|
22
|
+
if (this.options?.debug) {
|
|
23
|
+
console.log(`[webapps-proxy] ${req.method} ${pathname}`);
|
|
24
|
+
}
|
|
25
|
+
pathname = applyTrailingSlash(pathname, this.manifest.routing?.trailingSlash);
|
|
26
|
+
const match = matchRoute(pathname, this.basePath, this.manifest.routing?.rewrites, this.manifest.routing?.redirects);
|
|
27
|
+
if (match) {
|
|
28
|
+
if (match.type === "api") {
|
|
29
|
+
await this.handleSalesforceApi(req, res);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (match.type === "redirect" && match.target && match.statusCode) {
|
|
33
|
+
this.handleRedirect(res, match.target, match.statusCode);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (match.type === "rewrite" && match.target) {
|
|
37
|
+
url.pathname = `/${match.target}`.replace(/\/+/g, "/");
|
|
38
|
+
req.url = url.pathname + url.search;
|
|
39
|
+
if (this.options?.debug) {
|
|
40
|
+
console.log(`[webapps-proxy] Rewrite to ${req.url}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (next) {
|
|
45
|
+
next();
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
await this.forwardToDevServer(req, res);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
handleRedirect(res, location, statusCode) {
|
|
52
|
+
res.writeHead(statusCode, { Location: location });
|
|
53
|
+
res.end();
|
|
54
|
+
}
|
|
55
|
+
async handleSalesforceApi(req, res) {
|
|
56
|
+
try {
|
|
57
|
+
if (!this.orgInfo) {
|
|
58
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
59
|
+
res.end(JSON.stringify({
|
|
60
|
+
error: "NO_ORG_FOUND",
|
|
61
|
+
message: "No default Salesforce org found. Run 'sf org login web --set-default' to authenticate.",
|
|
62
|
+
}));
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
|
|
66
|
+
let pathIndex = url.pathname.indexOf("/lwr/apex/v");
|
|
67
|
+
if (pathIndex === -1) {
|
|
68
|
+
pathIndex = url.pathname.indexOf("/services/data/v");
|
|
69
|
+
}
|
|
70
|
+
const apiPath = url.pathname.substring(pathIndex);
|
|
71
|
+
let targetUrl = `${this.orgInfo.instanceUrl}${apiPath}${url.search}`;
|
|
72
|
+
if (this.options?.debug) {
|
|
73
|
+
console.log(`[webapps-proxy] Forwarding to Salesforce: ${targetUrl}`);
|
|
74
|
+
}
|
|
75
|
+
// Buffer the request body before sending. This allows us to retry requests
|
|
76
|
+
// with the same body in case of authentication failures (403).
|
|
77
|
+
// For GET/HEAD requests, body is undefined.
|
|
78
|
+
const body = req.method !== "GET" && req.method !== "HEAD" ? await getBody(req) : undefined;
|
|
79
|
+
let response = await fetch(targetUrl, {
|
|
80
|
+
method: req.method,
|
|
81
|
+
headers: {
|
|
82
|
+
...getFilteredHeaders(req.headers),
|
|
83
|
+
Cookie: `sid=${this.orgInfo.accessToken}`,
|
|
84
|
+
Accept: req.headers.accept ?? "application/json",
|
|
85
|
+
// necessary for Apex requests, for which SessionUtil.validateSessionUsage won't accept OAuth token as `sid` cookie
|
|
86
|
+
Authorization: `Bearer ${this.orgInfo.accessToken}`,
|
|
87
|
+
},
|
|
88
|
+
body: body,
|
|
89
|
+
});
|
|
90
|
+
if (response.status === 401 || response.status === 403) {
|
|
91
|
+
console.warn(`[webapps-proxy] Received ${response.status}, refreshing token...`);
|
|
92
|
+
// Use the orgAlias from the current orgInfo to maintain consistency
|
|
93
|
+
const updatedOrgInfo = await refreshOrgAuth(this.orgInfo.orgAlias);
|
|
94
|
+
if (!updatedOrgInfo) {
|
|
95
|
+
throw new Error("Failed to refresh token");
|
|
96
|
+
}
|
|
97
|
+
this.orgInfo = updatedOrgInfo;
|
|
98
|
+
if (this.orgInfo) {
|
|
99
|
+
if (this.options?.debug) {
|
|
100
|
+
console.log("[webapps-proxy] Token refreshed, retrying request");
|
|
101
|
+
}
|
|
102
|
+
// Update target URL with refreshed org info (instance URL may have changed)
|
|
103
|
+
targetUrl = `${this.orgInfo.instanceUrl}${url.pathname}${url.search}`;
|
|
104
|
+
// Retry with the same buffered body
|
|
105
|
+
response = await fetch(targetUrl, {
|
|
106
|
+
method: req.method,
|
|
107
|
+
headers: {
|
|
108
|
+
...getFilteredHeaders(req.headers),
|
|
109
|
+
Cookie: `sid=${this.orgInfo.accessToken}`,
|
|
110
|
+
Accept: req.headers.accept ?? "application/json",
|
|
111
|
+
},
|
|
112
|
+
body: body,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
await this.sendResponse(res, response);
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
console.error("[webapps-proxy] Salesforce API request failed:", error);
|
|
120
|
+
res.writeHead(502, { "Content-Type": "application/json" });
|
|
121
|
+
res.end(JSON.stringify({
|
|
122
|
+
error: "GATEWAY_ERROR",
|
|
123
|
+
message: "Failed to forward request to Salesforce",
|
|
124
|
+
}));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
async forwardToDevServer(req, res) {
|
|
128
|
+
try {
|
|
129
|
+
const url = new URL(req.url ?? "/", this.target);
|
|
130
|
+
if (this.options?.debug) {
|
|
131
|
+
console.log(`[webapps-proxy] Forwarding to dev server: ${url.href}`);
|
|
132
|
+
}
|
|
133
|
+
const body = req.method !== "GET" && req.method !== "HEAD" ? await getBody(req) : undefined;
|
|
134
|
+
const response = await fetch(url.href, {
|
|
135
|
+
method: req.method,
|
|
136
|
+
headers: getFilteredHeaders(req.headers),
|
|
137
|
+
body: body,
|
|
138
|
+
});
|
|
139
|
+
await this.sendResponse(res, response);
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
console.error("[webapps-proxy] Dev server request failed:", error);
|
|
143
|
+
res.writeHead(502, { "Content-Type": "application/json" });
|
|
144
|
+
res.end(JSON.stringify({
|
|
145
|
+
error: "GATEWAY_ERROR",
|
|
146
|
+
message: "Failed to forward request to dev server",
|
|
147
|
+
}));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
async sendResponse(res, response) {
|
|
151
|
+
const headers = {};
|
|
152
|
+
const skipHeaders = new Set(["content-encoding", "content-length", "transfer-encoding"]);
|
|
153
|
+
response.headers.forEach((value, key) => {
|
|
154
|
+
if (!skipHeaders.has(key.toLowerCase())) {
|
|
155
|
+
headers[key] = value;
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
res.writeHead(response.status, headers);
|
|
159
|
+
if (response.body) {
|
|
160
|
+
const reader = response.body.getReader();
|
|
161
|
+
while (true) {
|
|
162
|
+
const { done, value } = await reader.read();
|
|
163
|
+
if (done)
|
|
164
|
+
break;
|
|
165
|
+
res.write(value);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
res.end();
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Create proxy request handler
|
|
173
|
+
*
|
|
174
|
+
* @param manifest - WebApp manifest configuration
|
|
175
|
+
* @param orgInfo - Salesforce org information
|
|
176
|
+
* @param target - Target URL for dev server forwarding
|
|
177
|
+
* @param options - Proxy configuration options
|
|
178
|
+
* @returns Async request handler function for Node.js HTTP server
|
|
179
|
+
*/
|
|
180
|
+
export function createProxyHandler(manifest, orgInfo, target, basePath, options) {
|
|
181
|
+
const handler = new WebAppProxyHandler(manifest, orgInfo, target, basePath, options);
|
|
182
|
+
return (req, res, next) => handler.handle(req, res, next);
|
|
183
|
+
}
|
|
184
|
+
function getFilteredHeaders(headers) {
|
|
185
|
+
const filtered = {};
|
|
186
|
+
const hopByHopHeaders = new Set([
|
|
187
|
+
"connection",
|
|
188
|
+
"keep-alive",
|
|
189
|
+
"proxy-authenticate",
|
|
190
|
+
"proxy-authorization",
|
|
191
|
+
"te",
|
|
192
|
+
"trailer",
|
|
193
|
+
"transfer-encoding",
|
|
194
|
+
"upgrade",
|
|
195
|
+
]);
|
|
196
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
197
|
+
if (!hopByHopHeaders.has(key.toLowerCase()) && value) {
|
|
198
|
+
filtered[key] = Array.isArray(value) ? value.join(", ") : value;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return filtered;
|
|
202
|
+
}
|
|
203
|
+
function getBody(req) {
|
|
204
|
+
return new Promise((resolve, reject) => {
|
|
205
|
+
const chunks = [];
|
|
206
|
+
req.on("data", (chunk) => chunks.push(chunk));
|
|
207
|
+
req.on("end", () => resolve(Buffer.concat(chunks)));
|
|
208
|
+
req.on("error", reject);
|
|
209
|
+
});
|
|
210
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/proxy/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC/D,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createProxyHandler } from "./handler.js";
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { RedirectRule, RewriteRule } from "../app/index.js";
|
|
2
|
+
export interface RouteMatch {
|
|
3
|
+
type: "rewrite" | "redirect" | "api";
|
|
4
|
+
target?: string;
|
|
5
|
+
statusCode?: number;
|
|
6
|
+
params?: Record<string, string>;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Match URL path against routing rules
|
|
10
|
+
*
|
|
11
|
+
* @param pathname - The URL pathname to match
|
|
12
|
+
* @param rewrites - Optional array of rewrite rules
|
|
13
|
+
* @param redirects - Optional array of redirect rules
|
|
14
|
+
* @returns Route match result indicating the type and target, or null if no match
|
|
15
|
+
*/
|
|
16
|
+
export declare function matchRoute(pathname: string, basePath?: string, rewrites?: RewriteRule[], redirects?: RedirectRule[]): RouteMatch | null;
|
|
17
|
+
/**
|
|
18
|
+
* Check if a path matches any of the given glob patterns
|
|
19
|
+
* Supports glob wildcards: * (matches anything except /), ** (matches anything including /), ? (single character)
|
|
20
|
+
*
|
|
21
|
+
* @param path - The path to test
|
|
22
|
+
* @param patterns - Array of glob patterns to match against
|
|
23
|
+
* @returns True if the path matches any pattern, false otherwise
|
|
24
|
+
*/
|
|
25
|
+
export declare function matchesPattern(path: string, patterns: string[] | undefined): boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Apply trailing slash rules to pathname
|
|
28
|
+
*
|
|
29
|
+
* @param pathname - The URL pathname
|
|
30
|
+
* @param trailingSlash - Trailing slash handling strategy
|
|
31
|
+
* @returns Modified pathname with trailing slash applied according to rules
|
|
32
|
+
*/
|
|
33
|
+
export declare function applyTrailingSlash(pathname: string, trailingSlash?: "always" | "never" | "auto"): string;
|
|
34
|
+
//# sourceMappingURL=routing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routing.d.ts","sourceRoot":"","sources":["../../src/proxy/routing.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAEjE,MAAM,WAAW,UAAU;IAC1B,IAAI,EAAE,SAAS,GAAG,UAAU,GAAG,KAAK,CAAC;IACrC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAChC;AASD;;;;;;;GAOG;AACH,wBAAgB,UAAU,CACzB,QAAQ,EAAE,MAAM,EAChB,QAAQ,CAAC,EAAE,MAAM,EACjB,QAAQ,CAAC,EAAE,WAAW,EAAE,EACxB,SAAS,CAAC,EAAE,YAAY,EAAE,GACxB,UAAU,GAAG,IAAI,CA2DnB;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,SAAS,GAAG,OAAO,CAMpF;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CACjC,QAAQ,EAAE,MAAM,EAChB,aAAa,CAAC,EAAE,QAAQ,GAAG,OAAO,GAAG,MAAM,GACzC,MAAM,CAiBR"}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import micromatch from "micromatch";
|
|
2
|
+
import { match } from "path-to-regexp";
|
|
3
|
+
function normalizeRoute(route) {
|
|
4
|
+
let wildcardIndex = 0;
|
|
5
|
+
return route.replace(/\*/g, () => {
|
|
6
|
+
return `{*wildcard${wildcardIndex++}}`;
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Match URL path against routing rules
|
|
11
|
+
*
|
|
12
|
+
* @param pathname - The URL pathname to match
|
|
13
|
+
* @param rewrites - Optional array of rewrite rules
|
|
14
|
+
* @param redirects - Optional array of redirect rules
|
|
15
|
+
* @returns Route match result indicating the type and target, or null if no match
|
|
16
|
+
*/
|
|
17
|
+
export function matchRoute(pathname, basePath, rewrites, redirects) {
|
|
18
|
+
if (pathname.startsWith(`${basePath || ""}/services/data/v`) ||
|
|
19
|
+
pathname.startsWith(`${basePath || ""}/lwr/apex/v`)) {
|
|
20
|
+
return { type: "api" };
|
|
21
|
+
}
|
|
22
|
+
if (redirects) {
|
|
23
|
+
for (const redirect of redirects) {
|
|
24
|
+
const normalizedRoute = normalizeRoute(redirect.route);
|
|
25
|
+
const matcher = match(normalizedRoute, { decode: decodeURIComponent });
|
|
26
|
+
const result = matcher(pathname);
|
|
27
|
+
if (result) {
|
|
28
|
+
let target = redirect.target;
|
|
29
|
+
const params = result.params;
|
|
30
|
+
for (const [key, value] of Object.entries(params)) {
|
|
31
|
+
if (!key.startsWith("wildcard")) {
|
|
32
|
+
target = target.replace(`:${key}`, value);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
type: "redirect",
|
|
37
|
+
target,
|
|
38
|
+
statusCode: redirect.statusCode,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (rewrites) {
|
|
44
|
+
for (const rewrite of rewrites) {
|
|
45
|
+
const normalizedRoute = normalizeRoute(rewrite.route);
|
|
46
|
+
const matcher = match(normalizedRoute, { decode: decodeURIComponent });
|
|
47
|
+
const result = matcher(pathname);
|
|
48
|
+
if (result) {
|
|
49
|
+
const params = {};
|
|
50
|
+
const matchParams = result.params;
|
|
51
|
+
for (const [key, value] of Object.entries(matchParams)) {
|
|
52
|
+
if (!key.startsWith("wildcard")) {
|
|
53
|
+
params[key] = value;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
type: "rewrite",
|
|
58
|
+
target: rewrite.target,
|
|
59
|
+
params,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Check if a path matches any of the given glob patterns
|
|
68
|
+
* Supports glob wildcards: * (matches anything except /), ** (matches anything including /), ? (single character)
|
|
69
|
+
*
|
|
70
|
+
* @param path - The path to test
|
|
71
|
+
* @param patterns - Array of glob patterns to match against
|
|
72
|
+
* @returns True if the path matches any pattern, false otherwise
|
|
73
|
+
*/
|
|
74
|
+
export function matchesPattern(path, patterns) {
|
|
75
|
+
if (!patterns || patterns.length === 0) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
return micromatch.isMatch(path, patterns);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Apply trailing slash rules to pathname
|
|
82
|
+
*
|
|
83
|
+
* @param pathname - The URL pathname
|
|
84
|
+
* @param trailingSlash - Trailing slash handling strategy
|
|
85
|
+
* @returns Modified pathname with trailing slash applied according to rules
|
|
86
|
+
*/
|
|
87
|
+
export function applyTrailingSlash(pathname, trailingSlash) {
|
|
88
|
+
if (!trailingSlash || trailingSlash === "auto") {
|
|
89
|
+
return pathname;
|
|
90
|
+
}
|
|
91
|
+
const hasTrailingSlash = pathname.endsWith("/");
|
|
92
|
+
const isRoot = pathname === "/";
|
|
93
|
+
if (trailingSlash === "always" && !hasTrailingSlash && !isRoot) {
|
|
94
|
+
return `${pathname}/`;
|
|
95
|
+
}
|
|
96
|
+
if (trailingSlash === "never" && hasTrailingSlash && !isRoot) {
|
|
97
|
+
return pathname.slice(0, -1);
|
|
98
|
+
}
|
|
99
|
+
return pathname;
|
|
100
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@salesforce/webapp-experimental",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"module": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./api": {
|
|
14
|
+
"types": "./dist/api/index.d.ts",
|
|
15
|
+
"import": "./dist/api/index.js"
|
|
16
|
+
},
|
|
17
|
+
"./app": {
|
|
18
|
+
"types": "./dist/app/index.d.ts",
|
|
19
|
+
"import": "./dist/app/index.js"
|
|
20
|
+
},
|
|
21
|
+
"./proxy": {
|
|
22
|
+
"types": "./dist/proxy/index.d.ts",
|
|
23
|
+
"import": "./dist/proxy/index.js"
|
|
24
|
+
},
|
|
25
|
+
"./package.json": "./package.json"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"dist"
|
|
29
|
+
],
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "tsc --build",
|
|
32
|
+
"clean": "rm -rf dist tsconfig.tsbuildinfo",
|
|
33
|
+
"dev": "tsc --build --watch"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@conduit-client/salesforce-lightning-service-worker": "^3.7.0",
|
|
37
|
+
"@salesforce/core": "^8.23.4",
|
|
38
|
+
"axios": "^1.7.7",
|
|
39
|
+
"micromatch": "^4.0.8",
|
|
40
|
+
"path-to-regexp": "^8.3.0"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@types/micromatch": "^4.0.10",
|
|
44
|
+
"vitest": "^4.0.6"
|
|
45
|
+
},
|
|
46
|
+
"engines": {
|
|
47
|
+
"node": ">=20.0.0"
|
|
48
|
+
},
|
|
49
|
+
"gitHead": "a806910fe9c132ad3cc0ca63302072b0076699b9"
|
|
50
|
+
}
|