function emitConsumerTypeScriptFile()

in Clients/AmbrosiaJS/Ambrosia-Node/src/Meta.ts [2764:3456]


function emitConsumerTypeScriptFile(fileName: string, fileOptions: FileGenOptions, sourceFileName?: string): SourceFileProblemCheckResult | null
{
    try
    {
        const NL: string = Utils.NEW_LINE; // Just for short-hand
        const tab: string = " ".repeat(assertDefined(fileOptions.tabIndent));
        let pathedOutputFile: string = Path.join(assertDefined(fileOptions.outputPath), `${Path.basename(fileName).replace(Path.extname(fileName), "")}.g.ts`);
        let content: string = "";
        let namespaces: string[] = [];
        let namespaceComments: { [namespace: string]: string | undefined } = {}; // Effectively a rebuilt AST._namespaceJSDocComments but just for the namespaces that contain published entities
        let previousNsPath: string = "";

        checkForFileNameConflicts(pathedOutputFile, sourceFileName);
        checkForGitMergeMarkers(pathedOutputFile);

        // Note: We emit the types and methods using their original namespaces. If we didn't they'd all get emitted at the root level in the file (ie. no namespace) so we'd
        // lose the original organizational structure of the code (which imparts the logical grouping/meaning of members). However, using namespaces is not required to prevent
        // name collisions [in Ambrosia] because published types and methods must ALWAYS have unique names, even if defined in different TS namespaces.

        // 1) Populate the 'namespaces' list
        if (Object.keys(_publishedTypes).length > 0)
        {
            for (const typeName in _publishedTypes)
            {
                let type: Type = _publishedTypes[typeName];
                if (type.codeGenOptions?.nsPath && (namespaces.indexOf(type.codeGenOptions.nsPath) === -1))
                {
                    addNamespaces(type.codeGenOptions.nsPath);
                    namespaceComments[type.codeGenOptions.nsPath] = type.codeGenOptions.nsComment;
                }
            }
        }
        if (Object.keys(_publishedMethods).length > 0)
        {
            for (const name in _publishedMethods)
            {
                for (const version in _publishedMethods[name])
                {
                    let method: Method = _publishedMethods[name][version];
                    if (method.codeGenOptions?.nsPath && (namespaces.indexOf(method.codeGenOptions.nsPath) === -1))
                    {
                        addNamespaces(method.codeGenOptions.nsPath);
                        namespaceComments[method.codeGenOptions.nsPath] = method.codeGenOptions.nsComment;
                    }
                }
            }
        }

        // 2) Emit types and methods
        if (namespaces.length === 0) // The empty namespace (ie. no namespace)
        {
            // Emit types and methods at the root of the file
            emitTypesAndMethods(0);
            content = content.trimRight();
        }
        else
        {
            // Emit types and methods in their originating namespaces
            namespaces.push(""); // We manually add the empty namespace (ie. no namespace) to emit entities which have no nsPath
            namespaces = namespaces.sort(); // Eg. A, A.B, A.B.C, A.D, B, B.D, B.D.E, ...
            for (const nsPath of namespaces)
            {
                const nsNestDepth: number = nsPath.split(".").length - 1;
                const nsIndent: string = tab.repeat(nsNestDepth);
                const nsName: string = nsPath ? nsPath.split(".")[nsNestDepth] : "";
                const nsComment: string = namespaceComments[nsPath] ? AST.formatJSDocComment(assertDefined(namespaceComments[nsPath]), nsIndent.length) : "";
                const previousNsNestDepth: number = previousNsPath.split(".").length - 1;

                if (nsNestDepth > previousNsNestDepth)
                {
                    // Start new nested namespace
                    if (nsComment)
                    {
                        content += nsComment + NL;
                    }
                    content += `${nsIndent}export namespace ${nsName}` + NL;
                    content += `${nsIndent}{` + NL;
                }
                else
                {
                    if (previousNsPath)
                    {
                        // Emit closing braces (from the previous namespace depth back to the current depth)
                        content = content.trimRight() + NL;
                        for (let depth = previousNsNestDepth; depth >= nsNestDepth; depth--)
                        {
                            content += tab.repeat(depth) + "}" + NL;
                        }
                        content = content.trimRight() + NL.repeat(2);
                    }
                    if (nsPath)
                    {
                        if (nsComment)
                        {
                            content += nsComment + NL;
                        }
                        content += `${nsIndent}export namespace ${nsName}` + NL;
                        content += `${nsIndent}{` + NL;
                    }
                }

                emitTypesAndMethods(nsPath ? assertDefined(fileOptions.tabIndent) * (nsNestDepth + 1) : 0, nsPath);
                previousNsPath = nsPath;
            }

            // Emit closing braces (back to the root)
            content = content.trimRight() + NL;
            for (let depth = previousNsPath.split(".").length - 1; depth >= 0; depth--)
            {
                content += tab.repeat(depth) + "}" + (depth !== 0 ? NL : "");
            }
        }
        
        // 3) If there are published post methods, write a PostResultDispatcher().
        //    This is just a minimal outline with "TODO" placeholders where the developer should add their own code.
        //    Further, if the app includes multiple ConsumerInterface.g.ts files (because it uses more than one type of Ambrosia app/service), then the
        //    developer will need to unify the [potentially] multiple PostResultDispatcher's into a single dispatcher which can be passed to IC.start().
        if (publishedPostMethodsExist())
        {
            let postResultDispatcher: string = "";

            postResultDispatcher += "/**" + NL;
            postResultDispatcher += " * Handler for the results of previously called post methods (in Ambrosia, only 'post' methods return values). See Messages.PostResultDispatcher.\\" + NL;
            postResultDispatcher += " * Must return true only if the result (or error) was handled." + NL + " */" + NL;
            postResultDispatcher += "export function postResultDispatcher(senderInstanceName: string, methodName: string, methodVersion: number, callID: number, callContextData: any, result: any, errorMsg: string): boolean" + NL;
            postResultDispatcher += "{" + NL;
            postResultDispatcher += tab + "const sender: string = IC.isSelf(senderInstanceName) ? \"local\" : `'${senderInstanceName}'`;" + NL;
            postResultDispatcher += tab + "let handled: boolean = true;" + NL.repeat(2);

            // We do this to help catch the case where the developer forgets to create a "unified" PostResultDispatcher [ie. that can
            // handle post results for ALL used ConsumerInterface.g.ts files] so they end up accidentally re-using a PostResultDispatcher
            // that's specific to just one type of Ambrosia app/service (and one destination [or set of destinations])
            postResultDispatcher += tab + "if (_knownDestinations.indexOf(senderInstanceName) === -1)" + NL;
            postResultDispatcher += tab + "{" + NL;
            postResultDispatcher += tab + tab + `return (false); // Not handled: this post result is from a different instance than the destination instance currently (or previously) targeted by the '${fileOptions.apiName}' API` + NL;
            postResultDispatcher += tab + "}" + NL.repeat(2);

            postResultDispatcher += tab + "if (errorMsg)" + NL;
            postResultDispatcher += tab + "{" + NL;
            postResultDispatcher += tab + tab + "switch (methodName)" + NL;
            postResultDispatcher += tab + tab + "{" + NL;
            for (const name in _publishedMethods)
            {
                for (const version in _publishedMethods[name])
                {
                    const method: Method = _publishedMethods[name][version];
                    if (method.isPost)
                    {
                        postResultDispatcher += tab.repeat(3) + `case \"${method.nameForTSWrapper}\":` + NL;
                    }
                }
            }
            postResultDispatcher += tab.repeat(4) + "Utils.log(`Error: ${errorMsg}`);" + NL;
            postResultDispatcher += tab.repeat(4) + "break;" + NL;
            postResultDispatcher += tab.repeat(3) + "default:" + NL;
            postResultDispatcher += tab.repeat(4) + "handled = false;" + NL;
            postResultDispatcher += tab.repeat(4) + "break;" + NL;
            postResultDispatcher += tab + tab + "}" + NL;
            postResultDispatcher += tab + "}" + NL;
            postResultDispatcher += tab + "else" + NL;
            postResultDispatcher += tab + "{" + NL;
            postResultDispatcher += tab + tab + "switch (methodName)" + NL;
            postResultDispatcher += tab + tab + "{" + NL;
            for (const name in _publishedMethods)
            {
                for (const version in _publishedMethods[name])
                {
                    const method: Method = _publishedMethods[name][version];
                    if (method.isPost)
                    {
                        let nsPathOfReturnType: string = getPublishedType(Type.removeArraySuffix(method.returnType))?.codeGenOptions?.nsPath || "";
                        if (nsPathOfReturnType)
                        {
                            nsPathOfReturnType += ".";
                        }
                        postResultDispatcher += tab.repeat(3) + `case \"${method.nameForTSWrapper}\":` + NL;
                        if (method.returnType === "void")
                        {
                            postResultDispatcher += tab.repeat(4) + `// TODO: Handle the method completion (it returns void), optionally using the callContextData passed in the call` + NL;
                        }
                        else
                        {
                            postResultDispatcher += tab.repeat(4) + `const ${method.nameForTSWrapper}_Result: ${nsPathOfReturnType}${method.returnType} = result;` + NL;
                            postResultDispatcher += tab.repeat(4) + `// TODO: Handle the result, optionally using the callContextData passed in the call` + NL;
                        }
                        postResultDispatcher += tab.repeat(4) + "Utils.log(`Post method '${methodName}' from ${sender} IC succeeded`);" + NL;
                        postResultDispatcher += tab.repeat(4) + "break;" + NL;
                    }
                }
            }
            postResultDispatcher += tab.repeat(3) + "default:" + NL;
            postResultDispatcher += tab.repeat(4) + "handled = false;" + NL;
            postResultDispatcher += tab.repeat(4) + "break;" + NL;
            postResultDispatcher += tab + tab + "}" + NL;
            postResultDispatcher += tab + "}" + NL;
            postResultDispatcher += tab + "return (handled);" + NL;
            postResultDispatcher += "}";

            content += NL.repeat(2) + postResultDispatcher;
        }

        /** [Local function] Returns true if any published method is a post method. */
        function publishedPostMethodsExist(): boolean
        {
            for (const name in _publishedMethods)
            {
                for (const version in _publishedMethods[name])
                {
                    const method: Method = _publishedMethods[name][version];
                    if (method.isPost)
                    {
                        return (true);
                    }
                }
            }
            return (false);
        }

        /** [Local function] Adds all the sub-paths for a given namespace path. */
        function addNamespaces(nsPath: string): void
        {
            let nsSubPath: string = "";
            for (let namespace of nsPath.split("."))
            {
                nsSubPath += ((nsSubPath.length > 0) ? "." : "") + namespace;
                if (namespaces.indexOf(nsSubPath) === -1)
                {
                    namespaces.push(nsSubPath);
                }
            }
        }

        /** [Local function] Adds published Types (as classes, type-definitions, or enum definitions) and published Methods (as function wrappers) to the 'content'. */
        function emitTypesAndMethods(startingIndent: number, nsPath: string = ""): void
        {
            const pad: string = " ".repeat(startingIndent);

            if (Object.keys(_publishedTypes).length > 0)
            {
                for (const typeName in _publishedTypes)
                {
                    let type: Type = _publishedTypes[typeName];
                    if (type.codeGenOptions?.nsPath === nsPath)
                    {
                        content += type.makeTSType(startingIndent, fileOptions.tabIndent, type.codeGenOptions?.jsDocComment) + NL.repeat(2);
                    }
                }
            }

            if (Object.keys(_publishedMethods).length > 0)
            {
                for (const name in _publishedMethods)
                {
                    for (const version in _publishedMethods[name])
                    {
                        let method: Method = _publishedMethods[name][version];
                        if (method.codeGenOptions?.nsPath === nsPath)
                        {
                            content += method.makeTSWrappers(startingIndent, fileOptions, method.codeGenOptions?.jsDocComment) + NL.repeat(2);
                        }
                    }
                }
            }
        }

        if (content.length > 0)
        {
            let header: string = getHeaderCommentLines(GeneratedFileKind.Consumer, fileOptions).join(NL) + NL;
            header += "import Ambrosia = require(\"ambrosia-node\");" + NL;
            header += "import IC = Ambrosia.IC;" + NL;
            header += "import Utils = Ambrosia.Utils;" + NL.repeat(2);
            header += `const _knownDestinations: string[] = []; // All previously used destination instances (the '${fileOptions.apiName}' Ambrosia app/service can be running on multiple instances, potentially simultaneously); used by the postResultDispatcher (if any)` + NL;
            header += "let _destinationInstanceName: string = \"\"; // The current destination instance" + NL;
            header += "let _postTimeoutInMs: number = 8000; // -1 = Infinite" + NL.repeat(2);

            header += "/** " + NL;
            header += " * Sets the destination instance name that the API targets.\\" + NL;
            header += ` * Must be called at least once (with the name of a registered Ambrosia instance that implements the '${fileOptions.apiName}' API) before any other method in the API is used.` + NL;
            header += " */" + NL;
            header += "export function setDestinationInstance(instanceName: string): void" + NL;
            header += "{" + NL;
            header += `${tab}_destinationInstanceName = instanceName.trim();` + NL;
            header += `${tab}if (_destinationInstanceName && (_knownDestinations.indexOf(_destinationInstanceName) === -1))` + NL;
            header += `${tab}{` + NL;
            header += `${tab+tab}_knownDestinations.push(_destinationInstanceName);` + NL;
            header += `${tab}}` + NL;
            header += "}" + NL.repeat(2);
            header += "/** Returns the destination instance name that the API currently targets. */" + NL;
            header += "export function getDestinationInstance(): string" + NL;
            header += "{" + NL;
            header += `${tab}return (_destinationInstanceName);` + NL;
            header += "}" + NL.repeat(2);
            header += "/** Throws if _destinationInstanceName has not been set. */" + NL;
            header += "function checkDestinationSet(): void" + NL;
            header += "{" + NL;
            header += `${tab}if (!_destinationInstanceName)` + NL;
            header += `${tab}{` + NL;
            header += `${tab+tab}throw new Error("setDestinationInstance() must be called to specify the target destination before the '${fileOptions.apiName}' API can be used.");` + NL;
            header += `${tab}}` + NL;
            header += "}" + NL.repeat(2);

            header += "/**" + NL;
            header += " * Sets the post method timeout interval (in milliseconds), which is how long to wait for a post result from the destination instance before raising an error.\\" + NL;
            header += " * All post methods will use this timeout value. Specify -1 for no timeout. " + NL;
            header += " */" + NL;
            header += "export function setPostTimeoutInMs(timeoutInMs: number): void" + NL;
            header += "{" + NL;
            header += `${tab}_postTimeoutInMs = Math.max(-1, timeoutInMs);` + NL;
            header += "}" + NL.repeat(2);
            header += "/**" + NL;
            header += " * Returns the post method timeout interval (in milliseconds), which is how long to wait for a post result from the destination instance before raising an error.\\" + NL;
            header += " * A value of -1 means there is no timeout." + NL;
            header += " */" + NL;
            header += "export function getPostTimeoutInMs(): number" + NL;
            header += "{" + NL;
            header += `${tab}return (_postTimeoutInMs);` + NL;
            header += "}" + NL.repeat(2);

            content = header + content;
            writeGeneratedFile(content, pathedOutputFile, assertDefined(fileOptions.mergeType));
            Utils.log(`Code file generated: ${pathedOutputFile}${!fileOptions.checkGeneratedTS ? " (TypeScript checks skipped)" : ""}`);
            return (fileOptions.checkGeneratedTS ? checkGeneratedFile(pathedOutputFile, (fileOptions.mergeType !== FileMergeType.None)) : new SourceFileProblemCheckResult());
        }
        else
        {
            throw new Error(sourceFileName ? 
                `The input source file (${Path.basename(sourceFileName)}) does not publish any entities (exported functions, static methods, type aliases and enums annotated with an ${CODEGEN_TAG_NAME} JSDoc tag)` : 
                "No entities have been published; call publishType() / publishMethod() / publishPostMethod() then retry");
        }
    }
    catch (error: unknown)
    {
        Utils.log(`Error: emitConsumerTypeScriptFile() failed (reason: ${Utils.makeError(error).message})`);
        return (null);
    }
}

/** 
 * Writes the specified content to the specified pathedOutputFile, merging [using git] any existing changes in 
 * pathedOutputFile back into the newly generated file (according to mergeType). Throws if the merge fails. 
 * Returns the number of merge conflicts if 'mergeType' is FileMergeType.Annotate, or 0 otherwise.
 */
function writeGeneratedFile(content: string, pathedOutputFile: string, mergeType: FileMergeType): number
{
    let conflictCount: number = 0;
    let outputPath: string = Path.dirname(pathedOutputFile);

    if ((mergeType === FileMergeType.None) || !File.existsSync(pathedOutputFile))
    {
        File.writeFileSync(pathedOutputFile, content); // This will overwrite the file if it already exists
    }
    else
    {
        // See https://stackoverflow.com/questions/9122948/run-git-merge-algorithm-on-two-individual-files
        const pathedEmptyFile: string = Path.join(outputPath, "__empty.g.ts"); // This is a temporary file [but we don't want the name to collide with a real file]
        const pathedRenamedOutputFile: string = Path.join(outputPath, "__" + Path.basename(pathedOutputFile) + ".original"); // This is a temporary file [but we don't want the name to collide with a real file]

        try
        {
            // The output file already exists [possibly modified by the user], so merge the user's changes into new generated file 
            // using "git merge-file --union .\PublisherFramework.g.ts .\__empty.g.ts .\__PublisherFramework.g.ts.original". This will result in
            // PublisherFramework.g.ts containing BOTH the new [generated] changes and the existing [user] changes (from .\__PublisherFramework.g.ts.original).
            // Note: To just insert the merge markers that the developer will need to resolve manually, omit "--union" from "git merge-file"
            //       (ie. set mergeType to MergeType.Annotate).
            Utils.log(`${FileMergeType[mergeType]}-merging existing ${pathedOutputFile} into generated version...`);
            File.writeFileSync(pathedEmptyFile, ""); // The "common base" file
            File.renameSync(pathedOutputFile, pathedRenamedOutputFile); // Save the original version (which may contain user edits); Will overwrite pathedRenamedOutputFile if it already exists
            File.writeFileSync(pathedOutputFile, content); // This will overwrite the file

            // Note: In launch.json, set '"autoAttachChildProcesses": false' to prevent the VS Code debugger from attaching to this process
            // Note: See https://docs.npmjs.com/misc/config
            let mergeOutput: string = ChildProcess.execSync(`git merge-file ${(mergeType === FileMergeType.Auto) ? "--union " : ""}-L Generated -L Base -L Original ${pathedOutputFile} ${pathedEmptyFile} ${pathedRenamedOutputFile}`, { encoding: "utf8", windowsHide: true, stdio: ["ignore"] }).trim();
            if (mergeOutput)
            {
                throw (mergeOutput);
            }
        }
        catch (error: unknown)
        {
            // Note: 'error' will be an Error object with some additional properties (output, pid, signal, status, stderr, stdout)
            const err: Error = Utils.makeError(error); 

            // The 'git merge-file' exit code is negative on error, or the number of conflicts otherwise (truncated
            // to 127 if there are more than that many conflicts); if the merge was clean, the exit value is 0
            const gitExitCode: number = (err as any).status; // TODO: Hack to make compiler happy

            if (gitExitCode >= 0) // 0 = clean merge, >0 = conflict count (typically this will only happen for a non-automatic merge, ie. if "--union" is omitted from "git merge-file")
            {
                conflictCount = gitExitCode;
            }
            else
            {
                // An error occurred, so restore the original version
                File.renameSync(pathedRenamedOutputFile, pathedOutputFile);

                const errorMsg: string = err.message.replace(/\s+/g, " ").trim().replace(/\.$/, "");
                throw new Error(`Merge failed (reason: ${errorMsg} [exit code: ${gitExitCode}])`);
            }
        }
        finally
        {
            // Remove temporary files
            Utils.deleteFile(pathedEmptyFile);
            Utils.deleteFile(pathedRenamedOutputFile);
        }
        
        // Note: Resolving merges requires that the "Editor: Code Lens" setting is enabled in VSCode
        let userAction: string = (conflictCount === 0) ? "Please diff the changes to check merge correctness" : `Please manually resolve ${conflictCount} merge conflicts`;
        Utils.logWithColor(Utils.ConsoleForegroundColors.Yellow, `${FileMergeType[mergeType]}-merge succeeded - ${userAction}`);
    }
    return (conflictCount);
}

/** Checks the generated 'pathedOutputFile' for TypeScript errors. Returns a SourceFileProblemCheckResult. */
function checkGeneratedFile(pathedOutputFile: string, mergeConflictMarkersAllowed: boolean = false): SourceFileProblemCheckResult
{
    let result: SourceFileProblemCheckResult = AST.checkFileForTSProblems(pathedOutputFile, CodeGenerationFileType.Generated, mergeConflictMarkersAllowed);
    if ((result.errorCount > 0))
    {
        Utils.log(`Error: TypeScript [${TS.version}] check failed for generated file ${Path.basename(pathedOutputFile)}: ${result.errorCount} error(s) found`);
    }
    else
    {
        Utils.log(`Success: No TypeScript errors found in generated file ${Path.basename(pathedOutputFile)}`);
    }
    return (result);
}

/** Returns the fully pathed version of the supplied TypeScript template file name [which is shipped in the ambrosia-node package], or throws an the file cannot be found. */
function getPathedTemplateFile(templateFileName: string): string
{
    let pathedTemplateFile: string = "";
    let searchFolders: string[] = [process.cwd(), Path.join(process.cwd(), "node_modules/ambrosia-node")]; // This only works if ambrosia-node has been installed locally (not globally, which we handle below)
    
    for (const folder of searchFolders)
    {
        if (File.existsSync(Path.join(folder, templateFileName)))
        {
            pathedTemplateFile = Path.join(folder, templateFileName);
            break;
        }
    }

    if (pathedTemplateFile.length === 0)
    {
        // Last ditch (and costly) attempt, try to locate the global npm install location
        try
        {
            // Note: In launch.json, set '"autoAttachChildProcesses": false' to prevent the VS Code debugger from attaching to this process
            // Note: See https://docs.npmjs.com/misc/config
            let globalNpmInstallFolder: string = ChildProcess.execSync("npm config get prefix", { encoding: "utf8", windowsHide: true }).trim();
            // See https://docs.npmjs.com/files/folders
            globalNpmInstallFolder = Path.join(globalNpmInstallFolder, Utils.isWindows() ? "" : "lib", "node_modules/ambrosia-node"); 
            pathedTemplateFile = Path.join(globalNpmInstallFolder, templateFileName);

            if (!File.existsSync(pathedTemplateFile))
            {
                searchFolders.push(globalNpmInstallFolder); // So that we can report where we tried to look for the [shipped] template
                pathedTemplateFile = "";
            }
        }
        catch (error: unknown)
        {
            const errorMsg: string = Utils.makeError(error).message.replace(/\s+/g, " ").trim().replace(/\.$/, "");
            Utils.log(`Error: Unable to determine global npm install folder (reason: ${errorMsg})`);
        }

        if (pathedTemplateFile.length === 0)
        {
            throw new Error(`Unable to find template file ${templateFileName} in ${searchFolders.join(" or ")}`);
        }
    }

    return (pathedTemplateFile);
}

/** The [TypeScript] alias used to reference the input source file in the generated PublisherFramework.g.ts file. */
const SOURCE_MODULE_ALIAS: string = "PTM"; // PTM = "Published Types and Methods"

/** Generates TypeScript code for the specified template section [of PublisherFramework.template.ts]. May return an empty string if there is no code for the section. */
function codeGen(section: CodeGenSection, fileOptions: FileGenOptions, sourceFileName?: string): string
{
    const NL: string = Utils.NEW_LINE; // Just for short-hand
    const tab: string = " ".repeat(assertDefined(fileOptions.tabIndent));
    let lines: string[] = [];
    let moduleAlias: string = sourceFileName ? SOURCE_MODULE_ALIAS + "." : ""; 

    /** [Local function] Returns the TypeScript namespace (if any) of the supplied published type (if it exists), including the trailing ".". */
    function makeParamTypePrefix(publishedType?: Type): string 
    {
        if (publishedType)
        {
            if (publishedType.codeGenOptions?.nsPath)
            {
                return (moduleAlias + publishedType.codeGenOptions.nsPath + ".");
            }
            else
            {
                return (moduleAlias);
            }
        }
        else
        {
            // No prefix required for a non-published type (eg. "string")
            return ("");
        }
    }

    // Skip this section if requested
    if ((section & assertDefined(fileOptions.publisherSectionsToSkip)) === section)
    {
        return ("");
    }
    
    switch (section)
    {
        case CodeGenSection.Header:
            lines.push(...getHeaderCommentLines(GeneratedFileKind.Publisher, fileOptions));
            if (sourceFileName)
            {
                // Add an 'import' for the developer-provided source file that contains the implementations of published types and methods
                // Note: We don't just want to use an absolute path to the source file (even though that would be simpler for us) because 
                //       we want to retain any relative path so that it's easier for the user to move their code-base around. 

                // The reference to the source file must be relative to location of the generated file. For example, if the generated
                // file is in ./src and the input source file is ./test/Foo.ts, then the import file reference should be '../test/Foo'
                const relativeSourceFileName: string = Path.relative(assertDefined(fileOptions.outputPath), sourceFileName);
                
                let filePath: string = Path.dirname(relativeSourceFileName.replace(/\\/g, "/"));
                if ((filePath !== ".") && !filePath.startsWith("../") && !filePath.startsWith("./"))
                {
                    filePath = "./" + filePath;
                }
                let fileReference: string = filePath + "/" + Path.basename(relativeSourceFileName).replace(Path.extname(relativeSourceFileName), "");
                
                lines.push(`import * as ${SOURCE_MODULE_ALIAS} from "${fileReference}"; // ${SOURCE_MODULE_ALIAS} = "Published Types and Methods", but this file can also include app-state and app-event handlers`);
            }
            break;

        case CodeGenSection.AppState:
            if (sourceFileName)
            {
                if (AST.appStateVar() !== "")
                {
                    lines.push(`// ${CODEGEN_COMMENT_PREFIX}: '${CodeGenSection[section]}' section skipped (using provided state variable '${SOURCE_MODULE_ALIAS}.${AST.appStateVar()}' and class '${AST.appStateVarClassName()}' instead)`);
                    break;
                }
                else
                {
                    // Note: The _appState variable MUST be in an exported namespace (or module) so that it becomes a [mutable] property of an exported object [the namespace],
                    //       thus allowing it to be set [by checkpointConsumer()] at runtime (see https://stackoverflow.com/questions/53617972/exported-variables-are-read-only).
                    //       If it's exported from the root-level of the source file, the generated PublisherFramework.g.ts code will contain this error [in checkpointConsumer()]:
                    //         Cannot assign to '_myAppState' because it is a read-only property. (ts:2540)
                    lines.push(`// TODO: It's recommended that you move this namespace to your input file (${makeRelativePath(assertDefined(fileOptions.outputPath), sourceFileName)}) then re-run code-gen`);
                }
            }
            lines.push("export namespace State");
            lines.push("{");
            lines.push(tab + "export class AppState extends Ambrosia.AmbrosiaAppState" + NL + tab + "{");
            lines.push(tab.repeat(2) + "// TODO: Define your application state here" + NL);
            lines.push(tab.repeat(2) + "/**"); 
            lines.push(tab.repeat(2) + " * @param restoredAppState Supplied only when loading (restoring) a checkpoint, or (for a \"VNext\" AppState) when upgrading from the prior AppState.\\");
            lines.push(tab.repeat(2) + " * **WARNING:** When loading a checkpoint, restoredAppState will be an object literal, so you must use this to reinstantiate any members that are (or contain) class references.");
            lines.push(tab.repeat(2) + " */");
            lines.push(tab.repeat(2) + "constructor(restoredAppState?: AppState)");
            lines.push(tab.repeat(2) + "{");
            lines.push(tab.repeat(3) + "super(restoredAppState);" + NL);
            lines.push(tab.repeat(3) + "if (restoredAppState)");
            lines.push(tab.repeat(3) + "{");
            lines.push(tab.repeat(4) + "// TODO: Re-initialize your application state from restoredAppState here");
            lines.push(tab.repeat(4) + "// WARNING: You MUST reinstantiate all members that are (or contain) class references because restoredAppState is data-only");
            lines.push(tab.repeat(3) + "}");
            lines.push(tab.repeat(3) + "else");
            lines.push(tab.repeat(3) + "{");
            lines.push(tab.repeat(4) + "// TODO: Initialize your application state here");
            lines.push(tab.repeat(3) + "}");
            lines.push(tab.repeat(2) + "}" + NL + tab + "}" + NL);
            lines.push(tab + "/**");
            lines.push(tab + " * Only assign this using the return value of IC.start(), the return value of the upgrade() method of your AmbrosiaAppState");
            lines.push(tab + " * instance, and [if not using the generated checkpointConsumer()] in the 'onFinished' callback of an IncomingCheckpoint object.");
            lines.push(tab + " */");
            lines.push(tab + "export let _appState: AppState;");
            lines.push("}")
            break;
    
        case CodeGenSection.PostMethodHandlers:
            for (const name in _publishedMethods)
            {
                for (const version in _publishedMethods[name])
                {
                    let method: Method = _publishedMethods[name][version];
                    let variableNames: string[] = method.parameterNames.map(name => name.endsWith("?") ? name.slice(0, -1) : name);
                    let nsPathForMethod: string = method.codeGenOptions?.nsPath ? (method.codeGenOptions.nsPath + ".") : "";
                    
                    if (method.isPost)
                    {
                        let caseTab: string = tab;
                        lines.push(`case "${method.name}":`);
                        if (variableNames.length > 0) 
                        {
                            // To prevent variable name collisions in the switch statement (eg. if 2 methods use the same parameter name), if needed, we create a new block scope for each case statement
                            lines.push(`${tab}{`);
                            caseTab = tab + tab;
                        }
                        for (let i = 0; i < variableNames.length; i++)
                        {
                            let prefix: string = makeParamTypePrefix(_publishedTypes[Type.removeArraySuffix(method.parameterTypes[i])]); 
                            lines.push(`${caseTab}const ${Method.trimRest(variableNames[i])}: ${prefix}${method.parameterTypes[i]} = IC.getPostMethodArg(rpc, "${Method.trimRest(method.parameterNames[i])}");`);
                        }
                        let prefix: string = makeParamTypePrefix(_publishedTypes[Type.removeArraySuffix(method.returnType)]); 
                        lines.push(`${caseTab}IC.postResult<${prefix}${method.returnType}>(rpc, ${moduleAlias + nsPathForMethod}${method.name}(${variableNames.join(", ")}));`);
                        if (variableNames.length > 0)
                        {
                            lines.push(`${tab}}`);
                        }
                        lines.push(`${tab}break;${NL}`);
                    }
                }
            }
            break;

        case CodeGenSection.NonPostMethodHandlers:
            for (const name in _publishedMethods)
            {
                for (const version in _publishedMethods[name])
                {
                    let method: Method = _publishedMethods[name][version];
                    let variableNames: string[] = method.parameterNames.map(name => name.endsWith("?") ? name.slice(0, -1) : name);
                    let nsPathForMethod: string = method.codeGenOptions?.nsPath ? (method.codeGenOptions.nsPath + ".") : "";
                    
                    if (!method.isPost)
                    {
                        let caseTab: string = tab;
                        lines.push(`case ${method.id}:`);
                        if (variableNames.length > 0) 
                        {
                            // To prevent variable name collisions in the switch statement (eg. if 2 methods use the same parameter name), if needed, we create a new block scope for each case statement
                            lines.push(`${tab}{`);
                            caseTab = tab + tab;
                        }
                        for (let i = 0; i < variableNames.length; i++)
                        {
                            let prefix: string = makeParamTypePrefix(_publishedTypes[Type.removeArraySuffix(method.parameterTypes[i])]); 
                            if (method.takesRawParams)
                            {
                                lines.push(`${caseTab}const ${variableNames[i]}: ${prefix}${method.parameterTypes[i]} = rpc.getRawParams();`);
                            }
                            else
                            {
                                const isOptionalParam: boolean = method.parameterNames[i].endsWith("?");
                                const paramName: string = isOptionalParam ? method.parameterNames[i].slice(0, -1) : method.parameterNames[i];
                                lines.push(`${caseTab}const ${Method.trimRest(variableNames[i])}: ${prefix}${method.parameterTypes[i]} = rpc.getJsonParam("${Method.trimRest(paramName)}");${isOptionalParam ? " // Optional parameter" : ""}`);
                            }
                        }
                        lines.push(`${caseTab}${moduleAlias + nsPathForMethod}${method.name}(${variableNames.join(", ")});`);
                        if (variableNames.length > 0)
                        {
                            lines.push(`${tab}}`);
                        }
                        lines.push(`${tab}break;${NL}`);
                    }
                }
            }
            break;

        case CodeGenSection.PublishTypes:
            for (const typeName in _publishedTypes)
            {
                let type: Type = _publishedTypes[typeName];
                lines.push(`Meta.publishType("${type.name}", "${type.definition.replace(/"/g, "\\\"")}");`);
            }
            break;

        case CodeGenSection.PublishMethods:
            for (const name in _publishedMethods)
            {
                for (const version in _publishedMethods[name])
                {
                    let method: Method = _publishedMethods[name][version];
                    let paramList: string[] = [];
                    paramList.push(...method.parameterNames.map((name, index) => `"${name}: ${method.parameterTypes[index].replace(/"/g, "\\\"")}"`));
                    let methodParams: string = `[${paramList.join(", ")}]`;

                    if (method.isPost)
                    {
                        lines.push(`Meta.publishPostMethod("${method.name}", ${method.version}, ${methodParams}, "${method.returnType.replace(/"/g, "\\\"")}"${method.isTypeChecked ? "" : ", false"});`);
                    }
                    else
                    {
                        lines.push(`Meta.publishMethod(${method.id}, "${method.name}", ${methodParams});`);
                    }
                }
            }