├── .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 | [](https://github.com/remcohaszing/monaco-tailwindcss/actions/workflows/ci.yaml)
4 | [](https://www.npmjs.com/package/monaco-tailwindcss)
5 | [](https://prettier.io)
6 | [](https://monaco-tailwindcss.js.org)
7 | [](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 |
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 |
--------------------------------------------------------------------------------