Skip to content

Commit

Permalink
feat: create template on vite build and inject script tag (#8)
Browse files Browse the repository at this point in the history
Co-authored-by: Anbraten <anton@ju60.de>

BREAKING CHANGE: `env-config.template.js` will be generated during the vite build, provide the needed variables in `vite.config.ts`
  • Loading branch information
lukashass committed Jan 25, 2022
1 parent 6efa912 commit 8e92ac5
Show file tree
Hide file tree
Showing 5 changed files with 77 additions and 81 deletions.
34 changes: 6 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,43 +2,21 @@

Vite plugin for providing configurations from environment variables at runtime.

The generated template can be populated with [envsubst](https://github.com/a8m/envsubst) in production.

## Usage

Add `envConfig` plugin to `vite.config.js / vite.config.ts`:
Add `envConfig` plugin to `vite.config.js / vite.config.ts` and provide a list of environment variable names:

```js
// vite.config.js / vite.config.ts
import { envConfig } from '@geprog/vite-plugin-env-config';

export default {
plugins: [envConfig()],
plugins: [envConfig({ variables: ['BACKEND_URL'] })],
};
```

Add a template file to your project at `./src/assets/env-config.template.js`:

```js
// ./src/assets/env-config.template.js
(function (window) {
window.env = window.env || {};

// add a line for each environment variable
window['env']['BACKEND_URL'] = '${BACKEND_URL}';
})(this);
```

Include `/assets/env-config.js` in your `index.html`:

```html
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<script src="/assets/env-config.js"></script>
</head>
</html>
```

To access the environment variables use the built-in getter:

```ts
Expand All @@ -57,7 +35,7 @@ Instead of building your frontend on startup,
you can use a config template like the one above and populate it using `envsubst`:

```Dockerfile
CMD ["/bin/sh", "-c", "envsubst < ./src/assets/env-config.template.js > ./dist/assets/env-config.js && exec nginx -g 'daemon off;'"]
CMD ["/bin/sh", "-c", "envsubst < ./dist/assets/env-config.template.js > ./dist/assets/env-config.js && exec nginx -g 'daemon off;'"]
```

`@geprog/vite-plugin-env-config` provides the same functionality for your development environment.
`@geprog/vite-plugin-env-config` generates the required template from a list of variable names and provides the already populated file via the dev-server during development.
65 changes: 38 additions & 27 deletions src/envConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,54 +2,65 @@ import fs from 'fs';
import path from 'path';
import type { Plugin } from 'vite';

/**
* This plugin serves the `/assets/env-config.js` file with proper environment variables
*/

export function renderTemplateWithEnvVars(content: string): string {
let result = content;
const regex = /\$\{([a-zA-Z_]+[a-zA-Z0-9_]*?)\}/g;

let matched: RegExpExecArray | null;
while ((matched = regex.exec(content))) {
result = result.replace(`\${${matched[1]}}`, process.env[matched[1]] || '');
}
export function createEnvConfigContent(variables: string[], template: boolean): string {
let templateContent = '';
templateContent += '(function(window){window.env=window.env||{};';
variables.forEach((variable) => {
if (template) {
templateContent += `window.env['${variable}']='\${${variable}}';`;
} else {
templateContent += `window.env['${variable}']='${process.env[variable] || ''}';`;
}
});
templateContent += '})(this);';
return templateContent;
}

return result;
export interface EnvConfigOptions {
/**
* List of environment variables (case-sensitive) to make available in the frontend.
* @default []
* @example ['BACKEND_URL']
*/
variables: string[];
}

export function envConfig(): Plugin {
let root: string | undefined;
export function envConfig(userOptions: Partial<EnvConfigOptions> = {}): Plugin {
let root: string;
return {
name: 'vite-plugin-env-config',

configResolved(config) {
root = config.root;
},
configureServer(server) {
if (!root) {
throw new Error('envConfig plugin requires a root directory');
}
let appConfigContent = '';
const TEMPLATE_PATH = path.join(root, 'src', 'assets', 'env-config.template.js');

void (async () => {
const content = (await fs.promises.readFile(TEMPLATE_PATH)).toString();
appConfigContent = renderTemplateWithEnvVars(content);
})();
configureServer(server) {
const envConfigContent = createEnvConfigContent(userOptions.variables || [], false);

server.middlewares.use((req, res, next) => {
if (req.url === '/assets/env-config.js') {
// standard headers
res.setHeader('Content-Type', 'application/javascript; charset=utf-8');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Content-Length', Buffer.byteLength(appConfigContent, 'utf8'));
res.setHeader('Content-Length', Buffer.byteLength(envConfigContent, 'utf8'));

// body
res.end(appConfigContent, 'utf8');
res.end(envConfigContent, 'utf8');
return;
}
next();
});
},

closeBundle() {
const templateContent = createEnvConfigContent(userOptions.variables || [], true);

const TEMPLATE_PATH = path.join(root, 'dist', 'assets', 'env-config.template.js');
fs.writeFileSync(TEMPLATE_PATH, templateContent, 'utf8');
},

transformIndexHtml(html) {
return html.replace('</head>', '<script src="/assets/env-config.js"></script></head>');
},
};
}
4 changes: 2 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { envConfig } from './envConfig';
import { envConfig, EnvConfigOptions } from './envConfig';
import { EnvConfig, getEnvConfig } from './getEnvConfig';

export { EnvConfig, envConfig, getEnvConfig };
export { EnvConfig, envConfig, EnvConfigOptions, getEnvConfig };
7 changes: 7 additions & 0 deletions test/__snapshots__/envConfig.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`createEnvConfigContent creates an env-config containing given variables and placeholders 1`] = `"(function(window){window.env=window.env||{};window.env['BACKEND_URL']='\${BACKEND_URL}';window.env['FOO']='\${FOO}';})(this);"`;

exports[`createEnvConfigContent creates an env-config containing given variables and their values 1`] = `"(function(window){window.env=window.env||{};window.env['BACKEND_URL']='http://localhost:4000';window.env['FOO']='bar';window.env['NOT_SET']='';})(this);"`;

exports[`createEnvConfigContent creates an env-config that does not set any variables 1`] = `"(function(window){window.env=window.env||{};})(this);"`;
48 changes: 24 additions & 24 deletions test/envConfig.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { renderTemplateWithEnvVars } from '~/envConfig';
import { createEnvConfigContent } from '~/envConfig';

describe('renderTemplateWithEnvVars', () => {
describe('createEnvConfigContent', () => {
const env = process.env;

beforeEach(() => {
Expand All @@ -12,40 +12,40 @@ describe('renderTemplateWithEnvVars', () => {
process.env = env;
});

it('should replace ${BACKEND_URL} with the respective environment variable', () => {
it('creates an env-config containing given variables and their values', () => {
// given
const envConfigTemplate = `
(function (window) {
window.env = window.env || {};
// Environment variables
window['env']['BACKEND_URL'] = '\${BACKEND_URL}';
})(this);
`;
const variables = ['BACKEND_URL', 'FOO', 'NOT_SET'];
process.env.BACKEND_URL = 'http://localhost:4000';
process.env.FOO = 'bar';

// when
const result = renderTemplateWithEnvVars(envConfigTemplate);
const result = createEnvConfigContent(variables, false);

// then
expect(result).toBe(envConfigTemplate.replace('${BACKEND_URL}', 'http://localhost:4000'));
expect(result).toContain(`window.env['BACKEND_URL']='${process.env.BACKEND_URL}';`);
expect(result).toContain(`window.env['FOO']='${process.env.FOO}';`);
expect(result).toContain(`window.env['NOT_SET']='';`);
expect(result).toMatchSnapshot();
});

it('should replace ${BACKEND_URL} with an empty string when the respective environment variable is not set', () => {
it('creates an env-config containing given variables and placeholders', () => {
// given
const envConfigTemplate = `
(function (window) {
window.env = window.env || {};
// Environment variables
window['env']['BACKEND_URL'] = '\${BACKEND_URL}';
})(this);
`;
const variables = ['BACKEND_URL', 'FOO'];

// when
const result = renderTemplateWithEnvVars(envConfigTemplate);
const result = createEnvConfigContent(variables, true);

// then
expect(result).toBe(envConfigTemplate.replace('${BACKEND_URL}', ''));
expect(result).toContain(`window.env['BACKEND_URL']='\${BACKEND_URL}';`);
expect(result).toContain(`window.env['FOO']='\${FOO}';`);
expect(result).toMatchSnapshot();
});

it('creates an env-config that does not set any variables', () => {
// when
const result = createEnvConfigContent([], true);

// then
expect(result).toMatchSnapshot();
});
});

0 comments on commit 8e92ac5

Please sign in to comment.