├── .editorconfig ├── .eslintrc.yaml ├── .github └── workflows │ └── ci.yaml ├── .gitignore ├── .npmrc ├── .nvmrc ├── .prettierrc.yaml ├── .remarkrc.yaml ├── LICENSE.md ├── README.md ├── build.js ├── examples ├── demo │ ├── package.json │ ├── src │ │ ├── icon.svg │ │ ├── index.css │ │ ├── index.ejs │ │ ├── index.ts │ │ └── tailwindcssplugin.worker.js │ └── webpack.config.js ├── esbuild-demo │ ├── build.js │ ├── index.html │ ├── package.json │ └── src │ │ └── index.js └── vite-example │ ├── index.html │ ├── index.js │ ├── package.json │ └── vite.config.js ├── index.d.ts ├── netlify.toml ├── package-lock.json ├── package.json ├── src ├── cssData.ts ├── index.ts ├── languageFeatures.ts ├── stubs │ ├── braces.ts │ ├── crypto.ts │ ├── detect-indent.ts │ ├── fs.ts │ ├── path.ts │ ├── picocolors.ts │ ├── tailwindcss │ │ └── utils │ │ │ └── log.ts │ ├── url.ts │ ├── util-deprecate.ts │ ├── util.ts │ └── vscode-emmet-helper-bundled.ts ├── tailwindcss.worker.ts └── types.ts ├── tailwindcss.worker.d.ts ├── tsconfig.json └── types.d.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 2 8 | indent_style = space 9 | insert_final_newline = true 10 | max_line_length = 100 11 | trim_trailing_whitespace = true 12 | 13 | [COMMIT_EDITMSG] 14 | max_line_length = 72 15 | -------------------------------------------------------------------------------- /.eslintrc.yaml: -------------------------------------------------------------------------------- 1 | root: true 2 | extends: 3 | - remcohaszing 4 | - remcohaszing/typechecking 5 | rules: 6 | no-duplicate-imports: off 7 | 8 | import/no-extraneous-dependencies: off 9 | 10 | jsdoc/require-jsdoc: off 11 | overrides: 12 | - files: 13 | - examples/esbuild-demo/build.js 14 | rules: 15 | no-console: off 16 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: [main] 7 | tags: ['*'] 8 | 9 | jobs: 10 | eslint: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: actions/setup-node@v4 15 | with: { node-version: 22 } 16 | - run: npm ci 17 | - run: npx eslint . 18 | 19 | examples: 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v4 23 | - uses: actions/setup-node@v4 24 | with: { node-version: 22 } 25 | - run: npm ci 26 | - run: npm run prepack 27 | - run: npm run build --workspaces --if-present 28 | 29 | pack: 30 | runs-on: ubuntu-latest 31 | steps: 32 | - uses: actions/checkout@v4 33 | - uses: actions/setup-node@v4 34 | with: { node-version: 22 } 35 | - run: npm ci 36 | - run: npm pack 37 | - uses: actions/upload-artifact@v4 38 | with: 39 | name: package 40 | path: '*.tgz' 41 | 42 | prettier: 43 | runs-on: ubuntu-latest 44 | steps: 45 | - uses: actions/checkout@v4 46 | - uses: actions/setup-node@v4 47 | with: { node-version: 22 } 48 | - run: npm ci 49 | - run: npx prettier --check . 50 | 51 | remark: 52 | runs-on: ubuntu-latest 53 | steps: 54 | - uses: actions/checkout@v4 55 | - uses: actions/setup-node@v4 56 | with: { node-version: 22 } 57 | - run: npm ci 58 | - run: npx remark . --frail 59 | 60 | tsc: 61 | runs-on: ubuntu-latest 62 | steps: 63 | - uses: actions/checkout@v4 64 | - uses: actions/setup-node@v4 65 | with: { node-version: 22 } 66 | - run: npm ci 67 | - run: npx tsc 68 | 69 | release: 70 | runs-on: ubuntu-latest 71 | needs: 72 | - eslint 73 | - examples 74 | - pack 75 | - prettier 76 | - remark 77 | - tsc 78 | if: startsWith(github.ref, 'refs/tags/') 79 | steps: 80 | - uses: actions/setup-node@v4 81 | with: 82 | node-version: 22 83 | registry-url: https://registry.npmjs.org 84 | - uses: actions/download-artifact@v4 85 | with: { name: package } 86 | - run: npm publish *.tgz 87 | env: 88 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 89 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | /index.js 4 | /tailwindcss.worker.js 5 | *.map 6 | *.tgz 7 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | install-links = false 2 | lockfile-version = 3 3 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 20 2 | -------------------------------------------------------------------------------- /.prettierrc.yaml: -------------------------------------------------------------------------------- 1 | proseWrap: always 2 | semi: false 3 | singleQuote: true 4 | trailingComma: none 5 | -------------------------------------------------------------------------------- /.remarkrc.yaml: -------------------------------------------------------------------------------- 1 | plugins: 2 | - remark-preset-remcohaszing 3 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright © 2022 Remco Haszing 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and 6 | associated documentation files (the “Software”), to deal in the Software without restriction, 7 | including without limitation the rights to use, copy, modify, merge, publish, distribute, 8 | sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all copies or substantial 12 | portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT 15 | NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 16 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES 17 | OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Monaco Tailwindcss 2 | 3 | [![ci workflow](https://github.com/remcohaszing/monaco-tailwindcss/actions/workflows/ci.yaml/badge.svg)](https://github.com/remcohaszing/monaco-tailwindcss/actions/workflows/ci.yaml) 4 | [![npm version](https://img.shields.io/npm/v/monaco-tailwindcss)](https://www.npmjs.com/package/monaco-tailwindcss) 5 | [![prettier code style](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](https://prettier.io) 6 | [![demo](https://img.shields.io/badge/demo-monaco--tailwindcss.js.org-61ffcf.svg)](https://monaco-tailwindcss.js.org) 7 | [![netlify Status](https://api.netlify.com/api/v1/badges/d56b5f9b-3adc-4c22-a355-761e72c774ab/deploy-status)](https://app.netlify.com/sites/monaco-tailwindcss/deploys) 8 | 9 | [Tailwindcss](https://tailwindcss.com) integration for 10 | [Monaco editor](https://microsoft.github.io/monaco-editor). 11 | 12 | ## Table of Contents 13 | 14 | - [Installation](#installation) 15 | - [Usage](#usage) 16 | - [API](#api) 17 | - [`monaco-tailwindcss`](#monaco-tailwindcss-1) 18 | - [`monaco-tailwindcss/tailwindcss.worker`](#monaco-tailwindcsstailwindcssworker) 19 | - [Related projects](#related-projects) 20 | - [License](#license) 21 | 22 | ## Installation 23 | 24 | ```sh 25 | npm install monaco-tailwindcss 26 | ``` 27 | 28 | ## Usage 29 | 30 | Import `monaco-tailwindcss` and configure it before an editor instance is created. 31 | 32 | ```typescript 33 | import * as monaco from 'monaco-editor' 34 | import { configureMonacoTailwindcss, tailwindcssData } from 'monaco-tailwindcss' 35 | 36 | monaco.languages.css.cssDefaults.setOptions({ 37 | data: { 38 | dataProviders: { 39 | tailwindcssData 40 | } 41 | } 42 | }) 43 | 44 | configureMonacoTailwindcss(monaco) 45 | 46 | monaco.editor.create(document.createElement('editor'), { 47 | language: 'html', 48 | value: ` 49 | 50 | 51 | 52 | 53 | 54 |
55 | 56 | 57 | ` 58 | }) 59 | ``` 60 | 61 | Also make sure to register the web worker. When using Webpack 5, this looks like the code below. 62 | Other bundlers may use a different syntax, but the idea is the same. Languages you don’t used can be 63 | omitted. 64 | 65 | ```js 66 | window.MonacoEnvironment = { 67 | getWorker(moduleId, label) { 68 | switch (label) { 69 | case 'editorWorkerService': 70 | return new Worker(new URL('monaco-editor/esm/vs/editor/editor.worker', import.meta.url)) 71 | case 'css': 72 | case 'less': 73 | case 'scss': 74 | return new Worker(new URL('monaco-editor/esm/vs/language/css/css.worker', import.meta.url)) 75 | case 'handlebars': 76 | case 'html': 77 | case 'razor': 78 | return new Worker( 79 | new URL('monaco-editor/esm/vs/language/html/html.worker', import.meta.url) 80 | ) 81 | case 'json': 82 | return new Worker( 83 | new URL('monaco-editor/esm/vs/language/json/json.worker', import.meta.url) 84 | ) 85 | case 'javascript': 86 | case 'typescript': 87 | return new Worker( 88 | new URL('monaco-editor/esm/vs/language/typescript/ts.worker', import.meta.url) 89 | ) 90 | case 'tailwindcss': 91 | return new Worker(new URL('monaco-tailwindcss/tailwindcss.worker', import.meta.url)) 92 | default: 93 | throw new Error(`Unknown label ${label}`) 94 | } 95 | } 96 | } 97 | ``` 98 | 99 | ## API 100 | 101 | This package exposes two exports. One to setup the main logic, another to customize the Tailwind 102 | configuration in the worker. 103 | 104 | ### `monaco-tailwindcss` 105 | 106 | #### `configureMonacoTailwindcss(monaco, options?)` 107 | 108 | Configure `monaco-tailwindcss`. 109 | 110 | **Arguments**: 111 | 112 | - `monaco`: The `monaco-editor` module. (`object`) 113 | - `options`: An object with the following properties: 114 | - `languageSelector`: The language ID or IDs to which to apply `monaco-unified`. (`string` | 115 | `string[]`, optional, default: `['css', 'javascript', 'html', 'mdx', 'typescript']`) 116 | - `tailwindConfig`: The tailwind configuration to use. This may be either the Tailwind 117 | configuration object, or a string that gets processed in the worker. (`object` | `string`, 118 | optional) 119 | 120 | **Returns**: A disposable with the following additional properties: 121 | 122 | - `setTailwindConfig(tailwindConfig)`: Update the current Tailwind configuration. 123 | - `generateStylesFromContent(css, content)`: Generate a CSS string based on the current Tailwind 124 | configuration. 125 | 126 | #### `tailwindcssData` 127 | 128 | This data can be used with the default Monaco CSS support to support tailwind directives. It will 129 | provider hover information from the Tailwindcss documentation, including a link. 130 | 131 | ### `monaco-tailwindcss/tailwindcss.worker` 132 | 133 | #### `initialize(options)` 134 | 135 | Setup the Tailwindcss worker using a customized configuration. 136 | 137 | **Arguments**: 138 | 139 | - `options`: An object with the following properties: 140 | - `prepareTailwindConfig(tailwindConfig)` A functions which accepts the Tailwind configuration 141 | passed from the main thread, and returns a valid Tailwind configuration. 142 | 143 | ## Related projects 144 | 145 | - [monaco-unified](https://monaco-unified.js.org) 146 | - [monaco-yaml](https://monaco-yaml.js.org) 147 | 148 | ## License 149 | 150 | [MIT](LICENSE.md) © [Remco Haszing](https://github.com/remcohaszing) 151 | -------------------------------------------------------------------------------- /build.js: -------------------------------------------------------------------------------- 1 | import { readdir, readFile } from 'node:fs/promises' 2 | import { parse, sep } from 'node:path' 3 | import { fileURLToPath } from 'node:url' 4 | 5 | import { build } from 'esbuild' 6 | 7 | const [, , logLevel = 'info'] = process.argv 8 | const pkg = JSON.parse(await readFile(new URL('package.json', import.meta.url))) 9 | 10 | await build({ 11 | entryPoints: ['src/index.ts', 'src/tailwindcss.worker.ts'], 12 | bundle: true, 13 | external: Object.keys({ ...pkg.dependencies, ...pkg.peerDependencies }).filter( 14 | (name) => name !== 'tailwindcss' 15 | ), 16 | logLevel, 17 | outdir: '.', 18 | sourcemap: true, 19 | format: 'esm', 20 | target: ['es2020'], 21 | loader: { '.css': 'text' }, 22 | define: { 23 | 'process.env.DEBUG': 'undefined', 24 | 'process.env.JEST_WORKER_ID': '1', 25 | 'process.env.NODE_ENV': '"production"', 26 | __OXIDE__: 'undefined', 27 | __dirname: '"/"' 28 | }, 29 | plugins: [ 30 | { 31 | name: 'alias', 32 | async setup({ onLoad, onResolve, resolve }) { 33 | const stubFiles = await readdir('src/stubs', { withFileTypes: true }) 34 | // These packages are imported, but can be stubbed. 35 | const stubNames = stubFiles 36 | .filter((file) => file.isFile()) 37 | .map((file) => parse(file.name).name) 38 | 39 | onResolve({ filter: new RegExp(`^(${stubNames.join('|')})$`) }, ({ path }) => ({ 40 | path: fileURLToPath(new URL(`src/stubs/${path}.ts`, import.meta.url)) 41 | })) 42 | 43 | // The tailwindcss main export exports CJS, but we can get better tree shaking if we import 44 | // from the ESM src directoy instead. 45 | onResolve({ filter: /^tailwindcss$/ }, ({ path, ...options }) => 46 | resolve('tailwindcss/src', options) 47 | ) 48 | 49 | onResolve({ filter: /^tailwindcss\/lib/ }, ({ path, ...options }) => 50 | resolve(path.replace('lib', 'src'), options) 51 | ) 52 | 53 | // This file pulls in a number of dependencies, but we don’t really need it anyway. 54 | onResolve({ filter: /^\.+\/(util\/)?log$/, namespace: 'file' }, ({ path, ...options }) => { 55 | if (options.importer.includes(`${sep}tailwindcss${sep}`)) { 56 | return { 57 | path: fileURLToPath(new URL('src/stubs/tailwindcss/utils/log.ts', import.meta.url)) 58 | } 59 | } 60 | return resolve(path, { 61 | ...options, 62 | namespace: 'noRecurse' 63 | }) 64 | }) 65 | 66 | // CJS doesn’t require extensions, but ESM does. Since our package uses ESM, but dependant 67 | // bundled packages don’t, we need to add it ourselves. 68 | onResolve( 69 | { filter: /^(postcss-selector-parser|semver)\/.*\/\w+$/ }, 70 | ({ path, ...options }) => resolve(`${path}.js`, options) 71 | ) 72 | 73 | onResolve({ filter: /^postcss-value-parser$/ }, ({ path, ...options }) => 74 | resolve('tailwindcss/src/value-parser', options) 75 | ) 76 | 77 | onResolve({ filter: /^vscode-languageserver$/ }, ({ path, ...options }) => 78 | resolve('vscode-languageserver-types', options) 79 | ) 80 | 81 | // Rewrite the tailwind stubs from CJS to ESM, so our bundle doesn’t need to include any CJS 82 | // related logic. 83 | onLoad( 84 | { filter: /\/node_modules\/tailwindcss\/stubs\/defaultConfig\.stub\.js$/ }, 85 | async ({ path }) => { 86 | const cjs = await readFile(path, 'utf8') 87 | const esm = cjs.replace('module.exports =', 'export default') 88 | return { contents: esm } 89 | } 90 | ) 91 | 92 | // Rewrite the tailwind sharedState.env variables, so ESBuild can statically analyze and 93 | // remove dead code, including some problematic imports. 94 | onLoad({ filter: /\/node_modules\/tailwindcss\/.+\.js$/ }, async ({ path }) => { 95 | const source = await readFile(path, 'utf8') 96 | const contents = source 97 | .replaceAll(/(process\.)?env\.DEBUG/g, 'undefined') 98 | .replaceAll(/(process\.)?env\.ENGINE/g, '"stable"') 99 | .replaceAll(/(process\.)?env\.NODE_ENV/g, '"production"') 100 | return { contents } 101 | }) 102 | } 103 | } 104 | ] 105 | }) 106 | -------------------------------------------------------------------------------- /examples/demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo", 3 | "version": "1.0.0", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "start": "webpack serve --mode development", 8 | "build": "webpack --mode production" 9 | }, 10 | "dependencies": { 11 | "@tailwindcss/line-clamp": "^0.4.0", 12 | "@tailwindcss/typography": "^0.5.0", 13 | "@fortawesome/fontawesome-free": "^6.0.0", 14 | "css-loader": "^7.0.0", 15 | "css-minimizer-webpack-plugin": "^7.0.0", 16 | "html-webpack-plugin": "^5.0.0", 17 | "jsonc-parser": "^3.0.0", 18 | "mini-css-extract-plugin": "^2.0.0", 19 | "monaco-editor": "^0.52.0", 20 | "monaco-tailwindcss": "file:../..", 21 | "ts-loader": "^9.0.0", 22 | "webpack": "^5.0.0", 23 | "webpack-cli": "^6.0.0", 24 | "webpack-dev-server": "^5.0.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/demo/src/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /examples/demo/src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --background-color: hsl(0, 0%, 96%); 3 | --editor-background: hsl(60, 100%, 100%); 4 | --error-color: hsl(0, 85%, 62%); 5 | --foreground-color: hsl(0, 0%, 0%); 6 | --primary-color: hsl(189, 100%, 63%); 7 | --shadow-color: hsla(0, 0%, 27%, 0.239); 8 | --warning-color: hsl(49, 100%, 40%); 9 | } 10 | 11 | @media (prefers-color-scheme: dark) { 12 | :root { 13 | --background-color: hsl(0, 0%, 23%); 14 | --editor-background: hsl(0, 0%, 12%); 15 | --foreground-color: hsl(0, 0%, 100%); 16 | --shadow-color: hsl(0, 0%, 43%); 17 | } 18 | } 19 | 20 | body { 21 | background: var(--background-color); 22 | display: flex; 23 | flex-flow: column; 24 | font-family: sans-serif; 25 | height: 100vh; 26 | margin: 0; 27 | } 28 | 29 | h1 { 30 | margin: 0 1rem; 31 | } 32 | 33 | .navbar { 34 | align-items: center; 35 | background-color: var(--primary-color); 36 | display: flex; 37 | flex: 0 0 auto; 38 | height: 3rem; 39 | justify-content: space-between; 40 | } 41 | 42 | .nav-icon { 43 | text-decoration: none; 44 | } 45 | 46 | .nav-icon > img { 47 | height: 2rem; 48 | margin-right: 1rem; 49 | vertical-align: middle; 50 | } 51 | 52 | main { 53 | background: var(--editor-background); 54 | box-shadow: 0 0 10px var(--shadow-color); 55 | display: flex; 56 | flex: 1 1 auto; 57 | flex-flow: column; 58 | margin: 1.5rem; 59 | } 60 | 61 | .tabs { 62 | background: var(--editor-background); 63 | display: flex; 64 | flex: 0 0; 65 | flex-flow: wrap; 66 | width: 100%; 67 | } 68 | 69 | .tabs > a, 70 | .tabs > a:visited { 71 | background: transparent; 72 | box-shadow: inset 0 0 2px var(--shadow-color); 73 | color: var(--foreground-color); 74 | display: block; 75 | flex: 1 1 auto; 76 | padding: 1rem 1rem; 77 | text-align: center; 78 | text-decoration: none; 79 | transition: background 0.3s; 80 | } 81 | 82 | .tabs > button:hover, 83 | .tabs > a:hover, 84 | .tabs > a:target { 85 | background: var(--shadow-color); 86 | } 87 | 88 | .tabs > button { 89 | background: transparent; 90 | border: none; 91 | color: var(--foreground-color); 92 | cursor: pointer; 93 | padding: 0.5rem 2rem; 94 | } 95 | 96 | #editor { 97 | flex: 1 1 auto; 98 | } 99 | 100 | #problems, 101 | #output { 102 | border-top: 1px solid var(--shadow-color); 103 | flex: 0 0 20vh; 104 | color: var(--foreground-color); 105 | margin: 0; 106 | overflow-y: scroll; 107 | } 108 | 109 | .problem { 110 | align-items: center; 111 | cursor: pointer; 112 | display: flex; 113 | padding: 0.25rem; 114 | } 115 | 116 | .problem:hover { 117 | background-color: var(--shadow-color); 118 | } 119 | 120 | .problem-text { 121 | margin-left: 0.5rem; 122 | } 123 | 124 | .problem .codicon-warning { 125 | color: var(--warning-color); 126 | } 127 | 128 | .problem .codicon-error { 129 | color: var(--error-color); 130 | } 131 | 132 | *::-webkit-scrollbar { 133 | box-shadow: 1px 0 0 0 var(--scrollbar-color) inset; 134 | width: 14px; 135 | } 136 | 137 | *::-webkit-scrollbar-thumb { 138 | background: var(--scrollbar-color); 139 | } 140 | -------------------------------------------------------------------------------- /examples/demo/src/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Monaco Tailwindcss 7 | 8 | 9 | 10 | 11 | 32 |
33 | 39 |
40 | 44 |
45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /examples/demo/src/index.ts: -------------------------------------------------------------------------------- 1 | import { parse } from 'jsonc-parser' 2 | import * as monaco from 'monaco-editor' 3 | import { 4 | configureMonacoTailwindcss, 5 | type TailwindConfig, 6 | tailwindcssData 7 | } from 'monaco-tailwindcss' 8 | 9 | import './index.css' 10 | 11 | const tailwindConfig: TailwindConfig = { 12 | theme: { 13 | extend: { 14 | screens: { 15 | television: '90000px' 16 | }, 17 | spacing: { 18 | '128': '32rem' 19 | }, 20 | colors: { 21 | // https://icolorpalette.com/color/molten-lava 22 | lava: '#b5332e', 23 | // Taken from https://icolorpalette.com/color/ocean-blue 24 | ocean: { 25 | 50: '#f2fcff', 26 | 100: '#c1f2fe', 27 | 200: '#90e9ff', 28 | 300: '#5fdfff', 29 | 400: '#2ed5ff', 30 | 500: '#00cafc', 31 | 600: '#00a3cc', 32 | 700: '#007c9b', 33 | 800: '#00546a', 34 | 900: '#002d39' 35 | } 36 | } 37 | } 38 | } 39 | } 40 | 41 | const monacoTailwindcss = configureMonacoTailwindcss(monaco, { tailwindConfig }) 42 | 43 | window.MonacoEnvironment = { 44 | getWorker(moduleId, label) { 45 | switch (label) { 46 | case 'editorWorkerService': 47 | return new Worker(new URL('monaco-editor/esm/vs/editor/editor.worker.js', import.meta.url)) 48 | case 'css': 49 | return new Worker( 50 | new URL('monaco-editor/esm/vs/language/css/css.worker.js', import.meta.url) 51 | ) 52 | case 'html': 53 | return new Worker( 54 | new URL('monaco-editor/esm/vs/language/html/html.worker.js', import.meta.url) 55 | ) 56 | case 'json': 57 | return new Worker( 58 | new URL('monaco-editor/esm/vs/language/json/json.worker.js', import.meta.url) 59 | ) 60 | case 'tailwindcss': 61 | // We are using a custom worker instead of the default 62 | // 'monaco-tailwindcss/tailwindcss.worker.js' 63 | // This way we can enable custom plugins 64 | return new Worker(new URL('tailwindcssplugin.worker.js', import.meta.url)) 65 | default: 66 | throw new Error(`Unknown label ${label}`) 67 | } 68 | } 69 | } 70 | 71 | monaco.languages.css.cssDefaults.setOptions({ 72 | data: { 73 | dataProviders: { 74 | tailwind: tailwindcssData 75 | } 76 | } 77 | }) 78 | 79 | monaco.languages.json.jsonDefaults.setDiagnosticsOptions({ 80 | allowComments: true, 81 | trailingCommas: 'ignore' 82 | }) 83 | 84 | const tailwindrcModel = monaco.editor.createModel( 85 | `${JSON.stringify(tailwindConfig, undefined, 2)}\n`, 86 | 'json', 87 | monaco.Uri.parse('file:///.tailwindrc.json') 88 | ) 89 | const cssModel = monaco.editor.createModel( 90 | `@tailwind base; 91 | @tailwind components; 92 | @tailwind utilities; 93 | 94 | @layer base { 95 | h1 { 96 | @apply text-2xl; 97 | } 98 | h2 { 99 | @apply text-xl; 100 | } 101 | } 102 | 103 | @layer components { 104 | .btn-blue { 105 | @apply bg-blue-500 hover:bg-blue-700 text-white font-bold font-bold py-2 px-4 rounded; 106 | } 107 | } 108 | 109 | @layer utilities { 110 | .filter-none { 111 | filter: none; 112 | } 113 | .filter-grayscale { 114 | filter: grayscale(100%); 115 | } 116 | } 117 | 118 | .select2-dropdown { 119 | @apply rounded-b-lg shadow-md; 120 | } 121 | 122 | .select2-search { 123 | @apply border border-gray-300 rounded; 124 | } 125 | 126 | .select2-results__group { 127 | @apply text-lg font-bold text-gray-900; 128 | } 129 | `, 130 | 'css' 131 | ) 132 | const htmlModel = monaco.editor.createModel( 133 | ` 134 | 135 | 136 | 137 | 138 | 139 |
140 | 141 |

142 | Custom colors are supported too! 143 |

144 | 145 | 146 | 147 | 148 | `, 149 | 'html' 150 | ) 151 | const mdxModel = monaco.editor.createModel( 152 | `import { MyComponent } from './MyComponent' 153 | 154 | # Hello MDX 155 | 156 | 157 | 158 | This is **also** markdown. 159 | 160 | 161 | `, 162 | 'mdx' 163 | ) 164 | 165 | function getModel(): monaco.editor.ITextModel { 166 | switch (window.location.hash) { 167 | case '#tailwindrc': 168 | return tailwindrcModel 169 | case '#css': 170 | return cssModel 171 | case '#mdx': 172 | return mdxModel 173 | default: 174 | window.location.hash = '#html' 175 | return htmlModel 176 | } 177 | } 178 | 179 | const theme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'vs-dark' : 'vs-light' 180 | const ed = monaco.editor.create(document.getElementById('editor')!, { 181 | automaticLayout: true, 182 | theme, 183 | colorDecorators: true, 184 | model: getModel(), 185 | wordBasedSuggestions: 'off' 186 | }) 187 | 188 | const outputPane = document.getElementById('output')! 189 | const problemsPane = document.getElementById('problems')! 190 | const outputButton = document.getElementById('output-button')! 191 | const problemsButton = document.getElementById('problems-button')! 192 | 193 | problemsButton.addEventListener('click', () => { 194 | outputPane.hidden = true 195 | problemsPane.hidden = false 196 | }) 197 | 198 | outputButton.addEventListener('click', () => { 199 | problemsPane.hidden = true 200 | outputPane.hidden = false 201 | }) 202 | 203 | async function generateOutput(): Promise { 204 | const content = await monacoTailwindcss.generateStylesFromContent(cssModel.getValue(), [ 205 | { content: htmlModel.getValue(), extension: htmlModel.getLanguageId() }, 206 | { content: mdxModel.getValue(), extension: mdxModel.getLanguageId() } 207 | ]) 208 | outputPane.textContent = content 209 | monaco.editor.colorizeElement(outputPane, { mimeType: 'css', theme }) 210 | } 211 | 212 | // eslint-disable-next-line unicorn/prefer-top-level-await 213 | generateOutput() 214 | cssModel.onDidChangeContent(generateOutput) 215 | htmlModel.onDidChangeContent(generateOutput) 216 | mdxModel.onDidChangeContent(generateOutput) 217 | 218 | function updateMarkers(resource: monaco.Uri): void { 219 | const problems = document.getElementById('problems')! 220 | const markers = monaco.editor.getModelMarkers({ resource }) 221 | while (problems.lastChild) { 222 | problems.lastChild.remove() 223 | } 224 | for (const marker of markers) { 225 | if (marker.severity === monaco.MarkerSeverity.Hint) { 226 | continue 227 | } 228 | const wrapper = document.createElement('div') 229 | wrapper.setAttribute('role', 'button') 230 | const codicon = document.createElement('div') 231 | const text = document.createElement('div') 232 | wrapper.classList.add('problem') 233 | codicon.classList.add( 234 | 'codicon', 235 | marker.severity === monaco.MarkerSeverity.Warning ? 'codicon-warning' : 'codicon-error' 236 | ) 237 | text.classList.add('problem-text') 238 | text.textContent = marker.message 239 | wrapper.append(codicon, text) 240 | wrapper.addEventListener('click', () => { 241 | ed.setPosition({ lineNumber: marker.startLineNumber, column: marker.startColumn }) 242 | ed.focus() 243 | }) 244 | problems.append(wrapper) 245 | } 246 | } 247 | 248 | window.addEventListener('hashchange', () => { 249 | const model = getModel() 250 | ed.setModel(model) 251 | updateMarkers(model.uri) 252 | }) 253 | 254 | tailwindrcModel.onDidChangeContent(() => { 255 | let newConfig: unknown 256 | try { 257 | newConfig = parse(tailwindrcModel.getValue()) 258 | } catch { 259 | return 260 | } 261 | if (typeof newConfig !== 'object') { 262 | return 263 | } 264 | if (newConfig == null) { 265 | return 266 | } 267 | monacoTailwindcss.setTailwindConfig(newConfig as TailwindConfig) 268 | generateOutput() 269 | }) 270 | 271 | monaco.editor.onDidChangeMarkers(([resource]) => { 272 | if (String(resource) === String(getModel().uri)) { 273 | updateMarkers(resource) 274 | } 275 | }) 276 | -------------------------------------------------------------------------------- /examples/demo/src/tailwindcssplugin.worker.js: -------------------------------------------------------------------------------- 1 | import lineClamp from '@tailwindcss/line-clamp' 2 | import typography from '@tailwindcss/typography' 3 | import { initialize } from 'monaco-tailwindcss/tailwindcss.worker.js' 4 | 5 | initialize({ 6 | prepareTailwindConfig(tailwindConfig) { 7 | if (tailwindConfig.plugins) { 8 | // eslint-disable-next-line no-console 9 | console.error('Only preconfigured built in plugins are supported', tailwindConfig.plugins) 10 | } 11 | const plugins = [typography, lineClamp] 12 | return { ...tailwindConfig, plugins } 13 | } 14 | }) 15 | -------------------------------------------------------------------------------- /examples/demo/webpack.config.js: -------------------------------------------------------------------------------- 1 | import CssMinimizerPlugin from 'css-minimizer-webpack-plugin' 2 | import HtmlWebPackPlugin from 'html-webpack-plugin' 3 | import MiniCssExtractPlugin from 'mini-css-extract-plugin' 4 | 5 | export default { 6 | devtool: 'source-map', 7 | resolve: { 8 | extensions: ['.mjs', '.js', '.ts'] 9 | }, 10 | module: { 11 | rules: [ 12 | { 13 | test: /\.css$/, 14 | use: [MiniCssExtractPlugin.loader, 'css-loader'] 15 | }, 16 | { 17 | // Monaco editor uses .ttf icons. 18 | test: /\.(svg|ttf)$/, 19 | type: 'asset/resource' 20 | }, 21 | { 22 | test: /\.ts$/, 23 | loader: 'ts-loader', 24 | options: { transpileOnly: true } 25 | } 26 | ] 27 | }, 28 | optimization: { 29 | minimizer: ['...', new CssMinimizerPlugin()] 30 | }, 31 | plugins: [new HtmlWebPackPlugin(), new MiniCssExtractPlugin({ filename: '[contenthash].css' })] 32 | } 33 | -------------------------------------------------------------------------------- /examples/esbuild-demo/build.js: -------------------------------------------------------------------------------- 1 | const { join } = require('node:path') 2 | 3 | const esbuild = require('esbuild') 4 | 5 | const outputDir = join(__dirname, 'dist') 6 | 7 | /** 8 | * @param {import ('esbuild').BuildOptions} opts esbuild options 9 | */ 10 | function build(opts) { 11 | esbuild.build(opts).then((result) => { 12 | if (result.errors.length > 0) { 13 | console.error(result.errors) 14 | } 15 | if (result.warnings.length > 0) { 16 | console.error(result.warnings) 17 | } 18 | console.info('build done') 19 | }) 20 | } 21 | 22 | /** 23 | * Todo or implement something like https://github.com/evanw/esbuild/issues/802#issuecomment-955776480 24 | * 25 | * @param {import ('esbuild').BuildOptions} opts esbuild options 26 | */ 27 | function serve(opts) { 28 | esbuild 29 | .serve( 30 | { 31 | servedir: __dirname, 32 | host: '127.0.0.1' 33 | }, 34 | opts 35 | ) 36 | .then((result) => { 37 | const { host, port } = result 38 | console.info('serve done') 39 | console.log(`open: http://${host}:${port}`) 40 | }) 41 | } 42 | 43 | // Build the workers 44 | build({ 45 | entryPoints: Object.fromEntries( 46 | Object.entries({ 47 | 'json.worker': 'monaco-editor/esm/vs/language/json/json.worker.js', 48 | 'css.worker': 'monaco-editor/esm/vs/language/css/css.worker.js', 49 | 'html.worker': 'monaco-editor/esm/vs/language/html/html.worker.js', 50 | 'ts.worker': 'monaco-editor/esm/vs/language/typescript/ts.worker.js', 51 | 'editor.worker': 'monaco-editor/esm/vs/editor/editor.worker.js', 52 | 'tailwindcss.worker': 'monaco-tailwindcss/tailwindcss.worker.js' 53 | }).map(([outfile, entry]) => [outfile, require.resolve(entry)]) 54 | ), 55 | outdir: outputDir, 56 | format: 'iife', 57 | bundle: true, 58 | minify: true 59 | }) 60 | 61 | // Change this to `build()` for building. 62 | serve({ 63 | minify: true, 64 | entryPoints: ['src/index.js'], 65 | bundle: true, 66 | format: 'esm', 67 | // Format: 'iife', // then we must use document.currentScript.src instead of import.meta.src 68 | // splitting: true, // optional and only works for esm 69 | outdir: outputDir, 70 | loader: { 71 | '.ttf': 'file' 72 | } 73 | }) 74 | -------------------------------------------------------------------------------- /examples/esbuild-demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Foo 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/esbuild-demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "esbuild-demo", 3 | "version": "1.0.0", 4 | "private": true, 5 | "type": "commonjs", 6 | "scripts": { 7 | "start": "rm -rf ./dist && node build.js" 8 | }, 9 | "dependencies": { 10 | "monaco-editor": "^0.52.0", 11 | "monaco-tailwindcss": "file:../..", 12 | "esbuild": "^0.24.0" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/esbuild-demo/src/index.js: -------------------------------------------------------------------------------- 1 | import * as monaco from 'monaco-editor' 2 | import { configureMonacoTailwindcss } from 'monaco-tailwindcss' 3 | 4 | configureMonacoTailwindcss(monaco) 5 | 6 | // Required Js to initiate the workers created above. 7 | window.MonacoEnvironment = { 8 | getWorkerUrl(moduleId, label) { 9 | switch (label) { 10 | case 'json': 11 | return new URL('json.worker.js', import.meta.url).pathname 12 | case 'css': 13 | case 'scss': 14 | case 'less': 15 | return new URL('css.worker.js', import.meta.url).pathname 16 | case 'html': 17 | case 'handlebars': 18 | case 'razor': 19 | return new URL('html.worker.js', import.meta.url).pathname 20 | case 'typescript': 21 | case 'javascript': 22 | return new URL('ts.worker.js', import.meta.url).pathname 23 | case 'editorWorkerService': 24 | return new URL('editor.worker.js', import.meta.url).pathname 25 | case 'tailwindcss': 26 | return new URL('tailwindcss.worker.js', import.meta.url).pathname 27 | default: 28 | throw new Error(`Unknown label ${label}`) 29 | } 30 | } 31 | } 32 | 33 | const mount = document.getElementById('editor') 34 | 35 | monaco.editor.create(mount, { 36 | value: ` 37 | 38 | 39 | 40 | 41 | 42 |

Moin aus Husum

43 |
44 | 45 | 46 | `, 47 | language: 'html', 48 | roundedSelection: false, 49 | scrollBeyondLastLine: false, 50 | readOnly: false, 51 | theme: 'vs-dark' 52 | }) 53 | -------------------------------------------------------------------------------- /examples/vite-example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Monaco Tailwindcss 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /examples/vite-example/index.js: -------------------------------------------------------------------------------- 1 | import * as monaco from 'monaco-editor' 2 | import EditorWorker from 'monaco-editor/esm/vs/editor/editor.worker.js?worker' 3 | import { configureMonacoTailwindcss } from 'monaco-tailwindcss' 4 | import TailwindcssWorker from 'monaco-tailwindcss/tailwindcss.worker.js?worker' 5 | 6 | window.MonacoEnvironment = { 7 | getWorker(moduleId, label) { 8 | switch (label) { 9 | case 'editorWorkerService': 10 | return new EditorWorker() 11 | case 'tailwindcss': 12 | return new TailwindcssWorker() 13 | default: 14 | throw new Error(`Unknown label ${label}`) 15 | } 16 | } 17 | } 18 | 19 | configureMonacoTailwindcss(monaco, {}) 20 | 21 | monaco.editor.create(document.getElementById('editor'), { 22 | automaticLayout: true, 23 | language: 'html', 24 | value: ` 25 | 26 | 27 | 28 | 29 | 30 |
31 | 32 | 33 | ` 34 | }) 35 | -------------------------------------------------------------------------------- /examples/vite-example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vite-example", 3 | "version": "1.0.0", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "start": "vite", 8 | "build": "vite build" 9 | }, 10 | "dependencies": { 11 | "monaco-editor": "^0.52.0", 12 | "monaco-tailwindcss": "file:../..", 13 | "vite": "^6.0.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/vite-example/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | 3 | export default defineConfig({ 4 | build: { 5 | target: 'es2020' 6 | } 7 | }) 8 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import { type IDisposable, type languages, type MonacoEditor } from 'monaco-types' 2 | import { type Config } from 'tailwindcss' 3 | 4 | /** 5 | * A Tailwind configuration, but without content. 6 | */ 7 | export type TailwindConfig = Omit 8 | 9 | export interface MonacoTailwindcssOptions { 10 | /** 11 | * @default defaultLanguageSelector 12 | */ 13 | languageSelector?: languages.LanguageSelector 14 | 15 | /** 16 | * The tailwind configuration to use. 17 | * 18 | * This may be either the Tailwind configuration object, or a string that gets processed in the 19 | * worker. 20 | */ 21 | tailwindConfig?: TailwindConfig | string 22 | } 23 | 24 | /** 25 | * Contains the content of CSS classes to extract. 26 | * With optional "extension" key, which might be relevant 27 | * to properly extract css classed based on the content language. 28 | */ 29 | export interface Content { 30 | content: string 31 | extension?: string 32 | } 33 | 34 | export interface MonacoTailwindcss extends IDisposable { 35 | /** 36 | * Update the current Tailwind configuration. 37 | * 38 | * @param tailwindConfig 39 | * The new Tailwind configuration. 40 | */ 41 | setTailwindConfig: (tailwindConfig: TailwindConfig | string) => void 42 | 43 | /** 44 | * Generate styles using Tailwindcss. 45 | * 46 | * This generates CSS using the Tailwind JIT compiler. It uses the Tailwind configuration that has 47 | * previously been passed to {@link configureMonacoTailwindcss}. 48 | * 49 | * @param css 50 | * The CSS to process. Only one CSS file can be processed at a time. 51 | * @param content 52 | * All content that contains CSS classes to extract. 53 | * @returns 54 | * The CSS generated by the Tailwind JIT compiler. It has been optimized for the given content. 55 | * @example 56 | * monacoTailwindcss.generateStylesFromContent( 57 | * css, 58 | * editor.getModels().filter((model) => model.getLanguageId() === 'html') 59 | * ) 60 | */ 61 | generateStylesFromContent: (css: string, content: (Content | string)[]) => Promise 62 | } 63 | 64 | /** 65 | * Configure `monaco-tailwindcss`. 66 | * 67 | * @param monaco 68 | * The `monaco-editor` module. 69 | * @param options 70 | * Options for customizing the `monaco-tailwindcss`. 71 | */ 72 | export function configureMonacoTailwindcss( 73 | monaco: MonacoEditor, 74 | options?: MonacoTailwindcssOptions 75 | ): MonacoTailwindcss 76 | 77 | /** 78 | * This data can be used with the default Monaco CSS support to support tailwind directives. 79 | * 80 | * It will provider hover information from the Tailwindcss documentation, including a link. 81 | */ 82 | export const tailwindcssData: languages.css.CSSDataV1 83 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | publish = 'examples/demo/dist/' 3 | command = 'npm run prepack && npm --workspace demo run build' 4 | 5 | [[headers]] 6 | for = '/*' 7 | [headers.values] 8 | Content-Security-Policy = "default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'" 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "monaco-tailwindcss", 3 | "version": "0.6.1", 4 | "description": "Tailwindcss integration for Monaco editor", 5 | "files": [ 6 | "index.js", 7 | "index.js.map", 8 | "index.d.ts", 9 | "tailwindcss.worker.js", 10 | "tailwindcss.worker.js.map", 11 | "tailwindcss.worker.d.ts" 12 | ], 13 | "type": "module", 14 | "workspaces": [ 15 | "examples/*" 16 | ], 17 | "scripts": { 18 | "prepack": "node build.js", 19 | "start": "npm --workspace demo start" 20 | }, 21 | "exports": { 22 | ".": "./index.js", 23 | "./tailwindcss.worker": "./tailwindcss.worker.js", 24 | "./tailwindcss.worker.js": "./tailwindcss.worker.js" 25 | }, 26 | "repository": "remcohaszing/monaco-tailwindcss", 27 | "keywords": [ 28 | "monaco", 29 | "monaco-editor", 30 | "tailwind", 31 | "tailwindcss" 32 | ], 33 | "author": "Remco Haszing ", 34 | "license": "MIT", 35 | "bugs": "https://github.com/remcohaszing/monaco-tailwindcss/issues", 36 | "homepage": "https://monaco-tailwindcss.js.org", 37 | "funding": "https://github.com/sponsors/remcohaszing", 38 | "dependencies": { 39 | "@alloc/quick-lru": "^5.0.0", 40 | "@ctrl/tinycolor": "^4.0.0", 41 | "@csstools/css-parser-algorithms": "^2.0.0", 42 | "@csstools/css-tokenizer": "^2.0.0", 43 | "@csstools/media-query-list-parser": "^2.0.0", 44 | "brace-expansion": "^4.0.0", 45 | "color-name": "^2.0.0", 46 | "css.escape": "^1.0.0", 47 | "culori": "^4.0.0", 48 | "didyoumean": "^1.0.0", 49 | "dlv": "^1.0.0", 50 | "line-column": "^1.0.0", 51 | "monaco-languageserver-types": "^0.4.0", 52 | "monaco-marker-data-provider": "^1.0.0", 53 | "monaco-types": "^0.1.0", 54 | "monaco-worker-manager": "^2.0.0", 55 | "moo": "^0.5.0", 56 | "postcss": "^8.0.0", 57 | "postcss-js": "^4.0.0", 58 | "postcss-nested": "^6.0.0", 59 | "postcss-selector-parser": "^6.0.0", 60 | "semver": "^7.0.0", 61 | "sift-string": "^0.0.2", 62 | "stringify-object": "^5.0.0", 63 | "tailwindcss": "^3.0.0", 64 | "tmp-cache": "^1.0.0", 65 | "vscode-languageserver-textdocument": "^1.0.0", 66 | "vscode-languageserver-types": "^3.0.0" 67 | }, 68 | "peerDependencies": { 69 | "monaco-editor": ">=0.36" 70 | }, 71 | "devDependencies": { 72 | "@tailwindcss/language-service": "0.14.2", 73 | "@types/brace-expansion": "^1.0.0", 74 | "esbuild": "^0.24.0", 75 | "eslint": "^8.0.0", 76 | "eslint-config-remcohaszing": "^10.0.0", 77 | "prettier": "^3.0.0", 78 | "remark-cli": "^12.0.0", 79 | "remark-preset-remcohaszing": "^3.0.0", 80 | "tailwindcss": "3.4.15", 81 | "typescript": "^5.0.0", 82 | "vscode-languageserver-protocol": "^3.0.0" 83 | }, 84 | "overrides": { 85 | "@tailwindcss/language-service": { 86 | "@csstools/css-parser-algorithms": "^2.0.0", 87 | "@csstools/css-tokenizer": "^2.0.0", 88 | "@csstools/media-query-list-parser": "^2.0.0", 89 | "color-name": "^2.0.0", 90 | "moo": "^0.5.0", 91 | "postcss": "^8.0.0", 92 | "postcss-selector-parser": "^6.0.0", 93 | "semver": "^7.0.0", 94 | "stringify-object": "^5.0.0", 95 | "vscode-languageserver-protocol": "^3.0.0", 96 | "vscode-languageserver-textdocument": "^1.0.0" 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/cssData.ts: -------------------------------------------------------------------------------- 1 | import { type languages } from 'monaco-types' 2 | 3 | function createTailwindDirective(name: string, value: string): languages.css.IAtDirectiveData { 4 | return { 5 | name: `@${name}`, 6 | description: { kind: 'markdown', value }, 7 | references: [ 8 | { 9 | name: `@${name} documentation`, 10 | url: `https://tailwindcss.com/docs/functions-and-directives#${name}` 11 | } 12 | ] 13 | } 14 | } 15 | 16 | // The descriptions have been taken from 17 | // https://github.com/tailwindlabs/tailwindcss.com/blob/master/src/pages/docs/functions-and-directives.mdx 18 | 19 | const tailwindDirective = createTailwindDirective( 20 | 'tailwind', 21 | `Use the \`@tailwind\` directive to insert Tailwind's \`base\`, \`components\`, \`utilities\` and \`variants\` styles into your CSS. 22 | 23 | \`\`\`css 24 | /** 25 | * This injects Tailwind's base styles and any base styles registered by 26 | * plugins. 27 | */ 28 | @tailwind base; 29 | 30 | /** 31 | * This injects Tailwind's component classes and any component classes 32 | * registered by plugins. 33 | */ 34 | @tailwind components; 35 | 36 | /** 37 | * This injects Tailwind's utility classes and any utility classes registered 38 | * by plugins. 39 | */ 40 | @tailwind utilities; 41 | 42 | /** 43 | * Use this directive to control where Tailwind injects the hover, focus, 44 | * responsive, dark mode, and other variants of each class. 45 | * 46 | * If omitted, Tailwind will append these classes to the very end of 47 | * your stylesheet by default. 48 | */ 49 | @tailwind variants; 50 | \`\`\`` 51 | ) 52 | 53 | const layerDirective = createTailwindDirective( 54 | 'layer', 55 | `Use the \`@layer\` directive to tell Tailwind which "bucket" a set of custom styles belong to. Valid layers are \`base\`, \`components\`, and \`utilities\`. 56 | 57 | \`\`\`css 58 | @tailwind base; 59 | @tailwind components; 60 | @tailwind utilities; 61 | 62 | @layer base { 63 | h1 { 64 | @apply text-2xl; 65 | } 66 | h2 { 67 | @apply text-xl; 68 | } 69 | } 70 | 71 | @layer components { 72 | .btn-blue { 73 | @apply bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded; 74 | } 75 | } 76 | 77 | @layer utilities { 78 | .filter-none { 79 | filter: none; 80 | } 81 | .filter-grayscale { 82 | filter: grayscale(100%); 83 | } 84 | } 85 | \`\`\` 86 | 87 | Tailwind will automatically move any CSS within a \`@layer\` directive to the same place as the corresponding \`@tailwind\` rule, so you don't have to worry about authoring your CSS in a specific order to avoid specificity issues. 88 | 89 | Any custom CSS added to a layer will only be included in the final build if that CSS is actually used in your HTML, just like all of the classes built in to Tailwind by default. 90 | 91 | Wrapping any custom CSS in a \`@layer\` directive also makes it possible to use modifiers with those rules, like \`hover:\` and \`focus:\` or responsive modifiers like \`md:\` and \`lg:\`.` 92 | ) 93 | 94 | const applyDirective = createTailwindDirective( 95 | 'apply', 96 | `Use \`@apply\` to inline any existing utility classes into your own custom CSS. 97 | 98 | This is useful when you need to write custom CSS (like to override the styles in a third-party library) but still want to work with your design tokens and use the same syntax you're used to using in your HTML. 99 | 100 | \`\`\`css 101 | .select2-dropdown { 102 | @apply rounded-b-lg shadow-md; 103 | } 104 | .select2-search { 105 | @apply border border-gray-300 rounded; 106 | } 107 | .select2-results__group { 108 | @apply text-lg font-bold text-gray-900; 109 | } 110 | \`\`\` 111 | 112 | Any rules inlined with \`@apply\` will have \`!important\` **removed** by default to avoid specificity issues: 113 | 114 | \`\`\`css 115 | /* Input */ 116 | .foo { 117 | color: blue !important; 118 | } 119 | 120 | .bar { 121 | @apply foo; 122 | } 123 | 124 | /* Output */ 125 | .foo { 126 | color: blue !important; 127 | } 128 | 129 | .bar { 130 | color: blue; 131 | } 132 | \`\`\` 133 | 134 | If you'd like to \`@apply\` an existing class and make it \`!important\`, simply add \`!important\` to the end of the declaration: 135 | 136 | \`\`\`css 137 | /* Input */ 138 | .btn { 139 | @apply font-bold py-2 px-4 rounded !important; 140 | } 141 | 142 | /* Output */ 143 | .btn { 144 | font-weight: 700 !important; 145 | padding-top: .5rem !important; 146 | padding-bottom: .5rem !important; 147 | padding-right: 1rem !important; 148 | padding-left: 1rem !important; 149 | border-radius: .25rem !important; 150 | } 151 | \`\`\` 152 | 153 | Note that if you're using Sass/SCSS, you'll need to use Sass' interpolation feature to get this to work: 154 | 155 | \`\`\`css 156 | .btn { 157 | @apply font-bold py-2 px-4 rounded #{!important}; 158 | } 159 | \`\`\`` 160 | ) 161 | 162 | const configDirective = createTailwindDirective( 163 | 'config', 164 | `Use the \`@config\` directive to specify which config file Tailwind should use when compiling CSS file. This is useful for projects that need to use different configuration files for different CSS entry points. 165 | 166 | \`\`\`css 167 | @config "./tailwind.site.config.js"; 168 | @tailwind base; 169 | @tailwind components; 170 | @tailwind utilities; 171 | \`\`\` 172 | 173 | \`\`\`css 174 | @config "./tailwind.admin.config.js"; 175 | @tailwind base; 176 | @tailwind components; 177 | @tailwind utilities; 178 | \`\`\` 179 | 180 | The path you provide to the \`@config\` directive is relative to that CSS file, and will take precedence over a path defined in your PostCSS configuration or in the Tailwind CLI.` 181 | ) 182 | 183 | export const tailwindcssData: languages.css.CSSDataV1 = { 184 | version: 1.1, 185 | atDirectives: [tailwindDirective, layerDirective, applyDirective, configDirective] 186 | } 187 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { registerMarkerDataProvider } from 'monaco-marker-data-provider' 2 | import { type MonacoTailwindcssOptions } from 'monaco-tailwindcss' 3 | import { createWorkerManager } from 'monaco-worker-manager' 4 | 5 | import { 6 | createCodeActionProvider, 7 | createColorProvider, 8 | createCompletionItemProvider, 9 | createHoverProvider, 10 | createMarkerDataProvider 11 | } from './languageFeatures.js' 12 | import { type TailwindcssWorker } from './tailwindcss.worker.js' 13 | 14 | export const defaultLanguageSelector = ['css', 'javascript', 'html', 'mdx', 'typescript'] as const 15 | 16 | export { tailwindcssData } from './cssData.js' 17 | 18 | export const configureMonacoTailwindcss: typeof import('monaco-tailwindcss').configureMonacoTailwindcss = 19 | (monaco, { languageSelector = defaultLanguageSelector, tailwindConfig } = {}) => { 20 | const workerManager = createWorkerManager(monaco, { 21 | label: 'tailwindcss', 22 | moduleId: 'monaco-tailwindcss/tailwindcss.worker', 23 | createData: { tailwindConfig } 24 | }) 25 | 26 | const disposables = [ 27 | workerManager, 28 | monaco.languages.registerCodeActionProvider( 29 | languageSelector, 30 | createCodeActionProvider(workerManager.getWorker) 31 | ), 32 | monaco.languages.registerColorProvider( 33 | languageSelector, 34 | createColorProvider(monaco, workerManager.getWorker) 35 | ), 36 | monaco.languages.registerCompletionItemProvider( 37 | languageSelector, 38 | createCompletionItemProvider(workerManager.getWorker) 39 | ), 40 | monaco.languages.registerHoverProvider( 41 | languageSelector, 42 | createHoverProvider(workerManager.getWorker) 43 | ) 44 | ] 45 | 46 | // Monaco editor doesn’t provide a function to match language selectors, so let’s just support 47 | // strings here. 48 | for (const language of Array.isArray(languageSelector) 49 | ? languageSelector 50 | : [languageSelector]) { 51 | if (typeof language === 'string') { 52 | disposables.push( 53 | registerMarkerDataProvider( 54 | monaco, 55 | language, 56 | createMarkerDataProvider(workerManager.getWorker) 57 | ) 58 | ) 59 | } 60 | } 61 | 62 | return { 63 | dispose() { 64 | for (const disposable of disposables) { 65 | disposable.dispose() 66 | } 67 | }, 68 | 69 | setTailwindConfig(newTailwindConfig) { 70 | workerManager.updateCreateData({ tailwindConfig: newTailwindConfig }) 71 | }, 72 | 73 | async generateStylesFromContent(css, contents) { 74 | const client = await workerManager.getWorker() 75 | 76 | return client.generateStylesFromContent( 77 | css, 78 | contents.map((content) => (typeof content === 'string' ? { content } : content)) 79 | ) 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/languageFeatures.ts: -------------------------------------------------------------------------------- 1 | import { fromRatio, names as namedColors } from '@ctrl/tinycolor' 2 | import { 3 | fromCodeActionContext, 4 | fromCompletionContext, 5 | fromCompletionItem, 6 | fromPosition, 7 | fromRange, 8 | toCodeAction, 9 | toColorInformation, 10 | toCompletionItem, 11 | toCompletionList, 12 | toHover, 13 | toMarkerData 14 | } from 'monaco-languageserver-types' 15 | import { type MarkerDataProvider } from 'monaco-marker-data-provider' 16 | import { type editor, type languages, type MonacoEditor } from 'monaco-types' 17 | import { type WorkerGetter } from 'monaco-worker-manager' 18 | 19 | import { type TailwindcssWorker } from './tailwindcss.worker.js' 20 | 21 | type WorkerAccessor = WorkerGetter 22 | 23 | const colorNames = Object.values(namedColors) 24 | const editableColorRegex = new RegExp( 25 | `-\\[(${colorNames.join('|')}|((?:#|rgba?\\(|hsla?\\())[^\\]]+)\\]$` 26 | ) 27 | const sheet = new CSSStyleSheet() 28 | document.adoptedStyleSheets.push(sheet) 29 | 30 | function colorValueToHex(value: number): string { 31 | return Math.round(value * 255) 32 | .toString(16) 33 | .padStart(2, '0') 34 | } 35 | 36 | function createColorClass(color: languages.IColor): string { 37 | const hex = `${colorValueToHex(color.red)}${colorValueToHex(color.green)}${colorValueToHex( 38 | color.blue 39 | )}` 40 | const className = `tailwindcss-color-decoration-${hex}` 41 | const selector = `.${className}` 42 | for (const rule of sheet.cssRules) { 43 | if ((rule as CSSStyleRule).selectorText === selector) { 44 | return className 45 | } 46 | } 47 | sheet.insertRule(`${selector}{background-color:#${hex}}`) 48 | return className 49 | } 50 | 51 | export function createColorProvider( 52 | monaco: MonacoEditor, 53 | getWorker: WorkerAccessor 54 | ): languages.DocumentColorProvider { 55 | const modelMap = new WeakMap() 56 | 57 | monaco.editor.onWillDisposeModel((model) => { 58 | modelMap.delete(model) 59 | }) 60 | 61 | return { 62 | async provideDocumentColors(model) { 63 | const worker = await getWorker(model.uri) 64 | 65 | const editableColors: languages.IColorInformation[] = [] 66 | const nonEditableColors: editor.IModelDeltaDecoration[] = [] 67 | const colors = await worker.getDocumentColors(String(model.uri), model.getLanguageId()) 68 | if (colors) { 69 | for (const lsColor of colors) { 70 | const monacoColor = toColorInformation(lsColor) 71 | const text = model.getValueInRange(monacoColor.range) 72 | if (editableColorRegex.test(text)) { 73 | editableColors.push(monacoColor) 74 | } else { 75 | nonEditableColors.push({ 76 | range: monacoColor.range, 77 | options: { 78 | before: { 79 | content: '\u00A0', 80 | inlineClassName: `${createColorClass(monacoColor.color)} colorpicker-color-decoration`, 81 | inlineClassNameAffectsLetterSpacing: true 82 | } 83 | } 84 | }) 85 | } 86 | } 87 | } 88 | 89 | modelMap.set(model, model.deltaDecorations(modelMap.get(model) ?? [], nonEditableColors)) 90 | 91 | return editableColors 92 | }, 93 | 94 | provideColorPresentations(model, colorInformation) { 95 | const className = model.getValueInRange(colorInformation.range) 96 | const match = new RegExp( 97 | `-\\[(${colorNames.join('|')}|(?:(?:#|rgba?\\(|hsla?\\())[^\\]]+)\\]$`, 98 | 'i' 99 | ).exec(className) 100 | 101 | if (!match) { 102 | return [] 103 | } 104 | 105 | const [currentColor] = match 106 | 107 | const isNamedColor = colorNames.includes(currentColor) 108 | const color = fromRatio({ 109 | r: colorInformation.color.red, 110 | g: colorInformation.color.green, 111 | b: colorInformation.color.blue, 112 | a: colorInformation.color.alpha 113 | }) 114 | 115 | let hexValue = color.toHex8String( 116 | !isNamedColor && (currentColor.length === 4 || currentColor.length === 5) 117 | ) 118 | if (hexValue.length === 5) { 119 | hexValue = hexValue.replace(/f$/, '') 120 | } else if (hexValue.length === 9) { 121 | hexValue = hexValue.replace(/ff$/, '') 122 | } 123 | 124 | const rgbValue = color.toRgbString().replaceAll(' ', '') 125 | const hslValue = color.toHslString().replaceAll(' ', '') 126 | const prefix = className.slice(0, Math.max(0, match.index)) 127 | 128 | return [ 129 | { label: `${prefix}-[${hexValue}]` }, 130 | { label: `${prefix}-[${rgbValue}]` }, 131 | { label: `${prefix}-[${hslValue}]` } 132 | ] 133 | } 134 | } 135 | } 136 | 137 | export function createHoverProvider(getWorker: WorkerAccessor): languages.HoverProvider { 138 | return { 139 | async provideHover(model, position) { 140 | const worker = await getWorker(model.uri) 141 | 142 | const hover = await worker.doHover( 143 | String(model.uri), 144 | model.getLanguageId(), 145 | fromPosition(position) 146 | ) 147 | 148 | return hover && toHover(hover) 149 | } 150 | } 151 | } 152 | 153 | export function createCodeActionProvider(getWorker: WorkerAccessor): languages.CodeActionProvider { 154 | return { 155 | async provideCodeActions(model, range, context) { 156 | const worker = await getWorker(model.uri) 157 | 158 | const codeActions = await worker.doCodeActions( 159 | String(model.uri), 160 | model.getLanguageId(), 161 | fromRange(range), 162 | fromCodeActionContext(context) 163 | ) 164 | 165 | if (codeActions) { 166 | return { 167 | actions: codeActions.map(toCodeAction), 168 | dispose() { 169 | // Do nothing 170 | } 171 | } 172 | } 173 | } 174 | } 175 | } 176 | 177 | export function createCompletionItemProvider( 178 | getWorker: WorkerAccessor 179 | ): languages.CompletionItemProvider { 180 | return { 181 | async provideCompletionItems(model, position, context) { 182 | const worker = await getWorker(model.uri) 183 | 184 | const completionList = await worker.doComplete( 185 | String(model.uri), 186 | model.getLanguageId(), 187 | fromPosition(position), 188 | fromCompletionContext(context) 189 | ) 190 | 191 | if (!completionList) { 192 | return 193 | } 194 | 195 | const wordInfo = model.getWordUntilPosition(position) 196 | 197 | return toCompletionList(completionList, { 198 | range: { 199 | startLineNumber: position.lineNumber, 200 | startColumn: wordInfo.startColumn, 201 | endLineNumber: position.lineNumber, 202 | endColumn: wordInfo.endColumn 203 | } 204 | }) 205 | }, 206 | 207 | async resolveCompletionItem(item) { 208 | const worker = await getWorker() 209 | 210 | const result = await worker.resolveCompletionItem(fromCompletionItem(item)) 211 | 212 | return toCompletionItem(result, { range: item.range }) 213 | } 214 | } 215 | } 216 | 217 | export function createMarkerDataProvider(getWorker: WorkerAccessor): MarkerDataProvider { 218 | return { 219 | owner: 'tailwindcss', 220 | async provideMarkerData(model) { 221 | const worker = await getWorker(model.uri) 222 | 223 | const diagnostics = await worker.doValidate(String(model.uri), model.getLanguageId()) 224 | 225 | return diagnostics?.map(toMarkerData) 226 | } 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /src/stubs/braces.ts: -------------------------------------------------------------------------------- 1 | import expand from 'brace-expansion' 2 | 3 | export default { expand } 4 | -------------------------------------------------------------------------------- /src/stubs/crypto.ts: -------------------------------------------------------------------------------- 1 | export default null 2 | -------------------------------------------------------------------------------- /src/stubs/detect-indent.ts: -------------------------------------------------------------------------------- 1 | export default null 2 | -------------------------------------------------------------------------------- /src/stubs/fs.ts: -------------------------------------------------------------------------------- 1 | import preflight from 'tailwindcss/src/css/preflight.css' 2 | 3 | export default { 4 | // Reading the preflight CSS is the only use of fs at the moment of writing. 5 | readFileSync: () => preflight 6 | } 7 | -------------------------------------------------------------------------------- /src/stubs/path.ts: -------------------------------------------------------------------------------- 1 | export const join = (): string => '' 2 | -------------------------------------------------------------------------------- /src/stubs/picocolors.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | yellow: (input: string) => input 3 | } 4 | -------------------------------------------------------------------------------- /src/stubs/tailwindcss/utils/log.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-empty-function 2 | export function log(): void {} 3 | 4 | export function dim(input: string): string { 5 | return input 6 | } 7 | 8 | export default { 9 | info: log, 10 | warn: log, 11 | risk: log 12 | } 13 | -------------------------------------------------------------------------------- /src/stubs/url.ts: -------------------------------------------------------------------------------- 1 | export default null 2 | -------------------------------------------------------------------------------- /src/stubs/util-deprecate.ts: -------------------------------------------------------------------------------- 1 | export { deprecate as default } from './util.js' 2 | -------------------------------------------------------------------------------- /src/stubs/util.ts: -------------------------------------------------------------------------------- 1 | export const deprecate = (fn: Fn): Fn => fn 2 | -------------------------------------------------------------------------------- /src/stubs/vscode-emmet-helper-bundled.ts: -------------------------------------------------------------------------------- 1 | export const doComplete = null 2 | export const extractAbbreviation = null 3 | export const isAbbreviationValid = null 4 | -------------------------------------------------------------------------------- /src/tailwindcss.worker.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type AugmentedDiagnostic, 3 | doCodeActions, 4 | doComplete, 5 | doHover, 6 | doValidate, 7 | type EditorState, 8 | getColor, 9 | getDocumentColors, 10 | resolveCompletionItem 11 | } from '@tailwindcss/language-service' 12 | import { type MonacoTailwindcssOptions, type TailwindConfig } from 'monaco-tailwindcss' 13 | import { type TailwindWorkerOptions } from 'monaco-tailwindcss/tailwindcss.worker' 14 | import { initialize as initializeWorker } from 'monaco-worker-manager/worker' 15 | import postcss from 'postcss' 16 | import postcssSelectorParser from 'postcss-selector-parser' 17 | import { type Config } from 'tailwindcss' 18 | import expandApplyAtRules from 'tailwindcss/src/lib/expandApplyAtRules.js' 19 | import { generateRules } from 'tailwindcss/src/lib/generateRules.js' 20 | import { type ChangedContent, createContext } from 'tailwindcss/src/lib/setupContextUtils.js' 21 | import processTailwindFeatures from 'tailwindcss/src/processTailwindFeatures.js' 22 | import resolveConfig from 'tailwindcss/src/public/resolve-config.js' 23 | import { 24 | type CodeAction, 25 | type CodeActionContext, 26 | type ColorInformation, 27 | type CompletionContext, 28 | type CompletionItem, 29 | type CompletionList, 30 | type Hover, 31 | type Position, 32 | type Range 33 | } from 'vscode-languageserver-protocol' 34 | import { TextDocument } from 'vscode-languageserver-textdocument' 35 | 36 | import { type JitState } from './types.js' 37 | 38 | export interface TailwindcssWorker { 39 | doCodeActions: ( 40 | uri: string, 41 | languageId: string, 42 | range: Range, 43 | context: CodeActionContext 44 | ) => CodeAction[] | undefined 45 | 46 | doComplete: ( 47 | uri: string, 48 | languageId: string, 49 | position: Position, 50 | context: CompletionContext 51 | ) => CompletionList | undefined 52 | 53 | doHover: (uri: string, languageId: string, position: Position) => Hover | undefined 54 | 55 | doValidate: (uri: string, languageId: string) => AugmentedDiagnostic[] | undefined 56 | 57 | generateStylesFromContent: (css: string, content: ChangedContent[]) => string 58 | 59 | getDocumentColors: (uri: string, languageId: string) => ColorInformation[] | undefined 60 | 61 | resolveCompletionItem: (item: CompletionItem) => CompletionItem 62 | } 63 | 64 | async function stateFromConfig( 65 | configPromise: PromiseLike | TailwindConfig 66 | ): Promise { 67 | const preparedTailwindConfig = await configPromise 68 | const config = resolveConfig(preparedTailwindConfig) 69 | const jitContext = createContext(config) 70 | 71 | const state: JitState = { 72 | version: '3.0.0', 73 | blocklist: [], 74 | config, 75 | enabled: true, 76 | modules: { 77 | postcss: { 78 | module: postcss, 79 | version: '' 80 | }, 81 | postcssSelectorParser: { module: postcssSelectorParser }, 82 | jit: { 83 | createContext: { module: createContext }, 84 | expandApplyAtRules: { module: expandApplyAtRules }, 85 | generateRules: { module: generateRules } 86 | } 87 | }, 88 | classNames: { 89 | classNames: {}, 90 | context: {} 91 | }, 92 | jit: true, 93 | jitContext, 94 | separator: config.separator, 95 | screens: config.theme?.screens ? Object.keys(config.theme.screens) : [], 96 | variants: jitContext.getVariants(), 97 | editor: { 98 | userLanguages: {}, 99 | capabilities: { 100 | configuration: true, 101 | diagnosticRelatedInformation: true, 102 | itemDefaults: [] 103 | }, 104 | // eslint-disable-next-line require-await 105 | async getConfiguration() { 106 | return { 107 | editor: { tabSize: 2 }, 108 | // Default values are based on 109 | // https://github.com/tailwindlabs/tailwindcss-intellisense/blob/v0.9.1/packages/tailwindcss-language-server/src/server.ts#L259-L287 110 | tailwindCSS: { 111 | emmetCompletions: false, 112 | classAttributes: ['class', 'className', 'ngClass'], 113 | codeActions: true, 114 | hovers: true, 115 | suggestions: true, 116 | validate: true, 117 | colorDecorators: true, 118 | rootFontSize: 16, 119 | lint: { 120 | cssConflict: 'warning', 121 | invalidApply: 'error', 122 | invalidScreen: 'error', 123 | invalidVariant: 'error', 124 | invalidConfigPath: 'error', 125 | invalidSourceDirective: 'warning', 126 | invalidTailwindDirective: 'error', 127 | recommendedVariantOrder: 'warning' 128 | }, 129 | showPixelEquivalents: true, 130 | includeLanguages: {}, 131 | files: { 132 | // Upstream defines these values, but we don’t need them. 133 | exclude: [] 134 | }, 135 | experimental: { 136 | classRegex: [], 137 | // Upstream types are wrong 138 | configFile: {} 139 | } 140 | } 141 | } 142 | } 143 | // This option takes some properties that we don’t have nor need. 144 | } as Partial as EditorState 145 | } 146 | 147 | state.classList = jitContext 148 | .getClassList() 149 | .filter((className) => className !== '*') 150 | .map((className) => [className, { color: getColor(state, className) }]) 151 | 152 | return state 153 | } 154 | 155 | export function initialize(tailwindWorkerOptions?: TailwindWorkerOptions): void { 156 | initializeWorker((ctx, options) => { 157 | const preparedTailwindConfig = 158 | tailwindWorkerOptions?.prepareTailwindConfig?.(options.tailwindConfig) ?? 159 | options.tailwindConfig ?? 160 | ({} as Config) 161 | if (typeof preparedTailwindConfig !== 'object') { 162 | throw new TypeError( 163 | `Expected tailwindConfig to resolve to an object, but got: ${JSON.stringify( 164 | preparedTailwindConfig 165 | )}` 166 | ) 167 | } 168 | 169 | const statePromise = stateFromConfig(preparedTailwindConfig) 170 | 171 | const withDocument = 172 | ( 173 | fn: (state: JitState, document: TextDocument, ...args: A) => Promise 174 | ) => 175 | (uri: string, languageId: string, ...args: A): Promise | undefined => { 176 | const models = ctx.getMirrorModels() 177 | for (const model of models) { 178 | if (String(model.uri) === uri) { 179 | return statePromise.then((state) => 180 | fn( 181 | state, 182 | TextDocument.create(uri, languageId, model.version, model.getValue()), 183 | ...args 184 | ) 185 | ) 186 | } 187 | } 188 | } 189 | 190 | return { 191 | doCodeActions: withDocument((state, textDocument, range, context) => 192 | doCodeActions(state, { range, context, textDocument }, textDocument) 193 | ), 194 | 195 | doComplete: withDocument(doComplete), 196 | 197 | doHover: withDocument(doHover), 198 | 199 | doValidate: withDocument(doValidate), 200 | 201 | async generateStylesFromContent(css, content) { 202 | const { config } = await statePromise 203 | const tailwind = processTailwindFeatures( 204 | (processOptions) => () => processOptions.createContext(config, content) 205 | ) 206 | 207 | const processor = postcss([tailwind]) 208 | 209 | const result = await processor.process(css) 210 | return result.css 211 | }, 212 | 213 | getDocumentColors: withDocument(getDocumentColors), 214 | 215 | async resolveCompletionItem(item) { 216 | return resolveCompletionItem(await statePromise, item) 217 | } 218 | } 219 | }) 220 | } 221 | 222 | // Side effect initialization - but this function can be called more than once. Last applies. 223 | initialize() 224 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { type State } from '@tailwindcss/language-service' 2 | import { type Config } from 'tailwindcss' 3 | 4 | export interface JitState extends State { 5 | config: Config 6 | } 7 | -------------------------------------------------------------------------------- /tailwindcss.worker.d.ts: -------------------------------------------------------------------------------- 1 | import { type TailwindConfig } from 'monaco-tailwindcss' 2 | import { type Config } from 'tailwindcss' 3 | 4 | export interface TailwindWorkerOptions { 5 | /** 6 | * Hook that will run before the tailwind config is used. 7 | * 8 | * @param tailwindConfig 9 | * The Tailwind configuration passed from the main thread. 10 | * @returns 11 | * A valid Tailwind configuration. 12 | */ 13 | prepareTailwindConfig?: (tailwindConfig?: TailwindConfig | string) => Config | PromiseLike 14 | } 15 | 16 | /** 17 | * Setup the Tailwindcss worker using a customized configuration. 18 | */ 19 | export function initialize(options?: TailwindWorkerOptions): void 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "node16", 4 | "noEmit": true, 5 | "strict": true, 6 | "stripInternal": true, 7 | "target": "es2024" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /types.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.css' { 2 | const css: string 3 | export default css 4 | } 5 | 6 | declare module 'tailwindcss/src/lib/expandApplyAtRules.js' { 7 | export default function expandApplyAtRules(): void 8 | } 9 | 10 | declare module 'tailwindcss/src/lib/generateRules.js' { 11 | export function generateRules(): void 12 | } 13 | 14 | declare module 'tailwindcss/src/lib/setupContextUtils.js' { 15 | import { type Variant } from '@tailwindcss/language-service' 16 | import { type Config } from 'tailwindcss' 17 | 18 | interface ChangedContent { 19 | content: string 20 | extension?: string 21 | } 22 | 23 | export interface JitContext { 24 | changedContent: ChangedContent[] 25 | getClassList: () => string[] 26 | getVariants: () => Variant[] | undefined 27 | tailwindConfig: Config 28 | } 29 | 30 | export function createContext(config: Config, changedContent?: ChangedContent[]): JitContext 31 | } 32 | 33 | declare module 'tailwindcss/src/processTailwindFeatures.js' { 34 | import { type AtRule, type Plugin, type Result, type Root } from 'postcss' 35 | import { type createContext, type JitContext } from 'tailwindcss/src/lib/setupContextUtils.js' 36 | 37 | type SetupContext = (root: Root, result: Result) => JitContext 38 | 39 | interface ProcessTailwindFeaturesCallbackOptions { 40 | applyDirectives: Set 41 | createContext: typeof createContext 42 | registerDependency: () => unknown 43 | tailwindDirectives: Set 44 | } 45 | 46 | export default function processTailwindFeatures( 47 | callback: (options: ProcessTailwindFeaturesCallbackOptions) => SetupContext 48 | ): Plugin 49 | } 50 | 51 | declare module 'tailwindcss/src/public/resolve-config.js' { 52 | import { type TailwindConfig } from 'monaco-tailwindcss' 53 | import { type Config } from 'tailwindcss' 54 | 55 | export default function resolveConfig(tailwindConfig: TailwindConfig): Config 56 | } 57 | --------------------------------------------------------------------------------