in packages/type-safe-api/src/project/type-safe-websocket-api-project.ts [194:655]
constructor(options: TypeSafeWebSocketApiProjectOptions) {
super(options);
const nxWorkspace = this.getNxWorkspace(options);
const isNxWorkspace =
this.parent &&
(ProjectUtils.isNamedInstanceOf(this.parent, MonorepoTsProject) ||
ProjectUtils.isNamedInstanceOf(this.parent, MonorepoJavaProject) ||
ProjectUtils.isNamedInstanceOf(this.parent, MonorepoPythonProject));
const handlerLanguages = [...new Set(options.handlers?.languages ?? [])];
// Try to infer monorepo default release branch, otherwise default to mainline unless overridden
const defaultReleaseBranch =
nxWorkspace?.affected.defaultBase ?? "mainline";
const packageManager =
this.parent && ProjectUtils.isNamedInstanceOf(this.parent, NodeProject)
? this.parent.package.packageManager
: NodePackageManager.PNPM;
// API Definition project containing the model
const modelDir = "model";
const parsedSpecFile = ".api.json";
const asyncApiSpecFile = ".asyncapi.json";
this.model = generateAsyncModelProject({
parent: nxWorkspace ? this.parent : this,
outdir: nxWorkspace ? path.join(options.outdir!, modelDir) : modelDir,
name: `${options.name}-model`,
modelLanguage: options.model.language,
modelOptions: options.model.options,
handlerLanguages,
packageManager,
defaultReleaseBranch,
parsedSpecFile,
asyncApiSpecFile,
});
const modelProject = [
this.model.openapi,
this.model.smithy,
this.model.typeSpec,
].filter((m) => m)[0] as Project;
// Ensure we always generate a runtime project for the infrastructure language, regardless of what was specified by
// the user. Likewise we generate a runtime project for any handler languages specified
const runtimeLanguages = [
...new Set([
...(options.runtime?.languages ?? []),
options.infrastructure.language,
...(options.handlers?.languages ?? []),
]),
];
// TODO: Delete when python/java support is implemented
if (
runtimeLanguages.includes(Language.JAVA) ||
runtimeLanguages.includes(Language.PYTHON)
) {
const errorMessages = [
...(runtimeLanguages.includes(Language.PYTHON)
? [
"Python is not supported by Type Safe WebSocket API. Please +1 this issue if needed: https://github.com/aws/aws-pdk/issues/741",
]
: []),
...(runtimeLanguages.includes(Language.JAVA)
? [
"Java is not supported by Type Safe WebSocket API. Please +1 this issue if needed: https://github.com/aws/aws-pdk/issues/740",
]
: []),
];
throw new Error(errorMessages.join("\n"));
}
const generatedDir = "generated";
const runtimeDir = path.join(generatedDir, "runtime");
const runtimeDirRelativeToParent = nxWorkspace
? path.join(options.outdir!, runtimeDir)
: runtimeDir;
// Path from a generated package directory (eg api/generated/runtime/typescript) to the model dir (ie api/model)
const relativePathToModelDirFromGeneratedPackageDir = path.relative(
path.join(this.outdir, runtimeDir, "language"),
path.join(this.outdir, modelDir)
);
const parsedSpecRelativeToGeneratedPackageDir = path.join(
relativePathToModelDirFromGeneratedPackageDir,
this.model.parsedSpecFile
);
// Declare the generated runtime projects
const generatedRuntimeProjects = generateAsyncRuntimeProjects(
runtimeLanguages,
{
parent: nxWorkspace ? this.parent! : this,
parentPackageName: this.name,
generatedCodeDir: runtimeDirRelativeToParent,
isWithinMonorepo: isNxWorkspace,
// Spec path relative to each generated runtime dir
parsedSpecPath: parsedSpecRelativeToGeneratedPackageDir,
typescriptOptions: {
defaultReleaseBranch,
packageManager,
...options.runtime?.options?.typescript,
},
pythonOptions: {
authorName: "APJ Cope",
authorEmail: "apj-cope@amazon.com",
version: "0.0.0",
...options.runtime?.options?.python,
},
javaOptions: {
version: "0.0.0",
...options.runtime?.options?.java,
},
}
);
const documentationFormats = [
...new Set(options.documentation?.formats ?? []),
];
const docsDir = path.join(generatedDir, "documentation");
const docsDirRelativeToParent = nxWorkspace
? path.join(options.outdir!, docsDir)
: docsDir;
// AsyncAPI specification is used for WebSocket documentation generation
const asyncapiJsonFilePathRelativeToGeneratedPackageDir = path.join(
relativePathToModelDirFromGeneratedPackageDir,
this.model.asyncApiSpecFile
);
const generatedDocs = generateDocsProjects(documentationFormats, {
parent: nxWorkspace ? this.parent! : this,
parentPackageName: this.name,
generatedDocsDir: docsDirRelativeToParent,
// Spec path relative to each generated doc format dir
parsedSpecPath: asyncapiJsonFilePathRelativeToGeneratedPackageDir,
asyncDocumentationOptions: options.documentation?.options,
});
// Documentation projects use AsyncAPI generator which can intermittently fail
// when executed in parallel to other AsyncAPI generator commands. We protect against this
// by ensuring documentation projects are built sequentially.
const docProjects = Object.values(generatedDocs);
docProjects.forEach((docProject, i) => {
if (docProjects[i - 1]) {
NxProject.ensure(docProjects[i - 1]).addImplicitDependency(docProject);
}
});
this.documentation = {
html: generatedDocs[WebSocketDocumentationFormat.HTML],
markdown: generatedDocs[WebSocketDocumentationFormat.MARKDOWN],
};
const librarySet = new Set(options.library?.libraries ?? []);
// Hooks depend on client, so always add the client if we specified hooks
if (librarySet.has(WebSocketLibrary.TYPESCRIPT_WEBSOCKET_HOOKS)) {
librarySet.add(WebSocketLibrary.TYPESCRIPT_WEBSOCKET_CLIENT);
}
const libraries = [...librarySet];
const libraryDir = path.join(generatedDir, "libraries");
const libraryDirRelativeToParent = nxWorkspace
? path.join(options.outdir!, libraryDir)
: libraryDir;
// Declare the generated runtime projects
const generatedLibraryProjects = generateAsyncLibraryProjects(libraries, {
parent: nxWorkspace ? this.parent! : this,
parentPackageName: this.name,
generatedCodeDir: libraryDirRelativeToParent,
isWithinMonorepo: isNxWorkspace,
// Spec path relative to each generated library dir
parsedSpecPath: parsedSpecRelativeToGeneratedPackageDir,
typescriptWebSocketClientOptions: {
defaultReleaseBranch,
packageManager,
...options.library?.options?.typescriptWebSocketClient,
},
typescriptWebSocketHooksOptions: {
defaultReleaseBranch,
clientPackageName:
options.library?.options?.typescriptWebSocketClient?.name,
packageManager,
...options.library?.options?.typescriptWebSocketHooks,
},
});
// Ensure the generated runtime, libraries and docs projects have a dependency on the model project
if (this.parent) {
[
...Object.values(generatedRuntimeProjects),
...Object.values(generatedDocs),
...Object.values(generatedLibraryProjects),
].forEach((project) => {
NxProject.ensure(project).addImplicitDependency(modelProject);
});
}
this.runtime = {
typescript: generatedRuntimeProjects[Language.TYPESCRIPT]
? (generatedRuntimeProjects[Language.TYPESCRIPT] as TypeScriptProject)
: undefined,
java: generatedRuntimeProjects[Language.JAVA]
? (generatedRuntimeProjects[Language.JAVA] as JavaProject)
: undefined,
python: generatedRuntimeProjects[Language.PYTHON]
? (generatedRuntimeProjects[Language.PYTHON] as PythonProject)
: undefined,
};
this.library = {
typescriptWebSocketClient: generatedLibraryProjects[
WebSocketLibrary.TYPESCRIPT_WEBSOCKET_CLIENT
]
? (generatedLibraryProjects[
WebSocketLibrary.TYPESCRIPT_WEBSOCKET_CLIENT
] as TypeScriptProject)
: undefined,
typescriptWebSocketHooks: generatedLibraryProjects[
WebSocketLibrary.TYPESCRIPT_WEBSOCKET_HOOKS
]
? (generatedLibraryProjects[
WebSocketLibrary.TYPESCRIPT_WEBSOCKET_HOOKS
] as TypeScriptProject)
: undefined,
};
// For the hooks library, add a dependency on the client
if (
this.library.typescriptWebSocketHooks &&
this.library.typescriptWebSocketClient
) {
this.library.typescriptWebSocketHooks.addDeps(
this.library.typescriptWebSocketClient.package.packageName
);
}
const handlersDir = "handlers";
const handlersDirRelativeToParent = nxWorkspace
? path.join(options.outdir!, handlersDir)
: handlersDir;
const relativePathToModelDirFromHandlersDir = path.relative(
path.join(this.outdir, handlersDir, "language"),
path.join(this.outdir, modelDir)
);
const parsedSpecRelativeToHandlersDir = path.join(
relativePathToModelDirFromHandlersDir,
this.model.parsedSpecFile
);
// Declare the generated handlers projects
const generatedHandlersProjects = generateAsyncHandlersProjects(
handlerLanguages,
{
parent: nxWorkspace ? this.parent! : this,
parentPackageName: this.name,
generatedCodeDir: handlersDirRelativeToParent,
isWithinMonorepo: isNxWorkspace,
// Spec path relative to each generated handlers package dir
parsedSpecPath: parsedSpecRelativeToHandlersDir,
typescriptOptions: {
defaultReleaseBranch,
packageManager,
...options.handlers?.options?.typescript,
},
pythonOptions: {
authorName: "APJ Cope",
authorEmail: "apj-cope@amazon.com",
version: "0.0.0",
...options.handlers?.options?.python,
},
javaOptions: {
version: "0.0.0",
...options.handlers?.options?.java,
},
generatedRuntimes: {
typescript: this.runtime.typescript as
| GeneratedTypescriptAsyncRuntimeProject
| undefined,
python: this.runtime.python as
| GeneratedPythonAsyncRuntimeProject
| undefined,
java: this.runtime.java as
| GeneratedJavaAsyncRuntimeProject
| undefined,
},
}
);
this.handlers = {
typescript: generatedHandlersProjects[Language.TYPESCRIPT]
? (generatedHandlersProjects[Language.TYPESCRIPT] as TypeScriptProject)
: undefined,
java: generatedHandlersProjects[Language.JAVA]
? (generatedHandlersProjects[Language.JAVA] as JavaProject)
: undefined,
python: generatedHandlersProjects[Language.PYTHON]
? (generatedHandlersProjects[Language.PYTHON] as PythonProject)
: undefined,
};
// Ensure the handlers project depends on the appropriate runtime projects
if (this.handlers.typescript) {
NxProject.ensure(this.handlers.typescript).addImplicitDependency(
this.runtime.typescript!
);
}
if (this.handlers.java) {
NxProject.ensure(this.handlers.java).addImplicitDependency(
this.runtime.java!
);
}
if (this.handlers.python) {
NxProject.ensure(this.handlers.python).addImplicitDependency(
this.runtime.python!
);
}
const infraDir = path.join(generatedDir, "infrastructure");
const infraDirRelativeToParent = nxWorkspace
? path.join(options.outdir!, infraDir)
: infraDir;
// Infrastructure project
const infraProject = generateAsyncInfraProject(
options.infrastructure.language,
{
parent: nxWorkspace ? this.parent! : this,
parentPackageName: this.name,
generatedCodeDir: infraDirRelativeToParent,
isWithinMonorepo: isNxWorkspace,
// Spec path relative to each generated infra package dir
parsedSpecPath: parsedSpecRelativeToGeneratedPackageDir,
typescriptOptions: {
defaultReleaseBranch,
packageManager,
...options.infrastructure.options?.typescript,
},
pythonOptions: {
authorName: "APJ Cope",
authorEmail: "apj-cope@amazon.com",
version: "0.0.0",
...options.infrastructure.options?.python,
},
javaOptions: {
version: "0.0.0",
...options.infrastructure.options?.java,
},
generatedRuntimes: {
typescript: this.runtime.typescript as
| GeneratedTypescriptAsyncRuntimeProject
| undefined,
python: this.runtime.python as
| GeneratedPythonAsyncRuntimeProject
| undefined,
java: this.runtime.java as
| GeneratedJavaAsyncRuntimeProject
| undefined,
},
generatedHandlers: {
typescript: this.handlers.typescript as
| GeneratedTypescriptAsyncHandlersProject
| undefined,
python: this.handlers.python as
| GeneratedPythonAsyncHandlersProject
| undefined,
java: this.handlers.java as
| GeneratedJavaAsyncHandlersProject
| undefined,
},
}
);
const infraProjects: {
-readonly [K in keyof GeneratedCodeProjects]: GeneratedCodeProjects[K];
} = {};
// Add implicit dependencies and assign the appropriate infrastructure project member
switch (options.infrastructure.language) {
case Language.JAVA:
NxProject.ensure(infraProject).addImplicitDependency(
this.runtime.java!
);
infraProjects.java = infraProject as JavaProject;
break;
case Language.PYTHON:
NxProject.ensure(infraProject).addImplicitDependency(
this.runtime.python!
);
infraProjects.python = infraProject as PythonProject;
break;
case Language.TYPESCRIPT:
NxProject.ensure(infraProject).addImplicitDependency(
this.runtime.typescript!
);
infraProjects.typescript = infraProject as TypeScriptProject;
break;
default:
throw new Error(
`Unknown infrastructure language ${options.infrastructure.language}`
);
}
this.infrastructure = infraProjects;
NxProject.ensure(infraProject).addImplicitDependency(modelProject);
// Expose collections of projects
const allRuntimes = Object.values(generatedRuntimeProjects);
const allInfrastructure = [infraProject];
const allLibraries = Object.values(generatedLibraryProjects);
const allDocumentation = Object.values(generatedDocs);
const allHandlers = Object.values(generatedHandlersProjects);
this.all = {
model: [modelProject],
runtimes: allRuntimes,
infrastructure: allInfrastructure,
libraries: allLibraries,
documentation: allDocumentation,
handlers: allHandlers,
projects: [
modelProject,
...allRuntimes,
...allInfrastructure,
...allLibraries,
...allDocumentation,
...allHandlers,
],
};
if (!nxWorkspace) {
// Add a task for the non-monorepo case to build the projects in the right order
[
modelProject,
...Object.values(generatedRuntimeProjects),
infraProject,
...Object.values(generatedLibraryProjects),
...Object.values(generatedDocs),
].forEach((project) => {
this.compileTask.exec("npx projen build", {
cwd: path.relative(this.outdir, project.outdir),
});
});
}
// Add the README as a sample file which the user may edit
new SampleFile(this, "README.md", {
sourcePath: path.resolve(
__dirname,
"..",
"..",
"samples",
"type-safe-api",
"readme",
"TYPE_SAFE_API.md"
),
});
}