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:
Timothee Guerin 2022-12-01 15:47:24 -08:00 коммит произвёл GitHub
Родитель 6e8976a343
Коммит c8f8752782
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
27 изменённых файлов: 1182 добавлений и 204 удалений

4
.vscode/launch.json поставляемый
Просмотреть файл

@ -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`,