diff --git a/.changepacks/changepack_log_fRvRwIt0IIqYNraWrwcWG.json b/.changepacks/changepack_log_fRvRwIt0IIqYNraWrwcWG.json new file mode 100644 index 00000000..ba3f5641 --- /dev/null +++ b/.changepacks/changepack_log_fRvRwIt0IIqYNraWrwcWG.json @@ -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"} \ No newline at end of file diff --git a/bun.lock b/bun.lock index 056196b2..133724f4 100644 --- a/bun.lock +++ b/bun.lock @@ -389,7 +389,7 @@ }, "bindings/devup-ui-wasm": { "name": "@devup-ui/wasm", - "version": "1.0.68", + "version": "1.0.72", }, "packages/bun-plugin": { "name": "@devup-ui/bun-plugin", @@ -1976,7 +1976,7 @@ "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], - "csstype-extra": ["csstype-extra@0.1.25", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-iyfwd4BTweLGmGGEgEiNI/bRuf5M2rCGpNZkBl/b1BTE9Cdq5lMnD3kP1+Lzo39I6ZPCeiDcbyyYSy1Cr0Rb4g=="], + "csstype-extra": ["csstype-extra@0.1.28", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-S4TyHoPjQpy1nnvVS905ZEwkYbZF8XPNbsBHGTGVE3hfJhj6MgXKIzSnyHomvcM+cXQKwBgyG7P2S7RoBkmR6Q=="], "damerau-levenshtein": ["damerau-levenshtein@1.0.8", "", {}, "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA=="], @@ -3354,6 +3354,8 @@ "@babel/preset-env/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "@devup-ui/bun-plugin/@types/bun": ["@types/bun@1.3.13", "", { "dependencies": { "bun-types": "1.3.13" } }, "sha512-9fqXWk5YIHGGnUau9TEi+qdlTYDAnOj+xLCmSTwXfAIqXr2x4tytJb43E9uCvt09zJURKXwAtkoH4nLQfzeTXw=="], + "@emotion/babel-plugin/convert-source-map": ["convert-source-map@1.9.0", "", {}, "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="], "@emotion/babel-plugin/source-map": ["source-map@0.5.7", "", {}, "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ=="], @@ -3636,6 +3638,8 @@ "yargs/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], + "@devup-ui/bun-plugin/@types/bun/bun-types": ["bun-types@1.3.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-QXKeHLlOLqQX9LgYaHJfzdBaV21T63HhFJnvuRCcjZiaUDpbs5ED1MgxbMra71CsryN/1dAoXuJJJwIv/2drVA=="], + "@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], "@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], diff --git a/libs/extractor/src/extract_style/extract_style_value.rs b/libs/extractor/src/extract_style/extract_style_value.rs index 8e8fbad5..fe86f5b5 100644 --- a/libs/extractor/src/extract_style/extract_style_value.rs +++ b/libs/extractor/src/extract_style/extract_style_value.rs @@ -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); diff --git a/libs/extractor/src/prop_modify_utils.rs b/libs/extractor/src/prop_modify_utils.rs index b48e43a8..da1bbba2 100644 --- a/libs/extractor/src/prop_modify_utils.rs +++ b/libs/extractor/src/prop_modify_utils.rs @@ -490,7 +490,7 @@ fn replace_classes_in_string(s: &str, class_mapping: &FxHashMap) 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); diff --git a/packages/next-plugin/src/coordinator.ts b/packages/next-plugin/src/coordinator.ts index 7ec0ec7c..1c79a790 100644 --- a/packages/next-plugin/src/coordinator.ts +++ b/packages/next-plugin/src/coordinator.ts @@ -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, @@ -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 { return new Promise((resolve, reject) => { const chunks: Buffer[] = [] @@ -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((resolve, reject) => writeFile( diff --git a/packages/plugin-utils/src/__tests__/shared.test.ts b/packages/plugin-utils/src/__tests__/shared.test.ts index bcc26c34..d45c523b 100644 --- a/packages/plugin-utils/src/__tests__/shared.test.ts +++ b/packages/plugin-utils/src/__tests__/shared.test.ts @@ -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', () => { diff --git a/packages/plugin-utils/src/shared.ts b/packages/plugin-utils/src/shared.ts index 522bf36c..0e745a72 100644 --- a/packages/plugin-utils/src/shared.ts +++ b/packages/plugin-utils/src/shared.ts @@ -6,6 +6,12 @@ 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 */ @@ -13,9 +19,18 @@ 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 } /**