From f1b90987504027a7c9f275e4eb2d10692f207891 Mon Sep 17 00:00:00 2001 From: Joakim Carlstein Date: Fri, 26 Jan 2024 09:17:47 +0100 Subject: [PATCH] fix(migrations): don't include folders when collecting migrations It should be possible to have folders inside your migrations folder --- .changeset/cuddly-peaches-look.md | 5 ++ packages/cli/src/get-migrations.test.ts | 74 ++++++++++++++++++++++--- packages/cli/src/get-migrations.ts | 45 ++++++++------- 3 files changed, 95 insertions(+), 29 deletions(-) create mode 100644 .changeset/cuddly-peaches-look.md diff --git a/.changeset/cuddly-peaches-look.md b/.changeset/cuddly-peaches-look.md new file mode 100644 index 0000000..26aa479 --- /dev/null +++ b/.changeset/cuddly-peaches-look.md @@ -0,0 +1,5 @@ +--- +'@emigrate/cli': patch +--- + +Only include files when collecting migrations, i.e. it should be possible to have folders inside your migrations folder. diff --git a/packages/cli/src/get-migrations.test.ts b/packages/cli/src/get-migrations.test.ts index 3391b04..8c9e5d1 100644 --- a/packages/cli/src/get-migrations.test.ts +++ b/packages/cli/src/get-migrations.test.ts @@ -3,21 +3,27 @@ import { afterEach, beforeEach, describe, it, mock } from 'node:test'; import assert from 'node:assert'; import { getMigrations } from './get-migrations.js'; -const originalReaddir = fs.readdir; -const readdirMock = mock.fn(originalReaddir); +const originalOpendir = fs.opendir; +const opendirMock = mock.fn(originalOpendir); describe('get-migrations', () => { beforeEach(() => { - fs.readdir = readdirMock; + fs.opendir = opendirMock; }); afterEach(() => { - readdirMock.mock.restore(); - fs.readdir = originalReaddir; + opendirMock.mock.restore(); + fs.opendir = originalOpendir; }); it('should skip files with leading periods', async () => { - readdirMock.mock.mockImplementation(async () => ['.foo.js', 'bar.js', 'baz.js']); + opendirMock.mock.mockImplementation(async function* () { + yield* [ + { name: '.foo.js', isFile: () => true }, + { name: 'bar.js', isFile: () => true }, + { name: 'baz.js', isFile: () => true }, + ]; + }); const migrations = await getMigrations('/cwd/', 'directory'); @@ -42,7 +48,13 @@ describe('get-migrations', () => { }); it('should skip files with leading underscores', async () => { - readdirMock.mock.mockImplementation(async () => ['_foo.js', 'bar.js', 'baz.js']); + opendirMock.mock.mockImplementation(async function* () { + yield* [ + { name: '_foo.js', isFile: () => true }, + { name: 'bar.js', isFile: () => true }, + { name: 'baz.js', isFile: () => true }, + ]; + }); const migrations = await getMigrations('/cwd/', 'directory'); @@ -67,7 +79,44 @@ describe('get-migrations', () => { }); it('should skip files without file extensions', async () => { - readdirMock.mock.mockImplementation(async () => ['foo', 'bar.js', 'baz.js']); + opendirMock.mock.mockImplementation(async function* () { + yield* [ + { name: 'foo', isFile: () => true }, + { name: 'bar.js', isFile: () => true }, + { name: 'baz.js', isFile: () => true }, + ]; + }); + + const migrations = await getMigrations('/cwd/', 'directory'); + + assert.deepStrictEqual(migrations, [ + { + name: 'bar.js', + filePath: '/cwd/directory/bar.js', + relativeFilePath: 'directory/bar.js', + extension: '.js', + directory: 'directory', + cwd: '/cwd/', + }, + { + name: 'baz.js', + filePath: '/cwd/directory/baz.js', + relativeFilePath: 'directory/baz.js', + extension: '.js', + directory: 'directory', + cwd: '/cwd/', + }, + ]); + }); + + it('should skip non-files', async () => { + opendirMock.mock.mockImplementation(async function* () { + yield* [ + { name: 'foo.js', isFile: () => false }, + { name: 'bar.js', isFile: () => true }, + { name: 'baz.js', isFile: () => true }, + ]; + }); const migrations = await getMigrations('/cwd/', 'directory'); @@ -92,7 +141,14 @@ describe('get-migrations', () => { }); it('should sort them in lexicographical order', async () => { - readdirMock.mock.mockImplementation(async () => ['foo.js', 'bar_data.js', 'bar.js', 'baz.js']); + opendirMock.mock.mockImplementation(async function* () { + yield* [ + { name: 'foo.js', isFile: () => true }, + { name: 'bar_data.js', isFile: () => true }, + { name: 'bar.js', isFile: () => true }, + { name: 'baz.js', isFile: () => true }, + ]; + }); const migrations = await getMigrations('/cwd/', 'directory'); diff --git a/packages/cli/src/get-migrations.ts b/packages/cli/src/get-migrations.ts index 228b132..fb6033d 100644 --- a/packages/cli/src/get-migrations.ts +++ b/packages/cli/src/get-migrations.ts @@ -3,37 +3,42 @@ import fs from 'node:fs/promises'; import { type MigrationMetadata } from '@emigrate/types'; import { withLeadingPeriod } from './with-leading-period.js'; import { BadOptionError } from './errors.js'; +import { arrayFromAsync } from './array-from-async.js'; export type GetMigrationsFunction = typeof getMigrations; -const tryReadDirectory = async (directoryPath: string): Promise => { +async function* tryReadDirectory(directoryPath: string): AsyncIterable { try { - return await fs.readdir(directoryPath); + for await (const entry of await fs.opendir(directoryPath)) { + if ( + entry.isFile() && + !entry.name.startsWith('.') && + !entry.name.startsWith('_') && + path.extname(entry.name) !== '' + ) { + yield entry.name; + } + } } catch { throw BadOptionError.fromOption('directory', `Couldn't read directory: ${directoryPath}`); } -}; +} export const getMigrations = async (cwd: string, directory: string): Promise => { const directoryPath = path.resolve(cwd, directory); - const allFilesInMigrationDirectory = await tryReadDirectory(directoryPath); + const allFilesInMigrationDirectory = await arrayFromAsync(tryReadDirectory(directoryPath)); - const migrationFiles: MigrationMetadata[] = allFilesInMigrationDirectory - .filter((name) => !name.startsWith('.') && !name.startsWith('_') && path.extname(name) !== '') - .sort() - .map((name) => { - const filePath = path.join(directoryPath, name); + return allFilesInMigrationDirectory.sort().map((name) => { + const filePath = path.join(directoryPath, name); - return { - name, - filePath, - relativeFilePath: path.relative(cwd, filePath), - extension: withLeadingPeriod(path.extname(name)), - directory, - cwd, - }; - }); - - return migrationFiles; + return { + name, + filePath, + relativeFilePath: path.relative(cwd, filePath), + extension: withLeadingPeriod(path.extname(name)), + directory, + cwd, + } satisfies MigrationMetadata; + }); };