@@ -24,9 +24,23 @@ import { BundleSourceAdapter } from './bundleSourceAdapter';
2424Messages . importMessagesDirectory ( __dirname ) ;
2525const 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+ */
2743export 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