Skip to content

Commit

Permalink
gh-75 fixing babel cache issue (#288)
Browse files Browse the repository at this point in the history
* experimenting with new babel feature

* incremented to fixed v

* listening to changes on file change

* watching for changes

* duplicated dotenv filling at cache change

* rearranged reloads

* lint fixes

* re-instantiate plugin on file changes

* fix safe mode for dotenv

* lint fixes

* added safe test for coverage

Co-authored-by: Kemal Ahmed <kemal@ukko.ag>
  • Loading branch information
goatandsheep and Kemal Ahmed committed Mar 17, 2022
1 parent a553e6d commit 4684b5e
Show file tree
Hide file tree
Showing 8 changed files with 242 additions and 96 deletions.
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,11 @@ reports

.vs
.idea

.yarn/*
!.yarn/patches
!.yarn/releases
!.yarn/plugins
!.yarn/sdks
!.yarn/versions
.pnp.*
8 changes: 8 additions & 0 deletions __tests__/__fixtures__/default-safe/.babelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"plugins": [
["../../../", {
"path": "__tests__/__fixtures__/default/.env",
"safe": true
}]
]
}
2 changes: 2 additions & 0 deletions __tests__/__fixtures__/default-safe/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
API_KEY=abc123
DEV_USERNAME=username
4 changes: 4 additions & 0 deletions __tests__/__fixtures__/default-safe/source.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import {API_KEY, DEV_USERNAME} from '@env'

console.log(API_KEY)
console.log(DEV_USERNAME)
7 changes: 7 additions & 0 deletions __tests__/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,13 @@ describe('react-native-dotenv', () => {
expect(code).toBe('console.log("i win");\nconsole.log("username");')
})

it('should prioritize environment variables over variables defined in .env even when safe', () => {
process.env.API_KEY = 'i win'

const {code} = transformFileSync(FIXTURES + 'default-safe/source.js')
expect(code).toBe('console.log("i win");\nconsole.log("username");')
})

it('should load custom env file', () => {
const {code} = transformFileSync(FIXTURES + 'filename/source.js')
expect(code).toBe('console.log("abc123456");\nconsole.log("username123456");')
Expand Down
269 changes: 178 additions & 91 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const {readFileSync} = require('fs')
const {readFileSync, statSync} = require('fs')
const dotenv = require('dotenv')

function parseDotenvFile(path, verbose = false) {
Expand All @@ -18,110 +18,197 @@ 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,
function safeObjectAssign(targetObject, sourceObject, exceptions = []) {
const keys = Object.keys(targetObject)
for (let i = 0, length = keys.length; i < length; i++) {
if (targetObject[keys[i]] && sourceObject[keys[i]]) {
targetObject[keys[i]] = sourceObject[keys[i]]
}
}

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)
for (let j = 0, length = exceptions.length; j < length; j++) {
if (sourceObject[exceptions[j]]) {
targetObject[exceptions[j]] = sourceObject[exceptions[j]]
}
}

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',
silent: true,
})
dotenv.config({
path: this.opts.path + '.' + babelMode,
silent: true,
})
dotenv.config({
path: this.opts.path + '.local',
silent: true,
})
dotenv.config({
path: this.opts.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')
}
return targetObject
}

if (specifier.type === 'ImportNamespaceSpecifier') {
throw path.get('specifiers')[idx].buildCodeFrameError('Wildcard import is not supported')
}
function mtime(filePath) {
try {
return statSync(filePath).mtimeMs
} catch {
return null
}
}

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'
const localFilePath = options.path + '.local'
const modeFilePath = options.path + '.' + babelMode
const modeLocalFilePath = options.path + '.' + babelMode + '.local'

if (specifier.imported && specifier.local) {
const importedId = specifier.imported.name
const localId = specifier.local.name
if (options.verbose) {
console.log('dotenvMode', babelMode)
}

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`)
}
api.cache.using(() => mtime(options.path))
api.cache.using(() => mtime(localFilePath))
api.cache.using(() => mtime(modeFilePath))
api.cache.using(() => mtime(modeLocalFilePath))

const dotenvTemporary = Object.assign({}, process.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)

this.env = safeObjectAssign(Object.assign(Object.assign(Object.assign(parsed, modeParsed), localParsed), modeLocalParsed), dotenvTemporary, ['NODE_ENV', 'BABEL_ENV', options.envName])
this.env.NODE_ENV = process.env.NODE_ENV || babelMode
} else {
dotenv.config({
path: modeLocalFilePath,
silent: true,
})
dotenv.config({
path: modeFilePath,
silent: true,
})
dotenv.config({
path: localFilePath,
silent: true,
})
dotenv.config({
path: options.path,
})
this.env = process.env
this.env = Object.assign(this.env, dotenvTemporary)
}

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.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`)
const dotenvTemporary = Object.assign({}, process.env)
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 = safeObjectAssign(Object.assign(Object.assign(Object.assign(parsed, modeParsed), localParsed), modeLocalParsed), dotenvTemporary, ['NODE_ENV', 'BABEL_ENV', options.envName])
this.env.NODE_ENV = process.env.NODE_ENV || babelMode
} else {
dotenv.config({
path: modeLocalFilePath,
silent: true,
})
dotenv.config({
path: modeFilePath,
silent: true,
})
dotenv.config({
path: localFilePath,
silent: true,
})
dotenv.config({
path: options.path,
})
this.env = process.env
this.env = Object.assign(this.env, dotenvTemporary)
}
},

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

0 comments on commit 4684b5e

Please sign in to comment.