Skip to content

Commit

Permalink
refactor: use OpenAPI file path for generator instead of stdin & mode…
Browse files Browse the repository at this point in the history
…rnize generator-cli (#3214)

* refactor(generator-cli): modernize code

* test(generator-cli): add more tests

* test(generator-cli): add more tests [2]

* refactor(engine-core): use file path instead of stdin

* chore: remove meow package in favor of native parseArgs

* refactor(generator-cli): fix URL issue

* fix: address CI failures

* fix(generator-cli): use more reliable approach for calculating outputDir path

* refactor: address review comments

* refactor: address review comments

* fix: address eslint issue

---------

Co-authored-by: Soroosh Taefi <[email protected]>
Co-authored-by: Anton Platonov <[email protected]>
  • Loading branch information
3 people authored Feb 28, 2025
1 parent 1795b9d commit d87eb73
Show file tree
Hide file tree
Showing 11 changed files with 234 additions and 255 deletions.
17 changes: 1 addition & 16 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,6 @@
"glob": "11.0.1",
"lint-staged": "15.4.3",
"magic-string": "0.30.17",
"meow": "13.2.0",
"micromatch": "4.0.8",
"nx": "20.4.5",
"oxc-transform": "0.50.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,9 @@ public void process() throws GeneratorException {
return;
}

var arguments = new ArrayList<Object>();
var arguments = new ArrayList<>();
arguments.add(TSGEN_PATH);
prepareOpenAPI(arguments);
prepareOutputDir(arguments);
preparePlugins(arguments);
prepareVerbose(arguments);
Expand All @@ -55,13 +56,7 @@ public void process() throws GeneratorException {
var runner = new GeneratorShellRunner(baseDir.toFile(), nodeCommand,
arguments.stream().map(Objects::toString)
.toArray(String[]::new));
runner.run((stdIn) -> {
try {
Files.copy(openAPIFile, stdIn);
} catch (IOException e) {
throw new LambdaException(e);
}
});
runner.run(null);
} catch (LambdaException e) {
throw new GeneratorException("Node execution failed", e.getCause());
} catch (CommandNotFoundException e) {
Expand Down Expand Up @@ -139,6 +134,11 @@ private void applyPlugins(GeneratorConfiguration.@NonNull Plugins plugins) {
pluginsProcessor.setConfig(plugins);
}

private void prepareOpenAPI(ArrayList<Object> arguments) {
logger.debug("Using OpenAPI file: {}", openAPIFile);
arguments.add(openAPIFile);
}

private void prepareOutputDir(List<Object> arguments) {
var result = outputDirectory.isAbsolute() ? outputDirectory
: baseDir.resolve(outputDirectory);
Expand Down
4 changes: 1 addition & 3 deletions packages/ts/generator-cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,6 @@
},
"dependencies": {
"@vaadin/hilla-generator-core": "24.8.0-alpha1",
"@vaadin/hilla-generator-utils": "24.8.0-alpha1",
"get-stdin": "9.0.0",
"meow": "13.2.0"
"@vaadin/hilla-generator-utils": "24.8.0-alpha1"
}
}
128 changes: 53 additions & 75 deletions packages/ts/generator-cli/src/GeneratorIO.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,35 @@
import { constants } from 'node:fs';
import { access, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
import { createRequire } from 'node:module';
import { dirname, isAbsolute, join, resolve } from 'node:path';
import { pathToFileURL } from 'node:url';
/* eslint-disable @typescript-eslint/restrict-template-expressions */
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
import Plugin, { type PluginConstructor } from '@vaadin/hilla-generator-core/Plugin.js';
import type LoggerFactory from '@vaadin/hilla-generator-utils/LoggerFactory.js';
import GeneratorIOException from './GeneratorIOException.js';

const require = createRequire(import.meta.url);

type PluginConstructorModule = Readonly<{
default: PluginConstructor;
}>;

export const GENERATED_LIST_FILENAME = 'generated-file-list.txt';

export default class GeneratorIO {
static readonly INDEX_FILENAME = 'generated-file-list.txt';
declare ['constructor']: typeof GeneratorIO;
readonly cwd: string;
readonly #logger: LoggerFactory;
readonly #outputDir: string;
readonly #outputDir: URL;

constructor(outputDir: string, logger: LoggerFactory) {
this.cwd = process.cwd();
this.#outputDir = isAbsolute(outputDir) ? outputDir : resolve(this.cwd, outputDir);
constructor(outputDir: URL, logger: LoggerFactory) {
this.#outputDir = outputDir;
this.#logger = logger;

logger.global.debug(`Output directory: ${this.#outputDir}`);
}

/**
* Gets the list of files generated the last time. The info is found in {@link INDEX_FILENAME}.
* Gets the list of files generated the last time. The info is found in {@link GENERATED_LIST_FILENAME}.
* @returns a list of files that have been generated by us
*/
async getGeneratedFiles(): Promise<Set<string>> {
async getExistingGeneratedFiles(): Promise<readonly string[]> {
const files = new Set<string>();
try {
const indexFileContents = await this.read(this.resolveGeneratedFile(this.constructor.INDEX_FILENAME));
indexFileContents
const contents = await this.read(GENERATED_LIST_FILENAME);
contents
.split('\n')
.filter((n) => n.length)
.forEach((fileName) => files.add(fileName));
Expand All @@ -46,83 +39,71 @@ export default class GeneratorIO {
throw e;
}
}
return files;
return Array.from(files);
}

/**
* Cleans the output directory by keeping the generated files and deleting the rest of the given files.
*
* @returns a set containing deleted filenames
* @returns a list with names of deleted files
*/
async cleanOutputDir(generatedFiles: string[], filesToDelete: Set<string>): Promise<Set<string>> {
async cleanOutputDir(
generatedFiles: readonly string[],
filesToDelete: readonly string[],
): Promise<readonly string[]> {
this.#logger.global.debug(`Cleaning ${this.#outputDir}`);
await mkdir(this.#outputDir, { recursive: true });

generatedFiles.forEach((filename) => {
this.#logger.global.debug(`File ${filename} was re-written, should not delete it`);
filesToDelete.delete(filename);
});

const deletedFiles = new Set(
await Promise.all(
[...filesToDelete].map(async (filename) => {
const resolved = this.resolveGeneratedFile(filename);
if (await GeneratorIO.exists(resolved)) {
this.#logger.global.debug(`Deleting file ${filename}.`);
await rm(resolved);
}
return filename;
}),
const filtered = filesToDelete.filter((item) => !generatedFiles.includes(item));

return Array.from(
new Set(
await Promise.all(
filtered.map(async (filename) => {
const url = new URL(filename, this.#outputDir);
try {
await rm(url);
this.#logger.global.debug(`Deleted file ${url}.`);
return filename;
} catch (e: unknown) {
this.#logger.global.debug(`Cannot delete file ${url}: ${e instanceof Error ? e.message : String(e)}`);
return undefined;
}
}),
).then((files) => files.filter((filename) => filename != null)),
),
);

return deletedFiles;
}

async createFileIndex(filenames: string[]): Promise<void> {
await this.write(this.constructor.INDEX_FILENAME, filenames.join('\n'));
}

async writeGeneratedFiles(files: readonly File[]): Promise<string[]> {
await this.createFileIndex(files.map((file) => file.name));
this.#logger.global.debug(`created index`);
async writeGeneratedFiles(files: readonly File[]): Promise<readonly string[]> {
await this.write(
new File(
files.map((file) => `${file.name}\n`),
GENERATED_LIST_FILENAME,
),
);

return Promise.all(
files.map(async (file) => {
const newFileContent = await file.text();
let oldFileContent;
try {
oldFileContent = await this.read(this.resolveGeneratedFile(file.name));
oldFileContent = await this.read(file.name);
} catch (_e) {}

if (newFileContent !== oldFileContent) {
this.#logger.global.debug(`writing file ${file.name}`);
await this.write(file.name, await file.text());
await this.write(file);
} else {
this.#logger.global.debug(`File ${file.name} stayed the same`);
this.#logger.global.debug(`File ${new URL(file.name, this.#outputDir)} stayed the same`);
}
return file.name;
}),
);
}

/**
* Checks that a file exists (is visible)
* @param path - the file path to check
*/
// eslint-disable-next-line class-methods-use-this
static async exists(path: string): Promise<boolean> {
try {
await access(path, constants.F_OK);
return true;
} catch {
return false;
}
}

async loadPlugin(modulePath: string): Promise<PluginConstructor> {
this.#logger.global.debug(`Loading plugin: ${modulePath}`);
const module: PluginConstructorModule = await import(pathToFileURL(require.resolve(modulePath)).toString());
const module: PluginConstructorModule = await import(modulePath);
const ctr: PluginConstructor = module.default;

if (!Object.prototype.isPrototypeOf.call(Plugin, ctr)) {
Expand All @@ -132,20 +113,17 @@ export default class GeneratorIO {
return ctr;
}

resolveGeneratedFile(filename: string): string {
return resolve(this.#outputDir, filename);
}

async read(path: string): Promise<string> {
this.#logger.global.debug(`Reading file: ${path}`);
return readFile(path, 'utf8');
async read(filename: string): Promise<string> {
const url = new URL(filename, this.#outputDir);
this.#logger.global.debug(`Reading file: ${url}`);
return await readFile(url, 'utf8');
}

async write(filename: string, content: string): Promise<void> {
const filePath = join(this.#outputDir, filename);
async write(file: File): Promise<void> {
const filePath = new URL(file.name, this.#outputDir);
this.#logger.global.debug(`Writing file ${filePath}.`);
const dir = dirname(filePath);
const dir = new URL('./', filePath);
await mkdir(dir, { recursive: true });
return writeFile(filePath, content, 'utf-8');
return await writeFile(filePath, await file.text(), 'utf-8');
}
}
Loading

0 comments on commit d87eb73

Please sign in to comment.