Closed mnzsss closed 1 year ago
I'm not maintainer, but I think this is out of tsup role scope and you should create wrapping component of it outside of app
directory.
// components/AspectRatio.js
export { AspectRatio } from 'your-pkg'
// or inside app directory,
// app/.../AspectRatio.js
"use client"
export { AspectRatio } from 'your-pkg'
But if you have to, how about to use esbuild inject option?
export default defineConfig({
...
esbuildOptions(options, context) {
options.inject?.push('./inject.js');
},
})
// inject.js
"use client"
I'm not maintainer, but I think this is out of tsup role scope and you should create wrapping component of it outside of
app
directory.// components/AspectRatio.js export { AspectRatio } from 'your-pkg' // or inside app directory, // app/.../AspectRatio.js "use client" export { AspectRatio } from 'your-pkg'
But if you have to, how about to use esbuild inject option?
export default defineConfig({ ... esbuildOptions(options, context) { options.inject?.push('./inject.js'); }, })
// inject.js "use client"
I partial solved this problem using a wrapper too, but I guess using inject script is better. I will try that.
@mnzsss Did you clear your problem? I still have issues with use inject option..
export default defineConfig({
...
esbuildOptions(options, context) {
options.inject?.push('./inject.js');
},
})
does not work for me. any suggestion?
I'm not maintainer, but I think this is out of tsup role scope and you should create wrapping component of it outside of
app
directory.// components/AspectRatio.js export { AspectRatio } from 'your-pkg' // or inside app directory, // app/.../AspectRatio.js "use client" export { AspectRatio } from 'your-pkg'
But if you have to, how about to use esbuild inject option?
export default defineConfig({ ... esbuildOptions(options, context) { options.inject?.push('./inject.js'); }, })
// inject.js "use client"
I partial solved this problem using a wrapper too, but I guess using inject script is better. I will try that.
@michael-land @syjung-cookapps
This way not works for me too, I hadn't time to test it and today I found another way to made that.
// tsup.config.ts
import { defineConfig } from "tsup"
export default defineConfig((options) => ({
entry: ["src/index.tsx"],
format: ["esm", "cjs"],
treeshake: true,
splitting: true,
dts: true,
minify: true,
clean: true,
external: ["react"],
onSuccess: "./scripts/post-build.sh",
...options,
}))
# ./scripts/post-build.sh
#!/bin/bash
sed -i '1,1s/^/"use client"; /' ./dist/index.js
sed -i '1,1s/^/"use client"; /' ./dist/index.mjs
This script runs on index.js
and index.mjs
files.
If you use Next.js we can use the transpilePackages
option in next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
swcMinify: true,
transpilePackages: ["ui"],
}
export default nextConfig
Another option may be using these packages:
To ensure that a clear error is thrown, such as:
throw new Error(
"This module cannot be imported from a Server Component module. " +
"It should only be used from a Client Component."
);
Those packages do have side effects though https://webpack.js.org/guides/tree-shaking/#mark-the-file-as-side-effect-free
Though, I agree that the best way would be to keep those annotations with the component. The problem is that all files that import client components must always be marked with the "use client"
directive, including the root index file.
Perhaps multiple exports would help to separate shared, client, and server components:
After more research, I also found:
"use client"
directive https://twitter.com/souporserious/status/1608923451699630081 The solution I will go with today is the multiple exports + custom esbuild plugin (using the original file naming conventions, related)
The Custom ESBuild plugin won't work because of this: Module level directives cause errors when bundled, "use client" in "dist/index.js" was ignored.
- any clue how to fix it?
The Custom ESBuild plugin won't work because of this:
Module level directives cause errors when bundled, "use client" in "dist/index.js" was ignored.
- any clue how to fix it?
@altechzilla I think this lib can help you https://github.com/Ephem/rollup-plugin-preserve-directives
NextJS docs gives two examples on how to inject directives as wrapper.
https://nextjs.org/docs/getting-started/react-essentials#library-authors https://github.com/shuding/react-wrap-balancer/blob/main/tsup.config.ts#L10-L13
esbuildOptions: (options) => {
options.banner = {
js: '"use client"',
};
},
EDIT: It didn't work for me.
I faced the same issue with @tcolinpa , after some spike, I think the reason is tsup still using esbuild 0.17.6, and in 0.17.9 esbuild removed the filter of directives. To resolve this issue, we may need to upgrade esbuild version.
cc: @egoist
I raised a PR for this fix, after PR merged and use config from @tcolinpa should be fine.
@teobler nice, I was just going to post that the directive was being ignored
Module level directives cause errors when bundled, "use client" in "dist/index.js" was ignored.
:tada: This issue has been resolved in version 7.0.0 :tada:
The release is available on:
Your semantic-release bot :package::rocket:
Just updated to v7.0.0 and I still get the same error message.
react dev: Module level directives cause errors when bundled, "use client" in "dist/index.js" was ignored.
react dev: Module level directives cause errors when bundled, "use client" in "dist/index.mjs" was ignored.
Am I missing something?
@tcolinpa Same here, it works fine when removing treeshaking
& splitting
from my tsup config tho.
I believe treeshaking
is using rollup, hence the error
Good catch, I've disabled treeshaking from my config and it worked. Thanks @Gomah
But what if I want to keep the treeshake option enabled, I am still facing the same issue
I ended up using the banner solution but I went the extra mile because I wanted my packages to be "dynamic" and use the "use client" directive whenever the environment is the browser and not use it when the environment is the server. I ended up essentially creating two packages one for the client and one for the server. Inside my next application it's smart enough to know which package to import depending on the environment. This effectively achieves what I want but doubles my build time for any packages that I want to be "dynamic". At least this way I can define "use client" inside my app and know that the package that is imported will use the correct bundle.
// tsup.config.ts
import 'dotenv/config';
import { defineConfig } from 'tsup';
export default defineConfig({
entry: {
index: 'src/index.ts',
},
external: ['react'],
format: ['esm', 'cjs'],
dts: 'src/index.ts',
platform: 'browser',
esbuildOptions(options) {
if (options.platform === 'browser') {
options.banner = {
js: '"use client"',
};
}
},
});
// package.json
{
"name": "ui",
"version": "0.0.0",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"node": "./dist/server.js",
"import": "./dist/index.js",
"module": "./dist/index.mjs",
"default": "./dist/index.js",
"types": "./dist/index.d.ts"
},
"./styles.css": "./dist/styles.css"
},
"license": "MIT",
"scripts": {
"build:client": "tsup && tailwindcss -i ./src/styles.css -o ./dist/styles.css",
"build:server": "tsup --entry.server src/index.ts --platform=node && tailwindcss -i ./src/styles.css -o ./dist/styles.css",
"build": "yarn build:client && yarn build:server",
"dev": "concurrently \"yarn build:client --watch\" \"yarn build:server --watch\" \"tailwindcss -i ./src/styles.css -o ./dist/styles.css --watch\"",
"storybook": "storybook dev -s ./public -p 6006",
"clean": "rm -rf dist",
"build-storybook": "storybook build"
},
...
Any way to make this work at a component level? I don't want the ENTIRE library to have 'use client' directives (aka every single file). If you have 100 components and you only need 5 of them to have that banner, how would you go about it?
Because
esbuildOptions(options) {
options.banner = {
js: '"use client"'
};
},
adds it to every single file.
My temporary solution is to add the 'use client' directive at the beginning of each chunk file.
Is there no other way than post-processing? If so, shouldn't this ticket remain open?
I was able to create a package which has both server & client code and works in next 14. https://github.com/MiroslavPetrik/react-form-action
I simply grouped/exported all the client stuff into one client.ts
and then in package json added export for ./client
.
Maybe it helps somebody here.
I've ended doing like the following, a little bit ackward but works:
import { defineConfig, Format } from "tsup";
const cfg = {
splitting: true, //error triggerer
treeshake: true, //error triggerer
sourcemap: true,
clean: true,
dts: true,
format: ["esm"] as Format[],
minify: true,
bundle: false,
external: ["react"],
};
export default defineConfig([
{
...cfg, //in this part, I just used the non client components, but excluding the one that needed 'use client'
entry: ["components/**/*.tsx", "!components/layout/header.tsx"],
outDir: "dist/layout",
},
{
...cfg, //and here I've added the esbuildOptions banner and disabling the error trigger options
entry: ["components/layout/header.tsx"],
outDir: "dist/layout",
esbuildOptions: (options) => {
options.banner = {
js: '"use client";',
};
},
splitting: false,
treeshake: false,
},
]);
My temporary solution is to add the 'use client' directive at the beginning of each chunk file.
It worked for me.
I've ended doing like the following, a little bit ackward but works:
import { defineConfig, Format } from "tsup"; const cfg = { splitting: true, //error triggerer treeshake: true, //error triggerer sourcemap: true, clean: true, dts: true, format: ["esm"] as Format[], minify: true, bundle: false, external: ["react"], }; export default defineConfig([ { ...cfg, //in this part, I just used the non client components, but excluding the one that needed 'use client' entry: ["components/**/*.tsx", "!components/layout/header.tsx"], outDir: "dist/layout", }, { ...cfg, //and here I've added the esbuildOptions banner and disabling the error trigger options entry: ["components/layout/header.tsx"], outDir: "dist/layout", esbuildOptions: (options) => { options.banner = { js: '"use client";', }; }, splitting: false, treeshake: false, }, ]);
does this work when you have nesting of client and server components?
If I have a react package with both client and server components, I haven't been able to figure a way out to package it ( the chunks generated don't have the directives - they cant because some chunks have a mix of client and server components ).
This is what works for me, based upon Vercel Analytics example. It will generate multiple exports so that 1️⃣ can consolidate all of the 'use client'
components or not. In my case, I was separating out components from a bunch of constants.
import { defineConfig, Options } from 'tsup';
const cfg: Options = {
clean: false,
dts: true,
format: ['esm'],
minify: true,
sourcemap: false,
splitting: false,
target: 'es2022',
treeshake: false,
};
export default defineConfig([
{
...cfg,
// These are client components. They will get the 'use client' at the top.
entry: { index: 'src/components/index.ts' },
esbuildOptions(options) {
options.banner = {
js: '"use client"',
};
},
// However you like this.
external: [
'@twicpics/components',
'autoprefixer',
'postcss',
'react',
'react-dom',
'tailwindcss',
],
outDir: 'dist',
},
{
...cfg,
// I was doing something else with another file, but this could be 'server components' or whatever
entry: { index: 'src/types/constants.ts' },
outDir: 'dist/constants',
},
]);
You may need to also update your package.json
so that a consuming app knows where stuff is:
"types": "dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
},
"./types": {
"import": "./dist/constants/index.js",
"types": "./dist/constants/index.d.ts"
}
},
Wait for the stable release, this is my solution fix
Folder structure:
📦 ui
├─ src
│ └─ components
│ ├─ clients
│ │ └─ others
│ ├─ servers
│ │ └─ others
│ └─ index.ts
└─ tsup.config.ts
const ignoreBuilds = [
'!src/_stories/**/*.{ts,tsx,js,jsx}', // ignore custom storybook config
'!src/components/**/*.stories.{ts,tsx}', // ignore all file storybook
];
function readFilesRecursively(directory: string) {
const files: string[] = [];
function read(directory: string) {
const entries = fs.readdirSync(directory);
entries.forEach((entry) => {
const fullPath = path.join(directory, entry);
const stat = fs.statSync(fullPath);
if (stat.isDirectory()) {
read(fullPath);
} else {
files.push(fullPath);
}
});
}
read(directory);
return files;
}
async function addDirectivesToChunkFiles(distPath = 'dist'): Promise<void> {
try {
const files = readFilesRecursively(distPath);
for (const file of files) {
/**
* Skip chunk, sourcemap, other clients
* */
const isIgnoreFile =
file.includes('chunk-') ||
file.includes('.map') ||
!file.includes('/clients/');
if (isIgnoreFile) {
console.log(`⏭️ Directive 'use client'; has been skipped for ${file}`);
continue;
}
const filePath = path.join('', file);
const data = await fsPromises.readFile(filePath, 'utf8');
const updatedContent = `"use client";${data}`;
await fsPromises.writeFile(filePath, updatedContent, 'utf8');
console.log(`💚 Directive 'use client'; has been added to ${file}`);
}
} catch (err) {
// eslint-disable-next-line no-console -- We need to log the error
console.error('⚠️ Something error:', err);
}
}
export default defineConfig((options: Options) => {
return {
entry: ['src/**/*.{ts,tsx}', ...ignoreBuilds],
splitting: true,
treeshake: true,
sourcemap: true,
clean: true,
dts: true,
format: ['esm', 'cjs'],
target: 'es5',
bundle: true,
platform: 'browser',
minify: true,
minifyWhitespace: true,
tsconfig: new URL('./tsconfig.build.json', import.meta.url).pathname,
onSuccess: async () => {
await addDirectivesToChunkFiles();
},
...options,
};
});
I was able to create a package which has both server & client code and works in next 14. https://github.com/MiroslavPetrik/react-form-action
I simply grouped/exported all the client stuff into one
client.ts
and then in package json added export for./client
. Maybe it helps somebody here.
That was for me the simple and easiest way to do it. Thank you !
Hello everyone,
I wanted to follow up on this issue as I've developed a plugin that might help address this problem: esbuild-plugin-preserve-directives
This plugin is specifically designed to preserve directives like "use client" in esbuild output. I created it as a potential solution for those who need this functionality before it's natively implemented in esbuild.
Key features of the plugin:
Preserves "use client" and other directives in the output Works with both JavaScript and TypeScript files Configurable to preserve specific directives I'd love to get your feedback on this approach. Has anyone else been working on similar solutions? Do you see any potential issues or improvements?
If you try it out, please let me know how it works for you. I'm open to suggestions and contributions to make it more robust and useful for the community.
This could serve as a temporary solution for users who need to preserve the "use client" directive in their RSC builds while we wait for native support in esbuild.
Hello everyone,
I wanted to follow up on this issue as I've developed a plugin that might help address this problem: esbuild-plugin-preserve-directives
This plugin is specifically designed to preserve directives like "use client" in esbuild output. I created it as a potential solution for those who need this functionality before it's natively implemented in esbuild.
Key features of the plugin:
Preserves "use client" and other directives in the output Works with both JavaScript and TypeScript files Configurable to preserve specific directives I'd love to get your feedback on this approach. Has anyone else been working on similar solutions? Do you see any potential issues or improvements?
If you try it out, please let me know how it works for you. I'm open to suggestions and contributions to make it more robust and useful for the community.
This could serve as a temporary solution for users who need to preserve the "use client" directive in their RSC builds while we wait for native support in esbuild.
Does it work with chunked components (i.e. if a use client component gets split by tsup in multiple chunks) or does it only work with the entry point exports?
@oalexdoda
Does it work with chunked components (i.e. if a use client component gets split by tsup in multiple chunks) or does it only work with the entry point exports?
The plugin supports both scenarios: it works with chunked components where 'use client' components are split into multiple chunks by tsup, as well as with entry point exports. However, most users adopt this plugin specifically to handle the first case (chunked components), as that's the more common use case where directive preservation becomes critical.
You can check out the src structure and built chunks in this repository: https://github.com/Seojunhwan/esbuild-plugin-result-example to see a practical example of how the plugin handles directive preservation across chunks.
I have developed a solution that I have integrated into the @codefast/ui library.
addUseClientDirective
is a function that selectively adds the "use client"
directive to code chunks. It checks if the content includes client libraries or React hooks and only adds the directive when necessary, ensuring that only the required files are updated. This function works as an tsup
plugin, using the renderChunk
hook, so it processes code very quickly without reading directly from files. Here's the summary:
relative
from node:path
and Options
from tsup
."use client"
directive.containsClientLibsOrHooks
: Checks for client libraries or React hooks in the content.buildClientLibsRegex
: Builds a regex for matching client libraries.addUseClientDirective
: Adds the "use client"
directive based on specified client libraries.It processes and potentially modifies code chunks to include the directive if certain conditions are met, making the directive addition more efficient and targeted. You can check the implementation here.
@oalexdoda
Does it work with chunked components (i.e. if a use client component gets split by tsup in multiple chunks) or does it only work with the entry point exports?
The plugin supports both scenarios: it works with chunked components where 'use client' components are split into multiple chunks by tsup, as well as with entry point exports. However, most users adopt this plugin specifically to handle the first case (chunked components), as that's the more common use case where directive preservation becomes critical.
You can check out the src structure and built chunks in this repository: https://github.com/Seojunhwan/esbuild-plugin-result-example to see a practical example of how the plugin handles directive preservation across chunks.
Hey @Seojunhwan
Any clue what I'm doing wrong?
It works in dev, but when building & deploying the directive needs to be at the top of the file/chunk so it breaks.
@oalexdoda I think it's a bug. I'll check it out today.
Thank you @Seojunhwan , really appreciate it. I'm stuck on getting this to build so I can push the newly repackaged dependencies to my platform - so I'll be on the lookout for your update 🙏
I think the directives need to be moved to the start of the file, like this:
Instead of like this:
Also, if you enable treeshake
, the plugin seems to stop working entirely.
@Seojunhwan I tried a bunch of different things but it seems like the plugin gets executed before whatever ends up inserting the use strict directive. Perhaps there's a different way to be explored on how we run the plugin to have it take the right "priority" or order?
I have developed a solution that I have integrated into the @codefast/ui library.
addUseClientDirective
is a function that selectively adds the"use client"
directive to code chunks. It checks if the content includes client libraries or React hooks and only adds the directive when necessary, ensuring that only the required files are updated. This function works as anesbuild
plugin, using therenderChunk
hook, so it processes code very quickly without reading directly from files. Here's the summary:
- Imports: Includes
relative
fromnode:path
andOptions
fromtsup
.- Tracked Imports: Keeps track of imports in each code chunk.
- Directive: Defines the
"use client"
directive.- Regex: Detects React hooks and event handlers, excluding comments.
Functions:
containsClientLibsOrHooks
: Checks for client libraries or React hooks in the content.buildClientLibsRegex
: Builds a regex for matching client libraries.addUseClientDirective
: Adds the"use client"
directive based on specified client libraries.It processes and potentially modifies code chunks to include the directive if certain conditions are met, making the directive addition more efficient and targeted. You can check the implementation here.
This one seems to work! Not with treeshake
enabled, but it works!
Thank you @thevuong
@oalexdoda I’ve checked, and if you’re using tsup, it seems more appropriate to leverage the excellent solution by @thevuong
To elaborate, tsup builds using esbuild and then operates the renderChunk hook with JavaScript on the output, finally saving the file. As a result, directives in the file contents are preserved when the ESM flag is added, so this issue didn’t seem to arise.
In contrast, the onEnd hook available in esbuild plugins receives the output before the ESM flag is added, which seems to be the cause of this issue.
In summary, I’m not sure how to add directives after adding the ESM flag within an esbuild plugin—perhaps it’s even impossible.
If, like in the default settings of tsup, you’re not performing code splitting in CJS, then this issue likely wouldn’t arise. However, it seems that code splitting is essential in your case, so this may not be the best approach.
Additionally, since treeshaking isn’t handled by this plugin, it might be worth revisiting your configuration.
Thanks for helping to identify this plugin issue!
Thank you, @Seojunhwan, for your detailed feedback!
In the case where tsup uses treeshaking, tsup actually switches to Rollup instead of esbuild to handle this, especially for tree-shaking purposes. This opens up some other possibilities. While tsup doesn’t natively support custom Rollup configurations directly (though I’m not entirely certain), if there’s a way to add additional plugins for Rollup through tsup’s configuration, we could consider using the rollup-plugin-preserve-directives
library. This plugin would help preserve directives like "use client"
in the output during the build process.
This approach could fully address the issue of keeping the "use client" directive intact while applying tree-shaking and managing code-splitting effectively. It would ensure the directive is retained in the output without relying on the ESM flag or the default configurations of tsup and esbuild.
I tried create a package with ui components for use in Nextjs 13 web app, but I can't build components with
"use client"
in the beginning of code, like that:So when I build this code, the
"use client"
has removed:Error on import component of package in app: Obs.: The component needs to be imported into a server side file, in which case it would be the
layout.tsx
Have a workround or option for this?
I use:
tsconfig.json
of ui package:tsup.config.ts