Repository: FHIR/sushi
Pull request #331 (2020-04-02 21:18:00)

Ig pub support
This adds in additional support for running SUSHI from within an IG Publisher context. Based on the conversation in Slack, the following changes were made:

- The input path is no longer required in the `sushi` command. The path will default to the current directory if it is not provided.
- If a `fsh/` subdirectory is present in the input path/current directory, we will use that subdirectory as the path to the tank. However, someone can still directly call sushi on the subdirectory if they want to.
- If a `fsh/` subdirectory is present then we assume SUSHI is being run from a IG Publisher context. When running in this context, a few things change:
    - We default the output directory to the parent of the tank, rather than `build`. If we aren't in an IG publisher context (there is no `fsh/` subdirectory), the default output directory is still `build`. You can use `-o` to specify an output directory in either case.
    - We don't export any of the publisher related scripts during the IG export.

I tried to update the `--help` information to clarify some of these new cases. That is reflected in the README.

If we decide any of the changes add too much complexity, we can choose to remove any of them. SUSHI does successfully run within the publisher as it is now, so these are intended to make things simpler. If some of the inferring we now do is really to complex/sneaky, it doesn't have to stay. I'm interested to hear reviewers' thoughts on this.

Also, I added the `chmod` for `_gencontinous` and added a check for it in the tests, because apparently I forgot to do that when I added the script.

markkramerus: I didn't review the code, but the functionality is 100% on target.
jafeltra: @cmoesel all your comments should be addressed in 9d20dc6
cmoesel: Thanks, @jafeltra -- I'm holding off on merging this until we get 0.11.3 out the door or until/unless we decide to call our next release 0.12.0.

Patch:
diff --git a/README.md b/README.md
index eb05740..13f8423 100644
--- a/README.md
+++ b/README.md
@@ -23,14 +23,22 @@ After installation, the `sushi` commandline will be available on your path:
 
 ```sh
 $ sushi --help
-Usage: sushi <path-to-fsh-defs> [options]
+Usage: sushi [path-to-fsh-defs] [options]
 
 Options:
-  -o, --out <out>  the path to the output folder (default: "build")
+  -o, --out <out>  the path to the output folder
   -d, --debug      output extra debugging information
   -s, --snapshot   generate snapshot in Structure Definition output (default: false)
   -v, --version    print SUSHI version
   -h, --help       output usage information
+
+Additional information:
+  [path-to-fsh-defs]
+    Default: "."
+    If fsh/ subdirectory present, it is included in [path-to-fsh-defs]
+  -o, --out <out>
+    Default: "build"
+    If fsh/ subdirectory present, default output is one directory above fsh/
 ```
 
 # IG Generation
diff --git a/src/app.ts b/src/app.ts
index 0b674ce..23671ab 100644
--- a/src/app.ts
+++ b/src/app.ts
@@ -30,12 +30,22 @@ async function app() {
 
   program
     .name('sushi')
-    .usage('<path-to-fsh-defs> [options]')
-    .option('-o, --out <out>', 'the path to the output folder', path.join('.', 'build'))
+    .usage('[path-to-fsh-defs] [options]')
+    .option('-o, --out <out>', 'the path to the output folder')
     .option('-d, --debug', 'output extra debugging information')
     .option('-s, --snapshot', 'generate snapshot in Structure Definition output', false)
     .version(getVersion(), '-v, --version', 'print SUSHI version')
-    .arguments('<path-to-fsh-defs>')
+    .on('--help', () => {
+      console.log('');
+      console.log('Additional information:');
+      console.log('  [path-to-fsh-defs]');
+      console.log('    Default: "."');
+      console.log('    If fsh/ subdirectory present, it is included in [path-to-fsh-defs]');
+      console.log('  -o, --out <out>');
+      console.log('    Default: "build"');
+      console.log('    If fsh/ subdirectory present, default output is one directory above fsh/');
+    })
+    .arguments('[path-to-fsh-defs]')
     .action(function (pathToFshDefs) {
       input = pathToFshDefs;
     })
@@ -45,10 +55,25 @@ async function app() {
 
   logger.info(`Running SUSHI ${getVersion()}`);
 
-  // Check that input folder is specified
+  // If no input folder is specified, set default to current directory
   if (!input) {
-    logger.error('Missing path to FSH definition folder.');
-    program.help();
+    input = '.';
+    logger.info('path-to-fsh-defs defaulted to current working directory');
+  }
+
+  // Use fsh/ subdirectory if not already specified and present
+  const fshSubdirectoryPath = path.join(input, 'fsh');
+  if (fs.existsSync(fshSubdirectoryPath)) {
+    input = path.join(input, 'fsh');
+    logger.info('fsh/ subdirectory detected and add to input path');
+  }
+
+  // If a fsh subdirectory is used, we are in an IG Publisher context
+  const isIgPubContext = path.parse(input).base === 'fsh';
+  if (isIgPubContext) {
+    logger.info(
+      'Current FSH tank conforms to an IG Publisher context. Output will be adjusted accordingly.'
+    );
   }
 
   let files: string[];
@@ -111,10 +136,20 @@ async function app() {
   logger.info('Converting FSH to FHIR resources...');
   const outPackage = exportFHIR(tank, defs);
 
-  fs.ensureDirSync(program.out);
+  let outDir = program.out;
+  if (isIgPubContext && !program.out) {
+    // When running in an IG Publisher context, default output is the parent folder of the tank
+    outDir = path.join(input, '..');
+    logger.info(`No output path specified. Output to ${outDir}`);
+  } else if (!program.out) {
+    // Any other time, default output is just to 'build'
+    outDir = path.join('.', 'build');
+    logger.info(`No output path specified. Output to ${outDir}`);
+  }
+  fs.ensureDirSync(outDir);
 
   fs.writeFileSync(
-    path.join(program.out, 'package.json'),
+    path.join(outDir, 'package.json'),
     JSON.stringify(outPackage.config, null, 2),
     'utf8'
   );
@@ -125,7 +160,7 @@ async function app() {
     folder: string,
     resources: { getFileName: () => string; toJSON: (snapshot: boolean) => any }[]
   ) => {
-    const exportDir = path.join(program.out, 'input', folder);
+    const exportDir = path.join(outDir, 'input', folder);
     resources.forEach(resource => {
       fs.outputJSONSync(
         path.join(exportDir, resource.getFileName()),
@@ -161,8 +196,8 @@ async function app() {
   if (fs.existsSync(igDataPath)) {
     isIG = true;
     logger.info('Assembling Implementation Guide sources...');
-    const igExporter = new IGExporter(outPackage, defs, igDataPath);
-    igExporter.export(program.out);
+    const igExporter = new IGExporter(outPackage, defs, igDataPath, isIgPubContext);
+    igExporter.export(outDir);
     logger.info('Assembled Implementation Guide sources; ready for IG Publisher.');
   }
 
diff --git a/src/ig/IGExporter.ts b/src/ig/IGExporter.ts
index a9f4e09..861b684 100644
--- a/src/ig/IGExporter.ts
+++ b/src/ig/IGExporter.ts
@@ -44,7 +44,8 @@ export class IGExporter {
   constructor(
     private readonly pkg: Package,
     private readonly fhirDefs: FHIRDefinitions,
-    private readonly igDataPath: string
+    private readonly igDataPath: string,
+    private readonly isIgPubContext: boolean = false
   ) {
     this.packagePath = path.resolve(this.igDataPath, '..', 'package.json');
     this.outputLog = new Map();
@@ -191,7 +192,16 @@ export class IGExporter {
    */
   private addStaticFiles(igPath: string): void {
     const inputPath = path.join(__dirname, 'files');
-    this.copyAsIs(inputPath, igPath);
+    this.copyAsIs(inputPath, igPath, src => {
+      // If in an IG Publisher context, do not include any of the publisher scripts
+      if (this.isIgPubContext) {
+        if (path.parse(src).base.startsWith('_genonce.')) return false;
+        if (path.parse(src).base.startsWith('_gencontinuous.')) return false;
+        if (path.parse(src).base.startsWith('_updatePublisher.')) return false;
+        return true;
+      }
+      return true;
+    });
 
     // On Windows, the file permissions are not always preserved. This doesn't
     // cause a problem for the Windows user, but it may cause problems for
@@ -199,9 +209,11 @@ export class IGExporter {
     // To work around this, we set the necessary permissions on executable
     // scripts after copying them to the IG path.
     try {
-      chmodSync(path.join(igPath, '_genonce.sh'), 0o755);
-      chmodSync(path.join(igPath, '_gencontinuous.sh'), 0o755);
-      chmodSync(path.join(igPath, '_updatePublisher.sh'), 0o755);
+      if (!this.isIgPubContext) {
+        chmodSync(path.join(igPath, '_genonce.sh'), 0o755);
+        chmodSync(path.join(igPath, '_gencontinuous.sh'), 0o755);
+        chmodSync(path.join(igPath, '_updatePublisher.sh'), 0o755);
+      }
     } catch (e) {
       // We don't want to fail the whole export for this, but we should log it
       logger.warn(
@@ -802,12 +814,12 @@ export class IGExporter {
    * @param inputPath {string} - the input path to copy
    * @param outputPath {string} - the output path to copy to
    */
-  private copyAsIs(inputPath: string, outputPath: string): void {
+  private copyAsIs(inputPath: string, outputPath: string, filter?: (src: string) => boolean): void {
     if (!existsSync(inputPath)) {
       return;
     }
 
-    copySync(inputPath, outputPath);
+    copySync(inputPath, outputPath, { filter });
     this.updateOutputLogForCopiedPath(outputPath, inputPath);
   }
 


Test patch:
diff --git a/test/ig/IGExporter.test.ts b/test/ig/IGExporter.test.ts
index e2d2ae6..b4792a3 100644
--- a/test/ig/IGExporter.test.ts
+++ b/test/ig/IGExporter.test.ts
@@ -84,6 +84,8 @@ describe('IGExporter', () => {
     it('should copy over the static files', () => {
       expect(fs.existsSync(path.join(tempOut, '_genonce.bat'))).toBeTruthy();
       expect(fs.existsSync(path.join(tempOut, '_genonce.sh'))).toBeTruthy();
+      expect(fs.existsSync(path.join(tempOut, '_gencontinuous.bat'))).toBeTruthy();
+      expect(fs.existsSync(path.join(tempOut, '_gencontinuous.sh'))).toBeTruthy();
       expect(fs.existsSync(path.join(tempOut, '_updatePublisher.bat'))).toBeTruthy();
       expect(fs.existsSync(path.join(tempOut, '_updatePublisher.sh'))).toBeTruthy();
       expect(fs.existsSync(path.join(tempOut, 'input', 'ignoreWarnings.txt'))).toBeTruthy();
@@ -366,6 +368,43 @@ describe('IGExporter', () => {
     });
   });
 
+  describe('#ig-publisher-context', () => {
+    let pkg: Package;
+    let exporter: IGExporter;
+    let tempOut: string;
+
+    beforeAll(() => {
+      const defs = new FHIRDefinitions();
+      loadFromPath(
+        path.join(__dirname, '..', 'testhelpers', 'testdefs', 'package'),
+        'testPackage',
+        defs
+      );
+      const fixtures = path.join(__dirname, 'fixtures', 'simple-ig');
+      const config: Config = fs.readJSONSync(path.join(fixtures, 'package.json'));
+      pkg = new Package(config);
+
+      exporter = new IGExporter(pkg, defs, path.resolve(fixtures, 'ig-data'), true); // set to true to indicate ig publisher context
+      tempOut = temp.mkdirSync('sushi-test');
+      exporter.export(tempOut);
+    });
+
+    afterAll(() => {
+      temp.cleanupSync();
+    });
+
+    it('should not copy publisher scripts but should copy over other static files', () => {
+      expect(fs.existsSync(path.join(tempOut, '_genonce.bat'))).toBeFalsy();
+      expect(fs.existsSync(path.join(tempOut, '_genonce.sh'))).toBeFalsy();
+      expect(fs.existsSync(path.join(tempOut, '_gencontinuous.bat'))).toBeFalsy();
+      expect(fs.existsSync(path.join(tempOut, '_gencontinuous.sh'))).toBeFalsy();
+      expect(fs.existsSync(path.join(tempOut, '_updatePublisher.bat'))).toBeFalsy();
+      expect(fs.existsSync(path.join(tempOut, '_updatePublisher.sh'))).toBeFalsy();
+      expect(fs.existsSync(path.join(tempOut, 'input', 'ignoreWarnings.txt'))).toBeTruthy();
+      expect(fs.existsSync(path.join(tempOut, 'input', 'includes', 'menu.xml'))).toBeTruthy();
+    });
+  });
+
   describe('#non-hl7-ig', () => {
     let pkg: Package;
     let exporter: IGExporter;