Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

gh-75 fixing babel cache issue #288

Merged
merged 11 commits into from
Mar 17, 2022
194 changes: 116 additions & 78 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,110 +18,148 @@ function parseDotenvFile(path, verbose = false) {
return dotenv.parse(content)
}

module.exports = ({types: t}) => ({
name: 'dotenv-import',

pre() {
this.opts = {
envName: 'APP_ENV',
moduleName: '@env',
path: '.env',
whitelist: null,
blacklist: null,
allowlist: null,
blocklist: null,
safe: false,
allowUndefined: true,
verbose: false,
...this.opts,
}
module.exports = (api, options) => {
const t = api.types
this.env = {}
options = {
envName: 'APP_ENV',
moduleName: '@env',
path: '.env',
whitelist: null,
blacklist: null,
allowlist: null,
blocklist: null,
safe: false,
allowUndefined: true,
verbose: false,
...options,
}
const babelMode = process.env[options.envName] || (process.env.BABEL_ENV && process.env.BABEL_ENV !== 'undefined' && process.env.BABEL_ENV !== 'development' && process.env.BABEL_ENV) || process.env.NODE_ENV || 'development'

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You might want to wrap this in cache.using, so that if the env changes without restarting Node.js the plugin is reinstantiated.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thank you! I moved things around. I still don't know if it will reload properly in hot reload, but I don't think the environment mode can change without a js reload so I left that out. Hopefully it works in other tests when we release it in @next!

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am the second maintainer to this project so I'm not even sure about how all the babel features work

const localFilePath = options.path + '.local'
const modeFilePath = options.path + '.' + babelMode
const modeLocalFilePath = options.path + '.' + babelMode + '.local'

const babelMode = process.env[this.opts.envName] || (process.env.BABEL_ENV && process.env.BABEL_ENV !== 'undefined' && process.env.BABEL_ENV !== 'development' && process.env.BABEL_ENV) || process.env.NODE_ENV || 'development'
if (this.opts.verbose) {
console.log('dotenvMode', babelMode)
}
if (options.verbose) {
console.log('dotenvMode', babelMode)
}

api.cache.using(() => {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function passed to api.cache.using should return a value that, when it changes, causes the plugin to be re-instantiated. Since this one always returns undefined, the plugin will never be re-instantiated and it will always use the initial .env file.

I suggest doing something like this:

function mtime(filePath) {
  try {
    return fs.statSync(filePath).mtimeMs
  } catch {
    return null
  }
}

api.cache.using(() => mtime(options.path))
api.cache.using(() => mtime(localFilePath))
api.cache.using(() => mtime(modeFilePath))
api.cache.using(() => mtime(modeLocalFilePath))

let env

if (options.safe) {
  const parsed = parseDotenvFile(options.path, options.verbose)
  const localParsed = parseDotenvFile(localFilePath, options.verbose)
  const modeParsed = parseDotenvFile(modeFilePath, options.verbose)
  const modeLocalParsed = parseDotenvFile(modeLocalFilePath, options.verbose)
  
  env = Object.assign(Object.assign(Object.assign(parsed, modeParsed), localParsed), modeLocalParsed)
} else {
  dotenv.config({
    path: modeLocalFilePath,
    silent: true,
  })
  dotenv.config({
    path: modeFilePath,
    silent: true,
  })
  dotenv.config({
    path: localFilePath,
    silent: true,
  })
  dotenv.config({
    path: options.path,
  })
      
  env = process.env
}

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so when it is re-initialized, pre() is rerun?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so I found that this did not work in tests unfortunately. do I also need to use addExternalDependency?

if (options.safe) {
const parsed = parseDotenvFile(options.path, options.verbose)
const localParsed = parseDotenvFile(localFilePath, options.verbose)
const modeParsed = parseDotenvFile(modeFilePath, options.verbose)
const modeLocalParsed = parseDotenvFile(modeLocalFilePath, options.verbose)

if (this.opts.safe) {
const parsed = parseDotenvFile(this.opts.path, this.opts.verbose)
const localParsed = parseDotenvFile(this.opts.path + '.local')
const modeParsed = parseDotenvFile(this.opts.path + '.' + babelMode)
const modeLocalParsed = parseDotenvFile(this.opts.path + '.' + babelMode + '.local')
this.env = Object.assign(Object.assign(Object.assign(parsed, modeParsed), localParsed), modeLocalParsed)
this.env.NODE_ENV = process.env.NODE_ENV || babelMode
} else {
dotenv.config({
path: this.opts.path + '.' + babelMode + '.local',
path: modeLocalFilePath,
silent: true,
})
dotenv.config({
path: this.opts.path + '.' + babelMode,
path: modeFilePath,
silent: true,
})
dotenv.config({
path: this.opts.path + '.local',
path: localFilePath,
silent: true,
})
dotenv.config({
path: this.opts.path,
path: options.path,
})
this.env = process.env
}
},

visitor: {
ImportDeclaration(path, {opts}) {
if (path.node.source.value === opts.moduleName) {
for (const [idx, specifier] of path.node.specifiers.entries()) {
if (specifier.type === 'ImportDefaultSpecifier') {
throw path.get('specifiers')[idx].buildCodeFrameError('Default import is not supported')
}

if (specifier.type === 'ImportNamespaceSpecifier') {
throw path.get('specifiers')[idx].buildCodeFrameError('Wildcard import is not supported')
}

if (specifier.imported && specifier.local) {
const importedId = specifier.imported.name
const localId = specifier.local.name
})
api.addExternalDependency(options.path)
api.addExternalDependency(localFilePath)
api.addExternalDependency(modeFilePath)
api.addExternalDependency(modeLocalFilePath)
return ({
name: 'dotenv-import',

pre() {
this.opts = {
envName: 'APP_ENV',
moduleName: '@env',
path: '.env',
whitelist: null,
blacklist: null,
allowlist: null,
blocklist: null,
safe: false,
allowUndefined: true,
verbose: false,
...this.opts,
}

if (Array.isArray(opts.allowlist) && !opts.allowlist.includes(importedId)) {
throw path.get('specifiers')[idx].buildCodeFrameError(`"${importedId}" was not present in allowlist`)
} else if (Array.isArray(opts.whitelist) && !opts.whitelist.includes(importedId)) {
console.warn('[DEPRECATION WARNING] This option is will be deprecated soon. Use allowlist instead')
throw path.get('specifiers')[idx].buildCodeFrameError(`"${importedId}" was not whitelisted`)
}
if (this.opts.safe) {
const parsed = parseDotenvFile(this.opts.path, this.opts.verbose)
const localParsed = parseDotenvFile(localFilePath)
const modeParsed = parseDotenvFile(modeFilePath)
const modeLocalParsed = parseDotenvFile(modeLocalFilePath)
this.env = Object.assign(Object.assign(Object.assign(parsed, modeParsed), localParsed), modeLocalParsed)
this.env.NODE_ENV = process.env.NODE_ENV || babelMode
} else {
this.env = process.env
}
},

if (Array.isArray(opts.blocklist) && opts.blocklist.includes(importedId)) {
throw path.get('specifiers')[idx].buildCodeFrameError(`"${importedId}" was not present in blocklist`)
} else if (Array.isArray(opts.blacklist) && opts.blacklist.includes(importedId)) {
console.warn('[DEPRECATION WARNING] This option is will be deprecated soon. Use blocklist instead')
throw path.get('specifiers')[idx].buildCodeFrameError(`"${importedId}" was blacklisted`)
visitor: {
ImportDeclaration(path, {opts}) {
if (path.node.source.value === opts.moduleName) {
for (const [idx, specifier] of path.node.specifiers.entries()) {
if (specifier.type === 'ImportDefaultSpecifier') {
throw path.get('specifiers')[idx].buildCodeFrameError('Default import is not supported')
}

if (!opts.allowUndefined && !Object.prototype.hasOwnProperty.call(this.env, importedId)) {
throw path.get('specifiers')[idx].buildCodeFrameError(`"${importedId}" is not defined in ${opts.path}`)
if (specifier.type === 'ImportNamespaceSpecifier') {
throw path.get('specifiers')[idx].buildCodeFrameError('Wildcard import is not supported')
}

const binding = path.scope.getBinding(localId)
for (const refPath of binding.referencePaths) {
refPath.replaceWith(t.valueToNode(this.env[importedId]))
if (specifier.imported && specifier.local) {
const importedId = specifier.imported.name
const localId = specifier.local.name

if (Array.isArray(opts.allowlist) && !opts.allowlist.includes(importedId)) {
throw path.get('specifiers')[idx].buildCodeFrameError(`"${importedId}" was not present in allowlist`)
} else if (Array.isArray(opts.whitelist) && !opts.whitelist.includes(importedId)) {
console.warn('[DEPRECATION WARNING] This option is will be deprecated soon. Use allowlist instead')
throw path.get('specifiers')[idx].buildCodeFrameError(`"${importedId}" was not whitelisted`)
}

if (Array.isArray(opts.blocklist) && opts.blocklist.includes(importedId)) {
throw path.get('specifiers')[idx].buildCodeFrameError(`"${importedId}" was not present in blocklist`)
} else if (Array.isArray(opts.blacklist) && opts.blacklist.includes(importedId)) {
console.warn('[DEPRECATION WARNING] This option is will be deprecated soon. Use blocklist instead')
throw path.get('specifiers')[idx].buildCodeFrameError(`"${importedId}" was blacklisted`)
}

if (!opts.allowUndefined && !Object.prototype.hasOwnProperty.call(this.env, importedId)) {
throw path.get('specifiers')[idx].buildCodeFrameError(`"${importedId}" is not defined in ${opts.path}`)
}

const binding = path.scope.getBinding(localId)
for (const refPath of binding.referencePaths) {
refPath.replaceWith(t.valueToNode(this.env[importedId]))
}
}
}
}

path.remove()
}
},
MemberExpression(path, {opts}) {
if (path.get('object').matchesPattern('process.env')) {
const key = path.toComputedKey()
if (t.isStringLiteral(key)) {
const importedId = key.value
const value = (opts.env && importedId in opts.env) ? opts.env[importedId] : process.env[importedId]

path.replaceWith(t.valueToNode(value))
path.remove()
}
}
},
MemberExpression(path, {opts}) {
if (path.get('object').matchesPattern('process.env')) {
const key = path.toComputedKey()
if (t.isStringLiteral(key)) {
const importedId = key.value
const value = (opts.env && importedId in opts.env) ? opts.env[importedId] : process.env[importedId]

path.replaceWith(t.valueToNode(value))
}
}
},
},
},
})
})
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"dotenv": "^10.0.0"
},
"devDependencies": {
"@babel/core": "7.17.0",
"@babel/core": "7.17.2",
"codecov": "^3.8.3",
"jest": "27.4.4",
"jest-junit": "^13.0.0",
Expand Down
38 changes: 34 additions & 4 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,28 @@
resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.17.0.tgz#86850b8597ea6962089770952075dcaabb8dba34"
integrity sha512-392byTlpGWXMv4FbyWw3sAZ/FrW/DrwqLGXpy0mbyNe9Taqv1mg9yON5/o0cnr8XYCkFTZbC1eV+c+LAROgrng==

"@babel/core@7.17.0", "@babel/core@^7.1.0", "@babel/core@^7.12.16", "@babel/core@^7.7.2", "@babel/core@^7.7.5":
"@babel/core@7.17.2":
version "7.17.2"
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.17.2.tgz#2c77fc430e95139d816d39b113b31bf40fb22337"
integrity sha512-R3VH5G42VSDolRHyUO4V2cfag8WHcZyxdq5Z/m8Xyb92lW/Erm/6kM+XtRFGf3Mulre3mveni2NHfEUws8wSvw==
dependencies:
"@ampproject/remapping" "^2.0.0"
"@babel/code-frame" "^7.16.7"
"@babel/generator" "^7.17.0"
"@babel/helper-compilation-targets" "^7.16.7"
"@babel/helper-module-transforms" "^7.16.7"
"@babel/helpers" "^7.17.2"
"@babel/parser" "^7.17.0"
"@babel/template" "^7.16.7"
"@babel/traverse" "^7.17.0"
"@babel/types" "^7.17.0"
convert-source-map "^1.7.0"
debug "^4.1.0"
gensync "^1.0.0-beta.2"
json5 "^2.1.2"
semver "^6.3.0"

"@babel/core@^7.1.0", "@babel/core@^7.12.16", "@babel/core@^7.7.2", "@babel/core@^7.7.5":
version "7.17.0"
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.17.0.tgz#16b8772b0a567f215839f689c5ded6bb20e864d5"
integrity sha512-x/5Ea+RO5MvF9ize5DeVICJoVrNv0Mi2RnIABrZEKYvPEpldXwauPkgvYA17cKa6WpU3LoYvYbuEMFtSNFsarA==
Expand Down Expand Up @@ -170,6 +191,15 @@
"@babel/traverse" "^7.17.0"
"@babel/types" "^7.17.0"

"@babel/helpers@^7.17.2":
version "7.17.2"
resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.17.2.tgz#23f0a0746c8e287773ccd27c14be428891f63417"
integrity sha512-0Qu7RLR1dILozr/6M0xgj+DFPmi6Bnulgm9M8BVa9ZCWxDqlSnqt3cf8IDPB5m45sVXUZ0kuQAgUrdSFFH79fQ==
dependencies:
"@babel/template" "^7.16.7"
"@babel/traverse" "^7.17.0"
"@babel/types" "^7.17.0"

"@babel/highlight@^7.16.7":
version "7.16.10"
resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.16.10.tgz#744f2eb81579d6eea753c227b0f570ad785aba88"
Expand Down Expand Up @@ -4626,15 +4656,15 @@ xo@^0.47.0:
integrity sha512-QHRIpaPSG7tK7PX4K4fqe0V4EH1u2IkM7Pr356u1fKcVsZskw7i9gfEqyBLsnnc4e4Y8gnLtIqasLJsGPqM8sA==
dependencies:
"@eslint/eslintrc" "^1.0.4"
"@typescript-eslint/eslint-plugin" "*"
"@typescript-eslint/parser" "*"
"@typescript-eslint/eslint-plugin" "^5.4.0"
"@typescript-eslint/parser" "^5.4.0"
arrify "^3.0.0"
cosmiconfig "^7.0.1"
define-lazy-prop "^3.0.0"
eslint "^8.3.0"
eslint-config-prettier "^8.3.0"
eslint-config-xo "^0.39.0"
eslint-config-xo-typescript "*"
eslint-config-xo-typescript "^0.47.1"
eslint-formatter-pretty "^4.1.0"
eslint-import-resolver-webpack "^0.13.2"
eslint-plugin-ava "^13.1.0"
Expand Down