Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
name: Node.js CI

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
lint:
runs-on: ubuntu-latest

strategy:
matrix:
node-version: [16.x, 18.x]

steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: ${{ matrix.version }}
- run: npm ci
- run: npm run lint

test:
runs-on: ubuntu-latest

strategy:
matrix:
node-version: [16.x, 18.x]

steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: ${{ matrix.version }}
- run: npm ci
- run: npm run test

build:
runs-on: ubuntu-latest

strategy:
matrix:
node-version: [16.x, 18.x]

steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: ${{ matrix.version }}
- run: npm ci
- run: npm run build:next
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,5 @@ typings/
dist
**/.rts*
sample.js
.parcel-cache
.vscode
6 changes: 6 additions & 0 deletions .mocharc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module.exports = {
"spec": "test/**/*.spec.ts",
"watch-extensions": "ts",
"require": "ts-node/register",
"exit": true,
}
15 changes: 0 additions & 15 deletions .travis.yml

This file was deleted.

13 changes: 0 additions & 13 deletions .vscode/settings.json

This file was deleted.

30 changes: 20 additions & 10 deletions lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
#!/usr/bin/env node

import cp from "child_process";
import meow from "meow";
import { syncEnv } from "./lib";
import cp from 'child_process';
import meow from 'meow';
import { syncEnv } from './lib';

const cli = meow(
`
Expand All @@ -27,19 +27,29 @@ const cli = meow(
`,
{
flags: {
env: {
type: 'string',
alias: 'e',
},
sample: {
type: "string"
}
}
type: 'string',
alias: 's',
},
samples: {
type: 'string',
alias: 'S',
},
},
}
);

const { sample, s, env, e, samples, S } = cli.flags;
const { sample, env, samples } = cli.flags;

syncEnv(sample || s, env || e, samples || S)
.then(sampleEnv => cp.exec(`git add ${sampleEnv}`))
syncEnv(sample, env, samples)
.then((sampleEnv) => cp.exec(`git add ${sampleEnv}`))
.catch(({ message, code }) => {
console.log(message);
// eslint-disable-next-line no-console
console.error(message);
process.exit(code);
});

Expand Down
95 changes: 53 additions & 42 deletions lib/lib.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { resolve, basename } from "path";
import fs from "fs";
import { resolve, basename } from 'path';
import fs from 'fs';
import os from 'os';
import parseEnv from "parse-dotenv";
import globby from "globby";
import pkgConf from "pkg-conf";
import parseEnv from 'parse-dotenv';
import globby from 'globby';
import pkgConf from 'pkg-conf';

const DEFAULT_ENV_PATH = resolve(process.cwd(), ".env");
const DEFAULT_SAMPLE_ENV = resolve(process.cwd(), ".env.example");
const DEFAULT_ENV_PATH = resolve(process.cwd(), '.env');
const DEFAULT_SAMPLE_ENV = resolve(process.cwd(), '.env.example');

interface EnvObject {
[key: string]: any;
Expand All @@ -24,36 +24,38 @@ export const getObjKeys = (obj: object) => Object.keys(obj);

export const envToString = (parsed: EnvObject) =>
getObjKeys(parsed)
.map(key => `${key}=${parsed[key] || ""}`)
.map((key) => `${key}=${parsed[key] || ''}`)
.join(os.EOL)
.replace(/(__\w+_\d+__=)/g, "");
.replace(/(__\w+_\d+__=)/g, '');

export const writeToSampleEnv = (path: string, parsedEnv: object) => {
try {
fs.writeFileSync(path, envToString(parsedEnv));
} catch (e) {
throw new Error(`Sync failed. ${e.message}`);
} catch (error: unknown) {
throw new Error(
`Sync failed. ${error instanceof Error ? error.message : String(error)}`
);
}
};

export const emptyObjProps = (obj: EnvObject) => {
const objCopy = { ...obj };
Object.keys(objCopy).forEach(key => {
if (objCopy[key].includes("#")) {
Object.keys(objCopy).forEach((key) => {
if (objCopy[key].includes('#')) {
if (objCopy[key].match(/(".*"|'.*')/g)) {
const objArr = objCopy[key].split(/(".*"|'.*')/);
objCopy[key] = objArr.slice(-1)[0].trim();
} else {
const objArr = objCopy[key].split("#");
const objArr = objCopy[key].split('#');
objCopy[key] = `#${objArr.slice(-1)[0]}`;
}

return;
}

/* istanbul ignore else */
if (!key.startsWith("__COMMENT_")) {
objCopy[key] = "";
if (!key.startsWith('__COMMENT_')) {
objCopy[key] = '';
}
});

Expand All @@ -65,21 +67,22 @@ export const getUniqueVarsFromEnvs = async (
envExample: EnvObject,
config: Config = {}
) => {
let ignoreKeys = config.preserve || [];
const ignoreKeys = config.preserve || [];

const uniqueKeys = new Set(getObjKeys(env));
const uniqueKeysArray: Array<string> = Array.from(uniqueKeys);

let uniqueFromSource = uniqueKeysArray.map((key: string) => {
if (key.startsWith("__COMMENT_")) return { [key]: env[key] };
return { [key]: envExample[key] || "" };
const uniqueFromSource = uniqueKeysArray.map((key: string) => {
if (key.startsWith('__COMMENT_')) return { [key]: env[key] };
return { [key]: envExample[key] || '' };
});

let presevedVars = getObjKeys(envExample)
.map(key => ({ [key]: envExample[key] }))
.filter(env => {
return ignoreKeys.length && ignoreKeys.includes(getObjKeys(env)[0]);
});
const presevedVars = getObjKeys(envExample)
.map((key) => ({ [key]: envExample[key] }))
.filter(
// eslint-disable-next-line no-shadow
(env) => ignoreKeys.length && ignoreKeys.includes(getObjKeys(env)[0])
);

return [...uniqueFromSource, ...presevedVars];
};
Expand All @@ -90,45 +93,53 @@ export const syncWithSampleEnv = async (
initialConfig?: Config
) => {
// We do this so we can pass it via test as well
let config: Config = initialConfig || (await pkgConf("sync-dotenv")) as any;
const config: Config =
initialConfig || ((await pkgConf('sync-dotenv')) as any);

// Set defaults
config.comments = typeof config.comments === 'undefined' ? true : config.comments;
config.emptyLines = typeof config.emptyLines === 'undefined' ? true : config.comments;

let sourceEnv = emptyObjProps(
parseEnv(envPath, { emptyLines: !!config.emptyLines, comments: !!config.comments })
config.comments =
typeof config.comments === 'undefined' ? true : config.comments;
config.emptyLines =
typeof config.emptyLines === 'undefined' ? true : config.comments;

const sourceEnv = emptyObjProps(
parseEnv(envPath, {
emptyLines: !!config.emptyLines,
comments: !!config.comments,
})
);
let targetEnv = parseEnv(envExamplePath);
const targetEnv = parseEnv(envExamplePath);

const uniqueVars = await getUniqueVarsFromEnvs(sourceEnv, targetEnv, config);
let envCopy: EnvObject = {};
uniqueVars.forEach(env => {
let [key] = getObjKeys(env);
const envCopy: EnvObject = {};
uniqueVars.forEach((env) => {
const [key] = getObjKeys(env);
envCopy[key] = env[key];
});

writeToSampleEnv(envExamplePath, envCopy);
};

const exit = (message: string, code: number = 1) =>
// eslint-disable-next-line prefer-promise-reject-errors
Promise.reject({ message, code });

export const syncEnv = async (
sampleEnv?: string,
source?: string,
samples?: string
): Promise<{ msg: string; code: number } | string> => {
if (sampleEnv && (sampleEnv === ".env" || basename(sampleEnv) === ".env"))
return exit("Cannot sync .env with .env");
if (sampleEnv && (sampleEnv === '.env' || basename(sampleEnv) === '.env'))
return exit('Cannot sync .env with .env');

const SAMPLE_ENV_PATHS: string[] = !samples
? [resolve(process.cwd(), sampleEnv || DEFAULT_SAMPLE_ENV)]
: globby
.sync(samples)
.map((sample: string) => resolve(process.cwd(), sample));
.sync(samples)
.map((sample: string) => resolve(process.cwd(), sample));

let envPath = source
// eslint-disable-next-line no-nested-ternary
const envPath = source
? fileExists(source)
? source
: null
Expand All @@ -146,9 +157,9 @@ export const syncEnv = async (

const sourcePath = envPath;

for (let samplePath of SAMPLE_ENV_PATHS) {
for (const samplePath of SAMPLE_ENV_PATHS) {
await syncWithSampleEnv(sourcePath, samplePath);
}

return Promise.resolve(SAMPLE_ENV_PATHS.join(" "));
return Promise.resolve(SAMPLE_ENV_PATHS.join(' '));
};
Loading