-
-
Notifications
You must be signed in to change notification settings - Fork 228
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Build react package for use in nextjs 13 #835
Comments
I'm not maintainer, but I think this is out of tsup role scope and you should create wrapping component of it outside of
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 |
does not work for me. any suggestion? |
@michael-land 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 If you use Next.js we can use the /** @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 Perhaps multiple exports would help to separate shared, client, and server components:
After more research, I also found:
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: |
@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
EDIT: It didn't work for me. |
@teobler nice, I was just going to post that the directive was being ignored
|
🎉 This issue has been resolved in version 7.0.0 🎉 The release is available on: Your semantic-release bot 📦🚀 |
Just updated to v7.0.0 and I still get the same error message.
Am I missing something? |
@tcolinpa Same here, it works fine when removing I believe |
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.
|
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
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. I simply grouped/exported all the client stuff into one |
I've ended doing like the following, a little bit ackward but works:
|
It worked for me. |
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 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 "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:
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,
};
}); |
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 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? |
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.
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. |
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 |
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 |
@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? |
This one seems to work! Not with Thank you @thevuong |
@oalexdoda 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 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. |
@thevuong your solution made me cry!! thank you so much, it work flawlessly for me 🚀 |
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
The text was updated successfully, but these errors were encountered: