Skip to content

Commit 5e3e536

Browse files
authored
Merge webpack and stylepack configs into a single "weballpacka" task (#52)
1 parent fd31aae commit 5e3e536

File tree

32 files changed

+10038
-2
lines changed

32 files changed

+10038
-2
lines changed

.github/workflows/tests.yml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
name: Tests
2+
3+
on:
4+
push:
5+
branches: [ master ]
6+
pull_request:
7+
pull_request_target:
8+
9+
# Allows you to run this workflow manually from the Actions tab
10+
workflow_dispatch:
11+
12+
jobs:
13+
JSUnit:
14+
runs-on: ubuntu-latest
15+
16+
strategy:
17+
matrix:
18+
node-version: [ 20.x ]
19+
20+
steps:
21+
- uses: actions/checkout@v4
22+
23+
- name: Use Node.js ${{ matrix.node-version }}
24+
uses: actions/setup-node@v3
25+
with:
26+
node-version: ${{ matrix.node-version }}
27+
cache: 'npm'
28+
- run: yarn install --immutable --immutable-cache --check-cache
29+
- run: yarn test

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
node_modules
2+
yarn-error.log

jest.config.cjs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
module.exports = {
2+
testEnvironment: 'node',
3+
snapshotFormat: {
4+
escapeString: false,
5+
},
6+
};

package.json

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"@fullhuman/postcss-purgecss": "^4.0.3",
1010
"babel-loader": "^10.0.0",
1111
"browser-sync": "^2.26.7",
12-
"browserslist-config-webfactory": "^1.0.0",
12+
"browserslist-config-webfactory": "^2.0.0",
1313
"css-loader": "^7.1.2",
1414
"dotenv": "^10.0.0",
1515
"fancy-log": "^1.3.3",
@@ -43,5 +43,13 @@
4343
},
4444
"browserslist": [
4545
"extends browserslist-config-webfactory/default"
46-
]
46+
],
47+
"devDependencies": {
48+
"jest": "^30.2.0",
49+
"jquery": "^3.7.1",
50+
"sass-embedded": "^1.97.1"
51+
},
52+
"scripts": {
53+
"test": "jest"
54+
}
4755
}

tasks/weballpacka.js

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
const path = require('path');
2+
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
3+
const postcssPurgecss = require('@fullhuman/postcss-purgecss');
4+
5+
function utilityCssExtractor(content) {
6+
return content.match(/[a-zA-Z0-9-_.:@\/]+/g);
7+
}
8+
9+
function createMergedWebpackConfig(gulp, $, config) {
10+
const argv = require('minimist')(process.argv.slice(2));
11+
const purgeCssDisabled = argv.purgecss === false;
12+
13+
const entry = {};
14+
const entryCssMeta = {};
15+
16+
// ---- CSS entries ----
17+
(config.styles.files || []).forEach((file) => {
18+
// key must be unique and stable
19+
const entryName = `css_${file.name.replace(/[^a-zA-Z0-9_-]/g, '_')}`;
20+
21+
entry[entryName] = path.resolve(config.webdir, file.inputPath);
22+
entryCssMeta[entryName] = {
23+
destDir: file.destDir || 'css',
24+
filename: file.name,
25+
inputPath: file.inputPath,
26+
purgeCssConfig: file.purgeCss ?? config.styles.purgeCss ?? null,
27+
postCssPresetEnvConfig: file.postCssPresetEnv || config.styles.postCssPresetEnv || {},
28+
};
29+
});
30+
31+
32+
// ---- JS entries ----
33+
let includeModules = config.scripts.includeModules ? '|' + config.scripts.includeModules.join('|') : '';
34+
let svelteVersion = config.svelteVersion ? parseFloat(config.svelteVersion) : 3;
35+
36+
let resolveModulesPaths = [config.npmdir];
37+
if (config.scripts.resolveModulesPaths) {
38+
resolveModulesPaths = [...new Set([...(config.scripts.resolveModulesPaths), ...resolveModulesPaths])];
39+
}
40+
41+
(config.scripts.files || []).forEach((script) => {
42+
const entryName = `js_${script.name.replace(/[^a-zA-Z0-9_-]/g, '_')}`;
43+
entry[entryName] = path.resolve(config.webdir, script.inputPath);
44+
});
45+
46+
const webpackConfig = {
47+
entry,
48+
output: {
49+
// js_<name> -> js/<name>.js
50+
filename: (pathData) => {
51+
const name = pathData.chunk && pathData.chunk.name ? pathData.chunk.name : '[name]';
52+
if (name.startsWith('js_')) {
53+
const cleanName = name.replace(/^js_/, '');
54+
return `js/${cleanName}.js`;
55+
}
56+
return '[name].js';
57+
},
58+
path: path.resolve(config.webdir),
59+
},
60+
resolve: {
61+
alias: {
62+
svelte: svelteVersion < 4
63+
? $.path.resolve('node_modules', 'svelte')
64+
: $.path.resolve('node_modules', 'svelte/src/runtime')
65+
},
66+
extensions: ['.mjs', '.js', '.svelte', '.ts'],
67+
mainFields: ['svelte', 'browser', 'module', 'main'],
68+
modules: resolveModulesPaths,
69+
},
70+
module: {
71+
rules: [
72+
// JS + Svelte + Typescript
73+
{
74+
test: /(\.m?js?$)|(\.svelte$)/,
75+
exclude: new RegExp('node_modules\\/(?![svelte' + includeModules + '])'),
76+
use: {
77+
loader: 'babel-loader',
78+
options: {
79+
cacheDirectory: true,
80+
presets: ['@babel/preset-env'],
81+
plugins: ['@babel/plugin-transform-runtime'],
82+
}
83+
}
84+
},
85+
{
86+
test: /\.(html|svelte)$/,
87+
exclude: /node_modules\/(?!svelte)/,
88+
use: [
89+
'babel-loader',
90+
{
91+
loader: 'svelte-loader',
92+
options: {
93+
cacheDirectory: true,
94+
emitCss: false,
95+
},
96+
},
97+
],
98+
},
99+
{
100+
// required to prevent errors from Svelte on Webpack 5+, omit on Webpack 4
101+
test: /node_modules\/svelte\/.*\.mjs$/,
102+
resolve: {
103+
fullySpecified: false,
104+
}
105+
},
106+
{
107+
test: /\.tsx?$/,
108+
use: 'ts-loader',
109+
exclude: /node_modules/,
110+
},
111+
112+
// Assets used from SCSS
113+
{
114+
test: /\.(png|jpe?g|gif|svg|webp|avif)$/i,
115+
type: 'asset/resource',
116+
generator: {
117+
filename: 'img/[name].[hash][ext]'
118+
}
119+
},
120+
121+
// SCSS -> CSS (via MiniCssExtractPlugin)
122+
{
123+
test: /\.s[ac]ss$/i,
124+
use: [
125+
MiniCssExtractPlugin.loader,
126+
{
127+
loader: 'css-loader',
128+
options: { sourceMap: true }
129+
},
130+
{
131+
loader: 'resolve-url-loader',
132+
},
133+
{
134+
loader: 'postcss-loader',
135+
options: {
136+
sourceMap: true,
137+
postcssOptions: (loaderContext) => {
138+
// Match resource path to entry path from entryCssMeta
139+
const resourcePath = loaderContext.resourcePath;
140+
let cssEntry = null;
141+
142+
for (const [entryName, meta] of Object.entries(entryCssMeta)) {
143+
if (meta.inputPath && resourcePath.includes(meta.inputPath)) {
144+
cssEntry = meta;
145+
break;
146+
}
147+
}
148+
149+
if (!cssEntry) {
150+
throw new Error(
151+
`No CSS entry metadata found for resource: ${resourcePath}\n` +
152+
`Available entries: ${Object.keys(entryCssMeta).join(', ')}\n` +
153+
`Ensure the SCSS file is an exact webpack entry point from config.styles.files[].inputPath`
154+
);
155+
}
156+
157+
const postCssPresetEnvConfig = cssEntry.postCssPresetEnvConfig || {};
158+
159+
return {
160+
plugins: [
161+
require('postcss-preset-env')(postCssPresetEnvConfig),
162+
...(cssEntry.purgeCssConfig && !purgeCssDisabled
163+
? [postcssPurgecss({
164+
content: cssEntry.purgeCssConfig.content,
165+
extractors: [{
166+
extractor: utilityCssExtractor,
167+
extensions: ['php', 'twig', 'js', 'svg']
168+
}],
169+
safelist: cssEntry.purgeCssConfig.safelist,
170+
})]
171+
: []),
172+
],
173+
};
174+
},
175+
},
176+
},
177+
{
178+
loader: 'sass-loader',
179+
options: {
180+
implementation: 'sass-embedded',
181+
api: 'modern',
182+
sourceMap: true,
183+
sassOptions: {
184+
loadPaths: config.styles.includePaths
185+
? config.styles.includePaths
186+
: [config.npmdir],
187+
},
188+
},
189+
},
190+
],
191+
},
192+
],
193+
},
194+
plugins: [
195+
// jQuery globals for JS, as before
196+
new $.webpack.ProvidePlugin({
197+
$: 'jquery',
198+
jQuery: 'jquery',
199+
}),
200+
201+
// CSS extraction with per‑entry filenames
202+
new MiniCssExtractPlugin({
203+
filename: (pathData) => {
204+
const name = pathData.chunk && pathData.chunk.name ? pathData.chunk.name : '[name]';
205+
206+
if (name.startsWith('css_')) {
207+
const meta = entryCssMeta[name];
208+
if (meta) {
209+
const dir = meta.destDir ? meta.destDir.replace(/\/+$/, '') : 'css';
210+
return `${dir}/${meta.filename}`;
211+
}
212+
}
213+
214+
// fallback
215+
return 'css/[name].css';
216+
},
217+
}),
218+
],
219+
mode: config.development && $.argv.prod !== true ? 'development' : 'production',
220+
devtool: $.argv.debug === true ? 'source-map' : false,
221+
stats: {
222+
preset: 'normal',
223+
timings: true,
224+
},
225+
};
226+
227+
return webpackConfig;
228+
}
229+
230+
function webpackMerged(gulp, $, config) {
231+
const webpackStream = require('webpack-stream');
232+
const webpack = $.webpack;
233+
234+
return gulp.src(config.webdir + '/**/*.{js,scss}')
235+
.pipe(webpackStream(createMergedWebpackConfig(gulp, $, config), webpack))
236+
.pipe(gulp.dest(config.webdir))
237+
.pipe($.browserSync.reload({ stream: true }));
238+
}
239+
240+
exports.webpackMerged = webpackMerged;
241+
exports.createMergedWebpackConfig = createMergedWebpackConfig;
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
2+
3+
exports[`Compiling SCSS to CSS basic: basic-print-css 1`] = `
4+
"/*!***************************************************************************************************************************************************************************************************************************************************************************************************************************!*\\
5+
!*** css ../../../../node_modules/css-loader/dist/cjs.js??ruleSet[1].rules[5].use[1]!../../../../node_modules/resolve-url-loader/index.js!../../../../node_modules/postcss-loader/dist/cjs.js??ruleSet[1].rules[5].use[3]!../../../../node_modules/sass-loader/dist/cjs.js??ruleSet[1].rules[5].use[4]!./scss/print.scss ***!
6+
\\***************************************************************************************************************************************************************************************************************************************************************************************************************************/
7+
@media print {
8+
a[href]:after {
9+
content: " (" attr(href) ")";
10+
}
11+
}
12+
"
13+
`;
14+
15+
exports[`Compiling SCSS to CSS basic: basic-screen-css 1`] = `
16+
"/*!****************************************************************************************************************************************************************************************************************************************************************************************************************************!*\\
17+
!*** css ../../../../node_modules/css-loader/dist/cjs.js??ruleSet[1].rules[5].use[1]!../../../../node_modules/resolve-url-loader/index.js!../../../../node_modules/postcss-loader/dist/cjs.js??ruleSet[1].rules[5].use[3]!../../../../node_modules/sass-loader/dist/cjs.js??ruleSet[1].rules[5].use[4]!./scss/screen.scss ***!
18+
\\****************************************************************************************************************************************************************************************************************************************************************************************************************************/
19+
:root {
20+
--color-primary: blue;
21+
}
22+
23+
.button {
24+
display: flex;
25+
color: var(--color-primary);
26+
}
27+
.button--primary {
28+
color: red;
29+
}
30+
31+
div {
32+
margin-left: 1rem;
33+
}
34+
"
35+
`;
36+
37+
exports[`Compiling SCSS to CSS with custom per-file postcss-preset-env config: postcss-preset-env-print-css 1`] = `
38+
"/*!***************************************************************************************************************************************************************************************************************************************************************************************************************************!*\\
39+
!*** css ../../../../node_modules/css-loader/dist/cjs.js??ruleSet[1].rules[5].use[1]!../../../../node_modules/resolve-url-loader/index.js!../../../../node_modules/postcss-loader/dist/cjs.js??ruleSet[1].rules[5].use[3]!../../../../node_modules/sass-loader/dist/cjs.js??ruleSet[1].rules[5].use[4]!./scss/print.scss ***!
40+
\\***************************************************************************************************************************************************************************************************************************************************************************************************************************/
41+
@media print {
42+
a[href]:after {
43+
content: " (" attr(href) ")";
44+
}
45+
div {
46+
margin-top: 0;
47+
}
48+
}
49+
"
50+
`;
51+
52+
exports[`Compiling SCSS to CSS with custom per-file postcss-preset-env config: postcss-preset-env-screen-css 1`] = `
53+
"/*!****************************************************************************************************************************************************************************************************************************************************************************************************************************!*\\
54+
!*** css ../../../../node_modules/css-loader/dist/cjs.js??ruleSet[1].rules[5].use[1]!../../../../node_modules/resolve-url-loader/index.js!../../../../node_modules/postcss-loader/dist/cjs.js??ruleSet[1].rules[5].use[3]!../../../../node_modules/sass-loader/dist/cjs.js??ruleSet[1].rules[5].use[4]!./scss/screen.scss ***!
55+
\\****************************************************************************************************************************************************************************************************************************************************************************************************************************/
56+
div {
57+
height: 25ch;
58+
padding-bottom: 1rem;
59+
}
60+
"
61+
`;
62+
63+
exports[`Compiling SCSS to CSS with purgecss: purgecss-screen-css 1`] = `
64+
"/*!****************************************************************************************************************************************************************************************************************************************************************************************************************************!*\\
65+
!*** css ../../../../node_modules/css-loader/dist/cjs.js??ruleSet[1].rules[5].use[1]!../../../../node_modules/resolve-url-loader/index.js!../../../../node_modules/postcss-loader/dist/cjs.js??ruleSet[1].rules[5].use[3]!../../../../node_modules/sass-loader/dist/cjs.js??ruleSet[1].rules[5].use[4]!./scss/screen.scss ***!
66+
\\****************************************************************************************************************************************************************************************************************************************************************************************************************************/
67+
h2 {
68+
font-size: 2rem;
69+
}
70+
71+
.purgecss-should-not-remove {
72+
background-color: red;
73+
}
74+
"
75+
`;

0 commit comments

Comments
 (0)