-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathwebpack.config.js
279 lines (256 loc) Β· 8.34 KB
/
webpack.config.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
const path = require('path')
const { VanillaExtractPlugin } = require('@vanilla-extract/webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const TerserPlugin = require('terser-webpack-plugin')
const { DefinePlugin } = require('webpack')
const FileSizePlugin = require('./FileSizePlugin')
const glob = require('glob')
/**
* @returns {Array.<{import: string, name: string, layer: string, elementName: string}>}
*/
const getIslands = () => {
const paths = glob.sync('./src/**/*.island.{ts,tsx}')
return paths.map((path) => {
const name = path
.split('/')
.pop()
.replace(/.island.(tsx|ts)/g, '')
let elementName = `${name}-island`
/**
* If you want to name your web component something different than the filename of the island (not
* recommended). Please override them here.
*/
// if (name === 'call-to-action') {
// elementName = 'something-else'
// }
return {
path,
name,
elementName,
layer: name,
}
})
}
const islands = getIslands()
// This builds the entry points for all of your islands.
const buildEntryPoints = () => {
const entryPoints = {}
islands.forEach((island) => {
entryPoints[island.name] = {
import: island.path,
layer: island.layer,
}
})
return entryPoints
}
const buildCssLayersFromEntryPoints = () => {
return islands.map(({ layer, elementName }) => {
return {
issuerLayer: layer,
use: [
/**
* This injects the built styles as a single style tag in the UMD bundle for the project.
* This makes it to where consumers do not need to worry about an external stylesheet and
* saves a request on shopify websites where the waterfall is normally clogged.
*/
{
loader: 'style-loader',
options: {
injectType: 'singletonStyleTag',
attributes: {
'data-style-for': elementName,
},
/**
* It appears the node given to you is initially blank with styles applied after the fact so you
* can't rely on it to have information you need immediately.
*
* See: https://github.com/webpack-contrib/style-loader/blob/43bede4415c5ccb4680d558725e0066f715aa175/src/runtime/singletonStyleDomAPI.js#L83
*
* NOTE: This runs untranspiled in the browser so watch out!
*/
insert: (styleTag) => {
var styleTarget = styleTag.dataset.styleFor
if (!styleTarget) {
console.error(
'Did not get a style target in the insert command from the style loader. No styles will be inserted. Did you override something in getIslands incorrectly?',
)
return
}
window.addEventListener('web-component-mount', (e) => {
if (
styleTarget !== e.detail.target &&
styleTarget !== e.detail.parent
) {
return
}
var target = document.querySelector(e.detail.target).shadowRoot
if (!target) {
console.error(
`Could not find a web component query selector target for "${styleTarget}". No styles will be appended. Did you name the web component at createIslandWebComponent something different than your file name? If so, you will need to override it at getIslands inside of the webpack config. This is what is expected
createIslandWebComponent('${styleTarget}', YourComponent).render({
selector: ${styleTarget},
initialProps: {},
})`,
)
return
}
// We need to clone because it's going to be inserted into separate shadow doms. If you don't clone it
// the tag can only be active in one context
target.prepend(styleTag.cloneNode(true))
})
},
},
},
'css-loader',
],
}
})
}
module.exports = ({ dev, prod }) => {
const isDev = dev === true
const isProd = prod === true
if (isDev) {
console.log(
"Stubbing environmental variables for development from './env.local'",
)
require('dotenv').config({ path: './.env.local' })
}
/** @type { import('webpack').Configuration } */
const config = {
mode: isProd ? 'production' : 'development',
target: 'web',
resolve: {
extensions: ['.js', '.json', '.ts', '.tsx'],
/**
* From the docs to make Webpack compile Preact:
* https://preactjs.com/guide/v10/getting-started#aliasing-in-webpack
*/
alias: {
react: 'preact/compat',
'react-dom/test-utils': 'preact/test-utils',
'react-dom': 'preact/compat', // Must be below test-utils
'react/jsx-runtime': 'preact/jsx-runtime',
},
},
devServer: {
port: 7777,
hot: false,
},
devtool: isDev ? 'eval' : false,
entry: buildEntryPoints(),
output: {
path: path.join(__dirname, 'dist/islands'),
filename: '[name].island.umd.js',
libraryTarget: 'umd',
},
module: {
rules: [
{
test: /\.(js|ts|tsx)$/,
exclude: [/node_modules/],
use: [
{
loader: 'babel-loader',
options: {
babelrc: false,
presets: [
'@babel/preset-typescript',
['@babel/preset-react', { runtime: 'automatic' }],
[
'@babel/preset-env',
{ targets: { node: 16 }, modules: false },
],
],
plugins: ['@vanilla-extract/babel-plugin'],
},
},
],
},
{
test: /\.css$/i,
oneOf: buildCssLayersFromEntryPoints(),
},
{
test: /\.(png|jpe?g|gif)$/i,
use: [
{
loader: 'file-loader',
},
],
},
],
},
plugins: [
new HtmlWebpackPlugin({
inject: false,
templateContent: ({ htmlWebpackPlugin }) => `
<html>
<head>
<meta charset="utf-8" />
<title>Islands</title>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<style>
body {
font-family: -apple-system, system-ui, BlinkMacSystemFont, 'Segoe UI',
Roboto, 'Helvetica Neue', Arial, sans-serif;
}
.preview {
width: 100%;
max-width: 1100px;
margin: 80px auto;
border: 1px dashed rgba(0, 0, 0, 0.2);
position: relative;
}
.preview::before {
content: 'Island';
position: absolute;
display: block;
top: -18px;
font-size: 11px;
color: rgba(0, 0, 0, 0.5);
}
</style>
${htmlWebpackPlugin.tags.headTags}
</head>
<body>
${islands
.map((island) => {
return `<div class="preview">
<${island.elementName}></${island.elementName}>
</div>`
})
.join('')}
${htmlWebpackPlugin.tags.bodyTags}
</body>
</html>
`,
/**
* Islands are served from /islands in dist so we don't pollute the root domain since these islands are
* embedded into websites we do not control.
*
* In dev mode, we serve islands and the index.html from the root since it's dev mode. For production,
* the index.html file is served from the root.
*/
publicPath: isDev ? '/' : '/islands',
filename: isDev ? 'index.html' : '../index.html',
}),
new VanillaExtractPlugin(),
/**
* Define environmental variables here that you need for the islands to function.
*/
new DefinePlugin({
ISLAND_API_URL: JSON.stringify(process.env.ISLAND_API_URL),
}),
...(isProd ? [new FileSizePlugin()] : []),
],
stats: 'errors-warnings',
experiments: {
layers: true,
},
optimization: {
minimize: true,
minimizer: [new TerserPlugin()],
},
}
return config
}