Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Include pre-processor dependencies in ESBuild's watch files #29

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
83 changes: 33 additions & 50 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 3 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,7 @@
"license": "ISC",
"dependencies": {
"@types/less": "^3.0.3",
"@types/sass": "^1.43.1",
"@types/stylus": "^0.48.38",
"@types/stylus": "^0.48.42",
"glob": "^10.2.2",
"postcss": "^8.4.31",
"postcss-modules": "^6.0.0"
Expand All @@ -42,8 +41,8 @@
"postcss-import": "^15.1.0",
"postcss-preset-env": "^8.3.2",
"postcss-scss": "^4.0.6",
"sass": "^1.69.5",
"stylus": "^0.59.0",
"sass": "^1.70.0",
"stylus": "^0.62.0",
"tailwindcss": "^3.3.2",
"ts-jest": "^29.1.0",
"ts-node": "^10.9.1"
Expand Down
3 changes: 1 addition & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,8 @@ const onStyleLoad = (options: PluginOptions) => async (args: OnLoadArgs): Promis
const renderOptions = options.renderOptions

// Render whatever style currently on the loader .css, .sass, .styl, .less
let css = await renderStyle(args.path, renderOptions)
let { css, watchFiles } = await renderStyle(args.path, renderOptions)

let watchFiles = []
let mapping = { data: {} }
let { plugins = [], ...processOptions } = options.postcss || {}
let injectMapping = false
Expand Down
111 changes: 93 additions & 18 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,34 @@ import { TextDecoder } from 'util'
import path from 'path'
import fs from 'fs'
import { globSync } from 'glob'
import { Options as SassOptions } from 'sass'
import { RenderOptions as StylusOptions } from 'stylus'
import type * as Sass from 'sass'
import type * as Stylus from 'stylus'
import type * as Less from 'less'
import { AcceptedPlugin, Result } from 'postcss'

type SassOptions = Sass.LegacySharedOptions<'sync'>;

export interface RenderOptions {
sassOptions?: SassOptions<'sync'>
stylusOptions?: StylusOptions
sassOptions?: SassOptions
stylusOptions?: Stylus.RenderOptions
lessOptions?: Less.Options
}

export interface RenderResult {
css: string;
watchFiles: string[];
}

interface SourceMap {
version: number;
file: string;
sourceRoot?: string;
sources: string[];
sourcesContent?: string[];
names: string[];
mappings: string;
}

export const getModule = async (moduleName: string, checkFunc: string) => {
try {
const module = await import(moduleName)
Expand All @@ -27,40 +45,97 @@ export const getModule = async (moduleName: string, checkFunc: string) => {
}
}

const renderStylus = async (css: string, options: StylusOptions): Promise<string> => {
const stylus = await getModule('stylus', 'render')
const getWatchFilesFromSourceMap = (rootFile: string, sourceMap: SourceMap): string[] => {
try {
const baseDir = path.dirname(rootFile);
const watchFiles: string[] = sourceMap.sources.map((srcFile) => {
return path.resolve(baseDir, srcFile);
});
return watchFiles;
} catch (e) {
console.error(e);
return [];
}
};

const renderStylus = async (filePath: string, source: string, options: Stylus.RenderOptions): Promise<RenderResult> => {
const stylus: typeof Stylus = await getModule('stylus', 'render')
return new Promise((resolve, reject) => {
stylus.render(css, options, (err, css) => {
if (err) reject(err)
resolve(css)
})
})
}
const style = stylus.default(source, options)
.set('sourcemap', { inline: false });

style.render((err, css) => {
const sourceMap: SourceMap = (style as any).sourcemap
if (err) {
reject(err);
} else {
resolve({
css: css,
watchFiles: getWatchFilesFromSourceMap(filePath, sourceMap),
});
}
});
});
};

const renderSass = async (filePath: string, options: SassOptions): Promise<RenderResult> => {
const sass: typeof Sass = (await getModule('sass', 'renderSync'));
const sassResult = sass.renderSync({
...options,
file: filePath,
// Force sourcemap to be enabled so that we can parse the file sources out of it
sourceMap: `${filePath}.map`,
sourceMapEmbed: false,
});
const sourceMap: SourceMap = JSON.parse(sassResult.map.toString());
return {
css: sassResult.css.toString(),
watchFiles: getWatchFilesFromSourceMap(filePath, sourceMap),
};
};

const renderLess = async (filePath: string, source: string, options: Less.Options): Promise<RenderResult> => {
const less: typeof Less = await getModule('less', 'render')
const lessResults = await less.render(source, {
...options,
// Force sourcemap to be enabled so that we can parse the file sources out of it
sourceMap: {
sourceMapFileInline: false,
},
});
const sourceMap: SourceMap = JSON.parse(lessResults.map);
return {
css: lessResults.css,
watchFiles: getWatchFilesFromSourceMap(filePath, sourceMap),
};
};

export const renderStyle = async (filePath, options: RenderOptions = {}): Promise<string> => {
export const renderStyle = async (filePath: string, options: RenderOptions = {}): Promise<RenderResult> => {
const { ext } = path.parse(filePath)

if (ext === '.css') {
return (await fs.promises.readFile(filePath)).toString()
return {
css: (await fs.promises.readFile(filePath)).toString(),
watchFiles: [],
};
}

if (ext === '.sass' || ext === '.scss') {
const sassOptions = options.sassOptions || {}
const sass = await getModule('sass', 'renderSync')
return sass.renderSync({ ...sassOptions, file: filePath }).css.toString()
return renderSass(filePath, sassOptions);
}

if (ext === '.styl') {
const stylusOptions = options.stylusOptions || {}
const source = await fs.promises.readFile(filePath)
return await renderStylus(new TextDecoder().decode(source), { ...stylusOptions, filename: filePath })
return renderStylus(filePath, new TextDecoder().decode(source), { ...stylusOptions, filename: filePath });
}

if (ext === '.less') {
const lestOptions = options.lessOptions || {}
const source = await fs.promises.readFile(filePath)
const less = await getModule('less', 'render')
return (await less.render(new TextDecoder().decode(source), { ...lestOptions, filename: filePath })).css
return renderLess(filePath, new TextDecoder().decode(source), { ...lestOptions, filename: filePath });
}

throw new Error(`Can't render this style '${ext}'.`)
Expand Down
7 changes: 7 additions & 0 deletions test/preprocessors/src/styles.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
@font-stack: Helvetica, sans-serif;
@primary-color: #333;

body {
font: 100% @font-stack;
color: @primary-color;
}
4 changes: 2 additions & 2 deletions test/render_options/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ test('Test sassOptions', async () => {
plugins: [stylePlugin({
renderOptions: {
sassOptions: {
style: 'compressed'
outputStyle: 'compressed'
},
lessOptions: {
globalVars: {
Expand All @@ -26,4 +26,4 @@ test('Test sassOptions', async () => {
}
})]
})
})
})
Loading