diff --git a/.changeset/honest-crabs-sort.md b/.changeset/honest-crabs-sort.md
new file mode 100644
index 00000000000..448b7a13eef
--- /dev/null
+++ b/.changeset/honest-crabs-sort.md
@@ -0,0 +1,5 @@
+---
+"@remix-run/dev": patch
+---
+
+Support decorators in files using CSS side-effect imports
diff --git a/packages/remix-dev/__tests__/cssSideEffectsPlugin-test.ts b/packages/remix-dev/__tests__/cssSideEffectsPlugin-test.ts
index 7c67d4a031f..f9eac955a95 100644
--- a/packages/remix-dev/__tests__/cssSideEffectsPlugin-test.ts
+++ b/packages/remix-dev/__tests__/cssSideEffectsPlugin-test.ts
@@ -222,4 +222,255 @@ describe("addSuffixToCssSideEffectImports", () => {
`);
});
});
+
+ describe("parser support for language features", () => {
+ function languageFeaturesFixture(options: { ts: boolean; jsx: boolean }) {
+ let tsLanguageFeatures = dedent`
+ // TS
+ const exampleSatisfies = 'satisfies' satisfies string;
+ enum ExampleEnum {
+ Foo,
+ Bar,
+ Baz,
+ }
+ `;
+
+ let jsxLanguageFeatures = dedent`
+ // JSX
+ const ExampleComponent = () =>
JSX element
;
+ `;
+
+ let jsLanguageFeatures = dedent`
+ // JS
+ const topLevelAwait = await Promise.resolve('top level await');
+ function classDecorator(target) {
+ return target;
+ }
+ function methodDecorator(target) {
+ return target;
+ }
+ @classDecorator
+ class ExampleClass {
+ #privateField;
+ #privateFieldWithInitializer = 'private field with initializer';
+ #privateMethod() {
+ return 'private method';
+ }
+ @methodDecorator
+ decoratedMethod() {
+ return 'decorated method';
+ }
+ }
+ const numericSeparator = 1_000_000;
+ const nullishCoalescing = null ?? 'nullish coalescing';
+ const optionalChaining = (['optional', 'chaining'])?.join?.(' ');
+ let optionalCatchBinding;
+ try {
+ optionalCatchBinding = error();
+ } catch {
+ optionalCatchBinding = 'optional catch binding';
+ }
+ export async function* asyncGenerator() {
+ yield await Promise.resolve('async generator');
+ }
+ `;
+
+ return [
+ 'require("./foo.css")',
+ ...(options.ts ? [tsLanguageFeatures] : []),
+ ...(options.jsx ? [jsxLanguageFeatures] : []),
+ jsLanguageFeatures,
+ ].join("\n\n");
+ }
+
+ test("JS language features", () => {
+ let code = languageFeaturesFixture({ ts: false, jsx: false });
+
+ expect(addSuffixToCssSideEffectImports("js", code))
+ .toMatchInlineSnapshot(`
+ "require(\\"./foo.css?__remix_sideEffect__\\");
+
+ // JS
+ const topLevelAwait = await Promise.resolve('top level await');
+ function classDecorator(target) {
+ return target;
+ }
+ function methodDecorator(target) {
+ return target;
+ }
+ @classDecorator class
+ ExampleClass {
+ #privateField;
+ #privateFieldWithInitializer = 'private field with initializer';
+ #privateMethod() {
+ return 'private method';
+ }
+ @methodDecorator
+ decoratedMethod() {
+ return 'decorated method';
+ }
+ }
+ const numericSeparator = 1_000_000;
+ const nullishCoalescing = null ?? 'nullish coalescing';
+ const optionalChaining = ['optional', 'chaining']?.join?.(' ');
+ let optionalCatchBinding;
+ try {
+ optionalCatchBinding = error();
+ } catch {
+ optionalCatchBinding = 'optional catch binding';
+ }
+ export async function* asyncGenerator() {
+ yield await Promise.resolve('async generator');
+ }"
+ `);
+ });
+
+ test("JSX language features", () => {
+ let code = languageFeaturesFixture({ ts: false, jsx: true });
+
+ expect(addSuffixToCssSideEffectImports("jsx", code))
+ .toMatchInlineSnapshot(`
+ "require(\\"./foo.css?__remix_sideEffect__\\");
+
+ // JSX
+ const ExampleComponent = () => JSX element
;
+
+ // JS
+ const topLevelAwait = await Promise.resolve('top level await');
+ function classDecorator(target) {
+ return target;
+ }
+ function methodDecorator(target) {
+ return target;
+ }
+ @classDecorator class
+ ExampleClass {
+ #privateField;
+ #privateFieldWithInitializer = 'private field with initializer';
+ #privateMethod() {
+ return 'private method';
+ }
+ @methodDecorator
+ decoratedMethod() {
+ return 'decorated method';
+ }
+ }
+ const numericSeparator = 1_000_000;
+ const nullishCoalescing = null ?? 'nullish coalescing';
+ const optionalChaining = ['optional', 'chaining']?.join?.(' ');
+ let optionalCatchBinding;
+ try {
+ optionalCatchBinding = error();
+ } catch {
+ optionalCatchBinding = 'optional catch binding';
+ }
+ export async function* asyncGenerator() {
+ yield await Promise.resolve('async generator');
+ }"
+ `);
+ });
+
+ test("TS language features", () => {
+ let code = languageFeaturesFixture({ ts: true, jsx: false });
+
+ expect(addSuffixToCssSideEffectImports("tsx", code))
+ .toMatchInlineSnapshot(`
+ "require(\\"./foo.css?__remix_sideEffect__\\");
+
+ // TS
+ const exampleSatisfies = ('satisfies' satisfies string);
+ enum ExampleEnum {
+ Foo,
+ Bar,
+ Baz,
+ }
+
+ // JS
+ const topLevelAwait = await Promise.resolve('top level await');
+ function classDecorator(target) {
+ return target;
+ }
+ function methodDecorator(target) {
+ return target;
+ }
+ @classDecorator class
+ ExampleClass {
+ #privateField;
+ #privateFieldWithInitializer = 'private field with initializer';
+ #privateMethod() {
+ return 'private method';
+ }
+ @methodDecorator
+ decoratedMethod() {
+ return 'decorated method';
+ }
+ }
+ const numericSeparator = 1_000_000;
+ const nullishCoalescing = null ?? 'nullish coalescing';
+ const optionalChaining = ['optional', 'chaining']?.join?.(' ');
+ let optionalCatchBinding;
+ try {
+ optionalCatchBinding = error();
+ } catch {
+ optionalCatchBinding = 'optional catch binding';
+ }
+ export async function* asyncGenerator() {
+ yield await Promise.resolve('async generator');
+ }"
+ `);
+ });
+
+ test("TSX language features", () => {
+ let code = languageFeaturesFixture({ ts: true, jsx: true });
+
+ expect(addSuffixToCssSideEffectImports("tsx", code))
+ .toMatchInlineSnapshot(`
+ "require(\\"./foo.css?__remix_sideEffect__\\");
+
+ // TS
+ const exampleSatisfies = ('satisfies' satisfies string);
+ enum ExampleEnum {
+ Foo,
+ Bar,
+ Baz,
+ }
+
+ // JSX
+ const ExampleComponent = () => JSX element
;
+
+ // JS
+ const topLevelAwait = await Promise.resolve('top level await');
+ function classDecorator(target) {
+ return target;
+ }
+ function methodDecorator(target) {
+ return target;
+ }
+ @classDecorator class
+ ExampleClass {
+ #privateField;
+ #privateFieldWithInitializer = 'private field with initializer';
+ #privateMethod() {
+ return 'private method';
+ }
+ @methodDecorator
+ decoratedMethod() {
+ return 'decorated method';
+ }
+ }
+ const numericSeparator = 1_000_000;
+ const nullishCoalescing = null ?? 'nullish coalescing';
+ const optionalChaining = ['optional', 'chaining']?.join?.(' ');
+ let optionalCatchBinding;
+ try {
+ optionalCatchBinding = error();
+ } catch {
+ optionalCatchBinding = 'optional catch binding';
+ }
+ export async function* asyncGenerator() {
+ yield await Promise.resolve('async generator');
+ }"
+ `);
+ });
+ });
});
diff --git a/packages/remix-dev/compiler/plugins/cssSideEffectImportsPlugin.ts b/packages/remix-dev/compiler/plugins/cssSideEffectImportsPlugin.ts
index 6463c2d7b63..57ff4653d52 100644
--- a/packages/remix-dev/compiler/plugins/cssSideEffectImportsPlugin.ts
+++ b/packages/remix-dev/compiler/plugins/cssSideEffectImportsPlugin.ts
@@ -121,11 +121,13 @@ export const cssSideEffectImportsPlugin = ({
};
};
+const additionalLanguageFeatures: ParserOptions["plugins"] = ["decorators"];
+
const babelPluginsForLoader: Record = {
- js: [],
- jsx: ["jsx"],
- ts: ["typescript"],
- tsx: ["typescript", "jsx"],
+ js: [...additionalLanguageFeatures],
+ jsx: ["jsx", ...additionalLanguageFeatures],
+ ts: ["typescript", ...additionalLanguageFeatures],
+ tsx: ["typescript", "jsx", ...additionalLanguageFeatures],
};
const cache = new LRUCache({ max: 1000 });