create-validator-ts
Version:
Create JSON Schema validator from TypeScript.
186 lines (179 loc) • 6.55 kB
text/typescript
import globWatch from "glob-watcher";
import fs from "node:fs/promises";
import * as globby from "globby";
import assert from "node:assert";
import { generateValidator, TsJsonSchemaGeneratorOptions } from "./create-validator-ts.js";
import { CodeGenerator } from "./default-code-generator.js";
import path from "node:path";
import { createCache } from "@file-cache/core";
import { createNpmPackageKey } from "@file-cache/npm";
import { pathToFileURL } from "node:url";
export type { GenerateValidatorCodeOptions, CodeGenerator } from "./default-code-generator.js";
// dynamic import wrapper
// import() can not load Window file path
// convert file path to file URL before import()
// https://github.com/nodejs/node/issues/31710
export async function dynamicImport(targetPath: string) {
const fileUrl = pathToFileURL(targetPath).href;
return import(fileUrl);
}
export type CreateTSValidatorOptions = {
cwd: string;
verbose: boolean;
tsconfigFilePath: string;
codeGeneratorScript: string;
targetGlobs: string[];
noCache?: boolean; // true by default
} & TsJsonSchemaGeneratorOptions;
export async function watchValidator(options: CreateTSValidatorOptions) {
const { generator, generatorOptions = {} } = (await dynamicImport(
path.resolve(options.cwd, options.codeGeneratorScript)
)) as {
generator: CodeGenerator;
generatorOptions?: {
extraTags?: string[];
};
};
const watcher = globWatch(options.targetGlobs, {
ignoreInitial: true
});
const { sortProps, strictTuples, encodeRefs, skipTypeCheck, additionalProperties } = options;
return new Promise<void>((resolve, reject) => {
watcher.on("change", async (filePath) => {
const result = await generateValidator({
cwd: options.cwd,
filePath: filePath,
tsconfigFilePath: options.tsconfigFilePath,
validatorGenerator: generator,
extraTags: generatorOptions.extraTags || [],
sortProps,
strictTuples,
encodeRefs,
skipTypeCheck,
additionalProperties
});
if (!result) {
return;
}
if (options.verbose) {
console.log("Update validator: " + result.validatorFilePath);
}
return fs.writeFile(result.validatorFilePath, result.code, "utf-8");
});
watcher.on("close", () => {
resolve();
});
watcher.on("error", (error) => {
reject(error);
});
});
}
// --check: validate the difference current of source
export async function testGeneratedValidator(options: CreateTSValidatorOptions) {
const files = await globby.globby(options.targetGlobs, {
cwd: options.cwd,
absolute: true
});
const { generator, generatorOptions = {} } = (await dynamicImport(
path.resolve(options.cwd, options.codeGeneratorScript)
)) as {
generator: CodeGenerator;
generatorOptions?: {
extraTags?: string[];
};
};
const { sortProps, strictTuples, encodeRefs, skipTypeCheck, additionalProperties } = options;
return Promise.all(
files.map(async (filePath) => {
const result = await generateValidator({
cwd: options.cwd,
filePath: filePath,
tsconfigFilePath: options.tsconfigFilePath,
validatorGenerator: generator,
extraTags: generatorOptions.extraTags || [],
sortProps,
strictTuples,
encodeRefs,
skipTypeCheck,
additionalProperties
});
if (!result) {
return;
}
try {
await fs.access(result.validatorFilePath);
} catch {
return;
}
const prevValidatorCode = await fs.readFile(result.validatorFilePath, {
encoding: "utf-8"
});
try {
assert.strictEqual(result.code, prevValidatorCode);
} catch (error) {
console.error(
"Found diff between types and validator.\nPlease update validator: $ npx create-validator-ts " +
filePath
);
throw error;
}
if (options.verbose) {
console.log("OK: " + filePath);
}
})
);
}
export async function createValidator(options: CreateTSValidatorOptions) {
const { generator, generatorOptions = {} } = (await dynamicImport(
path.resolve(options.cwd, options.codeGeneratorScript)
)) as {
generator: CodeGenerator;
generatorOptions?: {
extraTags?: string[];
};
};
const files = await globby.globby(options.targetGlobs, {
cwd: options.cwd,
absolute: true
});
const { sortProps, strictTuples, encodeRefs, skipTypeCheck, additionalProperties } = options;
const cache = await createCache({
name: "create-validator-ts",
mode: "content",
keys: [() => createNpmPackageKey(["create-validator-ts"]), () => JSON.stringify(options)],
// disable cache by default
noCache: options.noCache ?? true
});
return Promise.all(
files.map(async (filePath) => {
const cacheStatus = await cache.getAndUpdateCache(filePath);
if (!cacheStatus.changed) {
if (options.verbose) {
console.log("Cached: " + filePath);
}
return;
}
const result = await generateValidator({
cwd: options.cwd,
filePath: filePath,
tsconfigFilePath: options.tsconfigFilePath,
validatorGenerator: generator,
extraTags: generatorOptions.extraTags || [],
sortProps,
strictTuples,
encodeRefs,
skipTypeCheck,
additionalProperties
});
if (!result) {
return;
}
if (options.verbose) {
console.log("Create: " + filePath);
}
return fs.writeFile(result.validatorFilePath, result.code, "utf-8");
})
).finally(() => {
return cache.reconcile();
});
}