Skip to content

Commit c11198b

Browse files
lesya7cursoragent
andcommitted
feat: validate webapplication.json descriptor and dist fallback
Replace the previous content-file-count check and mandatory webapplication.json requirement with two validation paths: 1. When webapplication.json is present (and not force-ignored), parse it and verify outputDir, routing.trailingSlash, routing.fallback, and any routing.rewrites targets exist on disk. 2. When webapplication.json is absent or force-ignored, fall back to requiring a non-empty dist/index.html. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent d52ebd0 commit c11198b

6 files changed

Lines changed: 414 additions & 98 deletions

File tree

src/resolve/adapters/webApplicationsSourceAdapter.ts

Lines changed: 93 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,23 @@ import { BundleSourceAdapter } from './bundleSourceAdapter';
2424
Messages.importMessagesDirectory(__dirname);
2525
const messages = Messages.loadMessages('@salesforce/source-deploy-retrieve', 'sdr');
2626

27+
type WebApplicationConfig = {
28+
outputDir: string;
29+
routing: {
30+
trailingSlash: string;
31+
fallback: string;
32+
rewrites?: Array<{ route: string; rewrite: string }>;
33+
};
34+
};
35+
36+
/**
37+
* Source adapter for WebApplication bundles.
38+
*
39+
* If `webapplication.json` is present (and not force-ignored) we validate its
40+
* required fields and check that the files it references exist on disk.
41+
* Otherwise we require a non-empty `dist/index.html`.
42+
*/
2743
export class WebApplicationsSourceAdapter extends BundleSourceAdapter {
28-
// Enforces WebApplication bundle requirements for source/deploy while staying
29-
// compatible with metadata-only retrievals.
3044
protected populate(
3145
trigger: SourcePath,
3246
component?: SourceComponent,
@@ -41,14 +55,11 @@ export class WebApplicationsSourceAdapter extends BundleSourceAdapter {
4155
const appName = baseName(contentPath);
4256
const expectedXmlPath = join(contentPath, `${appName}.webapplication-meta.xml`);
4357
if (!this.tree.exists(expectedXmlPath)) {
44-
throw new SfError(
45-
messages.getMessage('error_expected_source_files', [expectedXmlPath, this.type.name]),
46-
'ExpectedSourceFilesError'
47-
);
58+
this.expectedSourceError(expectedXmlPath);
4859
}
4960

5061
const resolvedSource =
51-
source.xml && source.xml === expectedXmlPath
62+
source.xml === expectedXmlPath
5263
? source
5364
: new SourceComponent(
5465
{
@@ -65,28 +76,84 @@ export class WebApplicationsSourceAdapter extends BundleSourceAdapter {
6576

6677
if (isResolvingSource) {
6778
const descriptorPath = join(contentPath, 'webapplication.json');
68-
const xmlFileName = `${appName}.webapplication-meta.xml`;
69-
const contentEntries = (this.tree.readDirectory(contentPath) ?? []).filter(
70-
(entry) => entry !== xmlFileName && entry !== 'webapplication.json'
71-
);
72-
if (contentEntries.length === 0) {
73-
// For deploy/source, we expect at least one non-metadata content file (e.g. index.html).
74-
throw new SfError(
75-
messages.getMessage('error_expected_source_files', [contentPath, this.type.name]),
76-
'ExpectedSourceFilesError'
77-
);
78-
}
79-
if (!this.tree.exists(descriptorPath)) {
80-
throw new SfError(
81-
messages.getMessage('error_expected_source_files', [descriptorPath, this.type.name]),
82-
'ExpectedSourceFilesError'
83-
);
84-
}
85-
if (this.forceIgnore.denies(descriptorPath)) {
86-
throw messages.createError('noSourceIgnore', [this.type.name, descriptorPath]);
79+
const hasDescriptor = this.tree.exists(descriptorPath) && !this.forceIgnore.denies(descriptorPath);
80+
81+
if (hasDescriptor) {
82+
this.validateDescriptor(descriptorPath, contentPath);
83+
} else {
84+
this.validateDistFolder(contentPath);
8785
}
8886
}
8987

9088
return resolvedSource;
9189
}
90+
91+
private validateDistFolder(contentPath: SourcePath): void {
92+
const distPath = join(contentPath, 'dist');
93+
const indexPath = join(distPath, 'index.html');
94+
95+
if (!this.tree.exists(distPath) || !this.tree.isDirectory(distPath)) {
96+
this.expectedSourceError(distPath);
97+
}
98+
if (!this.tree.exists(indexPath)) {
99+
this.expectedSourceError(indexPath);
100+
}
101+
if (this.tree.readFileSync(indexPath).length === 0) {
102+
this.expectedSourceError(indexPath);
103+
}
104+
}
105+
106+
private validateDescriptor(descriptorPath: SourcePath, contentPath: SourcePath): void {
107+
const raw = this.tree.readFileSync(descriptorPath);
108+
let config: WebApplicationConfig;
109+
110+
try {
111+
config = JSON.parse(raw.toString('utf8')) as WebApplicationConfig;
112+
} catch (e) {
113+
const detail = e instanceof Error ? e.message : String(e);
114+
throw new SfError(`Invalid JSON in webapplication.json: ${detail}`, 'InvalidJsonError');
115+
}
116+
117+
if (!config.outputDir || typeof config.outputDir !== 'string') {
118+
this.expectedSourceError(descriptorPath);
119+
}
120+
const outputDirPath = join(contentPath, config.outputDir);
121+
if (!this.tree.exists(outputDirPath) || !this.tree.isDirectory(outputDirPath)) {
122+
this.expectedSourceError(outputDirPath);
123+
}
124+
125+
if (!config.routing || typeof config.routing !== 'object') {
126+
this.expectedSourceError(descriptorPath);
127+
}
128+
if (!config.routing.trailingSlash || typeof config.routing.trailingSlash !== 'string') {
129+
this.expectedSourceError(descriptorPath);
130+
}
131+
if (!config.routing.fallback || typeof config.routing.fallback !== 'string') {
132+
this.expectedSourceError(descriptorPath);
133+
}
134+
135+
const fallbackPath = join(outputDirPath, config.routing.fallback.replace(/^\//, ''));
136+
if (!this.tree.exists(fallbackPath)) {
137+
this.expectedSourceError(fallbackPath);
138+
}
139+
140+
// rewrites are optional, but every target must resolve
141+
if (Array.isArray(config.routing.rewrites)) {
142+
for (const { rewrite } of config.routing.rewrites) {
143+
if (rewrite) {
144+
const rewritePath = join(outputDirPath, rewrite.replace(/^\//, ''));
145+
if (!this.tree.exists(rewritePath)) {
146+
this.expectedSourceError(rewritePath);
147+
}
148+
}
149+
}
150+
}
151+
}
152+
153+
private expectedSourceError(path: SourcePath): never {
154+
throw new SfError(
155+
messages.getMessage('error_expected_source_files', [path, this.type.name]),
156+
'ExpectedSourceFilesError'
157+
);
158+
}
92159
}

0 commit comments

Comments
 (0)