From c838ffb7f3ad281743675bc3612c6dc9979a6e0e Mon Sep 17 00:00:00 2001 From: Joakim Carlstein Date: Fri, 9 Feb 2024 13:50:16 +0100 Subject: [PATCH] fix(typescript): load config written in TypeScript without the `typescript` package when using Bun, Deno or `tsx` --- .changeset/breezy-sheep-yawn.md | 5 ++ .changeset/cool-spies-behave.md | 5 ++ .changeset/tidy-shrimps-hide.md | 5 ++ docs/src/content/docs/guides/typescript.mdx | 12 ++- packages/cli/package.json | 4 +- packages/cli/src/cli.ts | 83 +++++++++++++++------ packages/cli/src/deno.d.ts | 6 ++ packages/cli/src/get-config.ts | 11 ++- pnpm-lock.yaml | 33 +++++++- 9 files changed, 132 insertions(+), 32 deletions(-) create mode 100644 .changeset/breezy-sheep-yawn.md create mode 100644 .changeset/cool-spies-behave.md create mode 100644 .changeset/tidy-shrimps-hide.md create mode 100644 packages/cli/src/deno.d.ts 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==}