Skip to content

Tailwind v4 is not seeing classes in slang templates #17851

New issue

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

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

Already on GitHub? Sign in to your account

Open
hugopl opened this issue May 2, 2025 · 8 comments
Open

Tailwind v4 is not seeing classes in slang templates #17851

hugopl opened this issue May 2, 2025 · 8 comments
Labels

Comments

@hugopl
Copy link

hugopl commented May 2, 2025

What version of Tailwind CSS are you using?

For example: v4.1.4

What build tool (or framework if it abstracts the build tool) are you using?

None, using tailwindcss cli from Archlinux AUR package.

What version of Node.js are you using?

None

What browser are you using?

Chromium, Firefox

What operating system are you using?

Linux

Reproduction URL

Basic tailwindcss input.css file:

@import "tailwindcss";

Basic test.slang file:

doctype html
html
  head
    link rel="stylesheet" href="style.css"
  body.min-h-screen
    header.stick.top-0.z-10

Run tailwindcss -i input.css -o style.css and check that there will be no .z-10 CSS class there.

The resulting style.css is:

/*! tailwindcss v4.1.4 | MIT License | https://tailwindcss.com */
@layer theme, base, components, utilities;
@layer theme {
  :root, :host {
    --font-sans: ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
    'Noto Color Emoji';
    --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
    monospace;
    --default-font-family: var(--font-sans);
    --default-mono-font-family: var(--font-mono);
  }
}
@layer base {
  *, ::after, ::before, ::backdrop, ::file-selector-button {
    box-sizing: border-box;
    margin: 0;
    padding: 0;
    border: 0 solid;
  }
  html, :host {
    line-height: 1.5;
    -webkit-text-size-adjust: 100%;
    tab-size: 4;
    font-family: var(--default-font-family, ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji');
    font-feature-settings: var(--default-font-feature-settings, normal);
    font-variation-settings: var(--default-font-variation-settings, normal);
    -webkit-tap-highlight-color: transparent;
  }
  hr {
    height: 0;
    color: inherit;
    border-top-width: 1px;
  }
  abbr:where([title]) {
    -webkit-text-decoration: underline dotted;
    text-decoration: underline dotted;
  }
  h1, h2, h3, h4, h5, h6 {
    font-size: inherit;
    font-weight: inherit;
  }
  a {
    color: inherit;
    -webkit-text-decoration: inherit;
    text-decoration: inherit;
  }
  b, strong {
    font-weight: bolder;
  }
  code, kbd, samp, pre {
    font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace);
    font-feature-settings: var(--default-mono-font-feature-settings, normal);
    font-variation-settings: var(--default-mono-font-variation-settings, normal);
    font-size: 1em;
  }
  small {
    font-size: 80%;
  }
  sub, sup {
    font-size: 75%;
    line-height: 0;
    position: relative;
    vertical-align: baseline;
  }
  sub {
    bottom: -0.25em;
  }
  sup {
    top: -0.5em;
  }
  table {
    text-indent: 0;
    border-color: inherit;
    border-collapse: collapse;
  }
  :-moz-focusring {
    outline: auto;
  }
  progress {
    vertical-align: baseline;
  }
  summary {
    display: list-item;
  }
  ol, ul, menu {
    list-style: none;
  }
  img, svg, video, canvas, audio, iframe, embed, object {
    display: block;
    vertical-align: middle;
  }
  img, video {
    max-width: 100%;
    height: auto;
  }
  button, input, select, optgroup, textarea, ::file-selector-button {
    font: inherit;
    font-feature-settings: inherit;
    font-variation-settings: inherit;
    letter-spacing: inherit;
    color: inherit;
    border-radius: 0;
    background-color: transparent;
    opacity: 1;
  }
  :where(select:is([multiple], [size])) optgroup {
    font-weight: bolder;
  }
  :where(select:is([multiple], [size])) optgroup option {
    padding-inline-start: 20px;
  }
  ::file-selector-button {
    margin-inline-end: 4px;
  }
  ::placeholder {
    opacity: 1;
  }
  @supports (not (-webkit-appearance: -apple-pay-button))  or (contain-intrinsic-size: 1px) {
    ::placeholder {
      color: currentcolor;
      @supports (color: color-mix(in lab, red, red)) {
        color: color-mix(in oklab, currentcolor 50%, transparent);
      }
    }
  }
  textarea {
    resize: vertical;
  }
  ::-webkit-search-decoration {
    -webkit-appearance: none;
  }
  ::-webkit-date-and-time-value {
    min-height: 1lh;
    text-align: inherit;
  }
  ::-webkit-datetime-edit {
    display: inline-flex;
  }
  ::-webkit-datetime-edit-fields-wrapper {
    padding: 0;
  }
  ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month-field, ::-webkit-datetime-edit-day-field, ::-webkit-datetime-edit-hour-field, ::-webkit-datetime-edit-minute-field, ::-webkit-datetime-edit-second-field, ::-webkit-datetime-edit-millisecond-field, ::-webkit-datetime-edit-meridiem-field {
    padding-block: 0;
  }
  :-moz-ui-invalid {
    box-shadow: none;
  }
  button, input:where([type='button'], [type='reset'], [type='submit']), ::file-selector-button {
    appearance: button;
  }
  ::-webkit-inner-spin-button, ::-webkit-outer-spin-button {
    height: auto;
  }
  [hidden]:where(:not([hidden='until-found'])) {
    display: none !important;
  }
}
@layer utilities {
  .z-10 {
    z-index: 10;
  }
  .min-h-screen {
    min-height: 100vh;
  }
}

If I change the slang template to

doctype html
html
  head
    link rel="stylesheet" href="style.css"
  body.min-h-screen
    header class="stick top-0 z-10"

The result is ok, but I'm using slang to not have to declare classes this way.

/*! tailwindcss v4.1.4 | MIT License | https://tailwindcss.com */
@layer theme, base, components, utilities;
@layer theme {
  :root, :host {
    --font-sans: ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
    'Noto Color Emoji';
    --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
    monospace;
    --spacing: 0.25rem;
    --default-font-family: var(--font-sans);
    --default-mono-font-family: var(--font-mono);
  }
}
@layer base {
  *, ::after, ::before, ::backdrop, ::file-selector-button {
    box-sizing: border-box;
    margin: 0;
    padding: 0;
    border: 0 solid;
  }
  html, :host {
    line-height: 1.5;
    -webkit-text-size-adjust: 100%;
    tab-size: 4;
    font-family: var(--default-font-family, ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji');
    font-feature-settings: var(--default-font-feature-settings, normal);
    font-variation-settings: var(--default-font-variation-settings, normal);
    -webkit-tap-highlight-color: transparent;
  }
  hr {
    height: 0;
    color: inherit;
    border-top-width: 1px;
  }
  abbr:where([title]) {
    -webkit-text-decoration: underline dotted;
    text-decoration: underline dotted;
  }
  h1, h2, h3, h4, h5, h6 {
    font-size: inherit;
    font-weight: inherit;
  }
  a {
    color: inherit;
    -webkit-text-decoration: inherit;
    text-decoration: inherit;
  }
  b, strong {
    font-weight: bolder;
  }
  code, kbd, samp, pre {
    font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace);
    font-feature-settings: var(--default-mono-font-feature-settings, normal);
    font-variation-settings: var(--default-mono-font-variation-settings, normal);
    font-size: 1em;
  }
  small {
    font-size: 80%;
  }
  sub, sup {
    font-size: 75%;
    line-height: 0;
    position: relative;
    vertical-align: baseline;
  }
  sub {
    bottom: -0.25em;
  }
  sup {
    top: -0.5em;
  }
  table {
    text-indent: 0;
    border-color: inherit;
    border-collapse: collapse;
  }
  :-moz-focusring {
    outline: auto;
  }
  progress {
    vertical-align: baseline;
  }
  summary {
    display: list-item;
  }
  ol, ul, menu {
    list-style: none;
  }
  img, svg, video, canvas, audio, iframe, embed, object {
    display: block;
    vertical-align: middle;
  }
  img, video {
    max-width: 100%;
    height: auto;
  }
  button, input, select, optgroup, textarea, ::file-selector-button {
    font: inherit;
    font-feature-settings: inherit;
    font-variation-settings: inherit;
    letter-spacing: inherit;
    color: inherit;
    border-radius: 0;
    background-color: transparent;
    opacity: 1;
  }
  :where(select:is([multiple], [size])) optgroup {
    font-weight: bolder;
  }
  :where(select:is([multiple], [size])) optgroup option {
    padding-inline-start: 20px;
  }
  ::file-selector-button {
    margin-inline-end: 4px;
  }
  ::placeholder {
    opacity: 1;
  }
  @supports (not (-webkit-appearance: -apple-pay-button))  or (contain-intrinsic-size: 1px) {
    ::placeholder {
      color: currentcolor;
      @supports (color: color-mix(in lab, red, red)) {
        color: color-mix(in oklab, currentcolor 50%, transparent);
      }
    }
  }
  textarea {
    resize: vertical;
  }
  ::-webkit-search-decoration {
    -webkit-appearance: none;
  }
  ::-webkit-date-and-time-value {
    min-height: 1lh;
    text-align: inherit;
  }
  ::-webkit-datetime-edit {
    display: inline-flex;
  }
  ::-webkit-datetime-edit-fields-wrapper {
    padding: 0;
  }
  ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month-field, ::-webkit-datetime-edit-day-field, ::-webkit-datetime-edit-hour-field, ::-webkit-datetime-edit-minute-field, ::-webkit-datetime-edit-second-field, ::-webkit-datetime-edit-millisecond-field, ::-webkit-datetime-edit-meridiem-field {
    padding-block: 0;
  }
  :-moz-ui-invalid {
    box-shadow: none;
  }
  button, input:where([type='button'], [type='reset'], [type='submit']), ::file-selector-button {
    appearance: button;
  }
  ::-webkit-inner-spin-button, ::-webkit-outer-spin-button {
    height: auto;
  }
  [hidden]:where(:not([hidden='until-found'])) {
    display: none !important;
  }
}
@layer utilities {
  .top-0 {
    top: calc(var(--spacing) * 0);
  }
  .z-10 {
    z-index: 10;
  }
  .min-h-screen {
    min-height: 100vh;
  }
}

Describe your issue

Maybe I'm the only one in the world using tailwind with slang templates, Slang is a template almost identical to slim written in Crystal.

Using the very same process used to extract CSS classes from slim templates will just work for slang templates, I'm creating this issue because nowadays I need to write my templates like body class="min-h-screen" instead of just body.min-h-screen.

There was an issue for slim templates in the past, so I believe that just let this code also run to slang files (.slang extension) fixes that.

If I knew rust I would not even create this issue but just create a fork of tailwind and fix it on my fork just for myself since I know that the audience is really very little.

That being said, I understand if you guys don't want to fix that because no one uses slang templates, but if so can you guys just say where to tell tailwind to use the same slim/haml heuristics for files with .slang extension?

Thanks very much.

@hugopl
Copy link
Author

hugopl commented May 2, 2025

Maybe if I do just need to change crates/oxide/src/scanner/mod.rs

 match extension {
        "clj" | "cljs" | "cljc" => Clojure.process(content),
        "cshtml" | "razor" => Razor.process(content),
        "haml" => Haml.process(content),
        "json" => Json.process(content),
        "pug" => Pug.process(content),
        "rb" | "erb" => Ruby.process(content),
        "slim" => Slim.process(content),
        "svelte" => Svelte.process(content),
        "vue" => Vue.process(content),
        _ => content.to_vec(),
    }

to

 match extension {
        "clj" | "cljs" | "cljc" => Clojure.process(content),
        "cshtml" | "razor" => Razor.process(content),
        "haml" => Haml.process(content),
        "json" => Json.process(content),
        "pug" => Pug.process(content),
        "rb" | "erb" => Ruby.process(content),
        "slim" | "slang" => Slim.process(content),
        "svelte" => Svelte.process(content),
        "vue" => Vue.process(content),
        _ => content.to_vec(),
    }

@hugopl
Copy link
Author

hugopl commented May 2, 2025

Nice, I just have no idea how to build this thing 😅 , cargo build builds something to somewhere... but there's a lot of typescript files... and the Archlinux AUR packages didn't help because they use a JS package as source for the package 😢.

Would be nice to have a way to tell tailwind that an extension is an alias to another one, so issue like that could be solved by configuration, not a patches.

@philipp-spiess
Copy link
Member

@hugopl Hey! Seems like you're on the right track there already, awesome!

Did you do a pnpm install before you tried the cargo build? Usually it's enough to do pnpm test afterwards since that will build a test build of the Rust stuff as well. 👍

@hugopl
Copy link
Author

hugopl commented May 5, 2025

I'm tryign to compile this without my patch but without success.

On v4.1.5 tag I did:

$ pnpm install
$ cargo build -r
$ NODE_ENV=production pnpm run build

Then got:

 warning: tailwind-oxide@0.0.0: crt1-reactor.o not found at , the multi-threaded runtime may not be initialized correctly
│ error[E0463]: can't find crate for `std`
│   |
│   = note: the `wasm32-wasip1-threads` target may not be installed
│   = help: consider downloading the target with `rustup target add wasm32-wasip1-threads`
│ 
│ For more information about this error, try `rustc --explain E0463`.
│ error[E0463]: can't find crate for `core`
│   |
│   = note: the `wasm32-wasip1-threads` target may not be installed
│   = help: consider downloading the target with `rustup target add wasm32-wasip1-threads`
│ 
│ error: could not compile `same-file` (lib) due to 1 previous error
│ warning: build failed, waiting for other jobs to finish...
│ error: could not compile `once_cell` (lib) due to 1 previous error
│ error: could not compile `lazy_static` (lib) due to 1 previous error
│ error: could not compile `overload` (lib) due to 1 previous error
│ error: could not compile `log` (lib) due to 1 previous error
│ error: could not compile `either` (lib) due to 1 previous error
│ error: could not compile `bitflags` (lib) due to 1 previous error
│ error: could not compile `pin-project-lite` (lib) due to 1 previous error
│ error: could not compile `cfg-if` (lib) due to 1 previous error
│ error: could not compile `minimal-lexical` (lib) due to 1 previous error
│ error: could not compile `memchr` (lib) due to 1 previous error
│ error: could not compile `rustc-hash` (lib) due to 1 previous error
│ error: could not compile `dunce` (lib) due to 1 previous error
│ error: could not compile `crossbeam-utils` (lib) due to 1 previous error
│ error: could not compile `smallvec` (lib) due to 1 previous error
│ error: could not compile `arrayvec` (lib) due to 1 previous error
│ error: could not compile `napi-sys` (lib) due to 1 previous error
│ error: could not compile `regex-syntax` (lib) due to 1 previous error
│ error: could not compile `regex-syntax` (lib) due to 1 previous error
│ Internal Error: Build failed with exit code 101
│     at ChildProcess.<anonymous> (file:///home/hugo/src/estudos/tailwindcss/node_modules/.pnpm/@napi-rs+cli@3.0.0-alpha.78_@emnapi+runtime@1.4.3_@types+node@20.1
│ 4.13_emnapi@1.4.3_node-addon-api@8.3.0_/node_modules/@napi-rs/cli/dist/api/build.js:199:28)
│     at Object.onceWrapper (node:events:639:26)
│     at ChildProcess.emit (node:events:536:35)
│     at ChildProcess._handle.onexit (node:internal/child_process:293:12)
│  ELIFECYCLE  Command failed with exit code 1.
│  ELIFECYCLE  Command failed with exit code 1.
│ command finished with error: command (/home/hugo/src/estudos/tailwindcss/crates/node) /home/hugo/.local/share/pnpm/.tools/pnpm/9.6.0/bin/pnpm run build exited (
│ 1)
└────>
@tailwindcss/oxide#build: command (/home/hugo/src/estudos/tailwindcss/crates/node) /home/hugo/.local/share/pnpm/.tools/pnpm/9.6.0/bin/pnpm run build exited (1)

 Tasks:    0 successful, 1 total
Cached:    0 cached, 1 total
  Time:    30.57s 
Failed:    @tailwindcss/oxide#build

 ERROR  run failed: command  exited (1)
 ELIFECYCLE  Command failed with exit code 1.

When I run the tests some of them fails with:

Error: Failed to resolve entry for package "@tailwindcss/node". The package may have incorrect main/module/exports specified in its package.json.

@wongjn
Copy link
Collaborator

wongjn commented May 5, 2025

Try running

$ rustup target add wasm32-wasip1-threads

Before trying the commands again.

@philipp-spiess
Copy link
Member

@hugopl Working on some updated contribution docs that might be helpful for you: #17911

@iliakan
Copy link

iliakan commented May 8, 2025

@philipp-spiess trying to debug a similar issue, tried pnpm build on MacOS, got this:

error: failed to run custom build command for `tailwind-oxide v0.0.0 (/private/tmp/tailwindcss/crates/node)`

Caused by:
  process didn't exit successfully: `/private/tmp/tailwindcss/target/release/build/tailwind-oxide-87851038cefb8bb9/build-script-build` (exit status: 101)
  --- stdout
  cargo:rerun-if-env-changed=DEBUG_GENERATED_CODE
  cargo:rerun-if-env-changed=TYPE_DEF_TMP_PATH
  cargo:rerun-if-env-changed=CARGO_CFG_NAPI_RS_CLI_VERSION

  --- stderr

  thread 'main' panicked at /Users/iliakan/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/napi-build-2.1.6/src/wasi.rs:4:46:
  EMNAPI_LINK_DIR must be set: NotPresent
  note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Any ideas? 😀

@philipp-spiess
Copy link
Member

@iliakan I think you might be missing a pnpm install before the build, could that be? 🤔 Seems that emnapi is not found which is added here: https://github.com/tailwindlabs/tailwindcss/blob/main/crates/node/package.json#L38

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

4 participants