diff --git a/.vscode/settings.json b/.vscode/settings.json index aa1c94e..ebd5461 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,15 @@ { "deno.enable": true, - "deno.unstable": true + "deno.unstable": true, + "editor.formatOnSave": true, + "editor.defaultFormatter": "denoland.vscode-deno", + "[javascript]": { + "editor.defaultFormatter": "denoland.vscode-deno" + }, + "[typescript]": { + "editor.defaultFormatter": "denoland.vscode-deno" + }, + "[json]": { + "editor.defaultFormatter": "denoland.vscode-deno" + } } diff --git a/src/commands/generate.ts b/src/commands/generate.ts index c8e2009..94bc941 100644 --- a/src/commands/generate.ts +++ b/src/commands/generate.ts @@ -42,6 +42,7 @@ export async function fromConfig(configContents: string) { .split("---") .map((v) => v.trim()) .map((v) => yaml.parse(v) as Configuration); + const outputs: Output[] = []; for (const config of configs) { const o = await process(config); diff --git a/src/config.ts b/src/config.ts index 3d5d17f..39566a6 100644 --- a/src/config.ts +++ b/src/config.ts @@ -5,7 +5,8 @@ export type Config = { [key: string]: unknown }; export interface Configuration { spec: string; config?: Config; - generates: Record; + plugins?: string[]; + generates?: Record; } export interface Target { diff --git a/src/generate.ts b/src/generate.ts index 9ce2319..602aa61 100644 --- a/src/generate.ts +++ b/src/generate.ts @@ -5,13 +5,15 @@ import * as path from "https://deno.land/std@0.167.0/path/mod.ts"; import * as streams from "https://deno.land/std@0.167.0/streams/read_all.ts"; import { Config, Configuration, Output } from "./config.ts"; -import { existsSync } from "./utils.ts"; +import { existsSync, makeRelativeUrl } from "./utils.ts"; -async function processConfig(config: Configuration): Promise { +export async function processConfig(config: Configuration): Promise { const apexSource = await Deno.readTextFile(config.spec); // TODO: implement resolver callback const doc = apex.parse(apexSource); + config = await processPlugin(doc, config); + const output: Output[] = []; for (const file in config.generates) { const generatorConfig = config.generates[file]; @@ -21,11 +23,7 @@ async function processConfig(config: Configuration): Promise { //log.info(`Skipping ${file}`); continue; } - - const url = generatorConfig.module.startsWith(".") || - generatorConfig.module.startsWith("/") - ? new URL("file:///" + path.join(Deno.cwd(), generatorConfig.module)) - : new URL(generatorConfig.module); + const url = makeRelativeUrl(generatorConfig.module); const visitorConfig: Config = {}; if (config.config) { @@ -68,6 +66,22 @@ async function processConfig(config: Configuration): Promise { return output; } +export async function processPlugin( + doc: apex.ast.Document, + config: Configuration, +): Promise { + for (const file of config.plugins || []) { + const url = makeRelativeUrl(file); + + log.debug(`Generating configuration with plugin from ${url}`); + + const plugin = await import(url.toString()); + config = plugin.default(doc, config); + } + + return config; +} + // Detect piped input if (!Deno.isatty(Deno.stdin.rid) && import.meta.main) { const stdinContent = await streams.readAll(Deno.stdin); diff --git a/src/process.ts b/src/process.ts index 33a683a..9bb863d 100644 --- a/src/process.ts +++ b/src/process.ts @@ -6,6 +6,8 @@ import { Configuration, Output } from "./config.ts"; import { cliFormatters, sourceFormatters } from "./formatters.ts"; export async function process(config: Configuration): Promise { + log.debug(`Configuration is: ${JSON.stringify(config, null, 2)}`); + // Run the generation process with restricted permissions. const href = new URL("./generate.ts", import.meta.url).href; const p = await Deno.run({ @@ -38,6 +40,7 @@ export async function process(config: Configuration): Promise { } const output = new TextDecoder().decode(rawOutput); + log.debug(`Generator output: ${output}`); return JSON.parse(output) as Output[]; } @@ -68,7 +71,10 @@ export async function writeOutput(generated: Output): Promise { // Execute additional tooling via "runAfter". if (generated.runAfter) { generated.runAfter.forEach(async (cmdConfig) => { - const joined = cmdConfig.command.trim().split("\n").map((v) => v.trim()) + const joined = cmdConfig.command + .trim() + .split("\n") + .map((v) => v.trim()) .join(" "); const args = joined.split(/\s+/); const cmd = args.shift()!; diff --git a/src/utils.ts b/src/utils.ts index e22c74a..8c7ca11 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -84,3 +84,11 @@ export async function mkdirAll(path: string, mode: number) { } } } + +export function makeRelativeUrl(file: string): URL { + return file.startsWith(".") + ? new URL("file:///" + path.join(Deno.cwd(), file)) + : file.startsWith("/") + ? new URL("file:///" + file) + : new URL(file); +} diff --git a/test/config.test.ts b/test/config.test.ts index da661ab..71bc5a0 100644 --- a/test/config.test.ts +++ b/test/config.test.ts @@ -4,7 +4,6 @@ import * as path from "https://deno.land/std@0.167.0/path/mod.ts"; const __dirname = new URL(".", import.meta.url).pathname; -// Compact form: name and function Deno.test( "process", { permissions: { read: true, net: true, run: true } }, @@ -20,10 +19,12 @@ Deno.test( }); const fixture = await Deno.readTextFile(path.join(__dirname, "fixture.rs")); - assertEquals(generated, [{ - file: "file.rs", - source: fixture, - executable: false, - }]); - }, + assertEquals(generated, [ + { + file: "file.rs", + source: fixture, + executable: false, + }, + ]); + } ); diff --git a/test/plugin.test.ts b/test/plugin.test.ts new file mode 100644 index 0000000..d30c93a --- /dev/null +++ b/test/plugin.test.ts @@ -0,0 +1,69 @@ +import * as apex from "https://deno.land/x/apex_core@v0.1.1/mod.ts"; +import { assertEquals } from "https://deno.land/std@0.167.0/testing/asserts.ts"; +import { processPlugin, processConfig } from "../src/generate.ts"; +import * as path from "https://deno.land/std@0.167.0/path/mod.ts"; + +const __dirname = new URL(".", import.meta.url).pathname; +const spec = path.join(__dirname, "test.axdl"); +const plugin = path.join(__dirname, "test-plugin.ts"); +const generator = path.join(__dirname, "test-generator.ts"); + +Deno.test( + "plugin", + { permissions: { read: true, net: true, run: true } }, + async () => { + const apexSource = await Deno.readTextFile(spec); + const doc = apex.parse(apexSource); + const config = await processPlugin(doc, { + spec, + plugins: [plugin], + }); + + assertEquals(config.generates, { "Test.0.file": { module: generator } }); + } +); + +Deno.test( + "plugin", + { permissions: { read: true, net: true, run: true } }, + async () => { + const apexSource = await Deno.readTextFile(spec); + const doc = apex.parse(apexSource); + const config = await processPlugin(doc, { + spec, + plugins: [plugin, plugin], + }); + + assertEquals(config.generates, { + "Test.0.file": { module: generator }, + "Test.1.file": { module: generator }, + }); + } +); + +Deno.test( + "generate", + { permissions: { read: true, net: true, run: true } }, + async () => { + const apexSource = await Deno.readTextFile(spec); + const output = await processConfig({ + spec, + plugins: [plugin, plugin], + }); + + assertEquals(output, [ + { + file: "Test.0.file", + source: "startend", + executable: false, + runAfter: undefined, + }, + { + file: "Test.1.file", + source: "startend", + executable: false, + runAfter: undefined, + }, + ]); + } +); diff --git a/test/test-generator.ts b/test/test-generator.ts new file mode 100644 index 0000000..1fa1f50 --- /dev/null +++ b/test/test-generator.ts @@ -0,0 +1,15 @@ +import * as model from "https://deno.land/x/apex_core@v0.1.0/model/mod.ts"; + +type Context = model.Context; + +export default class DefaultVisitor extends model.BaseVisitor { + visitContextBefore(context: Context): void { + super.visitContextBefore(context); + this.write("start"); + } + + visitContextAfter(context: Context): void { + super.visitContextAfter(context); + this.write("end"); + } +} diff --git a/test/test-plugin.ts b/test/test-plugin.ts new file mode 100644 index 0000000..8ade21b --- /dev/null +++ b/test/test-plugin.ts @@ -0,0 +1,23 @@ +import { Configuration } from "../src/config.ts"; +import * as apex from "https://deno.land/x/apex_core@v0.1.0/mod.ts"; +import * as path from "https://deno.land/std@0.167.0/path/mod.ts"; + +const __dirname = new URL(".", import.meta.url).pathname; +const generator = path.join(__dirname, "test-generator.ts"); + +export default function ( + doc: apex.ast.Document, + config: Configuration +): Configuration { + config.generates ||= {}; + const interfaces = doc.definitions.filter( + (def) => def.getKind() === apex.ast.Kind.InterfaceDefinition + ) as apex.ast.InterfaceDefinition[]; + const num = Object.keys(config.generates).length; + for (const iface of interfaces) { + config.generates[`${iface.name.value}.${num}.file`] = { + module: generator, + }; + } + return config; +}