function codeGen()

in Clients/AmbrosiaJS/Ambrosia-Node/src/Meta.ts [3244:3450]


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"});`);
                    }