Skip to content

Commit

Permalink
Merge pull request #806 from preactjs/less
Browse files Browse the repository at this point in the history
  • Loading branch information
marvinhagemeister committed Aug 29, 2021
2 parents cb09f9b + ec49c1b commit 6e8d710
Show file tree
Hide file tree
Showing 70 changed files with 821 additions and 24 deletions.
5 changes: 5 additions & 0 deletions .changeset/polite-zebras-hunt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'wmr': minor
---

Add support for `.less` stylesheets.
1 change: 1 addition & 0 deletions packages/wmr/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
"jest": "^26.1.0",
"jest-puppeteer": "^5.0.4",
"kolorist": "^1.5.0",
"less": "^4.1.1",
"magic-string": "^0.25.7",
"mime": "^2.5.2",
"ncp": "^2.0.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/wmr/rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ const config = {
}
]
},
external: [...builtins],
external: [...builtins, 'less'],
// /* Logs all included npm dependencies: */
// external(source, importer) {
// const ch = source[0];
Expand Down
2 changes: 1 addition & 1 deletion packages/wmr/src/lib/npm-middleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export default function npmMiddleware({ source = 'npm', alias, optimize, cwd } =
res.setHeader('etag', etag);

// CSS files and proxy modules don't use Rollup.
if (/\.((css|s[ac]ss)|wasm|txt|json)$/.test(meta.path)) {
if (/\.((css|s[ac]ss|less)|wasm|txt|json)$/.test(meta.path)) {
return handleAsset(meta, res, url.searchParams.has('module'));
}

Expand Down
2 changes: 2 additions & 0 deletions packages/wmr/src/lib/plugins.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { importAssertionPlugin } from '../plugins/import-assertion.js';
import { acornDefaultPlugins } from './acorn-default-plugins.js';
import { prefreshPlugin } from '../plugins/preact/prefresh.js';
import { absolutePathPlugin } from '../plugins/absolute-path-plugin.js';
import { lessPlugin } from '../plugins/less-plugin.js';

/**
* @param {import("wmr").Options} options
Expand Down Expand Up @@ -66,6 +67,7 @@ export function getPlugins(options) {
}),
production && publicPathPlugin({ publicPath }),
sassPlugin({ production, sourcemap, root, mergedAssets }),
lessPlugin({ sourcemap, mergedAssets, alias }),
wmrStylesPlugin({ hot: !production, root, production, alias, sourcemap }),
processGlobalPlugin({
sourcemap,
Expand Down
11 changes: 10 additions & 1 deletion packages/wmr/src/plugins/html-entries-plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { transformHtml } from '../lib/transform-html.js';
import { yellow, bgYellow, bgRed, dim, bold, white, black, magenta, cyan } from 'kolorist';
import { codeFrame } from '../lib/output-utils.js';
import { transformSass } from './sass-plugin.js';
import { renderLess } from './less-plugin.js';

/** @typedef {import('rollup').OutputAsset & { referencedFiles: string[], importedIds: string[] }} ExtendedAsset */

Expand Down Expand Up @@ -96,7 +97,7 @@ export default function htmlEntriesPlugin({ root, publicPath, sourcemap, mergedA
let assetName = url;

// Ensure that stylesheets have `.css` as an extension
if (/\.s[ac]ss$/.test(assetName)) {
if (/\.(?:s[ac]ss|less)$/.test(assetName)) {
assetName = posix.join(posix.dirname(url), posix.basename(url, posix.extname(url)) + '.css');
}

Expand All @@ -112,6 +113,14 @@ export default function htmlEntriesPlugin({ root, publicPath, sourcemap, mergedA
for (let file of result.includedFiles) {
if (mergedAssets) mergedAssets.add(file);
}
this.setAssetSource(ref, result.css);
});
} else if (/\.less$/.test(abs)) {
return renderLess(source, { id: abs, sourcemap, resolve: this.resolve.bind(this) }).then(result => {
for (let file of result.imports) {
if (mergedAssets) mergedAssets.add(file);
}

this.setAssetSource(ref, result.css);
});
} else {
Expand Down
116 changes: 116 additions & 0 deletions packages/wmr/src/plugins/less-plugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import path from 'path';
import { createCodeFrame } from 'simple-code-frame';
import { resolveAlias } from '../lib/aliasing.js';

/** @type {import('less') | undefined} */
let less;

const lessFileLoader = (resolve, root) =>
class LessPluginWmr {
static install(less, pluginManager) {
class LessFileManagerWmr extends less.FileManager {
async loadFile(filename, currentDirectory, options, environment) {
let file = filename;
if (!file.endsWith('.less')) file = file + '.less';

if (!path.isAbsolute(currentDirectory)) {
currentDirectory = path.join(root, currentDirectory);
}

// Supply fake importer for relative resolution
const importer = path.join(currentDirectory, 'fake.less');
const resolved = await resolve(file, importer, { skipSelf: true });

let resolvedId = resolved ? resolved.id : file;

// Support bare imports: `@import "bar"`
if (!path.isAbsolute(resolvedId)) {
resolvedId = path.join(currentDirectory, filename);
}

// Pass loading to less
return less.FileManager.prototype.loadFile.call(this, resolvedId, '', options, environment);
}
}
pluginManager.addFileManager(new LessFileManagerWmr());
}
};

/**
* @param {string} code
* @param {{id: string, resolve: any, sourcemap: boolean }} options
* @returns {Promise<{ css: string, map?: string, imports: string[] }>}
*/
export async function renderLess(code, { id, resolve, sourcemap }) {
if (!less) {
if (process.env.DISABLE_LESS !== 'true') {
const mod = await import('less');
less = mod.default || mod;
}

if (!less) {
throw new Error(`Please install less to compile "*.less" files:\n npm i -D less`);
}
}

const lessOptions = {
filename: id,
plugins: [lessFileLoader(resolve, path.dirname(id))]
};
if (sourcemap) lessOptions.sourceMap = {};

try {
return await less.render(code, lessOptions);
} catch (err) {
if (err.extract && 'line' in err && 'column' in err) {
const code = err.extract.filter(l => l !== undefined).join('\n');
err.codeFrame = createCodeFrame(code, err.line - 1, err.column);
}

throw err;
}
}

/**
* @param {object} options
* @param {boolean} options.sourcemap
* @param {Set<string>} options.mergedAssets
* @param {Record<string, string>} options.alias
* @returns {import('rollup').Plugin}
*/
export function lessPlugin({ sourcemap, mergedAssets, alias }) {
/** @type {Map<string, Set<string>>} */
const fileToBundles = new Map();

return {
name: 'less',
async transform(code, id) {
if (!/\.less$/.test(id)) return;

// Use absolute file paths, otherwise nested alias resolution
// fails in less
const file = resolveAlias(alias, id);
const result = await renderLess(code, { resolve: this.resolve.bind(this), sourcemap, id: file });

for (let file of result.imports) {
mergedAssets.add(file);

if (!fileToBundles.has(file)) {
fileToBundles.set(file, new Set());
}
this.addWatchFile(file);
// @ts-ignore
fileToBundles.get(file).add(id);
}

return {
code: result.css,
map: result.map || null
};
},
watchChange(id) {
const bundle = fileToBundles.get(id);
if (bundle) return Array.from(bundle);
}
};
}
3 changes: 2 additions & 1 deletion packages/wmr/src/plugins/minify-css-plugin.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { posix } from 'path';
import cssnano from '../lib/cssnano-lite.js';
import postcss from 'postcss';
import { STYLE_REG } from './wmr/styles/styles-plugin.js';

const processor = postcss(cssnano());

Expand Down Expand Up @@ -73,4 +74,4 @@ function handleError(rollupContext, error) {
rollupContext.error(err, { line: error.line, column: error.column });
}

const isCssFilename = fileName => /\.(?:css|s[ac]ss)$/.test(fileName);
const isCssFilename = fileName => STYLE_REG.test(fileName);
3 changes: 2 additions & 1 deletion packages/wmr/src/plugins/optimize-graph-plugin.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { posix } from 'path';
import { hasDebugFlag } from '../lib/output-utils.js';
import { injectHead } from '../lib/transform-html.js';
import { STYLE_REG } from './wmr/styles/styles-plugin.js';

/** @typedef {import('rollup').OutputBundle} Bundle */
/** @typedef {import('rollup').OutputChunk} Chunk */
Expand Down Expand Up @@ -522,7 +523,7 @@ function replaceSimpleFunctionCall(code, replacer) {
});
}

const isCssFilename = fileName => /\.(?:css|s[ac]ss)$/.test(fileName);
const isCssFilename = fileName => STYLE_REG.test(fileName);

function getAssetSource(asset) {
let code = asset.source;
Expand Down
2 changes: 1 addition & 1 deletion packages/wmr/src/plugins/resolve-extensions-plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export default function resolveExtensionsPlugin({ extensions, index, mainFields
name: 'resolve-extensions-plugin',
async resolveId(id, importer) {
if (id[0] === '\0') return;
if (/\.(tsx?|css|s[ac]ss|wasm)$/.test(id)) return;
if (/\.(tsx?|css|s[ac]ss|less|wasm)$/.test(id)) return;

let resolved;
try {
Expand Down
2 changes: 1 addition & 1 deletion packages/wmr/src/plugins/sass-plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ export default function sassPlugin({ production, sourcemap, root, mergedAssets }
// `node-sass` always returns unix style paths,
// even on windows
file = path.normalize(file);
if (mergedAssets) mergedAssets.add(file);
mergedAssets.add(file);

if (!fileToBundles.has(file)) {
fileToBundles.set(file, new Set());
Expand Down
2 changes: 1 addition & 1 deletion packages/wmr/src/plugins/wmr/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ function handleMessage(e) {
data.changes.forEach(url => {
url = resolve(url);
if (!mods.get(url)) {
if (/\.(css|s[ac]ss)$/.test(url)) {
if (/\.(css|s[ac]ss|less)$/.test(url)) {
if (mods.has(url + '?module')) {
url += '?module';
} else {
Expand Down
8 changes: 5 additions & 3 deletions packages/wmr/src/plugins/wmr/styles/styles-plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { transformCss } from '../../../lib/transform-css.js';
import { matchAlias } from '../../../lib/aliasing.js';
import { modularizeCss } from './css-modules.js';

export const STYLE_REG = /\.(?:css|s[ac]ss|less)$/;

// invalid object keys
const RESERVED_WORDS =
/^(abstract|async|boolean|break|byte|case|catch|char|class|const|continue|debugger|default|delete|do|double|else|enum|export|extends|false|final|finally|float|for|function|goto|if|implements|import|in|instanceof|int|interface|let|long|native|new|null|package|private|protected|public|return|short|static|super|switch|synchronized|this|throw|throws|transient|true|try|typeof|var|void|volatile|while|with|yield)$/;
Expand All @@ -29,15 +31,15 @@ export default function wmrStylesPlugin({ root, hot, production, alias, sourcema
return {
name: 'wmr-styles',
async transform(source, id) {
if (!id.match(/\.(css|s[ac]ss)$/)) return;
if (!STYLE_REG.test(id)) return;
if (id[0] === '\0') return;

let idRelative = id;
let aliased = matchAlias(alias, id);
idRelative = aliased ? aliased.slice('/@alias/'.length) : relative(root, id);

const mappings = [];
if (/\.module\.(css|s[ac]ss)$/.test(id)) {
if (/\.module\.(css|s[ac]ss|less)$/.test(id)) {
source = await modularizeCss(source, idRelative, mappings, id);
} else {
if (/(composes:|:global|:local)/.test(source)) {
Expand Down Expand Up @@ -99,7 +101,7 @@ export default function wmrStylesPlugin({ root, hot, production, alias, sourcema

const ref = this.emitFile({
type: 'asset',
name: basename(id).replace(/\.s[ac]ss$/, '.css'),
name: basename(id).replace(/\.(s[ac]ss|less)$/, '.css'),
// Preserve asset path to avoid file clashes:
// foo/styles.module.css
// bar/styles.module.css
Expand Down
20 changes: 10 additions & 10 deletions packages/wmr/src/wmr-middleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { matchAlias, resolveAlias } from './lib/aliasing.js';
import { addTimestamp } from './lib/net-utils.js';
import { mergeSourceMaps } from './lib/sourcemap.js';
import { isFile } from './lib/fs-utils.js';
import { STYLE_REG } from './plugins/wmr/styles/styles-plugin.js';

const NOOP = () => {};

Expand Down Expand Up @@ -192,7 +193,7 @@ export default function wmrMiddleware(options) {
bubbleUpdates(file + '?module');
}

if (/\.(css|s[ac]ss)$/.test(file)) {
if (STYLE_REG.test(file)) {
pendingChanges.add('/' + file);
} else if (/\.(mjs|[tj]sx?)$/.test(file)) {
if (!moduleGraph.has(file)) {
Expand Down Expand Up @@ -282,8 +283,13 @@ export default function wmrMiddleware(options) {
// Force serving as a js module for proxy modules. Main use
// case is CSS-Modules.
const isModule = queryParams.has('module');
const isAsset = queryParams.has('asset');

let type;
if (isModule) type = 'application/javascript;charset=utf-8';
else if (isAsset && /\.(?:s[ac]ss|less)$/.test(file)) type = 'text/css';
else type = getMimeType(file);

let type = isModule ? 'application/javascript;charset=utf-8' : getMimeType(file);
if (type) {
res.setHeader('Content-Type', type);
}
Expand All @@ -300,13 +306,7 @@ export default function wmrMiddleware(options) {
} else if (queryParams.has('asset')) {
cacheKey += '?asset';
transform = TRANSFORMS.asset;
} else if (
prefix ||
hasIdPrefix ||
isModule ||
/\.([mc]js|[tj]sx?)$/.test(file) ||
/\.(css|s[ac]ss)$/.test(file)
) {
} else if (prefix || hasIdPrefix || isModule || /\.([mc]js|[tj]sx?)$/.test(file) || STYLE_REG.test(file)) {
transform = TRANSFORMS.js;
} else if (file.startsWith(root + sep) && (await isFile(file))) {
// Ignore dotfiles
Expand Down Expand Up @@ -344,7 +344,7 @@ export default function wmrMiddleware(options) {
// Grab the asset id out of the compiled js
// TODO: Wire this up into Style-Plugin by passing the
// import type through resolution somehow
if (!isModule && /\.(css|s[ac]ss)$/.test(file) && typeof result === 'string') {
if (!isModule && STYLE_REG.test(file) && typeof result === 'string') {
const match = result.match(/style\(["']\/([^"']+?)["'].*?\);/m);

if (match) {
Expand Down
1 change: 1 addition & 0 deletions packages/wmr/test/fixtures/css-less-absolute/bar.less
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@color: red;
5 changes: 5 additions & 0 deletions packages/wmr/test/fixtures/css-less-absolute/foo.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
@import '/bar.less';

h1 {
color: @color;
}
14 changes: 14 additions & 0 deletions packages/wmr/test/fixtures/css-less-absolute/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<link rel="stylesheet" href="./foo.less" />
</head>
<body>
<h1>hello</h1>
<script type="module" src="index.js"></script>
</body>
</html>
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Less</title>
<link rel="stylesheet" href="style.less" />
</head>
<body>
<h1>Less</h1>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@import '../src/foo.less';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@color: red;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
@import './bar.less';

h1 {
color: @color;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export default {
alias: {
'src/*': 'src'
}
};
1 change: 1 addition & 0 deletions packages/wmr/test/fixtures/css-less-alias/foo/foo.less
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@color: red;

0 comments on commit 6e8d710

Please sign in to comment.