Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.
 
 
 

276 рядки
8.1 KiB

  1. import { Plugin } from "esbuild";
  2. import { Plugin as PostCSSPlugin, Message } from "postcss";
  3. import {
  4. ensureDir,
  5. readFile,
  6. readdirSync,
  7. statSync,
  8. writeFile
  9. } from "fs-extra";
  10. import { TextDecoder } from "util";
  11. import {
  12. SassException,
  13. Result as SassResult,
  14. Options as SassOptions
  15. } from "sass";
  16. import path from "path";
  17. import tmp from "tmp";
  18. import postcss from "postcss";
  19. import postcssModules from "postcss-modules";
  20. import less from "less";
  21. import stylus from "stylus";
  22. import resolveFile from "resolve-file";
  23. type StylusRenderOptions = Parameters<typeof stylus.render>[1]; // The Stylus.RenderOptions interface doesn't seem to be exported... So next best
  24. interface PostCSSPluginOptions {
  25. plugins: PostCSSPlugin[];
  26. modules: boolean | any;
  27. rootDir?: string;
  28. sassOptions?: SassOptions;
  29. lessOptions?: Less.Options;
  30. stylusOptions?: StylusRenderOptions;
  31. fileIsModule?: (filename: string) => boolean;
  32. }
  33. interface CSSModule {
  34. path: string;
  35. map: {
  36. [key: string]: string;
  37. };
  38. }
  39. export const defaultOptions: PostCSSPluginOptions = {
  40. plugins: [],
  41. modules: true,
  42. rootDir: process.cwd(),
  43. sassOptions: {},
  44. lessOptions: {},
  45. stylusOptions: {},
  46. fileIsModule: null
  47. };
  48. const postCSSPlugin = ({
  49. plugins = [],
  50. modules = true,
  51. rootDir = process.cwd(),
  52. sassOptions = {},
  53. lessOptions = {},
  54. stylusOptions = {},
  55. fileIsModule
  56. }: PostCSSPluginOptions = defaultOptions): Plugin => ({
  57. name: "postcss2",
  58. setup(build) {
  59. // get a temporary path where we can save compiled CSS
  60. const tmpDirPath = tmp.dirSync().name,
  61. modulesMap: CSSModule[] = [];
  62. const modulesPlugin = postcssModules({
  63. generateScopedName: "[name]__[local]___[hash:base64:5]",
  64. ...(typeof modules !== "boolean" ? modules : {}),
  65. getJSON(filepath, json, outpath) {
  66. // Make sure to replace json map instead of pushing new map everytime with edit file on watch
  67. const mapIndex = modulesMap.findIndex((m) => m.path === filepath);
  68. if (mapIndex !== -1) {
  69. modulesMap[mapIndex].map = json;
  70. } else {
  71. modulesMap.push({
  72. path: filepath,
  73. map: json
  74. });
  75. }
  76. if (
  77. typeof modules !== "boolean" &&
  78. typeof modules.getJSON === "function"
  79. )
  80. return modules.getJSON(filepath, json, outpath);
  81. }
  82. });
  83. build.onResolve(
  84. { filter: /.\.(css|sass|scss|less|styl)$/ },
  85. async (args) => {
  86. // Namespace is empty when using CSS as an entrypoint
  87. if (args.namespace !== "file" && args.namespace !== "") return;
  88. // Resolve files from node_modules (ex: npm install normalize.css)
  89. let sourceFullPath = resolveFile(args.path);
  90. if (!sourceFullPath)
  91. sourceFullPath = path.resolve(args.resolveDir, args.path);
  92. const sourceExt = path.extname(sourceFullPath);
  93. const sourceBaseName = path.basename(sourceFullPath, sourceExt);
  94. const isModule = fileIsModule
  95. ? fileIsModule(sourceFullPath)
  96. : sourceBaseName.match(/\.module$/);
  97. const sourceDir = path.dirname(sourceFullPath);
  98. const watchFiles = [sourceFullPath];
  99. let tmpFilePath: string;
  100. if (args.kind === "entry-point") {
  101. // For entry points, we use <tempdir>/<path-within-project-root>/<file-name>.css
  102. const sourceRelDir = path.relative(
  103. path.dirname(rootDir),
  104. path.dirname(sourceFullPath)
  105. );
  106. tmpFilePath = path.resolve(
  107. tmpDirPath,
  108. sourceRelDir,
  109. `${sourceBaseName}.css`
  110. );
  111. await ensureDir(path.dirname(tmpFilePath));
  112. } else {
  113. // For others, we use <tempdir>/<unique-directory-name>/<file-name>.css
  114. //
  115. // This is a workaround for the following esbuild issue:
  116. // https://github.com/evanw/esbuild/issues/1101
  117. //
  118. // esbuild is unable to find the file, even though it does exist. This only
  119. // happens for files in a directory with several other entries, so by
  120. // creating a unique directory name per file on every build, we guarantee
  121. // that there will only every be a single file present within the directory,
  122. // circumventing the esbuild issue.
  123. const uniqueTmpDir = path.resolve(tmpDirPath, uniqueId());
  124. tmpFilePath = path.resolve(uniqueTmpDir, `${sourceBaseName}.css`);
  125. }
  126. await ensureDir(path.dirname(tmpFilePath));
  127. const fileContent = await readFile(sourceFullPath);
  128. let css = sourceExt === ".css" ? fileContent : "";
  129. // parse files with preprocessors
  130. if (sourceExt === ".sass" || sourceExt === ".scss") {
  131. const sassResult = await renderSass({
  132. ...sassOptions,
  133. file: sourceFullPath
  134. });
  135. css = sassResult.css.toString();
  136. watchFiles.push(...sassResult.stats.includedFiles);
  137. }
  138. if (sourceExt === ".styl")
  139. css = await renderStylus(new TextDecoder().decode(fileContent), {
  140. ...stylusOptions,
  141. filename: sourceFullPath
  142. });
  143. if (sourceExt === ".less")
  144. css = (
  145. await less.render(new TextDecoder().decode(fileContent), {
  146. ...lessOptions,
  147. filename: sourceFullPath,
  148. rootpath: path.dirname(args.path)
  149. })
  150. ).css;
  151. // wait for plugins to complete parsing & get result
  152. const result = await postcss(
  153. isModule ? [modulesPlugin, ...plugins] : plugins
  154. ).process(css, {
  155. from: sourceFullPath,
  156. to: tmpFilePath
  157. });
  158. watchFiles.push(...getPostCssDependencies(result.messages));
  159. // Write result CSS
  160. await writeFile(tmpFilePath, result.css);
  161. return {
  162. namespace: isModule ? "postcss-module" : "file",
  163. path: tmpFilePath,
  164. watchFiles,
  165. pluginData: {
  166. originalPath: sourceFullPath
  167. }
  168. };
  169. }
  170. );
  171. // load css modules
  172. build.onLoad(
  173. { filter: /.*/, namespace: "postcss-module" },
  174. async (args) => {
  175. const mod = modulesMap.find(
  176. ({ path }) => path === args?.pluginData?.originalPath
  177. ),
  178. resolveDir = path.dirname(args.path);
  179. return {
  180. resolveDir,
  181. contents: `import ${JSON.stringify(
  182. args.path
  183. )};\nexport default ${JSON.stringify(mod && mod.map ? mod.map : {})};`
  184. };
  185. }
  186. );
  187. }
  188. });
  189. function renderSass(options: SassOptions): Promise<SassResult> {
  190. return new Promise((resolve, reject) => {
  191. getSassImpl().render(options, (e: SassException, res: SassResult) => {
  192. if (e) reject(e);
  193. else resolve(res);
  194. });
  195. });
  196. }
  197. function renderStylus(
  198. str: string,
  199. options: StylusRenderOptions
  200. ): Promise<string> {
  201. return new Promise((resolve, reject) => {
  202. stylus.render(str, options, (e, res) => {
  203. if (e) reject(e);
  204. else resolve(res);
  205. });
  206. });
  207. }
  208. function getSassImpl() {
  209. let impl = "sass";
  210. try {
  211. require.resolve("sass");
  212. } catch {
  213. try {
  214. require.resolve("node-sass");
  215. impl = "node-sass";
  216. } catch {
  217. throw new Error('Please install "sass" or "node-sass" package');
  218. }
  219. }
  220. return require(impl);
  221. }
  222. function getFilesRecursive(directory: string): string[] {
  223. return readdirSync(directory).reduce((files, file) => {
  224. const name = path.join(directory, file);
  225. return statSync(name).isDirectory()
  226. ? [...files, ...getFilesRecursive(name)]
  227. : [...files, name];
  228. }, []);
  229. }
  230. let idCounter = 0;
  231. /**
  232. * Generates an id that is guaranteed to be unique for the Node.JS instance.
  233. */
  234. function uniqueId(): string {
  235. return Date.now().toString(16) + (idCounter++).toString(16);
  236. }
  237. function getPostCssDependencies(messages: Message[]): string[] {
  238. let dependencies = [];
  239. for (const message of messages) {
  240. if (message.type == "dir-dependency") {
  241. dependencies.push(...getFilesRecursive(message.dir));
  242. } else if (message.type == "dependency") {
  243. dependencies.push(message.file);
  244. }
  245. }
  246. return dependencies;
  247. }
  248. export default postCSSPlugin;