Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
b20a11d
Try to merge webpack and stylepack configs into a single 'weballpacka…
polarbirke Jan 1, 2026
4940fee
Add Jest testing via Snapshots with a POC example fixture
polarbirke Jan 2, 2026
df54850
Clean up comments
polarbirke Jan 2, 2026
d50accb
Remove debug logging
polarbirke Jan 2, 2026
4c51656
Remove unused variable
polarbirke Jan 2, 2026
ccdc011
Remove debug output
polarbirke Jan 2, 2026
bd58dd9
Add second POC snapshot test
polarbirke Jan 2, 2026
f52e564
Delete unnecessary comments
polarbirke Jan 2, 2026
9b2c688
Fix that browserslist is correctly taken into account; commit WIP fix…
polarbirke Jan 3, 2026
6ca2c34
Rename fixtures, add PurgeCss test case
polarbirke Jan 3, 2026
36e62cd
Add fixture/test for custom per-file PostCSS Preset Env config
polarbirke Jan 3, 2026
83f5deb
Fix tiny discrepancies
polarbirke Jan 3, 2026
cbca105
Add lockfile
polarbirke Jan 3, 2026
c21af11
Add git ignore rules
polarbirke Jan 3, 2026
e3c5c76
Add GH workflow for tests, modify folders for ease-of-use
polarbirke Jan 4, 2026
4613d55
Fix output for multi-entry configs and add tests
polarbirke Jan 4, 2026
fd5862b
Prepare subfolder structure in fixtures for addition of js tests
polarbirke Jan 5, 2026
e0708d7
Rename CSS test
polarbirke Jan 5, 2026
83d0150
Rename test suite
polarbirke Jan 5, 2026
8339e8a
Improve test names
polarbirke Jan 5, 2026
dd2cd36
Add fixtures and tests for asset rebasing, refactor test harness acco…
polarbirke Jan 5, 2026
1813577
Rename tmp folder
polarbirke Jan 5, 2026
5ad3d9b
Add fixtures and tests for JS bundling
polarbirke Jan 5, 2026
37f96fd
Remove unnecessary /www folders
polarbirke Jan 5, 2026
8864579
Rename CSS Base to CSS Basic test, fix package.json name collisions
polarbirke Jan 5, 2026
8a04c75
Merge remote-tracking branch 'origin/webpack-sass-loader' into weball…
polarbirke Jan 5, 2026
a8eb743
Update dev-branch with new feature from master PR #47
polarbirke Jan 5, 2026
7c6a82d
Update dev-branch with feat: Typescript from master PR #47
polarbirke Jan 5, 2026
00b8748
Fix package.json module naming collisions
polarbirke Jan 5, 2026
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
29 changes: 29 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: Tests

on:
push:
branches: [ master ]
pull_request:
pull_request_target:

# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:

jobs:
JSUnit:
runs-on: ubuntu-latest

strategy:
matrix:
node-version: [ 20.x ]

steps:
- uses: actions/checkout@v4

- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- run: yarn install --immutable --immutable-cache --check-cache
- run: yarn test
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules
yarn-error.log
6 changes: 6 additions & 0 deletions jest.config.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module.exports = {
testEnvironment: 'node',
snapshotFormat: {
escapeString: false,
},
};
12 changes: 10 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"@fullhuman/postcss-purgecss": "^4.0.3",
"babel-loader": "^10.0.0",
"browser-sync": "^2.26.7",
"browserslist-config-webfactory": "^1.0.0",
"browserslist-config-webfactory": "^2.0.0",
"css-loader": "^7.1.2",
"dotenv": "^10.0.0",
"fancy-log": "^1.3.3",
Expand Down Expand Up @@ -43,5 +43,13 @@
},
"browserslist": [
"extends browserslist-config-webfactory/default"
]
],
"devDependencies": {
"jest": "^30.2.0",
"jquery": "^3.7.1",
"sass-embedded": "^1.97.1"
},
"scripts": {
"test": "jest"
}
}
241 changes: 241 additions & 0 deletions tasks/weballpacka.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const postcssPurgecss = require('@fullhuman/postcss-purgecss');

function utilityCssExtractor(content) {
return content.match(/[a-zA-Z0-9-_.:@\/]+/g);
}

function createMergedWebpackConfig(gulp, $, config) {
const argv = require('minimist')(process.argv.slice(2));
const purgeCssDisabled = argv.purgecss === false;

const entry = {};
const entryCssMeta = {};

// ---- CSS entries ----
(config.styles.files || []).forEach((file) => {
// key must be unique and stable
const entryName = `css_${file.name.replace(/[^a-zA-Z0-9_-]/g, '_')}`;

entry[entryName] = path.resolve(config.webdir, file.inputPath);
entryCssMeta[entryName] = {
destDir: file.destDir || 'css',
filename: file.name,
inputPath: file.inputPath,
purgeCssConfig: file.purgeCss ?? config.styles.purgeCss ?? null,
postCssPresetEnvConfig: file.postCssPresetEnv || config.styles.postCssPresetEnv || {},
};
});


// ---- JS entries ----
let includeModules = config.scripts.includeModules ? '|' + config.scripts.includeModules.join('|') : '';
let svelteVersion = config.svelteVersion ? parseFloat(config.svelteVersion) : 3;

let resolveModulesPaths = [config.npmdir];
if (config.scripts.resolveModulesPaths) {
resolveModulesPaths = [...new Set([...(config.scripts.resolveModulesPaths), ...resolveModulesPaths])];
}

(config.scripts.files || []).forEach((script) => {
const entryName = `js_${script.name.replace(/[^a-zA-Z0-9_-]/g, '_')}`;
entry[entryName] = path.resolve(config.webdir, script.inputPath);
});

const webpackConfig = {
entry,
output: {
// js_<name> -> js/<name>.js
filename: (pathData) => {
const name = pathData.chunk && pathData.chunk.name ? pathData.chunk.name : '[name]';
if (name.startsWith('js_')) {
const cleanName = name.replace(/^js_/, '');
return `js/${cleanName}.js`;
}
return '[name].js';
},
path: path.resolve(config.webdir),
},
resolve: {
alias: {
svelte: svelteVersion < 4
? $.path.resolve('node_modules', 'svelte')
: $.path.resolve('node_modules', 'svelte/src/runtime')
},
extensions: ['.mjs', '.js', '.svelte', '.ts'],
mainFields: ['svelte', 'browser', 'module', 'main'],
modules: resolveModulesPaths,
},
module: {
rules: [
// JS + Svelte + Typescript
{
test: /(\.m?js?$)|(\.svelte$)/,
exclude: new RegExp('node_modules\\/(?![svelte' + includeModules + '])'),
use: {
loader: 'babel-loader',
options: {
cacheDirectory: true,
presets: ['@babel/preset-env'],
plugins: ['@babel/plugin-transform-runtime'],
}
}
},
{
test: /\.(html|svelte)$/,
exclude: /node_modules\/(?!svelte)/,
use: [
'babel-loader',
{
loader: 'svelte-loader',
options: {
cacheDirectory: true,
emitCss: false,
},
},
],
},
{
// required to prevent errors from Svelte on Webpack 5+, omit on Webpack 4
test: /node_modules\/svelte\/.*\.mjs$/,
resolve: {
fullySpecified: false,
}
},
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},

// Assets used from SCSS
{
test: /\.(png|jpe?g|gif|svg|webp|avif)$/i,
type: 'asset/resource',
generator: {
filename: 'img/[name].[hash][ext]'
}
},

// SCSS -> CSS (via MiniCssExtractPlugin)
{
test: /\.s[ac]ss$/i,
use: [
MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
options: { sourceMap: true }
},
{
loader: 'resolve-url-loader',
},
{
loader: 'postcss-loader',
options: {
sourceMap: true,
postcssOptions: (loaderContext) => {
// Match resource path to entry path from entryCssMeta
const resourcePath = loaderContext.resourcePath;
let cssEntry = null;

for (const [entryName, meta] of Object.entries(entryCssMeta)) {
if (meta.inputPath && resourcePath.includes(meta.inputPath)) {
cssEntry = meta;
break;
}
}

if (!cssEntry) {
throw new Error(
`No CSS entry metadata found for resource: ${resourcePath}\n` +
`Available entries: ${Object.keys(entryCssMeta).join(', ')}\n` +
`Ensure the SCSS file is an exact webpack entry point from config.styles.files[].inputPath`
);
}

const postCssPresetEnvConfig = cssEntry.postCssPresetEnvConfig || {};

return {
plugins: [
require('postcss-preset-env')(postCssPresetEnvConfig),
...(cssEntry.purgeCssConfig && !purgeCssDisabled
? [postcssPurgecss({
content: cssEntry.purgeCssConfig.content,
extractors: [{
extractor: utilityCssExtractor,
extensions: ['php', 'twig', 'js', 'svg']
}],
safelist: cssEntry.purgeCssConfig.safelist,
})]
: []),
],
};
},
},
},
{
loader: 'sass-loader',
options: {
implementation: 'sass-embedded',
api: 'modern',
sourceMap: true,
sassOptions: {
loadPaths: config.styles.includePaths
? config.styles.includePaths
: [config.npmdir],
},
},
},
],
},
],
},
plugins: [
// jQuery globals for JS, as before
new $.webpack.ProvidePlugin({
$: 'jquery',
jQuery: 'jquery',
}),

// CSS extraction with per‑entry filenames
new MiniCssExtractPlugin({
filename: (pathData) => {
const name = pathData.chunk && pathData.chunk.name ? pathData.chunk.name : '[name]';

if (name.startsWith('css_')) {
const meta = entryCssMeta[name];
if (meta) {
const dir = meta.destDir ? meta.destDir.replace(/\/+$/, '') : 'css';
return `${dir}/${meta.filename}`;
}
}

// fallback
return 'css/[name].css';
},
}),
],
mode: config.development && $.argv.prod !== true ? 'development' : 'production',
devtool: $.argv.debug === true ? 'source-map' : false,
stats: {
preset: 'normal',
timings: true,
},
};

return webpackConfig;
}

function webpackMerged(gulp, $, config) {
const webpackStream = require('webpack-stream');
const webpack = $.webpack;

return gulp.src(config.webdir + '/**/*.{js,scss}')
.pipe(webpackStream(createMergedWebpackConfig(gulp, $, config), webpack))
.pipe(gulp.dest(config.webdir))
.pipe($.browserSync.reload({ stream: true }));
}

exports.webpackMerged = webpackMerged;
exports.createMergedWebpackConfig = createMergedWebpackConfig;
75 changes: 75 additions & 0 deletions tests/__snapshots__/css.test.js.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing

exports[`Compiling SCSS to CSS basic: basic-print-css 1`] = `
"/*!***************************************************************************************************************************************************************************************************************************************************************************************************************************!*\\
!*** 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 ***!
\\***************************************************************************************************************************************************************************************************************************************************************************************************************************/
@media print {
a[href]:after {
content: " (" attr(href) ")";
}
}
"
`;

exports[`Compiling SCSS to CSS basic: basic-screen-css 1`] = `
"/*!****************************************************************************************************************************************************************************************************************************************************************************************************************************!*\\
!*** 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 ***!
\\****************************************************************************************************************************************************************************************************************************************************************************************************************************/
:root {
--color-primary: blue;
}

.button {
display: flex;
color: var(--color-primary);
}
.button--primary {
color: red;
}

div {
margin-left: 1rem;
}
"
`;

exports[`Compiling SCSS to CSS with custom per-file postcss-preset-env config: postcss-preset-env-print-css 1`] = `
"/*!***************************************************************************************************************************************************************************************************************************************************************************************************************************!*\\
!*** 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 ***!
\\***************************************************************************************************************************************************************************************************************************************************************************************************************************/
@media print {
a[href]:after {
content: " (" attr(href) ")";
}
div {
margin-top: 0;
}
}
"
`;

exports[`Compiling SCSS to CSS with custom per-file postcss-preset-env config: postcss-preset-env-screen-css 1`] = `
"/*!****************************************************************************************************************************************************************************************************************************************************************************************************************************!*\\
!*** 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 ***!
\\****************************************************************************************************************************************************************************************************************************************************************************************************************************/
div {
height: 25ch;
padding-bottom: 1rem;
}
"
`;

exports[`Compiling SCSS to CSS with purgecss: purgecss-screen-css 1`] = `
"/*!****************************************************************************************************************************************************************************************************************************************************************************************************************************!*\\
!*** 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 ***!
\\****************************************************************************************************************************************************************************************************************************************************************************************************************************/
h2 {
font-size: 2rem;
}

.purgecss-should-not-remove {
background-color: red;
}
"
`;
Loading