diff --git a/.babelrc b/.babelrc index 9aa643c0a..26bf4050d 100644 --- a/.babelrc +++ b/.babelrc @@ -10,5 +10,17 @@ ], "ignore": [ "src/assets" - ] + ], + "overrides": [{ + "test": "./src/helpers/import-helper.js", + "presets": [ + ["@babel/env", { + "targets": { + "node": "10.0" + }, + "shippedProposals": true, + "exclude": ["proposal-dynamic-import"], + }] + ], + }] } diff --git a/.gitignore b/.gitignore index b03a1ad5b..1118f2061 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ pids # Editor files .vscode +.idea # Directory for instrumented libs generated by jscoverage/JSCover lib-cov diff --git a/package.json b/package.json index db99c8881..487c17a2d 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "pg": "latest", "pg-hstore": "latest", "prettier": "^2.4.1", + "semver": "^7.3.5", "sequelize": "^6.9.0", "sqlite3": "latest", "through2": "^4.0.2" diff --git a/src/helpers/config-helper.js b/src/helpers/config-helper.js index 17b9ac6ea..dd37da9da 100644 --- a/src/helpers/config-helper.js +++ b/src/helpers/config-helper.js @@ -5,6 +5,7 @@ import _ from 'lodash'; import { promisify } from 'util'; import helpers from './index'; import getYArgs from '../core/yargs'; +import importHelper from './import-helper'; const args = getYArgs().argv; @@ -12,38 +13,33 @@ const api = { config: undefined, rawConfig: undefined, error: undefined, - init() { - return Promise.resolve() - .then(() => { - let config; - - if (args.url) { - config = api.parseDbUrl(args.url); - } else { - try { - config = require(api.getConfigFile()); - } catch (e) { - api.error = e; - } - } - return config; - }) - .then((config) => { - if (typeof config === 'object' || config === undefined) { - return config; - } else if (config.length === 1) { - return promisify(config)(); - } else { - return config(); - } - }) - .then((config) => { - api.rawConfig = config; - }) - .then(() => { - // Always return the full config api - return api; - }); + async init() { + let config; + + try { + if (args.url) { + config = api.parseDbUrl(args.url); + } else { + const module = await importHelper.importModule(api.getConfigFile()); + config = await module.default; + } + } catch (e) { + api.error = e; + } + + if (typeof config === 'function') { + // accepts callback parameter + if (config.length === 1) { + config = await promisify(config)(); + } else { + // returns a promise. + config = await config(); + } + } + + api.rawConfig = config; + + return api; }, getConfigFile() { if (args.config) { diff --git a/src/helpers/dummy-file.js b/src/helpers/dummy-file.js new file mode 100644 index 000000000..243883976 --- /dev/null +++ b/src/helpers/dummy-file.js @@ -0,0 +1 @@ +// this file is imported by import-helper to detect whether dynamic imports are supported. diff --git a/src/helpers/import-helper.js b/src/helpers/import-helper.js new file mode 100644 index 000000000..516ce9a21 --- /dev/null +++ b/src/helpers/import-helper.js @@ -0,0 +1,35 @@ +async function supportsDynamicImport() { + try { + // imports are cached. + // no need to worry about perf here. + // Don't remove .js: extension must be included for ESM imports! + await import('./dummy-file.js'); + return true; + } catch (e) { + return false; + } +} + +/** + * Imports a JSON, CommonJS or ESM module + * based on feature detection. + * + * @param modulePath path to the module to import + * @returns {Promise} the imported module. + */ +async function importModule(modulePath) { + // JSON modules are still behind a flag. Fallback to require for now. + // https://nodejs.org/api/esm.html#json-modules + if (!modulePath.endsWith('.json') && (await supportsDynamicImport())) { + return import(modulePath); + } + + // mimics what `import()` would return for + // cjs modules. + return { default: require(modulePath) }; +} + +module.exports = { + supportsDynamicImport, + importModule, +}; diff --git a/test/db/migrate.test.js b/test/db/migrate.test.js index 384796629..9690d95c0 100644 --- a/test/db/migrate.test.js +++ b/test/db/migrate.test.js @@ -3,6 +3,7 @@ const expect = require('expect.js'); const Support = require(__dirname + '/../support'); const helpers = require(__dirname + '/../support/helpers'); const gulp = require('gulp'); +const semver = require('semver'); const _ = require('lodash'); [ @@ -25,7 +26,7 @@ const _ = require('lodash'); let configPath = 'config/'; let migrationFile = options.migrationFile || 'createPerson'; const config = _.assign({}, helpers.getTestConfig(), options.config); - let configContent = JSON.stringify(config); + let configContent = JSON.stringify(config, null, 2); if (!migrationFile.match(/\.(cjs|ts)$/)) { migrationFile = migrationFile + '.js'; @@ -70,7 +71,11 @@ const _ = require('lodash'); it('creates a SequelizeMeta table', function (done) { const self = this; - prepare(() => { + prepare((e) => { + if (e) { + return done(e); + } + helpers.readTables(self.sequelize, (tables) => { expect(tables).to.have.length(2); expect(tables).to.contain('SequelizeMeta'); @@ -293,6 +298,58 @@ describe(Support.getTestDialectTeaser('db:migrate'), () => { }); }); +describeOnlyForESM(Support.getTestDialectTeaser('db:migrate'), () => { + describe('with config.mjs', () => { + const prepare = function (callback) { + const config = helpers.getTestConfig(); + const configContent = 'export default ' + JSON.stringify(config); + let result = ''; + + return gulp + .src(Support.resolveSupportPath('tmp')) + .pipe(helpers.clearDirectory()) + .pipe(helpers.runCli('init')) + .pipe(helpers.removeFile('config/config.json')) + .pipe(helpers.copyMigration('createPerson.js')) + .pipe(helpers.overwriteFile(configContent, 'config/config.mjs')) + .pipe(helpers.runCli('db:migrate --config config/config.mjs')) + .on('error', (e) => { + callback(e); + }) + .on('data', (data) => { + result += data.toString(); + }) + .on('end', () => { + callback(null, result); + }); + }; + + it('creates a SequelizeMeta table', function (done) { + prepare((e) => { + if (e) { + return done(e); + } + + helpers.readTables(this.sequelize, (tables) => { + expect(tables).to.have.length(2); + expect(tables).to.contain('SequelizeMeta'); + done(); + }); + }); + }); + + it('creates the respective table', function (done) { + prepare(() => { + helpers.readTables(this.sequelize, (tables) => { + expect(tables).to.have.length(2); + expect(tables).to.contain('Person'); + done(); + }); + }); + }); + }); +}); + describe(Support.getTestDialectTeaser('db:migrate'), () => { describe('with config.json and url option', () => { const prepare = function (callback) { @@ -525,3 +582,11 @@ describe(Support.getTestDialectTeaser('db:migrate'), () => { }); }); }); + +function describeOnlyForESM(title, fn) { + if (semver.satisfies(process.version, '^12.20.0 || ^14.13.1 || >=16.0.0')) { + describe(title, fn); + } else { + describe.skip(title, fn); + } +}