Skip to content

Commit

Permalink
feat(astro-tests): Allow file edit and resets to create/delete files
Browse files Browse the repository at this point in the history
  • Loading branch information
Fryuni committed Feb 9, 2025
1 parent 42eadc0 commit 86edf1c
Show file tree
Hide file tree
Showing 3 changed files with 82 additions and 18 deletions.
5 changes: 5 additions & 0 deletions .changeset/khaki-ravens-fail.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@inox-tools/astro-tests': minor
---

Allow file edits and resets to create and delete files in the source code
18 changes: 14 additions & 4 deletions docs/src/content/docs/astro-tests.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -208,22 +208,32 @@ Checks whether the given path exists on the build output (`outDir` config).
### `readFile`

<p>
**Type:** `(path: string) => Promise<string>`
**Type:** `(path: string) => Promise<string | null>`
</p>

Read a file from the build (relative to `outDir` config).
Read a file from the build (relative to the `outDir` config). Returns null if the file doesn't exist.

### `readSrcFile`

<p>
**Type:** `(path: string) => Promise<string | null>`
</p>

Read a file from the project (relative to the `root` config). Returns null if the file doesn't exist.

### `editFile`

<p>
**Type:** `(path: string, updater: string | ((content: string) => string)) => Promise<() => void>`
**Type:** `(path: string, updater: string | null | ((content: string | null) => string | null)) => Promise<() => void>`
</p>

Edit a file in the fixture directory.

The first parameter is a path relative to the root of the fixture.

The second parameter can be the new content of the file or a function that takes the current content and returns the new content.
The second parameter can be the new content of the file or a function that takes the current content and returns the new content. The content passed to the function will be null if the file doesn't exist.

If the given content or content returned from the given function is null, the file will be deleted.

This function returns a Promise that resolves to another function. This resolved function can be called to revert the changes.

Expand Down
77 changes: 63 additions & 14 deletions packages/astro-tests/src/astroFixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,17 +93,31 @@ type Fixture = {
pathExists: (path: string) => boolean;
/**
* Read a file from the build.
*
* Returns null if the file doesn't exist.
*/
readFile: (path: string) => Promise<string | null>;
/**
* Read a file from the project.
*
* Returns null if the file doesn't exist.
*/
readFile: (path: string) => Promise<string>;
readSrcFile: (path: string) => Promise<string | null>;
/**
* Edit a file in the fixture.
*
* The second parameter can be the new content of the file
* or a function that takes the current content and returns the new content.
*
* The content passed to the function will be null if the file doesn't exist.
* If the returned content is null, the file will be deleted.
*
* Returns a function that can be called to revert the edit.
*/
editFile: (path: string, updater: string | ((content: string) => string)) => Promise<() => void>;
editFile: (
path: string,
updater: string | null | ((content: string | null) => string | null)
) => Promise<() => void>;
/**
* Reset all changes made with .editFile()
*/
Expand Down Expand Up @@ -224,14 +238,17 @@ export async function loadFixture(inlineConfig: InlineConfig): Promise<Fixture>
const onNextChange = () =>
devServer
? new Promise<void>((resolve) =>
// TODO: Implement filter to only resolve on changes to a given file.
devServer.watcher.once('change', () => resolve())
)
// TODO: Implement filter to only resolve on changes to a given file.
devServer.watcher.once('change', () => resolve())
)
: Promise.reject(new Error('No dev server running'));

// Also do it on process exit, just in case.
process.on('exit', resetAllFiles);

const resolveOutPath = (path: string) => new URL(path.replace(/^\//, ''), config.outDir);
const resolveProjectPath = (path: string) => new URL(path.replace(/^\//, ''), config.root);

return {
config,
startDevServer: async (extraInlineConfig = {}) => {
Expand Down Expand Up @@ -274,7 +291,7 @@ export async function loadFixture(inlineConfig: InlineConfig): Promise<Fixture>
});

debug(`Removing node_modules/.astro`);
await fs.promises.rm(new URL('./node_modules/.astro', config.root), {
await fs.promises.rm(resolveProjectPath('./node_modules/.astro'), {
maxRetries: 10,
recursive: true,
force: true,
Expand All @@ -288,7 +305,7 @@ export async function loadFixture(inlineConfig: InlineConfig): Promise<Fixture>
});

debug(`Removing .astro`);
await fs.promises.rm(new URL('./.astro', config.root), {
await fs.promises.rm(resolveProjectPath('./.astro'), {
maxRetries: 10,
recursive: true,
force: true,
Expand Down Expand Up @@ -340,29 +357,61 @@ export async function loadFixture(inlineConfig: InlineConfig): Promise<Fixture>
}
},

pathExists: (p) => fs.existsSync(new URL(p.replace(/^\//, ''), config.outDir)),
readFile: (filePath) =>
fs.promises.readFile(new URL(filePath.replace(/^\//, ''), config.outDir), 'utf8'),
pathExists: (p) => fs.existsSync(resolveOutPath(p)),
readFile: async (filePath) => {
const path = resolveOutPath(filePath);

if (!fs.existsSync(path)) {
return null;
}

return fs.promises.readFile(path, 'utf8');
},
readSrcFile: async (filePath) => {
const path = resolveProjectPath(filePath);

if (!fs.existsSync(path)) {
return null;
}

return fs.promises.readFile(path, 'utf8');
},
editFile: async (filePath, newContentsOrCallback) => {
const fileUrl = new URL(filePath.replace(/^\//, ''), config.root);
const contents = await fs.promises.readFile(fileUrl, 'utf-8');
const fileUrl = resolveProjectPath(filePath);

const contents = fs.existsSync(fileUrl) ? await fs.promises.readFile(fileUrl, 'utf-8') : null;

const reset = () => {
debug(`Resetting ${filePath}`);
fs.writeFileSync(fileUrl, contents);
if (contents) {
fs.writeFileSync(fileUrl, contents);
} else {
fs.rmSync(fileUrl, { force: true });
}
};

// Only save this reset if not already in the map, in case multiple edits happen
// to the same file.
if (!fileEdits.has(fileUrl.toString())) {
fileEdits.set(fileUrl.toString(), reset);
}

debug(`Editing ${filePath}`);

const newContents =
typeof newContentsOrCallback === 'function'
? newContentsOrCallback(contents)
: newContentsOrCallback;

const nextChange = devServer ? onNextChange() : Promise.resolve();
await fs.promises.writeFile(fileUrl, newContents);

if (newContents) {
await fs.promises.mkdir(path.dirname(fileURLToPath(fileUrl)), { recursive: true });
await fs.promises.writeFile(fileUrl, newContents);
} else {
await fs.promises.rm(fileUrl, { force: true });
}

await nextChange;
return reset;
},
Expand Down

0 comments on commit 86edf1c

Please sign in to comment.