diff --git a/.changeset/breezy-sheep-yawn.md b/.changeset/breezy-sheep-yawn.md
new file mode 100644
index 0000000..1fc9664
--- /dev/null
+++ b/.changeset/breezy-sheep-yawn.md
@@ -0,0 +1,5 @@
+---
+'@emigrate/cli': minor
+---
+
+Make it possible to write the Emigrate configuration file in TypeScript and load it using `tsx` in a NodeJS environment by importing packages provided using the `--import` CLI option before loading the configuration file. This makes it possible to run Emigrate in production with a configuration file written in TypeScript without having the `typescript` package installed.
diff --git a/.changeset/cool-spies-behave.md b/.changeset/cool-spies-behave.md
new file mode 100644
index 0000000..8efe0e7
--- /dev/null
+++ b/.changeset/cool-spies-behave.md
@@ -0,0 +1,5 @@
+---
+'@emigrate/docs': patch
+---
+
+Add note on how to write Emigrate's config using TypeScript in a production environment without having `typescript` installed.
diff --git a/.changeset/tidy-shrimps-hide.md b/.changeset/tidy-shrimps-hide.md
new file mode 100644
index 0000000..3d86eaf
--- /dev/null
+++ b/.changeset/tidy-shrimps-hide.md
@@ -0,0 +1,5 @@
+---
+'@emigrate/cli': patch
+---
+
+Don't use the `typescript` package for loading an Emigrate configuration file written in TypeScript in a Bun or Deno environment
diff --git a/docs/src/content/docs/guides/typescript.mdx b/docs/src/content/docs/guides/typescript.mdx
index 8e580a4..267846a 100644
--- a/docs/src/content/docs/guides/typescript.mdx
+++ b/docs/src/content/docs/guides/typescript.mdx
@@ -7,14 +7,14 @@ import { Tabs, TabItem } from '@astrojs/starlight/components';
import Link from '@components/Link.astro';
:::tip[Using Bun or Deno?]
-If you are using [Bun](https://bun.sh) or [Deno](https://deno.land) you are already good to go as they both support TypeScript out of the box.
+If you are using [Bun](https://bun.sh) or [Deno](https://deno.land) you are already good to go as they both support TypeScript out of the box!
:::
-You have at least the two following options to support running TypeScript migration files in NodeJS.
+If you're using NodeJS you have at least the two following options to support running TypeScript migration files in NodeJS.
## Using `tsx`
-If you want to be able to write and run migration files written in TypeScript the easiest way is to install the [`tsx`](https://github.com/privatenumber/tsx) package.
+If you want to be able to write and run migration files written in TypeScript an easy way is to install the [`tsx`](https://github.com/privatenumber/tsx) package.
### Installing `tsx`
@@ -67,9 +67,13 @@ Using the `--import` flag y
+:::note
+This method is necessary if you want to write your configuration file in TypeScript without having `typescript` installed in your production environment, as `tsx` must be loaded before the configuration file is loaded.
+:::
+
#### Via configuration file
-You can also directly import `tsx` in your configuration file.
+You can also directly import `tsx` in your configuration file (will only work if you're not using TypeScript for your configuration file).
```js title="emigrate.config.js" {1}
import 'tsx';
diff --git a/packages/cli/package.json b/packages/cli/package.json
index 75feef4..9f0c34f 100644
--- a/packages/cli/package.json
+++ b/packages/cli/package.json
@@ -36,7 +36,9 @@
"immigration"
],
"devDependencies": {
- "@emigrate/tsconfig": "workspace:*"
+ "@emigrate/tsconfig": "workspace:*",
+ "@types/bun": "1.0.5",
+ "bun-types": "1.0.26"
},
"author": "Aboviq AB (https://www.aboviq.com)",
"homepage": "https://github.com/aboviq/emigrate/tree/main/packages/cli#readme",
diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts
index 377cffd..8bb797e 100644
--- a/packages/cli/src/cli.ts
+++ b/packages/cli/src/cli.ts
@@ -24,7 +24,6 @@ const importAll = async (cwd: string, modules: string[]) => {
};
const up: Action = async (args, abortSignal) => {
- const config = await getConfig('up');
const { values } = parseArgs({
args,
options: {
@@ -143,6 +142,14 @@ Examples:
}
const cwd = process.cwd();
+
+ if (values.import) {
+ await importAll(cwd, values.import);
+ }
+
+ const forceImportTypeScriptAsIs = values.import?.some((module) => module === 'tsx' || module.startsWith('tsx/'));
+
+ const config = await getConfig('up', forceImportTypeScriptAsIs);
const {
directory = config.directory,
storage = config.storage,
@@ -151,7 +158,6 @@ Examples:
from,
to,
limit: limitString,
- import: imports = [],
'abort-respite': abortRespiteString,
'no-execution': noExecution,
} = values;
@@ -177,8 +183,6 @@ Examples:
return;
}
- await importAll(cwd, imports);
-
try {
const { default: upCommand } = await import('./commands/up.js');
process.exitCode = await upCommand({
@@ -209,7 +213,6 @@ Examples:
};
const newMigration: Action = async (args) => {
- const config = await getConfig('new');
const { values, positionals } = parseArgs({
args,
options: {
@@ -239,6 +242,12 @@ const newMigration: Action = async (args) => {
multiple: true,
default: [],
},
+ import: {
+ type: 'string',
+ short: 'i',
+ multiple: true,
+ default: [],
+ },
color: {
type: 'boolean',
},
@@ -260,14 +269,24 @@ Arguments:
Options:
-h, --help Show this help message and exit
+
-d, --directory The directory where the migration files are located (required)
+
+ -i, --import Additional modules/packages to import before creating the migration (can be specified multiple times)
+ For example if you want to use Dotenv to load environment variables or when using TypeScript
+
-r, --reporter The reporter to use for reporting the migration file creation progress (default: pretty)
+
-p, --plugin The plugin(s) to use (can be specified multiple times)
+
-t, --template A template file to use as contents for the new migration file
(if the extension option is not provided the template file's extension will be used)
+
-x, --extension The extension to use for the new migration file
(if no template or plugin is provided an empty migration file will be created with the given extension)
+
--color Force color output (this option is passed to the reporter)
+
--no-color Disable color output (this option is passed to the reporter)
One of the --template, --extension or the --plugin options must be specified
@@ -287,6 +306,14 @@ Examples:
}
const cwd = process.cwd();
+
+ if (values.import) {
+ await importAll(cwd, values.import);
+ }
+
+ const forceImportTypeScriptAsIs = values.import?.some((module) => module === 'tsx' || module.startsWith('tsx/'));
+
+ const config = await getConfig('new', forceImportTypeScriptAsIs);
const {
directory = config.directory,
template = config.template,
@@ -312,7 +339,6 @@ Examples:
};
const list: Action = async (args) => {
- const config = await getConfig('list');
const { values } = parseArgs({
args,
options: {
@@ -355,12 +381,18 @@ List all migrations and their status. This command does not run any migrations.
Options:
-h, --help Show this help message and exit
+
-d, --directory The directory where the migration files are located (required)
+
-i, --import Additional modules/packages to import before listing the migrations (can be specified multiple times)
For example if you want to use Dotenv to load environment variables
+
-r, --reporter The reporter to use for reporting the migrations (default: pretty)
+
-s, --storage The storage to use to get the migration history (required)
+
--color Force color output (this option is passed to the reporter)
+
--no-color Disable color output (this option is passed to the reporter)
Examples:
@@ -376,14 +408,15 @@ Examples:
}
const cwd = process.cwd();
- const {
- directory = config.directory,
- storage = config.storage,
- reporter = config.reporter,
- import: imports = [],
- } = values;
- await importAll(cwd, imports);
+ if (values.import) {
+ await importAll(cwd, values.import);
+ }
+
+ const forceImportTypeScriptAsIs = values.import?.some((module) => module === 'tsx' || module.startsWith('tsx/'));
+
+ const config = await getConfig('list', forceImportTypeScriptAsIs);
+ const { directory = config.directory, storage = config.storage, reporter = config.reporter } = values;
try {
const { default: listCommand } = await import('./commands/list.js');
@@ -401,7 +434,6 @@ Examples:
};
const remove: Action = async (args) => {
- const config = await getConfig('remove');
const { values, positionals } = parseArgs({
args,
options: {
@@ -453,13 +485,20 @@ Arguments:
Options:
-h, --help Show this help message and exit
+
-d, --directory The directory where the migration files are located (required)
+
-i, --import Additional modules/packages to import before removing the migration (can be specified multiple times)
For example if you want to use Dotenv to load environment variables
+
-r, --reporter The reporter to use for reporting the removal process (default: pretty)
+
-s, --storage The storage to use to get the migration history (required)
+
-f, --force Force removal of the migration history entry even if the migration is not in a failed state
+
--color Force color output (this option is passed to the reporter)
+
--no-color Disable color output (this option is passed to the reporter)
Examples:
@@ -477,15 +516,15 @@ Examples:
}
const cwd = process.cwd();
- const {
- directory = config.directory,
- storage = config.storage,
- reporter = config.reporter,
- force,
- import: imports = [],
- } = values;
- await importAll(cwd, imports);
+ if (values.import) {
+ await importAll(cwd, values.import);
+ }
+
+ const forceImportTypeScriptAsIs = values.import?.some((module) => module === 'tsx' || module.startsWith('tsx/'));
+
+ const config = await getConfig('remove', forceImportTypeScriptAsIs);
+ const { directory = config.directory, storage = config.storage, reporter = config.reporter, force } = values;
try {
const { default: removeCommand } = await import('./commands/remove.js');
diff --git a/packages/cli/src/deno.d.ts b/packages/cli/src/deno.d.ts
new file mode 100644
index 0000000..bc23737
--- /dev/null
+++ b/packages/cli/src/deno.d.ts
@@ -0,0 +1,6 @@
+declare global {
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ const Deno: any;
+}
+
+export {};
diff --git a/packages/cli/src/get-config.ts b/packages/cli/src/get-config.ts
index 0d9dbd4..4db33a7 100644
--- a/packages/cli/src/get-config.ts
+++ b/packages/cli/src/get-config.ts
@@ -1,11 +1,16 @@
-import { cosmiconfig } from 'cosmiconfig';
+import process from 'node:process';
+import { cosmiconfig, defaultLoaders } from 'cosmiconfig';
import { type Config, type EmigrateConfig } from './types.js';
const commands = ['up', 'list', 'new', 'remove'] as const;
type Command = (typeof commands)[number];
+const canImportTypeScriptAsIs = Boolean(process.isBun) || typeof Deno !== 'undefined';
-export const getConfig = async (command: Command): Promise => {
- const explorer = cosmiconfig('emigrate');
+export const getConfig = async (command: Command, forceImportTypeScriptAsIs = false): Promise => {
+ const explorer = cosmiconfig('emigrate', {
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ loaders: forceImportTypeScriptAsIs || canImportTypeScriptAsIs ? { '.ts': defaultLoaders['.js'] } : undefined,
+ });
const result = await explorer.search();
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 2910926..de45949 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -108,6 +108,12 @@ importers:
'@emigrate/tsconfig':
specifier: workspace:*
version: link:../tsconfig
+ '@types/bun':
+ specifier: 1.0.5
+ version: 1.0.5
+ bun-types:
+ specifier: 1.0.26
+ version: 1.0.26
packages/mysql:
dependencies:
@@ -1759,6 +1765,12 @@ packages:
'@babel/types': 7.23.6
dev: false
+ /@types/bun@1.0.5:
+ resolution: {integrity: sha512-c14fs5QLLanldcZpX/GjIEKeo++NDzOlixUZ7IUWzN7AoBTisYyWxaxdXNhpAP5I1mPcd92Zagq8sdgTnUXWjg==}
+ dependencies:
+ bun-types: 1.0.26
+ dev: true
+
/@types/debug@4.1.12:
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
dependencies:
@@ -1858,7 +1870,12 @@ packages:
resolution: {integrity: sha512-D08YG6rr8X90YB56tSIuBaddy/UXAA9RKJoFvrsnogAum/0pmjkgi4+2nx96A330FmioegBWmEYQ+syqCFaveg==}
dependencies:
undici-types: 5.26.5
- dev: false
+
+ /@types/node@20.11.17:
+ resolution: {integrity: sha512-QmgQZGWu1Yw9TDyAP9ZzpFJKynYNeOvwMJmaxABfieQoVoiVOS6MN1WSpqpRcbeA5+RW82kraAVxCCJg+780Qw==}
+ dependencies:
+ undici-types: 5.26.5
+ dev: true
/@types/normalize-package-data@2.4.4:
resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==}
@@ -1886,6 +1903,12 @@ packages:
resolution: {integrity: sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==}
dev: false
+ /@types/ws@8.5.10:
+ resolution: {integrity: sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==}
+ dependencies:
+ '@types/node': 20.10.4
+ dev: true
+
/@typescript-eslint/eslint-plugin@6.10.0(@typescript-eslint/parser@6.10.0)(eslint@8.53.0)(typescript@5.3.3):
resolution: {integrity: sha512-uoLj4g2OTL8rfUQVx2AFO1hp/zja1wABJq77P6IclQs6I/m9GLrm7jCdgzZkvWdDCQf1uEvoa8s8CupsgWQgVg==}
engines: {node: ^16.0.0 || >=18.0.0}
@@ -2646,6 +2669,13 @@ packages:
semver: 7.5.4
dev: false
+ /bun-types@1.0.26:
+ resolution: {integrity: sha512-VcSj+SCaWIcMb0uSGIAtr8P92zq9q+unavcQmx27fk6HulCthXHBVrdGuXxAZbFtv7bHVjizRzR2mk9r/U8Nkg==}
+ dependencies:
+ '@types/node': 20.11.17
+ '@types/ws': 8.5.10
+ dev: true
+
/bundle-name@3.0.0:
resolution: {integrity: sha512-PKA4BeSvBpQKQ8iPOGCSiell+N8P+Tf1DlwqmYhpe2gAhKPHn8EYOxVT+ShuGmhg8lN8XiSlS80yiExKXrURlw==}
engines: {node: '>=12'}
@@ -8603,7 +8633,6 @@ packages:
/undici-types@5.26.5:
resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
- dev: false
/unherit@3.0.1:
resolution: {integrity: sha512-akOOQ/Yln8a2sgcLj4U0Jmx0R5jpIg2IUyRrWOzmEbjBtGzBdHtSeFKgoEcoH4KYIG/Pb8GQ/BwtYm0GCq1Sqg==}