Feature: Cadl project file variable interpolation and `emitter-output-dir` (#1262)
* Interpolate * Fix existing tests * Fix regen-samples * Add some cli tests * Interpolate each other * validate no relative paths * update docs * update docs * ADd validation for unknown args * :Changelog * Fix lint * Update packages/compiler/core/messages.ts Co-authored-by: Mark Cowlishaw <markcowl@microsoft.com> * fix test Co-authored-by: Mark Cowlishaw <markcowl@microsoft.com>
This commit is contained in:
Родитель
6e8976a343
Коммит
c8f8752782
|
@ -66,7 +66,7 @@
|
|||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Compile Scratch",
|
||||
"program": "${workspaceFolder}/packages/compiler/dist/core/cli.js",
|
||||
"program": "${workspaceFolder}/packages/compiler/dist/core/cli/cli.js",
|
||||
"args": [
|
||||
"compile",
|
||||
"../samples/scratch",
|
||||
|
@ -89,7 +89,7 @@
|
|||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Compile Scratch (nostdlib)",
|
||||
"program": "${workspaceFolder}/packages/compiler/dist/core/cli.js",
|
||||
"program": "${workspaceFolder}/packages/compiler/dist/core/cli/cli.js",
|
||||
"args": ["compile", "../samples/scratch", "--output-dir=temp/scratch-output", "--nostdlib"],
|
||||
"smartStep": true,
|
||||
"sourceMaps": true,
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"changes": [
|
||||
{
|
||||
"packageName": "@cadl-lang/compiler",
|
||||
"comment": "Add variable interpolation functionality in the cadl-project.yaml",
|
||||
"type": "minor"
|
||||
},
|
||||
{
|
||||
"packageName": "@cadl-lang/compiler",
|
||||
"comment": "Add built-in `emitter-output-dir` options for all emitter.",
|
||||
"type": "minor"
|
||||
},
|
||||
{
|
||||
"packageName": "@cadl-lang/compiler",
|
||||
"comment": "**Api Breaking change** $onEmit signature was updated to take an EmitContext object as only parmaeter.",
|
||||
"type": "minor"
|
||||
}
|
||||
],
|
||||
"packageName": "@cadl-lang/compiler"
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"changes": [
|
||||
{
|
||||
"packageName": "@cadl-lang/html-program-viewer",
|
||||
"comment": "Uptake change to `onEmit` signature",
|
||||
"type": "minor"
|
||||
},
|
||||
{
|
||||
"packageName": "@cadl-lang/html-program-viewer",
|
||||
"comment": "**Breaking change** using new built-in `emitter-output-dir` option instead of custom `output-dir`.",
|
||||
"type": "minor"
|
||||
}
|
||||
],
|
||||
"packageName": "@cadl-lang/html-program-viewer"
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"changes": [
|
||||
{
|
||||
"packageName": "@cadl-lang/openapi3",
|
||||
"comment": "Uptake change to `onEmit` signature",
|
||||
"type": "minor"
|
||||
},
|
||||
{
|
||||
"packageName": "@cadl-lang/openapi3",
|
||||
"comment": "**Breaking change** using new built-in `emitter-output-dir` option instead of custom `output-dir`.",
|
||||
"type": "minor"
|
||||
}
|
||||
],
|
||||
"packageName": "@cadl-lang/openapi3"
|
||||
}
|
|
@ -24,8 +24,8 @@ For example, the following will write a text file to the output directory:
|
|||
import { Program } from "@cadl-lang/compiler";
|
||||
import Path from "path";
|
||||
|
||||
export async function $onEmit(program: Program) {
|
||||
const outputDir = Path.join(program.compilerOptions.outputDir!, "hello.txt");
|
||||
export async function $onEmit(context: EmitContext) {
|
||||
const outputDir = Path.join(context.emitterOutputDir, "hello.txt");
|
||||
await program.host.writeFile(outputDir, "hello world!");
|
||||
}
|
||||
```
|
||||
|
@ -65,9 +65,9 @@ export const $lib = createCadlLibrary({
|
|||
},
|
||||
});
|
||||
|
||||
export async function $onEmit(program: Program, options: EmitterOptions) {
|
||||
const outputDir = Path.join(program.compilerOptions.outputDir!, "hello.txt");
|
||||
const name = options.targetName;
|
||||
export async function $onEmit(context: EmitContext<EmitterOptions>) {
|
||||
const outputDir = Path.join(context.emitterOutputDir, "hello.txt");
|
||||
const name = context.options.targetName;
|
||||
await program.host.writeFile(outputDir, `hello ${name}!`);
|
||||
}
|
||||
```
|
||||
|
@ -79,9 +79,9 @@ export async function $onEmit(program: Program, options: EmitterOptions) {
|
|||
|
||||
#### Emitter options vs. decorators
|
||||
|
||||
Generally speaking, emitter options and decorators can solve the same problems: allowing the user to customize how the emit works. For example, the `outputDir` option could be passed on the command line, or we could have an `@outputPath` decorator that has the same effect. Which do you use?
|
||||
Generally speaking, emitter options and decorators can solve the same problems: allowing the user to customize how the emit works. For example, the `outputFilename` option could be passed on the command line, or we could have an `@outputFilename` decorator that has the same effect. Which do you use?
|
||||
|
||||
The general guideline is to use a decorator when the customization is intrinsic to the API itself. In other words, when all uses of the Cadl program would use the same configuration. This is not the case for `outputDir` because different users of the API might want to emit the files in different locations depending on how their code generation pipeline is set up.
|
||||
The general guideline is to use a decorator when the customization is intrinsic to the API itself. In other words, when all uses of the Cadl program would use the same configuration. This is not the case for `outputFilename` because different users of the API might want to emit the files in different locations depending on how their code generation pipeline is set up.
|
||||
|
||||
## Querying the program
|
||||
|
||||
|
@ -130,7 +130,7 @@ export function $emitThis(context: DecoratorContext, target: Model) {
|
|||
context.program.stateSet(emitThisKey).add(target);
|
||||
}
|
||||
|
||||
export async function $onEmit(program: Program) {
|
||||
export async function $onEmit(context: EmitContext) {
|
||||
for (const model of program.stateSet(emitThisKey)) {
|
||||
emitModel(model);
|
||||
}
|
||||
|
@ -177,4 +177,4 @@ Since an emitter is a node library, you could use standard `fs` APIs to write fi
|
|||
|
||||
Instead, use the compiler [`host` interface](#todo) to access the file system. The API is equivalent to the node API but works in a wider range of scenarios.
|
||||
|
||||
In order to know where to emit files, the compiler has options with an `outputPath` property. This is set to the current working directory's `cadl-output` directory by default, but can be overridden by the user.
|
||||
In order to know where to emit files, the emitter context has a `emitterOutputDir` property that is automatically resolved using the `emitter-output-dir` built-in emitter options. This is set to `{cwd}/cadl-output/{emitter-name}` by default, but can be overridden by the user. Do not use the `compilerOptions.outputDir`
|
||||
|
|
|
@ -28,6 +28,8 @@ The file is a `yaml` document with the following structure. See the [next sectio
|
|||
```cadl
|
||||
model CadlProjectSchema {
|
||||
extends?: string;
|
||||
parameters?: Record<{default: string}>
|
||||
"environment-variables"?: Record<{default: string}>
|
||||
"warn-as-error"?: boolean;
|
||||
"output-dir"?: boolean;
|
||||
"trace"?: string | string[];
|
||||
|
@ -65,6 +67,90 @@ emitters:
|
|||
emitter3: true
|
||||
```
|
||||
|
||||
### Variable interpolation
|
||||
|
||||
The cadl project file provide variable interpolation using:
|
||||
|
||||
- built-in variables
|
||||
- environment variables
|
||||
- config file parameters
|
||||
- emitter options can reference each other
|
||||
|
||||
Variable interpolation is done using an variable expression surrounded by `{` and `}`. (`{<expression>}`)
|
||||
|
||||
Examples:
|
||||
|
||||
- `{output-dir}/my-path`
|
||||
- `{env.SHARED_PATH}/my-path`
|
||||
|
||||
#### Built-in variables
|
||||
|
||||
| Variable name | Scope | Description |
|
||||
| -------------- | --------------- | ------------------------------------------------------------------------------------ |
|
||||
| `cwd` | \* | Points to the current working directory |
|
||||
| `project-root` | \* | Points to the the cadl-project.yaml file containing folder. |
|
||||
| `output-dir` | emitter options | Common `output-dir` See [output-dir](#output-dir---configure-the-default-output-dir) |
|
||||
| `emitter-name` | emitter options | Name of the emitter |
|
||||
|
||||
#### Project parameters
|
||||
|
||||
A cadl project file can specify some parameters that can then be specified via the CLI.
|
||||
|
||||
`{cwd}` and `{project-root}` variables can be used in the default value of those parmeters.
|
||||
|
||||
The parameters can then be referenced by their name in a variable interpolation expression.
|
||||
|
||||
Parameters must have a default value.
|
||||
**Example:**
|
||||
|
||||
```yaml
|
||||
parameters:
|
||||
base-dir:
|
||||
default: "{cwd}"
|
||||
|
||||
outout-dir: {base-dir}/output
|
||||
```
|
||||
|
||||
The parameter can then be specified with `--arg` in this format `--arg "<parameter-name>=<value>"`
|
||||
|
||||
```bash
|
||||
cadl compile . --arg "base-dir=/path/to/base"
|
||||
```
|
||||
|
||||
#### Environment variables
|
||||
|
||||
A cadl project file can define which environment variables it can interpolate.
|
||||
|
||||
`{cwd}` and `{project-root}` variables can be used in the default value of the environment variables.
|
||||
|
||||
The environment variables can then be referenced by their name in a variable interpolation expression with the `env.` prefix.
|
||||
|
||||
Environment variables must have a default value.
|
||||
|
||||
**Example:**
|
||||
|
||||
```yaml
|
||||
environment-variables:
|
||||
BASE_DIR:
|
||||
default: "{cwd}"
|
||||
|
||||
outout-dir: {env.BASE_DIR}/output
|
||||
```
|
||||
|
||||
#### Emitter options
|
||||
|
||||
Emitter options can reference each other using the other option name as the variable expresion.
|
||||
|
||||
Can only interpolate emitter options from the same emitter.
|
||||
|
||||
```yaml
|
||||
emitters:
|
||||
@cadl-lang/openapi3:
|
||||
emitter-output-dir: {output-dir}/{emitter-sub-folder}
|
||||
emitter-sub-folder: bar
|
||||
|
||||
```
|
||||
|
||||
## Cadl Configuration Options
|
||||
|
||||
| Config | Cli | Description |
|
||||
|
@ -80,7 +166,7 @@ emitters:
|
|||
Specify which emitters to use and their options if applicable.
|
||||
|
||||
```yaml
|
||||
output-dir: ./cadl-build
|
||||
output-dir: {cwd}/cadl-build
|
||||
```
|
||||
|
||||
Output dir can be provided using the `--output-dir` cli flag
|
||||
|
@ -89,6 +175,8 @@ Output dir can be provided using the `--output-dir` cli flag
|
|||
cadl compile . --output-dir "./cadl-build"
|
||||
```
|
||||
|
||||
Output dir must be an absolute path in the config. Use `{cwd}` or `{project-root}` to explicitly specify what it should be relative to.
|
||||
|
||||
### `trace` - Configure what to trace
|
||||
|
||||
Configure what area to trace. See [tracing docs](./tracing.md)
|
||||
|
@ -169,6 +257,14 @@ Emitters selection can be overridden in the command line via `--emit` [flag](#em
|
|||
cadl compile . --emit emitter1 --option emitter1.option2="option2-value"
|
||||
```
|
||||
|
||||
#### Emitters built-in options
|
||||
|
||||
##### `emitter-output-dir`
|
||||
|
||||
Represent the path where the emitter should be outputing the generated files.
|
||||
|
||||
Default: `{output-dir}/{emitter-name}`
|
||||
|
||||
## Emitter control cli flags
|
||||
|
||||
### `--emit`
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
#!/usr/bin/env node
|
||||
import { runScript } from "../dist/cmd/runner.js";
|
||||
await runScript("dist/core/cli.js");
|
||||
await runScript("dist/core/cli/cli.js");
|
||||
|
|
|
@ -0,0 +1,142 @@
|
|||
import { createDiagnosticCollector, ignoreDiagnostics } from "../core/diagnostics.js";
|
||||
import { createDiagnostic } from "../core/messages.js";
|
||||
import { Diagnostic, NoTarget } from "../core/types.js";
|
||||
import { CadlConfig, ConfigEnvironmentVariable, ConfigParameter } from "./types.js";
|
||||
|
||||
export interface ExpandConfigOptions {
|
||||
readonly cwd: string;
|
||||
readonly outputDir?: string;
|
||||
readonly env?: Record<string, string | undefined>;
|
||||
readonly args?: Record<string, string>;
|
||||
}
|
||||
|
||||
export function expandConfigVariables(
|
||||
config: CadlConfig,
|
||||
options: ExpandConfigOptions
|
||||
): [CadlConfig, readonly Diagnostic[]] {
|
||||
const diagnostics = createDiagnosticCollector();
|
||||
const builtInVars = {
|
||||
"project-root": config.projectRoot,
|
||||
cwd: options.cwd,
|
||||
};
|
||||
|
||||
const commonVars = {
|
||||
...builtInVars,
|
||||
...diagnostics.pipe(resolveArgs(config.parameters, options.args, builtInVars)),
|
||||
env: diagnostics.pipe(resolveArgs(config.environmentVariables, options.env, builtInVars)),
|
||||
};
|
||||
const outputDir = diagnostics.pipe(
|
||||
resolveValue(options.outputDir ?? config.outputDir, commonVars)
|
||||
);
|
||||
|
||||
const emitters: Record<string, Record<string, unknown>> = {};
|
||||
|
||||
for (const [name, emitterOptions] of Object.entries(config.emitters)) {
|
||||
const emitterVars = { ...commonVars, "output-dir": outputDir, "emitter-name": name };
|
||||
emitters[name] = diagnostics.pipe(resolveValues(emitterOptions, emitterVars));
|
||||
}
|
||||
|
||||
return diagnostics.wrap({ ...config, outputDir, emitters });
|
||||
}
|
||||
|
||||
function resolveArgs(
|
||||
declarations: Record<string, ConfigParameter | ConfigEnvironmentVariable> | undefined,
|
||||
args: Record<string, string | undefined> | undefined,
|
||||
predefinedVariables: Record<string, string | Record<string, string>>
|
||||
): [Record<string, string>, readonly Diagnostic[]] {
|
||||
const unmatchedArgs = new Set(Object.keys(args ?? {}));
|
||||
const result: Record<string, string> = {};
|
||||
if (declarations !== undefined) {
|
||||
for (const [name, definition] of Object.entries(declarations)) {
|
||||
unmatchedArgs.delete(name);
|
||||
result[name] =
|
||||
args?.[name] ?? ignoreDiagnostics(resolveValue(definition.default, predefinedVariables));
|
||||
}
|
||||
}
|
||||
|
||||
const diagnostics: Diagnostic[] = [...unmatchedArgs].map((unmatchedArg) => {
|
||||
return createDiagnostic({
|
||||
code: "config-invalid-argument",
|
||||
format: { name: unmatchedArg },
|
||||
target: NoTarget,
|
||||
});
|
||||
});
|
||||
return [result, diagnostics];
|
||||
}
|
||||
|
||||
const VariableInterpolationRegex = /{([a-zA-Z-_.]+)}/g;
|
||||
|
||||
function resolveValue(
|
||||
value: string,
|
||||
predefinedVariables: Record<string, string | Record<string, string>>
|
||||
): [string, readonly Diagnostic[]] {
|
||||
const [result, diagnostics] = resolveValues({ value }, predefinedVariables);
|
||||
return [result.value, diagnostics];
|
||||
}
|
||||
|
||||
export function resolveValues<T extends Record<string, unknown>>(
|
||||
values: T,
|
||||
predefinedVariables: Record<string, string | Record<string, string>> = {}
|
||||
): [T, readonly Diagnostic[]] {
|
||||
const diagnostics: Diagnostic[] = [];
|
||||
const resolvedValues: Record<string, unknown> = {};
|
||||
const resolvingValues = new Set<string>();
|
||||
|
||||
function resolveValue(key: string) {
|
||||
resolvingValues.add(key);
|
||||
const value = values[key];
|
||||
if (!(typeof value === "string")) {
|
||||
return value;
|
||||
}
|
||||
return value.replace(VariableInterpolationRegex, (match, expression) => {
|
||||
return (resolveExpression(expression) as string) ?? `{${expression}}`;
|
||||
});
|
||||
}
|
||||
|
||||
function resolveExpression(expression: string): unknown | undefined {
|
||||
if (expression in resolvedValues) {
|
||||
return resolvedValues[expression];
|
||||
}
|
||||
|
||||
if (resolvingValues.has(expression)) {
|
||||
diagnostics.push(
|
||||
createDiagnostic({
|
||||
code: "config-circular-variable",
|
||||
target: NoTarget,
|
||||
format: { name: expression },
|
||||
})
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (expression in values) {
|
||||
return resolveValue(expression) as any;
|
||||
}
|
||||
|
||||
const segments = expression.split(".");
|
||||
let resolved: any = predefinedVariables;
|
||||
for (const segment of segments) {
|
||||
resolved = resolved[segment];
|
||||
if (resolved === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof resolved === "string") {
|
||||
return resolved;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
for (const key of Object.keys(values)) {
|
||||
resolvingValues.clear();
|
||||
if (key in resolvedValues) {
|
||||
continue;
|
||||
}
|
||||
|
||||
resolvedValues[key] = resolveValue(key) as any;
|
||||
}
|
||||
|
||||
return [resolvedValues as any, diagnostics];
|
||||
}
|
|
@ -1,14 +1,16 @@
|
|||
import jsyaml from "js-yaml";
|
||||
import { getDirectoryPath, joinPaths, resolvePath } from "../core/path-utils.js";
|
||||
import { createDiagnostic } from "../core/messages.js";
|
||||
import { getDirectoryPath, isPathAbsolute, joinPaths, resolvePath } from "../core/path-utils.js";
|
||||
import { createJSONSchemaValidator } from "../core/schema-validator.js";
|
||||
import { CompilerHost, Diagnostic } from "../core/types.js";
|
||||
import { deepClone, deepFreeze, doIO, loadFile } from "../core/util.js";
|
||||
import { CompilerHost, Diagnostic, NoTarget } from "../core/types.js";
|
||||
import { deepClone, deepFreeze, doIO, loadFile, omitUndefined } from "../core/util.js";
|
||||
import { CadlConfigJsonSchema } from "./config-schema.js";
|
||||
import { CadlConfig, CadlRawConfig } from "./types.js";
|
||||
|
||||
export const CadlConfigFilename = "cadl-project.yaml";
|
||||
|
||||
export const defaultConfig: CadlConfig = deepFreeze({
|
||||
export const defaultConfig = deepFreeze({
|
||||
outputDir: "{cwd}/cadl-output",
|
||||
diagnostics: [],
|
||||
emitters: {},
|
||||
});
|
||||
|
@ -16,7 +18,6 @@ export const defaultConfig: CadlConfig = deepFreeze({
|
|||
/**
|
||||
* Look for the project root by looking up until a `cadl-project.yaml` is found.
|
||||
* @param path Path to start looking
|
||||
* @param lookIn
|
||||
*/
|
||||
export async function findCadlConfigPath(
|
||||
host: CompilerHost,
|
||||
|
@ -53,7 +54,7 @@ export async function loadCadlConfigForPath(
|
|||
): Promise<CadlConfig> {
|
||||
const cadlConfigPath = await findCadlConfigPath(host, directoryPath);
|
||||
if (cadlConfigPath === undefined) {
|
||||
return deepClone(defaultConfig);
|
||||
return { ...deepClone(defaultConfig), projectRoot: directoryPath };
|
||||
}
|
||||
return loadCadlConfigFile(host, cadlConfigPath);
|
||||
}
|
||||
|
@ -111,18 +112,62 @@ async function loadConfigFile(
|
|||
data = deepClone(defaultConfig);
|
||||
}
|
||||
|
||||
return cleanUndefined({
|
||||
let emitters: Record<string, Record<string, unknown>> | undefined = undefined;
|
||||
if (data.emitters) {
|
||||
emitters = {};
|
||||
for (const [name, options] of Object.entries(data.emitters)) {
|
||||
if (options === true) {
|
||||
emitters[name] = {};
|
||||
} else if (options === false) {
|
||||
} else {
|
||||
emitters[name] = options;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return omitUndefined({
|
||||
projectRoot: getDirectoryPath(filename),
|
||||
filename,
|
||||
diagnostics,
|
||||
outputDir: data["output-dir"],
|
||||
extends: data.extends,
|
||||
environmentVariables: data["environment-variables"],
|
||||
parameters: data.parameters,
|
||||
outputDir: data["output-dir"] ?? "{cwd}/cadl-output",
|
||||
warnAsError: data["warn-as-error"],
|
||||
imports: data.imports,
|
||||
extends: data.extends,
|
||||
trace: typeof data.trace === "string" ? [data.trace] : data.trace,
|
||||
emitters: data.emitters!,
|
||||
emitters: emitters!,
|
||||
});
|
||||
}
|
||||
|
||||
function cleanUndefined<T extends Record<string, unknown>>(data: T): T {
|
||||
return Object.fromEntries(Object.entries(data).filter(([k, v]) => v !== undefined)) as any;
|
||||
export function validateConfigPathsAbsolute(config: CadlConfig): readonly Diagnostic[] {
|
||||
const diagnostics: Diagnostic[] = [];
|
||||
|
||||
function checkPath(value: string | undefined) {
|
||||
if (value === undefined) {
|
||||
return;
|
||||
}
|
||||
const diagnostic = validatePathAbsolute(value);
|
||||
if (diagnostic) {
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
checkPath(config.outputDir);
|
||||
for (const emitterOptions of Object.values(config.emitters)) {
|
||||
checkPath(emitterOptions["emitter-output-dir"]);
|
||||
}
|
||||
return diagnostics;
|
||||
}
|
||||
|
||||
function validatePathAbsolute(path: string): Diagnostic | undefined {
|
||||
if (path.startsWith(".") || !isPathAbsolute(path)) {
|
||||
return createDiagnostic({
|
||||
code: "config-path-absolute",
|
||||
format: { path },
|
||||
target: NoTarget,
|
||||
});
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,14 @@
|
|||
import { JSONSchemaType } from "ajv";
|
||||
import { CadlRawConfig } from "./types.js";
|
||||
import { CadlRawConfig, EmitterOptions } from "./types.js";
|
||||
|
||||
const emitterOptionsSchema: JSONSchemaType<EmitterOptions> = {
|
||||
type: "object",
|
||||
additionalProperties: true,
|
||||
required: [],
|
||||
properties: {
|
||||
"emitter-output-dir": { type: "string", nullable: true } as any,
|
||||
},
|
||||
};
|
||||
|
||||
export const CadlConfigJsonSchema: JSONSchemaType<CadlRawConfig> = {
|
||||
type: "object",
|
||||
|
@ -9,6 +18,31 @@ export const CadlConfigJsonSchema: JSONSchemaType<CadlRawConfig> = {
|
|||
type: "string",
|
||||
nullable: true,
|
||||
},
|
||||
"environment-variables": {
|
||||
type: "object",
|
||||
nullable: true,
|
||||
required: [],
|
||||
additionalProperties: {
|
||||
type: "object",
|
||||
properties: {
|
||||
default: { type: "string" },
|
||||
},
|
||||
required: ["default"],
|
||||
},
|
||||
},
|
||||
parameters: {
|
||||
type: "object",
|
||||
nullable: true,
|
||||
required: [],
|
||||
additionalProperties: {
|
||||
type: "object",
|
||||
properties: {
|
||||
default: { type: "string" },
|
||||
},
|
||||
required: ["default"],
|
||||
},
|
||||
},
|
||||
|
||||
"output-dir": {
|
||||
type: "string",
|
||||
nullable: true,
|
||||
|
@ -36,7 +70,7 @@ export const CadlConfigJsonSchema: JSONSchemaType<CadlRawConfig> = {
|
|||
nullable: true,
|
||||
required: [],
|
||||
additionalProperties: {
|
||||
oneOf: [{ type: "boolean" }, { type: "object" }],
|
||||
oneOf: [{ type: "boolean" }, emitterOptionsSchema],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -4,6 +4,11 @@ import { Diagnostic } from "../core";
|
|||
* Represent the normalized user configuration.
|
||||
*/
|
||||
export interface CadlConfig {
|
||||
/**
|
||||
* Project root.
|
||||
*/
|
||||
projectRoot: string;
|
||||
|
||||
/**
|
||||
* Path to the config file used to create this configuration.
|
||||
*/
|
||||
|
@ -19,14 +24,25 @@ export interface CadlConfig {
|
|||
*/
|
||||
extends?: string;
|
||||
|
||||
/**
|
||||
* Environment variables configuration
|
||||
*/
|
||||
environmentVariables?: Record<string, ConfigEnvironmentVariable>;
|
||||
|
||||
/**
|
||||
* Parameters that can be used
|
||||
*/
|
||||
parameters?: Record<string, ConfigParameter>;
|
||||
|
||||
/**
|
||||
* Treat warning as error.
|
||||
*/
|
||||
warnAsError?: boolean;
|
||||
|
||||
/**
|
||||
* Output directory
|
||||
*/
|
||||
outputDir?: string;
|
||||
outputDir: string;
|
||||
|
||||
/**
|
||||
* Trace options.
|
||||
|
@ -41,19 +57,32 @@ export interface CadlConfig {
|
|||
/**
|
||||
* Emitter configuration
|
||||
*/
|
||||
emitters: Record<string, boolean | Record<string, unknown>>;
|
||||
emitters: Record<string, EmitterOptions>;
|
||||
}
|
||||
|
||||
export type RuleValue = "on" | "off" | Record<string, unknown>;
|
||||
|
||||
/**
|
||||
* Represent the configuration that can be provided in a config file.
|
||||
*/
|
||||
export interface CadlRawConfig {
|
||||
extends?: string;
|
||||
"environment-variables"?: Record<string, ConfigEnvironmentVariable>;
|
||||
parameters?: Record<string, ConfigParameter>;
|
||||
|
||||
"warn-as-error"?: boolean;
|
||||
"output-dir"?: string;
|
||||
trace?: string | string[];
|
||||
imports?: string[];
|
||||
emitters?: Record<string, boolean | Record<string, unknown>>;
|
||||
emitters?: Record<string, boolean | EmitterOptions>;
|
||||
}
|
||||
|
||||
export interface ConfigEnvironmentVariable {
|
||||
default: string;
|
||||
}
|
||||
|
||||
export interface ConfigParameter {
|
||||
default: string;
|
||||
}
|
||||
|
||||
export type EmitterOptions = Record<string, unknown> & {
|
||||
"emitter-output-dir"?: string;
|
||||
};
|
||||
|
|
|
@ -0,0 +1,162 @@
|
|||
import { expandConfigVariables } from "../../config/config-interpolation.js";
|
||||
import { loadCadlConfigForPath, validateConfigPathsAbsolute } from "../../config/config-loader.js";
|
||||
import { CadlConfig } from "../../config/types.js";
|
||||
import { createDiagnosticCollector } from "../index.js";
|
||||
import { CompilerOptions } from "../options.js";
|
||||
import { resolvePath } from "../path-utils.js";
|
||||
import { CompilerHost, Diagnostic } from "../types.js";
|
||||
import { omitUndefined } from "../util.js";
|
||||
|
||||
export interface CompileCliArgs {
|
||||
"output-dir"?: string;
|
||||
"output-path"?: string;
|
||||
nostdlib?: boolean;
|
||||
options?: string[];
|
||||
import?: string[];
|
||||
watch?: boolean;
|
||||
emit?: string[];
|
||||
trace?: string[];
|
||||
debug?: boolean;
|
||||
"warn-as-error"?: boolean;
|
||||
"no-emit"?: boolean;
|
||||
args?: string[];
|
||||
}
|
||||
|
||||
export async function getCompilerOptions(
|
||||
host: CompilerHost,
|
||||
cwd: string,
|
||||
args: CompileCliArgs,
|
||||
env: Record<string, string | undefined>
|
||||
): Promise<[CompilerOptions | undefined, readonly Diagnostic[]]> {
|
||||
const diagnostics = createDiagnosticCollector();
|
||||
const pathArg = args["output-dir"] ?? args["output-path"];
|
||||
|
||||
const config = await loadCadlConfigForPath(host, cwd);
|
||||
if (config.diagnostics.length > 0) {
|
||||
if (config.diagnostics.some((d) => d.severity === "error")) {
|
||||
return [undefined, config.diagnostics];
|
||||
}
|
||||
config.diagnostics.forEach((x) => diagnostics.add(x));
|
||||
}
|
||||
|
||||
const cliOptions = resolveOptions(args);
|
||||
|
||||
const configWithCliArgs: CadlConfig = {
|
||||
...config,
|
||||
outputDir: config.outputDir,
|
||||
imports: args["import"] ?? config["imports"],
|
||||
warnAsError: args["warn-as-error"] ?? config.warnAsError,
|
||||
trace: args.trace ?? config.trace,
|
||||
emitters: resolveEmitters(config, cliOptions, args),
|
||||
};
|
||||
const cliOutputDir = pathArg ? resolvePath(cwd, pathArg) : undefined;
|
||||
|
||||
const expandedConfig = diagnostics.pipe(
|
||||
expandConfigVariables(configWithCliArgs, {
|
||||
cwd: cwd,
|
||||
outputDir: cliOutputDir,
|
||||
env,
|
||||
args: resolveConfigArgs(args),
|
||||
})
|
||||
);
|
||||
validateConfigPathsAbsolute(expandedConfig).forEach((x) => diagnostics.add(x));
|
||||
|
||||
const options: CompilerOptions = omitUndefined({
|
||||
nostdlib: args["nostdlib"],
|
||||
watchForChanges: args["watch"],
|
||||
noEmit: args["no-emit"],
|
||||
miscOptions: cliOptions.miscOptions,
|
||||
|
||||
outputDir: expandedConfig.outputDir,
|
||||
additionalImports: expandedConfig["imports"],
|
||||
warningAsError: expandedConfig.warnAsError,
|
||||
trace: expandedConfig.trace,
|
||||
emitters: expandedConfig.emitters,
|
||||
});
|
||||
return diagnostics.wrap(options);
|
||||
}
|
||||
|
||||
function resolveConfigArgs(args: CompileCliArgs): Record<string, string> {
|
||||
const map: Record<string, string> = {};
|
||||
for (const arg of args.args ?? []) {
|
||||
const optionParts = arg.split("=");
|
||||
if (optionParts.length !== 2) {
|
||||
throw new Error(`The --arg parameter value "${arg}" must be in the format: arg-name=value`);
|
||||
}
|
||||
|
||||
map[optionParts[0]] = optionParts[1];
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
function resolveOptions(
|
||||
args: CompileCliArgs
|
||||
): Record<string | "miscOptions", Record<string, unknown>> {
|
||||
const options: Record<string, Record<string, string>> = {};
|
||||
for (const option of args.options ?? []) {
|
||||
const optionParts = option.split("=");
|
||||
if (optionParts.length !== 2) {
|
||||
throw new Error(
|
||||
`The --option parameter value "${option}" must be in the format: <emitterName>.some-options=value`
|
||||
);
|
||||
}
|
||||
const optionKeyParts = optionParts[0].split(".");
|
||||
if (optionKeyParts.length === 1) {
|
||||
const key = optionKeyParts[0];
|
||||
if (!("miscOptions" in options)) {
|
||||
options.miscOptions = {};
|
||||
}
|
||||
options.miscOptions[key] = optionParts[1];
|
||||
} else if (optionKeyParts.length > 2) {
|
||||
throw new Error(
|
||||
`The --option parameter value "${option}" must be in the format: <emitterName>.some-options=value`
|
||||
);
|
||||
}
|
||||
const emitterName = optionKeyParts[0];
|
||||
const key = optionKeyParts[1];
|
||||
if (!(emitterName in options)) {
|
||||
options[emitterName] = {};
|
||||
}
|
||||
options[emitterName][key] = optionParts[1];
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
function resolveEmitters(
|
||||
config: CadlConfig,
|
||||
options: Record<string | "miscOptions", Record<string, unknown>>,
|
||||
args: CompileCliArgs
|
||||
): Record<string, Record<string, unknown>> {
|
||||
const emitters = resolveSelectedEmittersFromConfig(config, args.emit);
|
||||
|
||||
const configuredEmitters: Record<string, Record<string, unknown>> = {};
|
||||
|
||||
for (const [emitterName, emitterConfig] of Object.entries(emitters)) {
|
||||
const cliOptionOverride = options[emitterName];
|
||||
|
||||
if (cliOptionOverride) {
|
||||
configuredEmitters[emitterName] = {
|
||||
...emitterConfig,
|
||||
...cliOptionOverride,
|
||||
};
|
||||
} else {
|
||||
configuredEmitters[emitterName] = emitterConfig;
|
||||
}
|
||||
}
|
||||
|
||||
return configuredEmitters;
|
||||
}
|
||||
|
||||
function resolveSelectedEmittersFromConfig(
|
||||
config: CadlConfig,
|
||||
selectedEmitters: string[] | undefined
|
||||
): Record<string, Record<string, unknown>> {
|
||||
if (selectedEmitters) {
|
||||
const emitters: Record<string, Record<string, unknown>> = {};
|
||||
for (const emitter of selectedEmitters) {
|
||||
emitters[emitter] = config.emitters[emitter] ?? {};
|
||||
}
|
||||
return emitters;
|
||||
}
|
||||
return config.emitters;
|
||||
}
|
|
@ -9,25 +9,25 @@ try {
|
|||
/* eslint-disable no-console */
|
||||
import { spawnSync, SpawnSyncOptionsWithStringEncoding } from "child_process";
|
||||
import { mkdtemp, readdir, rm } from "fs/promises";
|
||||
import mkdirp from "mkdirp";
|
||||
import watch from "node-watch";
|
||||
import os from "os";
|
||||
import { resolve } from "path";
|
||||
import prompts from "prompts";
|
||||
import url from "url";
|
||||
import yargs from "yargs";
|
||||
import { CadlConfig, loadCadlConfigForPath } from "../config/index.js";
|
||||
import { CompilerOptions } from "../core/options.js";
|
||||
import { compile, Program } from "../core/program.js";
|
||||
import { initCadlProject } from "../init/index.js";
|
||||
import { compilerAssert, logDiagnostics } from "./diagnostics.js";
|
||||
import { findUnformattedCadlFiles, formatCadlFiles } from "./formatter-fs.js";
|
||||
import { installCadlDependencies } from "./install.js";
|
||||
import { createConsoleSink } from "./logger/index.js";
|
||||
import { NodeHost } from "./node-host.js";
|
||||
import { getAnyExtensionFromPath, getBaseFileName, joinPaths, resolvePath } from "./path-utils.js";
|
||||
import { CompilerHost, Diagnostic } from "./types.js";
|
||||
import { cadlVersion, ExternalError } from "./util.js";
|
||||
import { loadCadlConfigForPath } from "../../config/index.js";
|
||||
import { initCadlProject } from "../../init/index.js";
|
||||
import { compilerAssert, logDiagnostics } from "../diagnostics.js";
|
||||
import { findUnformattedCadlFiles, formatCadlFiles } from "../formatter-fs.js";
|
||||
import { installCadlDependencies } from "../install.js";
|
||||
import { createConsoleSink } from "../logger/index.js";
|
||||
import { NodeHost } from "../node-host.js";
|
||||
import { CompilerOptions } from "../options.js";
|
||||
import { getAnyExtensionFromPath, getBaseFileName, joinPaths } from "../path-utils.js";
|
||||
import { compile, Program } from "../program.js";
|
||||
import { CompilerHost, Diagnostic } from "../types.js";
|
||||
import { cadlVersion, ExternalError } from "../util.js";
|
||||
import { CompileCliArgs, getCompilerOptions } from "./args.js";
|
||||
|
||||
async function main() {
|
||||
console.log(`Cadl compiler v${cadlVersion}\n`);
|
||||
|
@ -113,11 +113,16 @@ async function main() {
|
|||
type: "boolean",
|
||||
default: false,
|
||||
describe: "Run emitters but do not emit any output.",
|
||||
})
|
||||
.option("arg", {
|
||||
type: "array",
|
||||
string: true,
|
||||
describe: "Key/value of arguments that are used in the configuration.",
|
||||
});
|
||||
},
|
||||
async (args) => {
|
||||
const host = createCLICompilerHost(args);
|
||||
const cliOptions = await getCompilerOptions(host, args);
|
||||
const cliOptions = await getCompilerOptionsOrExit(host, args);
|
||||
|
||||
const program = await compileInput(host, args.path, cliOptions);
|
||||
if (program.hasError()) {
|
||||
|
@ -332,125 +337,20 @@ function createCLICompilerHost(args: { pretty?: boolean }): CompilerHost {
|
|||
return { ...NodeHost, logSink: createConsoleSink({ pretty: args.pretty }) };
|
||||
}
|
||||
|
||||
interface CompileCliArgs {
|
||||
"output-dir"?: string;
|
||||
"output-path"?: string;
|
||||
nostdlib?: boolean;
|
||||
options?: string[];
|
||||
import?: string[];
|
||||
watch?: boolean;
|
||||
emit?: string[];
|
||||
trace?: string[];
|
||||
debug?: boolean;
|
||||
"warn-as-error"?: boolean;
|
||||
"no-emit"?: boolean;
|
||||
}
|
||||
|
||||
async function getCompilerOptions(
|
||||
async function getCompilerOptionsOrExit(
|
||||
host: CompilerHost,
|
||||
args: CompileCliArgs
|
||||
): Promise<CompilerOptions> {
|
||||
const config = await loadCadlConfigForPath(host, process.cwd());
|
||||
|
||||
if (config.diagnostics.length > 0) {
|
||||
logDiagnostics(config.diagnostics, host.logSink);
|
||||
logDiagnosticCount(config.diagnostics);
|
||||
if (config.diagnostics.some((d) => d.severity === "error")) {
|
||||
process.exit(1);
|
||||
}
|
||||
const [options, diagnostics] = await getCompilerOptions(host, process.cwd(), args, process.env);
|
||||
if (options === undefined) {
|
||||
logDiagnostics(diagnostics, host.logSink);
|
||||
logDiagnosticCount(diagnostics);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const pathArg = args["output-dir"] ?? args["output-path"] ?? config.outputDir ?? "./cadl-output";
|
||||
const outputPath = resolvePath(process.cwd(), pathArg);
|
||||
await mkdirp(outputPath);
|
||||
|
||||
const cliOptions = resolveOptions(args);
|
||||
|
||||
return {
|
||||
outputDir: outputPath,
|
||||
nostdlib: args["nostdlib"],
|
||||
additionalImports: args["import"] ?? config["imports"],
|
||||
watchForChanges: args["watch"],
|
||||
warningAsError: args["warn-as-error"] ?? config.warnAsError,
|
||||
noEmit: args["no-emit"],
|
||||
miscOptions: cliOptions.miscOptions,
|
||||
trace: args.trace ?? config.trace,
|
||||
emitters: resolveEmitters(config, cliOptions, args),
|
||||
};
|
||||
}
|
||||
|
||||
function resolveOptions(
|
||||
args: CompileCliArgs
|
||||
): Record<string | "miscOptions", Record<string, unknown>> {
|
||||
const options: Record<string, Record<string, string>> = {};
|
||||
for (const option of args.options ?? []) {
|
||||
const optionParts = option.split("=");
|
||||
if (optionParts.length !== 2) {
|
||||
throw new Error(
|
||||
`The --option parameter value "${option}" must be in the format: <emitterName>.some-options=value`
|
||||
);
|
||||
}
|
||||
const optionKeyParts = optionParts[0].split(".");
|
||||
if (optionKeyParts.length === 1) {
|
||||
const key = optionKeyParts[0];
|
||||
if (!("miscOptions" in options)) {
|
||||
options.miscOptions = {};
|
||||
}
|
||||
options.miscOptions[key] = optionParts[1];
|
||||
} else if (optionKeyParts.length > 2) {
|
||||
throw new Error(
|
||||
`The --option parameter value "${option}" must be in the format: <emitterName>.some-options=value`
|
||||
);
|
||||
}
|
||||
const emitterName = optionKeyParts[0];
|
||||
const key = optionKeyParts[1];
|
||||
if (!(emitterName in options)) {
|
||||
options[emitterName] = {};
|
||||
}
|
||||
options[emitterName][key] = optionParts[1];
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
function resolveEmitters(
|
||||
config: CadlConfig,
|
||||
options: Record<string | "miscOptions", Record<string, unknown>>,
|
||||
args: CompileCliArgs
|
||||
): Record<string, Record<string, unknown> | boolean> {
|
||||
const emitters = resolveSelectedEmittersFromConfig(config, args.emit);
|
||||
|
||||
const configuredEmitters: Record<string, Record<string, unknown> | boolean> = {};
|
||||
|
||||
for (const [emitterName, emitterConfig] of Object.entries(emitters)) {
|
||||
const cliOptionOverride = options[emitterName];
|
||||
|
||||
if (cliOptionOverride) {
|
||||
configuredEmitters[emitterName] = {
|
||||
...(emitterConfig === true ? {} : emitterConfig),
|
||||
...cliOptionOverride,
|
||||
};
|
||||
} else {
|
||||
configuredEmitters[emitterName] = emitterConfig;
|
||||
}
|
||||
}
|
||||
|
||||
return configuredEmitters;
|
||||
}
|
||||
|
||||
function resolveSelectedEmittersFromConfig(
|
||||
config: CadlConfig,
|
||||
selectedEmitters: string[] | undefined
|
||||
): Record<string, Record<string, unknown> | boolean> {
|
||||
if (selectedEmitters) {
|
||||
const emitters: Record<string, Record<string, unknown> | boolean> = {};
|
||||
for (const emitter of selectedEmitters) {
|
||||
emitters[emitter] = config.emitters[emitter] ?? true;
|
||||
}
|
||||
return emitters;
|
||||
}
|
||||
return config.emitters;
|
||||
}
|
||||
|
||||
async function installVsix(pkg: string, install: (vsixPaths: string[]) => void, debug: boolean) {
|
||||
// download npm package to temporary directory
|
||||
const temp = await mkdtemp(joinPaths(os.tmpdir(), "cadl"));
|
|
@ -0,0 +1 @@
|
|||
export * from "./args.js";
|
|
@ -446,6 +446,27 @@ const diagnostics = {
|
|||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Configuration
|
||||
*/
|
||||
"config-invalid-argument": {
|
||||
severity: "error",
|
||||
messages: {
|
||||
default: paramMessage`Argument "${"name"}" is not defined as a parameter in the config.`,
|
||||
},
|
||||
},
|
||||
"config-circular-variable": {
|
||||
severity: "error",
|
||||
messages: {
|
||||
default: paramMessage`There is a circular reference to variable "${"name"}" in the cli configuration or arguments.`,
|
||||
},
|
||||
},
|
||||
"config-path-absolute": {
|
||||
severity: "error",
|
||||
messages: {
|
||||
default: paramMessage`Path "${"path"}" cannot be relative. Use {cwd} or {project-root} to specify what the path should be relative to.`,
|
||||
},
|
||||
},
|
||||
/**
|
||||
* Program
|
||||
*/
|
||||
|
|
|
@ -13,7 +13,8 @@ export interface CompilerOptions {
|
|||
* @deprecated use outputDir.
|
||||
*/
|
||||
outputPath?: string;
|
||||
emitters?: Record<string, Record<string, unknown> | boolean>;
|
||||
|
||||
emitters?: Record<string, Record<string, unknown>>;
|
||||
nostdlib?: boolean;
|
||||
noEmit?: boolean;
|
||||
additionalImports?: string[];
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { EmitterOptions } from "../config/types.js";
|
||||
import { createBinder } from "./binder.js";
|
||||
import { Checker, createChecker } from "./checker.js";
|
||||
import { compilerAssert, createSourceFile } from "./diagnostics.js";
|
||||
|
@ -25,8 +26,8 @@ import {
|
|||
DiagnosticTarget,
|
||||
Directive,
|
||||
DirectiveExpressionNode,
|
||||
EmitContext,
|
||||
EmitterFunc,
|
||||
EmitterOptions,
|
||||
JsSourceFileNode,
|
||||
LiteralType,
|
||||
Namespace,
|
||||
|
@ -48,6 +49,7 @@ import {
|
|||
doIO,
|
||||
ExternalError,
|
||||
findProjectRoot,
|
||||
isDefined,
|
||||
loadFile,
|
||||
mapEquals,
|
||||
mutate,
|
||||
|
@ -115,7 +117,8 @@ interface EmitterRef {
|
|||
emitFunction: EmitterFunc;
|
||||
main: string;
|
||||
metadata: LibraryMetadata;
|
||||
options: EmitterOptions;
|
||||
emitterOutputDir: string;
|
||||
options: Record<string, unknown>;
|
||||
}
|
||||
|
||||
class StateMap extends Map<undefined | Projector, Map<Type, unknown>> {}
|
||||
|
@ -596,7 +599,7 @@ export async function compile(
|
|||
}
|
||||
}
|
||||
|
||||
async function loadEmitters(mainFile: string, emitters: Record<string, Record<string, unknown>>) {
|
||||
async function loadEmitters(mainFile: string, emitters: Record<string, EmitterOptions>) {
|
||||
for (const [emitterPackage, options] of Object.entries(emitters)) {
|
||||
await loadEmitter(mainFile, emitterPackage, options);
|
||||
}
|
||||
|
@ -605,7 +608,7 @@ export async function compile(
|
|||
async function loadEmitter(
|
||||
mainFile: string,
|
||||
emitterPackage: string,
|
||||
options: Record<string, unknown>
|
||||
emitterOptionsInput: EmitterOptions
|
||||
) {
|
||||
const basedir = getDirectoryPath(mainFile);
|
||||
// attempt to resolve a node module with this name
|
||||
|
@ -630,6 +633,12 @@ export async function compile(
|
|||
|
||||
const emitFunction = file.esmExports.$onEmit;
|
||||
const libDefinition: CadlLibrary<any> | undefined = file.esmExports.$lib;
|
||||
const metadata = computeLibraryMetadata(module);
|
||||
|
||||
let { "emitter-output-dir": emitterOutputDir, ...emitterOptions } = emitterOptionsInput;
|
||||
if (emitterOutputDir === undefined) {
|
||||
emitterOutputDir = [options.outputDir, metadata.name].filter(isDefined).join("/");
|
||||
}
|
||||
if (libDefinition?.requireImports) {
|
||||
for (const lib of libDefinition.requireImports) {
|
||||
requireImports.set(lib, libDefinition.name);
|
||||
|
@ -637,7 +646,10 @@ export async function compile(
|
|||
}
|
||||
if (emitFunction !== undefined) {
|
||||
if (libDefinition?.emitter?.options) {
|
||||
const diagnostics = libDefinition?.emitterOptionValidator?.validate(options, NoTarget);
|
||||
const diagnostics = libDefinition?.emitterOptionValidator?.validate(
|
||||
emitterOptions,
|
||||
NoTarget
|
||||
);
|
||||
if (diagnostics && diagnostics.length > 0) {
|
||||
program.reportDiagnostics(diagnostics);
|
||||
return;
|
||||
|
@ -646,8 +658,9 @@ export async function compile(
|
|||
emitters.push({
|
||||
main: entrypoint,
|
||||
emitFunction,
|
||||
metadata: computeLibraryMetadata(module),
|
||||
options,
|
||||
metadata,
|
||||
emitterOutputDir,
|
||||
options: emitterOptions,
|
||||
});
|
||||
} else {
|
||||
program.reportDiagnostic(
|
||||
|
@ -683,8 +696,13 @@ export async function compile(
|
|||
* @param emitter Emitter ref to run
|
||||
*/
|
||||
async function runEmitter(emitter: EmitterRef) {
|
||||
const context: EmitContext<any> = {
|
||||
program,
|
||||
emitterOutputDir: emitter.emitterOutputDir,
|
||||
options: emitter.options,
|
||||
};
|
||||
try {
|
||||
await emitter.emitFunction(program, emitter.options);
|
||||
await emitter.emitFunction(context);
|
||||
} catch (error: any) {
|
||||
const msg = [`Emitter "${emitter.metadata.name ?? emitter.main}" failed!`];
|
||||
if (emitter.metadata.bugs?.url) {
|
||||
|
|
|
@ -1400,8 +1400,7 @@ export interface JsSourceFileNode extends DeclarationNode, BaseNode {
|
|||
readonly namespaceSymbols: Sym[];
|
||||
}
|
||||
|
||||
export type EmitterOptions = { name?: string } & Record<string, any>;
|
||||
export type EmitterFunc = (program: Program, options: EmitterOptions) => Promise<void> | void;
|
||||
export type EmitterFunc = (context: EmitContext) => Promise<void> | void;
|
||||
|
||||
export interface SourceFile {
|
||||
/** The source code text. */
|
||||
|
@ -1753,6 +1752,23 @@ export interface DecoratorContext {
|
|||
): R;
|
||||
}
|
||||
|
||||
export interface EmitContext<TOptions extends object = Record<string, never>> {
|
||||
/**
|
||||
* Cadl Program.
|
||||
*/
|
||||
program: Program;
|
||||
|
||||
/**
|
||||
* Configured output dir for the emitter. Emitter should emit all output under that directory.
|
||||
*/
|
||||
emitterOutputDir: string;
|
||||
|
||||
/**
|
||||
* Emitter custom options defined in createCadlLibrary
|
||||
*/
|
||||
options: TOptions;
|
||||
}
|
||||
|
||||
export type LogLevel = "trace" | "warning" | "error";
|
||||
|
||||
export interface LogInfo {
|
||||
|
|
|
@ -252,6 +252,13 @@ export function isDefined<T>(arg: T | undefined): arg is T {
|
|||
return arg !== undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove undefined properties from object.
|
||||
*/
|
||||
export function omitUndefined<T extends Record<string, unknown>>(data: T): T {
|
||||
return Object.fromEntries(Object.entries(data).filter(([k, v]) => v !== undefined)) as any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Look for the project root by looking up until a `package.json` is found.
|
||||
* @param path Path to start looking
|
||||
|
|
|
@ -476,7 +476,7 @@ export function createServer(host: ServerHost): Server {
|
|||
async function getConfig(mainFile: string, path: string): Promise<CadlConfig> {
|
||||
const configPath = await findCadlConfigPath(compilerHost, mainFile);
|
||||
if (!configPath) {
|
||||
return defaultConfig;
|
||||
return { ...defaultConfig, projectRoot: getDirectoryPath(mainFile) };
|
||||
}
|
||||
|
||||
const cached = await fileSystemCache.get(configPath);
|
||||
|
|
|
@ -0,0 +1,142 @@
|
|||
import { deepStrictEqual, strictEqual } from "assert";
|
||||
import { dump } from "js-yaml";
|
||||
import { CompileCliArgs, getCompilerOptions } from "../core/cli/args.js";
|
||||
import {
|
||||
createTestHost,
|
||||
expectDiagnosticEmpty,
|
||||
expectDiagnostics,
|
||||
resolveVirtualPath,
|
||||
TestHost,
|
||||
} from "../testing/index.js";
|
||||
|
||||
describe("compiler: cli", () => {
|
||||
let host: TestHost;
|
||||
|
||||
const cwd = resolveVirtualPath("ws");
|
||||
|
||||
beforeEach(async () => {
|
||||
host = await createTestHost();
|
||||
host.addCadlFile("ws/main.cadl", "");
|
||||
});
|
||||
|
||||
describe("resolving compiler options", () => {
|
||||
async function resolveCompilerOptions(args: CompileCliArgs, env: Record<string, string> = {}) {
|
||||
const [options, diagnostics] = await getCompilerOptions(host.compilerHost, cwd, args, env);
|
||||
expectDiagnosticEmpty(diagnostics);
|
||||
return options;
|
||||
}
|
||||
|
||||
it("no args and config: return empty options with output-dir at {cwd}/cadl-output", async () => {
|
||||
const options = await resolveCompilerOptions({});
|
||||
deepStrictEqual(options, {
|
||||
emitters: {},
|
||||
outputDir: `${cwd}/cadl-output`,
|
||||
});
|
||||
});
|
||||
|
||||
context("config file with emitters", () => {
|
||||
beforeEach(() => {
|
||||
host.addCadlFile(
|
||||
"ws/cadl-project.yaml",
|
||||
dump({
|
||||
parameters: {
|
||||
"custom-arg": {
|
||||
default: "/default-arg-value",
|
||||
},
|
||||
},
|
||||
emitters: {
|
||||
"@cadl-lang/openapi3": {
|
||||
"emitter-output-dir": "{output-dir}/custom",
|
||||
},
|
||||
"@cadl-lang/with-args": {
|
||||
"emitter-output-dir": "{custom-arg}/custom",
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("interpolate default output-dir in emitter output-dir", async () => {
|
||||
const options = await resolveCompilerOptions({});
|
||||
|
||||
strictEqual(
|
||||
options?.emitters?.["@cadl-lang/openapi3"]?.["emitter-output-dir"],
|
||||
`${cwd}/cadl-output/custom`
|
||||
);
|
||||
});
|
||||
|
||||
it("override output-dir from cli args", async () => {
|
||||
const options = await resolveCompilerOptions({ "output-dir": `${cwd}/my-output-dir` });
|
||||
|
||||
strictEqual(
|
||||
options?.emitters?.["@cadl-lang/openapi3"]?.["emitter-output-dir"],
|
||||
`${cwd}/my-output-dir/custom`
|
||||
);
|
||||
});
|
||||
|
||||
it("override emitter-output-dir from cli args", async () => {
|
||||
const options = await resolveCompilerOptions({
|
||||
options: [`@cadl-lang/openapi3.emitter-output-dir={cwd}/relative-to-cwd`],
|
||||
});
|
||||
|
||||
strictEqual(
|
||||
options?.emitters?.["@cadl-lang/openapi3"]?.["emitter-output-dir"],
|
||||
`${cwd}/relative-to-cwd`
|
||||
);
|
||||
});
|
||||
|
||||
describe("arg interpolation", () => {
|
||||
it("use default arg value", async () => {
|
||||
const options = await resolveCompilerOptions({});
|
||||
|
||||
strictEqual(
|
||||
options?.emitters?.["@cadl-lang/with-args"]?.["emitter-output-dir"],
|
||||
`/default-arg-value/custom`
|
||||
);
|
||||
});
|
||||
|
||||
it("passing --arg interpolate args in the cli", async () => {
|
||||
const options = await resolveCompilerOptions({
|
||||
args: [`custom-arg=/my-updated-arg-value`],
|
||||
});
|
||||
|
||||
strictEqual(
|
||||
options?.emitters?.["@cadl-lang/with-args"]?.["emitter-output-dir"],
|
||||
`/my-updated-arg-value/custom`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("emit diagnostic if passing unknown parameter", async () => {
|
||||
const [_, diagnostics] = await getCompilerOptions(
|
||||
host.compilerHost,
|
||||
cwd,
|
||||
{
|
||||
args: ["not-defined-arg=my-value"],
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
expectDiagnostics(diagnostics, {
|
||||
code: "config-invalid-argument",
|
||||
message: `Argument "not-defined-arg" is not defined as a parameter in the config.`,
|
||||
});
|
||||
});
|
||||
|
||||
it("emit diagnostic if using relative path in config paths", async () => {
|
||||
host.addCadlFile(
|
||||
"ws/cadl-project.yaml",
|
||||
dump({
|
||||
"output-dir": "./my-output",
|
||||
})
|
||||
);
|
||||
const [_, diagnostics] = await getCompilerOptions(host.compilerHost, cwd, {}, {});
|
||||
|
||||
expectDiagnostics(diagnostics, {
|
||||
code: "config-path-absolute",
|
||||
message: `Path "./my-output" cannot be relative. Use {cwd} or {project-root} to specify what the path should be relative to.`,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,316 @@
|
|||
import { deepStrictEqual } from "assert";
|
||||
import {
|
||||
ExpandConfigOptions,
|
||||
expandConfigVariables,
|
||||
resolveValues,
|
||||
} from "../../config/config-interpolation.js";
|
||||
import { defaultConfig, validateConfigPathsAbsolute } from "../../config/config-loader.js";
|
||||
import { CadlConfig } from "../../config/types.js";
|
||||
import { expectDiagnosticEmpty, expectDiagnostics } from "../../testing/index.js";
|
||||
|
||||
describe("compiler: config interpolation", () => {
|
||||
describe("resolveValues", () => {
|
||||
const commonVars = {
|
||||
"output-dir": "/test/output",
|
||||
env: {
|
||||
GITHUB_DIR: "/github",
|
||||
},
|
||||
};
|
||||
|
||||
function expectResolveValues(values: Record<string, string>): Record<string, string> {
|
||||
const [resolved, diagnostics] = resolveValues(values, commonVars);
|
||||
expectDiagnosticEmpty(diagnostics);
|
||||
return resolved;
|
||||
}
|
||||
|
||||
it("no-op if there is nothing to interpolate", () => {
|
||||
const resolved = expectResolveValues({
|
||||
one: "one",
|
||||
two: "two",
|
||||
});
|
||||
|
||||
deepStrictEqual(resolved, {
|
||||
one: "one",
|
||||
two: "two",
|
||||
});
|
||||
});
|
||||
|
||||
it("no-op if interpolate variable that doesn't exists", () => {
|
||||
const resolved = expectResolveValues({
|
||||
one: "{not-defined}/output",
|
||||
});
|
||||
|
||||
deepStrictEqual(resolved, {
|
||||
one: "{not-defined}/output",
|
||||
});
|
||||
});
|
||||
|
||||
it("interpolate variables from common vars", () => {
|
||||
const resolved = expectResolveValues({
|
||||
one: "{output-dir}/custom",
|
||||
});
|
||||
|
||||
deepStrictEqual(resolved, {
|
||||
one: "/test/output/custom",
|
||||
});
|
||||
});
|
||||
|
||||
it("interpolate variables from nested common vars", () => {
|
||||
const resolved = expectResolveValues({
|
||||
one: "{env.GITHUB_DIR}/custom",
|
||||
});
|
||||
|
||||
deepStrictEqual(resolved, {
|
||||
one: "/github/custom",
|
||||
});
|
||||
});
|
||||
|
||||
it("interpolate another variable", () => {
|
||||
const resolved = expectResolveValues({
|
||||
one: "{two}/one",
|
||||
two: "/two",
|
||||
});
|
||||
|
||||
deepStrictEqual(resolved, {
|
||||
one: "/two/one",
|
||||
two: "/two",
|
||||
});
|
||||
});
|
||||
|
||||
it("interpolate another variable also needing interpolation", () => {
|
||||
const resolved = expectResolveValues({
|
||||
three: "/three",
|
||||
one: "{two}/one",
|
||||
two: "{three}/two",
|
||||
});
|
||||
|
||||
deepStrictEqual(resolved, {
|
||||
one: "/three/two/one",
|
||||
two: "/three/two",
|
||||
three: "/three",
|
||||
});
|
||||
});
|
||||
|
||||
it("emit diagnostic if variable has circular references", () => {
|
||||
const [_, diagnostics] = resolveValues({
|
||||
three: "{one}/three",
|
||||
one: "{two}/one",
|
||||
two: "{three}/two",
|
||||
});
|
||||
|
||||
expectDiagnostics(diagnostics, {
|
||||
code: "config-circular-variable",
|
||||
message: `There is a circular reference to variable "three" in the cli configuration or arguments.`,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("expandConfigVariables", () => {
|
||||
function expectExpandConfigVariables(config: CadlConfig, options: ExpandConfigOptions) {
|
||||
const [resolved, diagnostics] = expandConfigVariables(config, options);
|
||||
expectDiagnosticEmpty(diagnostics);
|
||||
return resolved;
|
||||
}
|
||||
|
||||
it("expand {cwd}", () => {
|
||||
const config = {
|
||||
...defaultConfig,
|
||||
projectRoot: "/dev/ws",
|
||||
outputDir: "{cwd}/my-output",
|
||||
};
|
||||
const resolved = expectExpandConfigVariables(config, { cwd: "/dev/wd" });
|
||||
deepStrictEqual(resolved, {
|
||||
...config,
|
||||
outputDir: "/dev/wd/my-output",
|
||||
});
|
||||
});
|
||||
|
||||
it("expand {project-root}", () => {
|
||||
const config = {
|
||||
...defaultConfig,
|
||||
projectRoot: "/dev/ws",
|
||||
outputDir: "{project-root}/my-output",
|
||||
};
|
||||
const resolved = expectExpandConfigVariables(config, { cwd: "/dev/wd" });
|
||||
deepStrictEqual(resolved, {
|
||||
...config,
|
||||
outputDir: "/dev/ws/my-output",
|
||||
});
|
||||
});
|
||||
|
||||
describe("interpolating args", () => {
|
||||
const config: CadlConfig = {
|
||||
...defaultConfig,
|
||||
parameters: {
|
||||
"repo-dir": {
|
||||
default: "{cwd}",
|
||||
},
|
||||
},
|
||||
projectRoot: "/dev/ws",
|
||||
outputDir: "{repo-dir}/my-output",
|
||||
};
|
||||
|
||||
it("expand args using default value if not provided", () => {
|
||||
const resolved = expectExpandConfigVariables(config, {
|
||||
cwd: "/dev/wd",
|
||||
});
|
||||
deepStrictEqual(resolved, {
|
||||
...config,
|
||||
outputDir: "/dev/wd/my-output",
|
||||
});
|
||||
});
|
||||
|
||||
it("expand args with value passed", () => {
|
||||
const resolved = expectExpandConfigVariables(config, {
|
||||
cwd: "/dev/wd",
|
||||
args: { "repo-dir": "/github-dir" },
|
||||
});
|
||||
deepStrictEqual(resolved, {
|
||||
...config,
|
||||
outputDir: "/github-dir/my-output",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("interpolating env", () => {
|
||||
const config: CadlConfig = {
|
||||
...defaultConfig,
|
||||
environmentVariables: {
|
||||
REPO_DIR: {
|
||||
default: "{cwd}",
|
||||
},
|
||||
},
|
||||
projectRoot: "/dev/ws",
|
||||
outputDir: "{env.REPO_DIR}/my-output",
|
||||
};
|
||||
|
||||
it("expand args using default value if not provided", () => {
|
||||
const resolved = expectExpandConfigVariables(config, {
|
||||
cwd: "/dev/wd",
|
||||
});
|
||||
deepStrictEqual(resolved, {
|
||||
...config,
|
||||
outputDir: "/dev/wd/my-output",
|
||||
});
|
||||
});
|
||||
|
||||
it("expand env with value passed", () => {
|
||||
const resolved = expectExpandConfigVariables(config, {
|
||||
cwd: "/dev/wd",
|
||||
env: { REPO_DIR: "/github-dir" },
|
||||
});
|
||||
deepStrictEqual(resolved, {
|
||||
...config,
|
||||
outputDir: "/github-dir/my-output",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("expand {output-dir} in emitter options", () => {
|
||||
const config = {
|
||||
...defaultConfig,
|
||||
projectRoot: "/dev/ws",
|
||||
outputDir: "/my-custom-output-dir",
|
||||
emitters: {
|
||||
emitter1: {
|
||||
"emitter-output-dir": "{output-dir}/emitter1",
|
||||
},
|
||||
},
|
||||
};
|
||||
const resolved = expectExpandConfigVariables(config, { cwd: "/dev/wd" });
|
||||
deepStrictEqual(resolved, {
|
||||
...config,
|
||||
emitters: {
|
||||
emitter1: {
|
||||
"emitter-output-dir": "/my-custom-output-dir/emitter1",
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("emitter options can interpolate each other", () => {
|
||||
const config = {
|
||||
...defaultConfig,
|
||||
projectRoot: "/dev/ws",
|
||||
outputDir: "/my-custom-output-dir",
|
||||
emitters: {
|
||||
emitter1: {
|
||||
"emitter-output-dir": "{output-dir}/{emitter-folder}",
|
||||
"emitter-folder": "custom-1",
|
||||
},
|
||||
},
|
||||
};
|
||||
const resolved = expectExpandConfigVariables(config, { cwd: "/dev/wd" });
|
||||
deepStrictEqual(resolved, {
|
||||
...config,
|
||||
emitters: {
|
||||
emitter1: {
|
||||
"emitter-output-dir": "/my-custom-output-dir/custom-1",
|
||||
"emitter-folder": "custom-1",
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("validateConfigPathsAbsolute", () => {
|
||||
it("emit diagnostic for using a relative path starting with ./", () => {
|
||||
const config = {
|
||||
...defaultConfig,
|
||||
projectRoot: "/dev/ws",
|
||||
outputDir: "./my-output",
|
||||
};
|
||||
const diagnostics = validateConfigPathsAbsolute(config);
|
||||
expectDiagnostics(diagnostics, {
|
||||
code: "config-path-absolute",
|
||||
message: `Path "./my-output" cannot be relative. Use {cwd} or {project-root} to specify what the path should be relative to.`,
|
||||
});
|
||||
});
|
||||
|
||||
it("emit diagnostic for using a relative path starting with ../", () => {
|
||||
const config = {
|
||||
...defaultConfig,
|
||||
projectRoot: "/dev/ws",
|
||||
outputDir: "../my-output",
|
||||
};
|
||||
const diagnostics = validateConfigPathsAbsolute(config);
|
||||
expectDiagnostics(diagnostics, {
|
||||
code: "config-path-absolute",
|
||||
message: `Path "../my-output" cannot be relative. Use {cwd} or {project-root} to specify what the path should be relative to.`,
|
||||
});
|
||||
});
|
||||
|
||||
it("emit diagnostic for using a relative path", () => {
|
||||
const config = {
|
||||
...defaultConfig,
|
||||
projectRoot: "/dev/ws",
|
||||
outputDir: "my-output",
|
||||
};
|
||||
const diagnostics = validateConfigPathsAbsolute(config);
|
||||
expectDiagnostics(diagnostics, {
|
||||
code: "config-path-absolute",
|
||||
message: `Path "my-output" cannot be relative. Use {cwd} or {project-root} to specify what the path should be relative to.`,
|
||||
});
|
||||
});
|
||||
|
||||
it("succeed if using unix absolute path", () => {
|
||||
const config = {
|
||||
...defaultConfig,
|
||||
projectRoot: "/dev/ws",
|
||||
outputDir: "/my-output",
|
||||
};
|
||||
const diagnostics = validateConfigPathsAbsolute(config);
|
||||
expectDiagnosticEmpty(diagnostics);
|
||||
});
|
||||
|
||||
it("succeed if using windows absolute path", () => {
|
||||
const config = {
|
||||
...defaultConfig,
|
||||
projectRoot: "/dev/ws",
|
||||
outputDir: "C:/my-output",
|
||||
};
|
||||
const diagnostics = validateConfigPathsAbsolute(config);
|
||||
expectDiagnosticEmpty(diagnostics);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -14,7 +14,10 @@ describe("compiler: config file loading", () => {
|
|||
const scenarioRoot = resolve(__dirname, "../../../test/config/scenarios");
|
||||
const loadTestConfig = async (folderName: string) => {
|
||||
const folderPath = join(scenarioRoot, folderName);
|
||||
const { filename, ...config } = await loadCadlConfigForPath(NodeHost, folderPath);
|
||||
const { filename, projectRoot, ...config } = await loadCadlConfigForPath(
|
||||
NodeHost,
|
||||
folderPath
|
||||
);
|
||||
return config;
|
||||
};
|
||||
|
||||
|
@ -22,7 +25,8 @@ describe("compiler: config file loading", () => {
|
|||
const config = await loadTestConfig("simple");
|
||||
deepStrictEqual(config, {
|
||||
diagnostics: [],
|
||||
emitters: { openapi: true },
|
||||
outputDir: "{cwd}/cadl-output",
|
||||
emitters: { openapi: {} },
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -31,7 +35,8 @@ describe("compiler: config file loading", () => {
|
|||
deepStrictEqual(config, {
|
||||
diagnostics: [],
|
||||
extends: "./cadl-base.yaml",
|
||||
emitters: { openapi: true },
|
||||
outputDir: "{cwd}/cadl-output",
|
||||
emitters: { openapi: {} },
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -39,30 +44,33 @@ describe("compiler: config file loading", () => {
|
|||
const config = await loadTestConfig("empty");
|
||||
deepStrictEqual(config, {
|
||||
diagnostics: [],
|
||||
outputDir: "{cwd}/cadl-output",
|
||||
emitters: {},
|
||||
});
|
||||
});
|
||||
|
||||
it("deep clones defaults when not found", async () => {
|
||||
let config = await loadTestConfig("empty");
|
||||
config.emitters["x"] = true;
|
||||
config.emitters["x"] = {};
|
||||
|
||||
config = await loadTestConfig("empty");
|
||||
deepStrictEqual(config, {
|
||||
diagnostics: [],
|
||||
outputDir: "{cwd}/cadl-output",
|
||||
emitters: {},
|
||||
});
|
||||
});
|
||||
|
||||
it("deep clones defaults when found", async () => {
|
||||
let config = await loadTestConfig("simple");
|
||||
config.emitters["x"] = true;
|
||||
config.emitters["x"] = {};
|
||||
|
||||
config = await loadTestConfig("simple");
|
||||
deepStrictEqual(config, {
|
||||
diagnostics: [],
|
||||
outputDir: "{cwd}/cadl-output",
|
||||
emitters: {
|
||||
openapi: true,
|
||||
openapi: {},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
@ -100,7 +108,7 @@ describe("compiler: config file loading", () => {
|
|||
});
|
||||
|
||||
it("succeeds if config is valid", () => {
|
||||
deepStrictEqual(validate({ emitters: { openapi: true } }), []);
|
||||
deepStrictEqual(validate({ emitters: { openapi: {} } }), []);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Program, resolvePath } from "@cadl-lang/compiler";
|
||||
import { EmitContext, emitFile, resolvePath } from "@cadl-lang/compiler";
|
||||
import { renderProgram } from "./ui.js";
|
||||
|
||||
import { createCadlLibrary, JSONSchemaType } from "@cadl-lang/compiler";
|
||||
|
@ -29,12 +29,12 @@ export const libDef = {
|
|||
|
||||
export const $lib = createCadlLibrary(libDef);
|
||||
|
||||
export async function $onEmit(program: Program, options: HtmlProgramViewerOptions) {
|
||||
const html = renderProgram(program);
|
||||
const outputDir = options["output-dir"] ?? program.compilerOptions.outputDir!;
|
||||
export async function $onEmit(context: EmitContext<HtmlProgramViewerOptions>) {
|
||||
const html = renderProgram(context.program);
|
||||
const outputDir = context.emitterOutputDir;
|
||||
const htmlPath = resolvePath(outputDir, "cadl-program.html");
|
||||
await program.host.writeFile(
|
||||
htmlPath,
|
||||
`<!DOCTYPE html><html lang="en"><link rel="stylesheet" href="style.css"><body>${html}</body></html>`
|
||||
);
|
||||
await emitFile(context.program, {
|
||||
path: htmlPath,
|
||||
content: `<!DOCTYPE html><html lang="en"><link rel="stylesheet" href="style.css"><body>${html}</body></html>`,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,11 +1,6 @@
|
|||
import { createCadlLibrary, JSONSchemaType, paramMessage } from "@cadl-lang/compiler";
|
||||
|
||||
export interface OpenAPI3EmitterOptions {
|
||||
/**
|
||||
* Override compiler output-dir
|
||||
*/
|
||||
"output-dir"?: string;
|
||||
|
||||
"output-file"?: string;
|
||||
|
||||
/**
|
||||
|
@ -25,7 +20,6 @@ const EmitterOptionsSchema: JSONSchemaType<OpenAPI3EmitterOptions> = {
|
|||
type: "object",
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
"output-dir": { type: "string", nullable: true },
|
||||
"output-file": { type: "string", nullable: true },
|
||||
"new-line": { type: "string", enum: ["crlf", "lf"], nullable: true },
|
||||
"omit-unreachable-types": { type: "boolean", nullable: true },
|
||||
|
|
|
@ -2,8 +2,8 @@ import {
|
|||
BooleanLiteral,
|
||||
compilerAssert,
|
||||
DiscriminatedUnion,
|
||||
EmitContext,
|
||||
emitFile,
|
||||
EmitOptionsFor,
|
||||
Enum,
|
||||
EnumMember,
|
||||
getAllTags,
|
||||
|
@ -88,7 +88,7 @@ import {
|
|||
} from "@cadl-lang/rest/http";
|
||||
import { buildVersionProjections } from "@cadl-lang/versioning";
|
||||
import { getOneOf, getRef } from "./decorators.js";
|
||||
import { OpenAPI3EmitterOptions, OpenAPILibrary, reportDiagnostic } from "./lib.js";
|
||||
import { OpenAPI3EmitterOptions, reportDiagnostic } from "./lib.js";
|
||||
import {
|
||||
OpenAPI3Discriminator,
|
||||
OpenAPI3Document,
|
||||
|
@ -110,25 +110,21 @@ const defaultOptions = {
|
|||
"omit-unreachable-types": false,
|
||||
} as const;
|
||||
|
||||
export async function $onEmit(p: Program, emitterOptions?: EmitOptionsFor<OpenAPILibrary>) {
|
||||
const options = resolveOptions(p, emitterOptions ?? {});
|
||||
const emitter = createOAPIEmitter(p, options);
|
||||
export async function $onEmit(context: EmitContext<OpenAPI3EmitterOptions>) {
|
||||
const options = resolveOptions(context);
|
||||
const emitter = createOAPIEmitter(context.program, options);
|
||||
await emitter.emitOpenAPI();
|
||||
}
|
||||
|
||||
export function resolveOptions(
|
||||
program: Program,
|
||||
options: OpenAPI3EmitterOptions
|
||||
context: EmitContext<OpenAPI3EmitterOptions>
|
||||
): ResolvedOpenAPI3EmitterOptions {
|
||||
const resolvedOptions = { ...defaultOptions, ...options };
|
||||
const resolvedOptions = { ...defaultOptions, ...context.options };
|
||||
|
||||
return {
|
||||
newLine: resolvedOptions["new-line"],
|
||||
omitUnreachableTypes: resolvedOptions["omit-unreachable-types"],
|
||||
outputFile: resolvePath(
|
||||
resolvedOptions["output-dir"] ?? program.compilerOptions.outputDir ?? "./cadl-output",
|
||||
resolvedOptions["output-file"]
|
||||
),
|
||||
outputFile: resolvePath(context.emitterOutputDir, resolvedOptions["output-file"]),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -38,10 +38,10 @@ async function main() {
|
|||
mkdirp(outputPath);
|
||||
|
||||
await run(process.execPath, [
|
||||
"../../packages/compiler/dist/core/cli.js",
|
||||
"../../packages/compiler/dist/core/cli/cli.js",
|
||||
"compile",
|
||||
inputPath,
|
||||
`--output-dir=${outputPath}`,
|
||||
`--option="@cadl-lang/openapi3.emitter-output-dir=${outputPath}"`,
|
||||
`--emit=@cadl-lang/openapi3`,
|
||||
`--warn-as-error`,
|
||||
`--debug`,
|
||||
|
|
Загрузка…
Ссылка в новой задаче