Skip to content

Commit 24b1703

Browse files
authored
Merge pull request #121 from launchcodedev/webpack5-compat
Webpack v5 Support
2 parents 47f65a8 + 8e06a33 commit 24b1703

File tree

26 files changed

+701
-76
lines changed

26 files changed

+701
-76
lines changed

.github/workflows/cypress.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,3 +76,12 @@ jobs:
7676
start: yarn serve -l 8994 ./dist
7777
env:
7878
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
79+
- uses: cypress-io/github-action@v2
80+
with:
81+
group: app-config-webpack5
82+
working-directory: tests/webpack-projects/webpack5
83+
install: false
84+
record: true
85+
start: yarn serve -l 8995 ./dist
86+
env:
87+
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}

app-config-webpack/package.json

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,15 @@
3535
},
3636
"peerDependencies": {
3737
"@app-config/main": "^2.3.4",
38-
"html-webpack-plugin": "4",
39-
"webpack": "4"
38+
"html-webpack-plugin": "4 || 5",
39+
"webpack": "4 || 5"
4040
},
4141
"devDependencies": {
4242
"@app-config/main": "^2.3.4",
4343
"@types/loader-utils": "1",
44-
"html-webpack-plugin": "4",
45-
"webpack": "4"
44+
"@webpack-cli/serve": "1",
45+
"html-webpack-plugin": "5",
46+
"webpack": "5"
4647
},
4748
"prettier": "@lcdev/prettier",
4849
"jest": {

app-config-webpack/src/index.test.ts

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,11 @@ describe('frontend-webpack-project example', () => {
2929
it('builds the project without header injection', async () => {
3030
await new Promise<void>((done, reject) => {
3131
webpack([createOptions({})], (err, stats) => {
32-
if (err) reject(err);
32+
if (err) return reject(err);
33+
if (!stats) return reject(new Error('no stats'));
3334
if (stats.hasErrors()) reject(stats.toString());
3435

35-
const { children } = stats.toJson();
36+
const { children } = stats.toJson({ source: true });
3637
const [{ modules = [] }] = children || [];
3738

3839
expect(
@@ -51,10 +52,11 @@ describe('frontend-webpack-project example', () => {
5152

5253
await new Promise<void>((done, reject) => {
5354
webpack([createOptions({})], (err, stats) => {
54-
if (err) reject(err);
55+
if (err) return reject(err);
56+
if (!stats) return reject(new Error('no stats'));
5557
if (stats.hasErrors()) reject(stats.toString());
5658

57-
const { children } = stats.toJson();
59+
const { children } = stats.toJson({ source: true });
5860
const [{ modules = [] }] = children || [];
5961

6062
expect(
@@ -73,10 +75,11 @@ describe('frontend-webpack-project example', () => {
7375

7476
await new Promise<void>((done, reject) => {
7577
webpack([createOptions({ noGlobal: true })], (err, stats) => {
76-
if (err) reject(err);
78+
if (err) return reject(err);
79+
if (!stats) return reject(new Error('no stats'));
7780
if (stats.hasErrors()) reject(stats.toString());
7881

79-
const { children } = stats.toJson();
82+
const { children } = stats.toJson({ source: true });
8083
const [{ modules = [] }] = children || [];
8184

8285
expect(
@@ -97,10 +100,11 @@ describe('frontend-webpack-project example', () => {
97100

98101
await new Promise<void>((done, reject) => {
99102
webpack([createOptions({ intercept: /@app-config\/main/ })], (err, stats) => {
100-
if (err) reject(err);
103+
if (err) return reject(err);
104+
if (!stats) return reject(new Error('no stats'));
101105
if (stats.hasErrors()) reject(stats.toString());
102106

103-
const { children } = stats.toJson();
107+
const { children } = stats.toJson({ source: true });
104108
const [{ modules = [] }] = children || [];
105109

106110
expect(
@@ -121,7 +125,8 @@ describe('frontend-webpack-project example', () => {
121125
new Promise<void>((done, reject) => {
122126
webpack([createOptions({})], (err, stats) => {
123127
if (err) return reject(err);
124-
if (stats.hasErrors()) return reject(stats.toString());
128+
if (!stats) return reject(new Error('no stats'));
129+
if (stats.hasErrors()) reject(stats.toString());
125130

126131
done();
127132
});
@@ -136,10 +141,11 @@ describe('frontend-webpack-project example', () => {
136141
webpack(
137142
[createOptions({ loading: { environmentVariableName: 'MY_CONFIG' } })],
138143
(err, stats) => {
139-
if (err) reject(err);
144+
if (err) return reject(err);
145+
if (!stats) return reject(new Error('no stats'));
140146
if (stats.hasErrors()) reject(stats.toString());
141147

142-
const { children } = stats.toJson();
148+
const { children } = stats.toJson({ source: true });
143149
const [{ modules = [] }] = children || [];
144150

145151
expect(
@@ -154,15 +160,16 @@ describe('frontend-webpack-project example', () => {
154160
});
155161
});
156162

157-
it('does not bundle the validateConfig function', async () => {
163+
it.skip('does not bundle the validateConfig function', async () => {
158164
process.env.APP_CONFIG = JSON.stringify({ externalApiUrl: 'https://localhost:3999' });
159165

160166
await new Promise<void>((done, reject) => {
161167
webpack([createOptions({}, true)], (err, stats) => {
162-
if (err) reject(err);
168+
if (err) return reject(err);
169+
if (!stats) return reject(new Error('no stats'));
163170
if (stats.hasErrors()) reject(stats.toString());
164171

165-
const { children } = stats.toJson();
172+
const { children } = stats.toJson({ source: true });
166173
const [{ modules = [] }] = children || [];
167174

168175
expect(modules.some(({ source }) => source?.includes('validateConfig'))).toBe(false);

app-config-webpack/src/index.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,6 @@ export default class AppConfigPlugin {
6464
// eslint-disable-next-line no-param-reassign
6565
resolve.request = `${join(__dirname, '..', '.config-placeholder')}?${queryString}`;
6666
}
67-
68-
return resolve;
6967
},
7068
);
7169
});
@@ -84,6 +82,7 @@ export default class AppConfigPlugin {
8482
attributes: { id: 'app-config', type: 'text/javascript' },
8583
innerHTML: ``,
8684
voidTag: false,
85+
meta: {},
8786
});
8887

8988
return {

app-config-webpack/src/loader.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1-
import * as wp from 'webpack';
21
import { getOptions, parseQuery } from 'loader-utils';
32
import { loadValidatedConfig } from '@app-config/main';
43
import type { Options } from './index';
54

5+
type LoaderContext = Parameters<typeof getOptions>[0];
6+
interface Loader extends LoaderContext {}
7+
68
const privateName = '_appConfig';
79

8-
const loader: wp.loader.Loader = function AppConfigLoader() {
10+
const loader = function AppConfigLoader(this: Loader) {
911
if (this.cacheable) this.cacheable();
1012

1113
const callback = this.async()!;
@@ -66,7 +68,11 @@ const loader: wp.loader.Loader = function AppConfigLoader() {
6668

6769
return callback(null, generateText(JSON.stringify(fullConfig)));
6870
})
69-
.catch((err) => callback(err));
71+
.catch((err) => {
72+
this.emitWarning(new Error(`There was an error when trying to load @app-config`));
73+
74+
callback(err);
75+
});
7076
};
7177

7278
export default loader;

docs/guide/webpack/README.md

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ The webpack plugin is a separate package, which you'll need to install:
1212
yarn add @app-config/webpack@2
1313
```
1414

15+
Note that this plugin is compatible with both **Webpack v4** and **Webpack v5**, from `2.4` onwards.
16+
1517
In your webpack.config.js file, add the following:
1618

1719
```javascript
@@ -55,16 +57,16 @@ plugins: [
5557
]
5658
```
5759

58-
This change tells webpack to add a `<script id="app-config">` tag, which contains
60+
This change tells webpack to add a `<script id="app-config">` tag, which can contain
5961
some JavaScript code that sets `window._appConfig`. The loader cooperates with this
6062
by reading from that global variable instead of inserting the config directly.
6163

6264
Why go through this trouble? As stated above, [app-config-inject](./inject.md)
6365
uses this fact. When you're deploying your frontend application, you can add a
6466
short injection script that mutates the `index.html` with a new `<script id="app-config">`.
6567
By doing that, the web app will have different configuration, without changing
66-
the JavaScript bundle at all. If you really wanted to, you could even change the
67-
HTML by hand.
68+
the JavaScript bundle at all (allowing it to be cached). If you really wanted to,
69+
you could even change the HTML by hand.
6870

6971
### Loading Options
7072

@@ -96,6 +98,21 @@ plugins: [
9698
]
9799
```
98100

101+
The same goes for schema options. Use the `schemaLoading` property to define custom schema loading options.
102+
103+
### Static App Config
104+
105+
Use the `noGlobal: true` option and don't enable `headerInjection` - this will result in a statically
106+
analyzable config object, allowing it to be used like a constant variable (helpful for tree shaking).
107+
108+
Note of course, that this has downsides - you can't inject configuration this way. We recommend using
109+
**multiple app configs** for that reason. You can get the best of both worlds, "feature flags" in one
110+
configuration file (for tree shaking), and more dynamic/environment configuration in another.
111+
112+
### Multiple App Configs
113+
114+
This section needs more written about it - you can find an example [here](https://github.com/launchcodedev/app-config/tree/master/tests/webpack-projects/two-app-config-sources).
115+
99116
### Validation
100117

101118
A niche but useful way to use App Config involves using query parameters as configuration override.

examples/frontend-webpack-project/webpack.config.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@ const { default: AppConfigPlugin } = require('@app-config/webpack');
55
// Important parts are in module->rules (the AppConfigPlugin.loader), and plugins
66
// AppConfigPlugin relies on HtmlPlugin (html-webpack-plugin), when using headerInjection
77

8+
const appConfigOptions = {
9+
headerInjection: true,
10+
};
11+
812
module.exports = {
13+
mode: 'development',
914
entry: './src/index.ts',
1015
output: {
1116
publicPath: '/',
@@ -21,11 +26,11 @@ module.exports = {
2126
},
2227
{
2328
test: AppConfigPlugin.regex,
24-
use: { loader: AppConfigPlugin.loader, options: { headerInjection: true } },
29+
use: { loader: AppConfigPlugin.loader, options: appConfigOptions },
2530
},
2631
],
2732
},
28-
plugins: [new HtmlPlugin(), new AppConfigPlugin({ headerInjection: true })],
33+
plugins: [new HtmlPlugin(), new AppConfigPlugin(appConfigOptions)],
2934
devServer: {
3035
host: '0.0.0.0',
3136
port: 8080,

tests/webpack-projects/app-config-core-in-browser/webpack.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ const path = require('path');
22
const HtmlPlugin = require('html-webpack-plugin');
33

44
module.exports = {
5+
mode: 'development',
56
entry: './src/index.ts',
67
output: {
78
publicPath: '/',

tests/webpack-projects/cypress-plugin/webpack.config.js

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@ const path = require('path');
22
const HtmlPlugin = require('html-webpack-plugin');
33
const { default: AppConfigPlugin } = require('@app-config/webpack');
44

5-
// Important parts are in module->rules (the AppConfigPlugin.loader), and plugins
6-
// AppConfigPlugin relies on HtmlPlugin (html-webpack-plugin), when using headerInjection
5+
const appConfigOptions = {
6+
headerInjection: true,
7+
};
78

89
module.exports = {
10+
mode: 'development',
911
entry: './src/index.ts',
1012
output: {
1113
publicPath: '/',
@@ -21,11 +23,11 @@ module.exports = {
2123
},
2224
{
2325
test: AppConfigPlugin.regex,
24-
use: { loader: AppConfigPlugin.loader, options: { headerInjection: true } },
26+
use: { loader: AppConfigPlugin.loader, options: appConfigOptions },
2527
},
2628
],
2729
},
28-
plugins: [new HtmlPlugin(), new AppConfigPlugin({ headerInjection: true })],
30+
plugins: [new HtmlPlugin(), new AppConfigPlugin(appConfigOptions)],
2931
devServer: {
3032
host: '0.0.0.0',
3133
port: 8991,

tests/webpack-projects/extending-other-files/webpack.config.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ const path = require('path');
22
const HtmlPlugin = require('html-webpack-plugin');
33
const { default: AppConfigPlugin } = require('@app-config/webpack');
44

5-
// Important parts are in module->rules (the AppConfigPlugin.loader), and plugins
6-
// AppConfigPlugin relies on HtmlPlugin (html-webpack-plugin), when using headerInjection
5+
const appConfigOptions = {};
76

87
module.exports = {
8+
mode: 'development',
99
entry: './src/index.ts',
1010
output: {
1111
publicPath: '/',
@@ -21,11 +21,11 @@ module.exports = {
2121
},
2222
{
2323
test: AppConfigPlugin.regex,
24-
use: { loader: AppConfigPlugin.loader },
24+
use: { loader: AppConfigPlugin.loader, options: appConfigOptions },
2525
},
2626
],
2727
},
28-
plugins: [new HtmlPlugin(), new AppConfigPlugin()],
28+
plugins: [new HtmlPlugin(), new AppConfigPlugin(appConfigOptions)],
2929
devServer: {
3030
host: '0.0.0.0',
3131
port: 8994,

0 commit comments

Comments
 (0)