swc-project / swc

Rust-based platform for the Web
https://swc.rs
Apache License 2.0
30.96k stars 1.21k forks source link

'use client' directive is not preserved at top level when module type is 'umd' #8728

Open sohnjunior opened 6 months ago

sohnjunior commented 6 months ago

Describe the bug

// input.ts

'use client';

export function ClientComponent() {
  return 'Hello world';
}
> swc input.ts -o output.js -C module.type=umd
// output.js

(function(global, factory) {
    if (typeof module === "object" && typeof module.exports === "object") factory(exports);
    else if (typeof define === "function" && define.amd) define([
        "exports"
    ], factory);
    else if (global = typeof globalThis !== "undefined" ? globalThis : global || self) factory(global.input = {});
})(this, function(exports) {
    "use client";
    "use strict";
    Object.defineProperty(exports, "__esModule", {
        value: true
    });
    Object.defineProperty(exports, "ClientComponent", {
        enumerable: true,
        get: function() {
            return ClientComponent;
        }
    });
    function ClientComponent() {
        return "Hello world";
    }
});

The 'use client' directive is defined inside the factory function.

Perhaps because of this, an error saying 'use client directive does not exist' is occurring in the Next.js environment that uses the App directory.

This issue is related with #7315

Input code

No response

Config

No response

Playground link (or link to the minimal reproduction)

https://play.swc.rs/?version=1.3.100&code=H4sIAAAAAAAAAx3LMQqAMAxG4T2n%2BLfqYg%2Fg2MV71AiFmpQ2RUG8u8XtwcfzHklKt8Uaud4YMScWcysR30Wr4egSLakg%2FBL0LCojphkPAZWtV4HbOGfFpTXvY37pA1C%2FhblZAAAA&config=H4sIAAAAAAAAA1WPSw7DIAwF95wCed1tN71DD4GIE1GFj2wjFUW5eyGFtNnh98YMbEpreLGFh97qsQ7JECOdc024BDHvmoCUhGzJJYHbaIVbNZuV8Yj2bwNiaEFpW8j3jsMaI%2BPAe%2BZdcHP5F9roEyHzFWyoCcuKV53qSvBxykfZP9Ie2%2FTZT%2FCDhuy8GBw%2Fx6ZQRrV%2FAL%2FpEOoUAQAA

SWC Info output

No response

Expected behavior

// output.js

+ "use client";
+ "use strict";

(function(global, factory) {
    if (typeof module === "object" && typeof module.exports === "object") factory(exports);
    else if (typeof define === "function" && define.amd) define([
        "exports"
    ], factory);
    else if (global = typeof globalThis !== "undefined" ? globalThis : global || self) factory(global.input = {});
})(this, function(exports) {
-    "use client";
-    "use strict";
    Object.defineProperty(exports, "__esModule", {
        value: true
    });
    Object.defineProperty(exports, "ClientComponent", {
        enumerable: true,
        get: function() {
            return ClientComponent;
        }
    });
    function ClientComponent() {
        return "Hello world";
    }
});

The 'use client' directive is kept at the top of the file.

Actual behavior

No response

Version

1.3.101

Additional context

No response

magic-akari commented 6 months ago

Investigation:

Babel

(function (global, factory) {
  if (typeof define === "function" && define.amd) {
    define(["exports"], factory);
  } else if (typeof exports !== "undefined") {
    factory(exports);
  } else {
    var mod = {
      exports: {}
    };
    factory(mod.exports);
    global.repl = mod.exports;
  }
})(typeof globalThis !== "undefined" ? globalThis : typeof self !== "undefined" ? self : this, function (_exports) {
  "use strict";
  'use client';

  Object.defineProperty(_exports, "__esModule", {
    value: true
  });
  _exports.ClientComponent = ClientComponent;
  function ClientComponent() {
    return 'Hello world';
  }
});

TypeScript

(function (factory) {
    if (typeof module === "object" && typeof module.exports === "object") {
        var v = factory(require, exports);
        if (v !== undefined) module.exports = v;
    }
    else if (typeof define === "function" && define.amd) {
        define(["require", "exports"], factory);
    }
})(function (require, exports) {
    "use strict";
    // @module: umd
    'use client';
    Object.defineProperty(exports, "__esModule", { value: true });
    exports.ClientComponent = void 0;
    function ClientComponent() {
        return 'Hello world';
    }
    exports.ClientComponent = ClientComponent;
});
magic-akari commented 6 months ago

The use strict directive must be inside the UMD function and not outside, because UMD is safe to be used with simple file concatenation and thus directives at the top of the files would risk getting lost (and just transformed into normal string expression statements)

from @nicolo-ribaudo (https://x.com/NicoloRibaudo/status/1767871497400373272)

I fully agree with this statement. For UMD (as well as similar AMD and SystemJS), the actual module code resides within its function body segment. Given that the toolchain needs to accommodate more generic logic, we don't want to mess with how things currently work.

In the case of UMD, there is a common expectation that it can be easily concatenated together. In such scenarios, we want directives to only affect the code inside the module, rather than the global code.

The root of this issue lies in the differences between toolchain's expectations of directives and how React perceives directives. Within React, directives need to be at the file level and must be at the top of the file to take effect. Conversely, in the toolchain's perspective, directives function at the block level and can be safely relocated to their new scope during the transformation process.

This misalignment of expectations has led to several challenges. We've got a few ways to tackle it:

  1. The first involves starting from React, enabling React to recognize UMD/AMD/SystemJS module code. However, due to the flexibility of module wrappers, this approach proves challenging in practice.
  2. Acknowledge that UMD and React don't mix well for client components that you can't just stick together. It might be better to use CJS or stick with ESM instead.
  3. Create a custom SWC plugin to move directives where they need to go within the plugin.
sohnjunior commented 5 months ago

Thank you for your feedback. Fortunately, I identified that I no longer needed to build modules with UMD in my environment, so I decided to adopt the CJS modular approach.