Repository: FHIR/sushi (ts)

Issue:
Warn when config doesn't include menu and there is no menu.xml
The IG Publisher requires a menu.xml file -- and will either crash or hang if it does not exist.

There are two ways users can provide a menu when using SUSHI:
1. Via the `menu` property in `sushi-config.yaml`
2. Via `input/includes/menu.xml`

If SUSHI detects that there is no `menu` property in `sushi-config.yaml` and no `menu.xml` in `input/includes`, then it should issue a _warning_ that no menu was provided.

_NOTE: This applies only to FSH IG projects; not FSHOnly projects._

Pull request description:
Warn when config does not include menu and there is no menu.xml
Fixes #1161

This PR includes a check if there is a config.menu or menu.xml, and if not issues a warning to the user. Tests affected by this addition of a warning message were modified to include a config.menu.



Patch:
diff --git a/src/ig/IGExporter.ts b/src/ig/IGExporter.ts
index debc912..aed50d8 100644
--- a/src/ig/IGExporter.ts
+++ b/src/ig/IGExporter.ts
@@ -665,16 +665,17 @@ export class IGExporter {
   /**
    * Adds menu.xml
    * A user can define a menu in sushi-config.yaml or provide one in ./input/includes.
-   * If neither is provided, the static one SUSHI provides will be used.
+   * If neither is provided, a warning is issued to the user.
    *
    * @param {string} igPath - the path where the IG is exported to
    */
   addMenuXML(igPath: string): void {
     const menuXMLDefaultPath = path.join(this.inputPath, 'input', 'includes', 'menu.xml');
     const menuXMLOutputPath = path.join(igPath, 'fsh-generated', 'includes', 'menu.xml');
+    const menuXMLExists = existsSync(menuXMLDefaultPath);
 
     // If user provided file and config, log a warning but prefer the file.
-    if (existsSync(menuXMLDefaultPath) && this.config.menu) {
+    if (menuXMLExists && this.config.menu) {
       const filePathString = path.join(
         path.basename(this.inputPath),
         'input',
@@ -716,6 +717,14 @@ export class IGExporter {
       );
       outputFileSync(menuXMLOutputPath, `${warning}${menu}`, 'utf8');
     }
+
+    // If user did not provide file or config, log a warning.
+    if (!menuXMLExists && !this.config.menu) {
+      const filePathString = path.join('input', 'includes');
+      logger.warn(
+        `No "menu" property or file was found. Generate a menu.xml in the ${filePathString} folder or specify a "menu" property in ${this.configName}.`
+      );
+    }
   }
 
   /**


Test patch:
diff --git a/test/ig/IGExporter.IG.test.ts b/test/ig/IGExporter.IG.test.ts
index 0f3965b..a4c47cc 100644
--- a/test/ig/IGExporter.IG.test.ts
+++ b/test/ig/IGExporter.IG.test.ts
@@ -17,6 +17,7 @@ import { FHIRDefinitions, loadCustomResources } from '../../src/fhirdefs';
 import { loggerSpy, TestFisher } from '../testhelpers';
 import { cloneDeep } from 'lodash';
 import { minimalConfig } from '../utils/minimalConfig';
+import { minimalConfigWithMenu } from '../utils/minimalConfigWithMenu';
 
 describe('IGExporter', () => {
   temp.track();
@@ -1912,7 +1913,7 @@ describe('IGExporter', () => {
     beforeEach(() => {
       loggerSpy.reset();
       tempOut = temp.mkdirSync('sushi-test');
-      config = cloneDeep(minimalConfig);
+      config = cloneDeep(minimalConfigWithMenu);
       config.resources = [
         {
           reference: { reference: 'Patient/BazPatient' },
@@ -2927,7 +2928,7 @@ describe('IGExporter', () => {
     beforeEach(() => {
       loggerSpy.reset();
       tempOut = temp.mkdirSync('sushi-test');
-      config = cloneDeep(minimalConfig);
+      config = cloneDeep(minimalConfigWithMenu);
       config.parameters = [];
       config.parameters.push({
         code: 'path-resource',
@@ -3019,7 +3020,7 @@ describe('IGExporter', () => {
     });
 
     it('should not warn on deeply nested resources that are included in the path-resource parameter with a directory and wildcard', () => {
-      const config = cloneDeep(minimalConfig);
+      const config = cloneDeep(minimalConfigWithMenu);
       config.parameters = [];
       config.parameters.push({
         code: 'path-resource',
@@ -3039,7 +3040,7 @@ describe('IGExporter', () => {
     });
 
     it('should warn on deeply nested resources that are included in the path-resource parameter with a directory but NO wildcard', () => {
-      const config = cloneDeep(minimalConfig);
+      const config = cloneDeep(minimalConfigWithMenu);
       config.parameters = [];
       config.parameters.push({
         code: 'path-resource',
@@ -3081,7 +3082,7 @@ describe('IGExporter', () => {
     beforeEach(() => {
       loggerSpy.reset();
       tempOut = temp.mkdirSync('sushi-test');
-      config = cloneDeep(minimalConfig);
+      config = cloneDeep(minimalConfigWithMenu);
       config.resources = [
         {
           reference: {
@@ -3240,7 +3241,7 @@ describe('IGExporter', () => {
     beforeEach(() => {
       loggerSpy.reset();
       tempOut = temp.mkdirSync('sushi-test');
-      config = cloneDeep(minimalConfig);
+      config = cloneDeep(minimalConfigWithMenu);
       config.resources = [
         {
           reference: {
@@ -3345,6 +3346,10 @@ describe('IGExporter', () => {
             code: 'releaselabel',
             value: 'CI Build'
           }
+        ],
+        menu: [
+          { name: 'Home', url: 'index.html' },
+          { name: 'Artifacts', url: 'artifacts.html' }
         ]
       };
       pkg = new Package(config);
diff --git a/test/ig/IGExporter.menu-xml.test.ts b/test/ig/IGExporter.menu-xml.test.ts
index 6b4179d..95dc2c5 100644
--- a/test/ig/IGExporter.menu-xml.test.ts
+++ b/test/ig/IGExporter.menu-xml.test.ts
@@ -30,14 +30,17 @@ describe('IGExporter', () => {
       temp.cleanupSync();
     });
 
-    it('should do nothing when config.menu is undefined and none provided', () => {
+    it('should log a warning when config.menu is undefined and none provided', () => {
       const pkg = new Package(minimalConfig);
       const igDataPath = path.resolve(__dirname, 'fixtures', 'simple-ig');
       const exporter = new IGExporter(pkg, null, igDataPath);
       exporter.addMenuXML(tempOut);
       const menuPath = path.join(tempOut, 'input', 'includes', 'menu.xml');
       expect(fs.existsSync(menuPath)).toBeFalsy();
-      expect(loggerSpy.getAllMessages()).toHaveLength(0);
+      expect(loggerSpy.getAllMessages('warn')).toHaveLength(1);
+      expect(loggerSpy.getLastMessage()).toMatch(
+        /No "menu" property or file was found\. .*input[\/\\]includes folder.* or .* in sushi-config\.yaml/s
+      );
     });
 
     it('should use but not copy user-provided menu.xml when config.menu is not defined', () => {
@@ -63,7 +66,7 @@ describe('IGExporter', () => {
 
       expect(loggerSpy.getAllMessages('warn')).toHaveLength(1);
       expect(loggerSpy.getLastMessage()).toMatch(
-        /Found both a "menu" property in sushi-config.yaml and a menu.xml file.*File: .*menu.xml/s
+        /Found both a "menu" property in sushi-config\.yaml and a menu\.xml file.*File: .*menu.xml/s
       );
       expect(loggerSpy.getLastMessage()).toMatch(
         /the "menu" property in the sushi-config\.yaml will be ignored/s
diff --git a/test/utils/minimalConfigWithMenu.ts b/test/utils/minimalConfigWithMenu.ts
new file mode 100644
index 0000000..0af3a3d
--- /dev/null
+++ b/test/utils/minimalConfigWithMenu.ts
@@ -0,0 +1,15 @@
+import { Configuration } from '../../src/fshtypes';
+
+export const minimalConfigWithMenu: Configuration = {
+  filePath: 'sushi-config.yaml',
+  id: 'fhir.us.minimal',
+  canonical: 'http://hl7.org/fhir/us/minimal',
+  name: 'MinimalIG',
+  status: 'draft',
+  version: '1.0.0',
+  fhirVersion: ['4.0.1'],
+  menu: [
+    { name: 'Home', url: 'index.html' },
+    { name: 'Artifacts', url: 'artifacts.html' }
+  ]
+};