Skip to content

Commit

Permalink
feature: support .fernignore in local generation (fern-api#1905)
Browse files Browse the repository at this point in the history
* empty

* support for .fernignore when generating locally Fixes fern-api#1780  (fern-api#1809)

* support for .fernignore when generating locally (fern-api#1780)

* ensuring dependecies are up to date

* refactor test

* refactor into LocalTaskHandler

---------

Co-authored-by: dsinghvi <[email protected]>

* fix

* remove glob package

* downgrade glob

---------

Co-authored-by: Mendie Uwemedimo <[email protected]>
  • Loading branch information
dsinghvi and SlamChillz authored Jul 14, 2023
1 parent 1768043 commit b43a25e
Show file tree
Hide file tree
Showing 11 changed files with 199 additions and 17 deletions.
2 changes: 2 additions & 0 deletions .pnp.cjs

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

Binary file not shown.
Binary file not shown.
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ export const DOCS_DIRECTORY = "docs";
export const DOCS_CONFIGURATION_FILENAME = "docs.yml";
export const PROJECT_CONFIG_FILENAME = "fern.config.json";
export const DEFAULT_WORSPACE_FOLDER_NAME = "api";
export const FERNIGNORE_FILENAME = ".fernignore";
54 changes: 53 additions & 1 deletion packages/cli/ete-tests/src/tests/generate/generate.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
/* eslint-disable jest/expect-expect */
import { AbsoluteFilePath, doesPathExist, join, RelativeFilePath } from "@fern-api/fs-utils";
import { FERNIGNORE_FILENAME } from "@fern-api/project-configuration";
import { writeFile } from "fs/promises";
import { runFernCli } from "../../utils/runFernCli";
import { init } from "../init/init";

Expand All @@ -17,7 +20,7 @@ describe("fern generate", () => {

const fixturesDir = join(AbsoluteFilePath.of(__dirname), RelativeFilePath.of("fixtures"));
for (const fixtureName of FIXTURES) {
// eslint-disable-next-line jest/expect-expect, jest/no-disabled-tests
// eslint-disable-next-line jest/no-disabled-tests
it.skip(
// eslint-disable-next-line jest/valid-title
fixtureName,
Expand All @@ -30,3 +33,52 @@ describe("fern generate", () => {
);
}
});

const FERNIGNORE_FILECONTENTS = `
fern.js
**/*.txt
`;

const FERN_JS_FILENAME = "fern.js";
const FERN_JS_FILECONTENTS = `
#!/usr/bin/env node
console.log('Water the plants')
`;

const DUMMY_TXT_FILENAME = "dummy.txt";
const DUMMY_TXT_FILECONTENTS = `
Practice schema-first API design with Fern
`;

describe("fern generate --local", () => {
it("Keep files listed in .fernignore from unmodified", async () => {
const pathOfDirectory = await init();
await runFernCli(["generate", "--local", "--keepDocker"], {
cwd: pathOfDirectory,
});

// write custom files and override
const absolutePathToLocalOutput = join(pathOfDirectory, RelativeFilePath.of("generated/typescript"));

const absolutePathToFernignore = join(absolutePathToLocalOutput, RelativeFilePath.of(FERNIGNORE_FILENAME));
await writeFile(absolutePathToFernignore, FERNIGNORE_FILECONTENTS);

const absolutePathToFernJs = join(absolutePathToLocalOutput, RelativeFilePath.of(FERN_JS_FILENAME));
await writeFile(absolutePathToFernJs, FERN_JS_FILECONTENTS);

const absolutePathToDummyText = join(absolutePathToLocalOutput, RelativeFilePath.of(DUMMY_TXT_FILENAME));
await writeFile(absolutePathToDummyText, DUMMY_TXT_FILECONTENTS);

await runFernCli(["generate", "--local", "--keepDocker"], {
cwd: pathOfDirectory,
});

await expectPathExists(absolutePathToFernignore);
await expectPathExists(absolutePathToFernJs);
await expectPathExists(absolutePathToDummyText);
}, 180_000);
});

async function expectPathExists(absoluteFilePath: AbsoluteFilePath): Promise<void> {
expect(await doesPathExist(absoluteFilePath)).toBe(true);
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
"@fern-api/generators-configuration": "workspace:*",
"@fern-api/ir-generator": "workspace:*",
"@fern-api/ir-migrations": "workspace:*",
"@fern-api/logging-execa": "workspace:*",
"@fern-api/project-configuration": "workspace:*",
"@fern-api/task-context": "workspace:*",
"@fern-api/workspace-loader": "workspace:*",
"@fern-fern/generator-exec-sdk": "0.0.101",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { AbsoluteFilePath, doesPathExist, join, RelativeFilePath } from "@fern-api/fs-utils";
import { loggingExeca } from "@fern-api/logging-execa";
import { FERNIGNORE_FILENAME } from "@fern-api/project-configuration";
import { TaskContext } from "@fern-api/task-context";
import decompress from "decompress";
import { cp, readdir, readFile } from "fs/promises";
import tmp from "tmp-promise";

export declare namespace LocalTaskHandler {
export interface Init {
context: TaskContext;
absolutePathToTmpOutputDirectory: AbsoluteFilePath;
absolutePathToLocalOutput: AbsoluteFilePath;
}
}

export class LocalTaskHandler {
private context: TaskContext;
private absolutePathToTmpOutputDirectory: AbsoluteFilePath;
private absolutePathToLocalOutput: AbsoluteFilePath;

constructor({ context, absolutePathToTmpOutputDirectory, absolutePathToLocalOutput }: LocalTaskHandler.Init) {
this.context = context;
this.absolutePathToLocalOutput = absolutePathToLocalOutput;
this.absolutePathToTmpOutputDirectory = absolutePathToTmpOutputDirectory;
}

public async copyGeneratedFiles(): Promise<void> {
if (await this.isFernIgnorePresent()) {
await this.copyGeneratedFilesWithFernIgnore();
} else {
await this.copyGeneratedFilesNoFernIgnore();
}
}

private async isFernIgnorePresent(): Promise<boolean> {
const absolutePathToFernignore = AbsoluteFilePath.of(
join(this.absolutePathToLocalOutput, RelativeFilePath.of(FERNIGNORE_FILENAME))
);
return await doesPathExist(absolutePathToFernignore);
}

private async copyGeneratedFilesWithFernIgnore(): Promise<void> {
// Step 1: Create temp directory to resolve .fernignore
const tmpOutputResolutionDir = AbsoluteFilePath.of((await tmp.dir({})).path);

// Step 2: Read all .fernignore paths
const absolutePathToFernignore = AbsoluteFilePath.of(
join(this.absolutePathToLocalOutput, RelativeFilePath.of(FERNIGNORE_FILENAME))
);
const fernIngnorePaths = await getFernIgnorePaths({ absolutePathToFernignore });

// Step 3: Copy files from local output to tmp directory
await cp(this.absolutePathToLocalOutput, tmpOutputResolutionDir, { recursive: true });

// Step 3: In tmp directory initialize a `.git` directory
await this.runGitCommand(["init"], tmpOutputResolutionDir);
await this.runGitCommand(["add", "."], tmpOutputResolutionDir);
await this.runGitCommand(["commit", "-m", '"init"'], tmpOutputResolutionDir);

// Step 4: Stage deletions `git rm -rf .`
await this.runGitCommand(["rm", "-rf", "."], tmpOutputResolutionDir);

// Step 5: Copy all files from generated temp dir
await this.copyGeneratedFilesToDirectory(tmpOutputResolutionDir);

// Step 6: Undo changes to fernignore paths
await this.runGitCommand(["reset", "--", ...fernIngnorePaths], tmpOutputResolutionDir);
await this.runGitCommand(["restore", "."], tmpOutputResolutionDir);

// Step 8: Delete local output directory and copy all files from the generated directory
this.context.logger.debug(`rm -rf ${this.absolutePathToLocalOutput}`);
await cp(tmpOutputResolutionDir, this.absolutePathToLocalOutput, { recursive: true });
}

/**
* If no `.fernignore` is present we can delete the local output directory entirely and
* copy the generated output from the tmp directory.
*/
private async copyGeneratedFilesNoFernIgnore(): Promise<void> {
this.context.logger.debug(`rm -rf ${this.absolutePathToLocalOutput}`);
await this.copyGeneratedFilesToDirectory(this.absolutePathToLocalOutput);
}

private async copyGeneratedFilesToDirectory(outputPath: AbsoluteFilePath): Promise<void> {
const [firstLocalOutputItem, ...remaininglocalOutputItems] = await readdir(
this.absolutePathToTmpOutputDirectory
);
if (firstLocalOutputItem == null) {
return;
}

this.context.logger.debug(`Copying generated files to ${outputPath}`);
if (firstLocalOutputItem.endsWith(".zip") && remaininglocalOutputItems.length === 0) {
await decompress(
join(this.absolutePathToTmpOutputDirectory, RelativeFilePath.of(firstLocalOutputItem)),
this.absolutePathToLocalOutput,
{
strip: 1,
}
);
} else {
await cp(this.absolutePathToTmpOutputDirectory, this.absolutePathToLocalOutput, { recursive: true });
}
}

private async runGitCommand(options: string[], cwd: AbsoluteFilePath): Promise<void> {
await loggingExeca(this.context.logger, "git", options, {
cwd,
});
}
}

const NEW_LINE_REGEX = /\r?\n/;

async function getFernIgnorePaths({
absolutePathToFernignore,
}: {
absolutePathToFernignore: AbsoluteFilePath;
}): Promise<string[]> {
const fernIgnoreFileContents = (await readFile(absolutePathToFernignore)).toString();
return [
FERNIGNORE_FILENAME,
...fernIgnoreFileContents
.trim()
.split(NEW_LINE_REGEX)
.filter((line) => !line.startsWith("#")),
];
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,10 @@ import { migrateIntermediateRepresentationForGenerator } from "@fern-api/ir-migr
import { TaskContext } from "@fern-api/task-context";
import { FernWorkspace } from "@fern-api/workspace-loader";
import chalk from "chalk";
import decompress from "decompress";
import { cp, readdir, rm } from "fs/promises";
import os from "os";
import path, { join } from "path";
import path from "path";
import tmp, { DirectoryResult } from "tmp-promise";
import { LocalTaskHandler } from "./LocalTaskHandler";
import { runGenerator } from "./run-generator/runGenerator";

export async function runLocalGenerationForWorkspace({
Expand Down Expand Up @@ -115,19 +114,12 @@ async function writeFilesToDiskAndRunGenerator({
keepDocker,
});

const [firstLocalOutputItem, ...remaininglocalOutputItems] = await readdir(absolutePathToTmpOutputDirectory);
if (firstLocalOutputItem == null) {
return;
}
await rm(absolutePathToLocalOutput, { force: true, recursive: true });

if (firstLocalOutputItem.endsWith(".zip") && remaininglocalOutputItems.length === 0) {
await decompress(join(absolutePathToTmpOutputDirectory, firstLocalOutputItem), absolutePathToLocalOutput, {
strip: 1,
});
} else {
await cp(absolutePathToTmpOutputDirectory, absolutePathToLocalOutput, { recursive: true });
}
const taskHandler = new LocalTaskHandler({
context,
absolutePathToLocalOutput,
absolutePathToTmpOutputDirectory,
});
await taskHandler.copyGeneratedFiles();
}

async function writeIrToFile({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
"include": ["./src"],
"references": [
{ "path": "../../../../commons/fs-utils" },
{ "path": "../../../../commons/logging-execa" },
{ "path": "../../../config-management/commons" },
{ "path": "../../../config-management/generators-configuration" },
{ "path": "../../../config-management/project-configuration" },
{ "path": "../../../task-context" },
{ "path": "../../../workspace-loader" },
{ "path": "../../ir-generator" },
Expand Down
2 changes: 2 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3243,6 +3243,8 @@ __metadata:
"@fern-api/generators-configuration": "workspace:*"
"@fern-api/ir-generator": "workspace:*"
"@fern-api/ir-migrations": "workspace:*"
"@fern-api/logging-execa": "workspace:*"
"@fern-api/project-configuration": "workspace:*"
"@fern-api/task-context": "workspace:*"
"@fern-api/workspace-loader": "workspace:*"
"@fern-fern/generator-exec-sdk": 0.0.101
Expand Down

0 comments on commit b43a25e

Please sign in to comment.