Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .changepacks/changepack_log_fRvRwIt0IIqYNraWrwcWG.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"changes":{"packages/plugin-utils/package.json":"Patch","packages/next-plugin/package.json":"Patch"},"note":"Fix assetPrefix issue","date":"2026-05-10T14:40:26.939688400Z"}
8 changes: 6 additions & 2 deletions bun.lock

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

6 changes: 2 additions & 4 deletions libs/extractor/src/extract_style/extract_style_value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,8 @@ impl ExtractStyleValue {
}
pub fn set_style_order(&mut self, order: u8) {
match self {
ExtractStyleValue::Static(style) => {
if style.style_order.is_none() {
style.style_order = Some(order);
}
ExtractStyleValue::Static(style) if style.style_order.is_none() => {
style.style_order = Some(order);
}
ExtractStyleValue::Dynamic(style) => {
style.style_order = Some(order);
Expand Down
2 changes: 1 addition & 1 deletion libs/extractor/src/prop_modify_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -490,7 +490,7 @@ fn replace_classes_in_string(s: &str, class_mapping: &FxHashMap<String, String>)
let mut result = s.to_string();
// Sort by length descending to avoid partial replacements (e.g., "text-3xl" before "text-3")
let mut sorted_classes: Vec<_> = class_mapping.iter().collect();
sorted_classes.sort_by(|a, b| b.0.len().cmp(&a.0.len()));
sorted_classes.sort_by_key(|b| std::cmp::Reverse(b.0.len()));

for (tailwind_class, generated_class) in sorted_classes {
result = result.replace(tailwind_class, generated_class);
Expand Down
8 changes: 2 additions & 6 deletions packages/next-plugin/src/coordinator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { unlinkSync, writeFile, writeFileSync } from 'node:fs'
import { createServer, type IncomingMessage, type Server } from 'node:http'
import { basename, dirname, join, relative } from 'node:path'

import { getFileNumByFilename } from '@devup-ui/plugin-utils'
import {
codeExtract,
exportClassMap,
Expand All @@ -21,11 +22,6 @@ export interface CoordinatorOptions {
coordinatorPortFile: string
}

function getFileNumFromCssFile(cssFile: string): number | null {
if (cssFile.endsWith('devup-ui.css')) return null
return parseInt(cssFile.split('devup-ui-')[1].split('.')[0])
}

function readBody(req: IncomingMessage): Promise<string> {
return new Promise((resolve, reject) => {
const chunks: Buffer[] = []
Expand Down Expand Up @@ -161,7 +157,7 @@ export function startCoordinator(options: CoordinatorOptions): {
}

if (result.cssFile) {
const fileNum = getFileNumFromCssFile(result.cssFile)
const fileNum = getFileNumByFilename(result.cssFile)
promises.push(
new Promise<void>((resolve, reject) =>
writeFile(
Expand Down
32 changes: 32 additions & 0 deletions packages/plugin-utils/src/__tests__/shared.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,38 @@ describe('getFileNumByFilename', () => {
it('should return null for path/to/devup-ui.css (no number, no query)', () => {
expect(getFileNumByFilename('path/to/devup-ui.css')).toBeNull()
})

// Regression: when Next.js `assetPrefix` is set, Turbopack appends extra
// query parameters (e.g. `?dpl=DEPLOYMENT_ID`) to module URLs. The base
// CSS file must still be detected correctly, otherwise `@layer b` styles
// are dropped from the build output.
it('should return null for devup-ui.css with non-fileNum query (?dpl=...)', () => {
expect(getFileNumByFilename('devup-ui.css?dpl=abc')).toBeNull()
})

it('should return null for devup-ui.css with version query (?v=...)', () => {
expect(getFileNumByFilename('devup-ui.css?v=12345')).toBeNull()
})

it('should return null for path/to/devup-ui.css with deployment query', () => {
expect(getFileNumByFilename('/path/to/devup-ui.css?dpl=abc')).toBeNull()
})

it('should still extract fileNum when extra queries follow it', () => {
expect(getFileNumByFilename('devup-ui.css?fileNum=5&dpl=abc')).toBe(5)
})

it('should still extract fileNum when extra queries precede it', () => {
expect(getFileNumByFilename('devup-ui.css?dpl=abc&fileNum=7')).toBe(7)
})

it('should extract file number from devup-ui-5.css with query', () => {
expect(getFileNumByFilename('devup-ui-5.css?dpl=abc')).toBe(5)
})

it('should return null for unrelated css filename', () => {
expect(getFileNumByFilename('something-else.css')).toBeNull()
})
})

describe('createNodeModulesExcludeRegex', () => {
Expand Down
21 changes: 18 additions & 3 deletions packages/plugin-utils/src/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,31 @@ import type { ImportAliases } from './types'
* Handles both standard filenames (devup-ui-5.css) and query parameter
* format (devup-ui.css?fileNum=79) used by Turbopack.
*
* Next.js may append additional query parameters (e.g. `?dpl=DEPLOYMENT_ID`)
* to module URLs when `assetPrefix` is set. Such queries must be stripped
* before matching the base filename, otherwise the base CSS request would
* be misidentified and the `@layer b` styles would be dropped from the
* build output.
*
* @param filename - CSS filename or path to parse
* @returns The file number, or null for the base devup-ui.css file
*/
export function getFileNumByFilename(filename: string): number | null {
// Handle query parameter format: devup-ui.css?fileNum=79
// Turbopack may embed query params in resourcePath
const queryMatch = filename.match(/[?&]fileNum=(\d+)/)
if (queryMatch) return parseInt(queryMatch[1])
if (filename.endsWith('devup-ui.css')) return null
return parseInt(filename.split('devup-ui-')[1].split('.')[0])
if (queryMatch) return parseInt(queryMatch[1], 10)

// Strip query string before matching the filename pattern. Next.js can
// append arbitrary queries (e.g. `?dpl=...`) when assetPrefix is set, and
// those must not interfere with base CSS detection.
const pathOnly = filename.split('?')[0]
if (pathOnly.endsWith('devup-ui.css')) return null

const numericPart = pathOnly.split('devup-ui-')[1]?.split('.')[0]
if (numericPart === undefined) return null
const num = parseInt(numericPart, 10)
return Number.isNaN(num) ? null : num
}

/**
Expand Down
Loading