microsoft / fluentui

Fluent UI web represents a collection of utilities, React components, and web components for building web applications.
https://react.fluentui.dev
Other
18.33k stars 2.72k forks source link

[Feature]: Add `.js` file extension to built output to improve native ESM support #30634

Open spmonahan opened 6 months ago

spmonahan commented 6 months ago

Library

React Components / v9 (@fluentui/react-components)

Describe the feature that you would like added

I want Fluent's ESM output (CJS can go either way I think) to include the .js extension for all relative JS imports. For example,

// Source Typescript
import { feature } from './file';

// Output ESM Javascript
import { feature } from './file.js';

This update will better allow Fluent's ESM output to be loaded directly in the browser (import maps will still be required to resolve bare package imports but that's no problem).

Ideally we should do this without changing source code and leverage something like SWC's @swc/plugin-transform-imports (which transforms exports in addition to imports despite the name).

Have you discussed this feature with our team

@hotell @chrisdholt

Additional context

Currently the ESM output from Fluent is "extensionless" which prevents the ESM output from working correctly in a browser.

// Example output of current output
import { FluentProvider } from '@fluentui/react-components'; // bare module
export { something } from './somewhere'; // bare JS file

// Example of what this needs to be for native ESM support
import { FluentProvider { from '@fluentui/react-components;
export { something } from './somewhere.js';

In this example there is a "bare module" import where code is imported from a package name (that lacks a .js extension) and a "bare JS file" import. The latter is typically used within a package to import other JS files using relative paths.

We can address "bare modules" with importmaps, allowing import $whatever from 'some-module-without-an-extension' to resolve correctly but importmaps cannot, effectivelyโ€ , handle the internal bare JS imports. To handle the bare JS imports we must update the output to include the .js extension.

โ€  I say effectively because it is possible to generate an importmap for all the base JS imports within a package but such an importmap would be quite large and then require end users to correctly merge it with any other importmaps they are using. Technically possible but not a good solution :)

Related issues

Validations

Priority

Normal

Hotell commented 6 months ago

@miroslavstastny I'm re-assigning this to v-build as we will be shipping it

Hotell commented 6 months ago

Update:

Previously I mentioned that in order to make this work, our packages need to be either type:module or ship .mjs under exports#import mapping.

I explored this further and possiblity to add "just .js extensions" is also possible but has some issues which I'll expand on in 1st option description.

Approach options:

1. adding explicit .js extension on build time

๐Ÿ‘๏ธ This has following gotchas:

We need to enforce that relative imports always point to existing physical file no matter what module loader is in place (node vs browser).

๐Ÿงช Example:

// @filename src/index.ts
export {Hi} from './hello'

// @filename src/hello/index.ts
export {Hi} from './hi'

โฌ‡๏ธ swc transpile ( + @swc/plugin-transform-imports ) โฌ‡๏ธ

// @filename lib/index.js
// ๐Ÿšจ๐Ÿšจ๐Ÿšจ this is invalid path - will throw in all environements
export {Hi} from './hello.js'

// @filename src/hello/index.js
export {Hi} from './hi.js'

Rollup handles this as expected

// @filename src/index.ts
export {Hi} from './hello'

// @filename src/hello/index.ts
export {Hi} from './hi'

โฌ‡๏ธ rollup โฌ‡๏ธ

// @filename lib/index.js
// โœ… โœ… โœ… unwraps the import path to it's source 
export {Hi} from './hello/hi.js'

// @filename src/hello/index.js - wont be created
// โœ… โœ… โœ… nested barrel files are completely removed from output as their are not necessary 

What are our implementation options:

2. declaring package as ESM native via type:module pragma

3. shipping all ESM files with .mjs extension

By default in ESM native environments, if a package.json has no type specified it fallbacks to type:commonjs by default (our packages situation).

While we have export maps in place, they are invalid for native ESM loader ( it works solely for build tools with current output ).

In order to support native ESM loader without making the package native ESM, all files that are accessed via exports#import need to:


Implementation requirements:

NOTE: solutions need changes to webpack ( because storybook, cypress use webpack v4)

1. adding explicit .js extension on build time

2. declaring package as ESM native via type:module pragma

3. shipping all ESM files with .mjs extension

spmonahan commented 6 months ago

Re-opening since we had to revert this in #30803

yuntian001 commented 5 months ago

I need it. When do you plan to fix it

AnujAgrawal-1 commented 2 months ago

@spmonahan @Hotell Any update on above issue as i am also getting same issue SyntaxError: Unexpected token 'export' at Runtime.createScriptFromCode (node_modules/jest-runtime/build/index.js:1728:14) at Object.<anonymous> (node_modules/@fluentui/react-provider/lib-commonjs/components/FluentProvider/renderFluentProvider.js:15:20)