diff --git a/packages/plugin-rsc/e2e/basic.test.ts b/packages/plugin-rsc/e2e/basic.test.ts index 30ffc231d..6e6aef111 100644 --- a/packages/plugin-rsc/e2e/basic.test.ts +++ b/packages/plugin-rsc/e2e/basic.test.ts @@ -46,6 +46,28 @@ test.describe('build-default', () => { defineTest(f) }) +test.describe('dev-non-optimized-cjs', () => { + test.beforeAll(async () => { + // remove explicitly added optimizeDeps.include + const editor = f.createEditor('vite.config.ts') + editor.edit((s) => + s.replace( + `'@vitejs/test-dep-transitive-cjs > use-sync-external-store/shim/index.js',`, + ``, + ), + ) + }) + + const f = useFixture({ root: 'examples/basic', mode: 'dev' }) + + test('show warning', async ({ page }) => { + await page.goto(f.url()) + expect(f.proc().stderr()).toContain( + `[vite-rsc] found non-optimized CJS dependency in 'ssr' environment.`, + ) + }) +}) + function defineTest(f: Fixture) { test('basic', async ({ page }) => { using _ = expectNoPageError(page) diff --git a/packages/plugin-rsc/e2e/fixture.ts b/packages/plugin-rsc/e2e/fixture.ts index b21ac65ca..aa61f332a 100644 --- a/packages/plugin-rsc/e2e/fixture.ts +++ b/packages/plugin-rsc/e2e/fixture.ts @@ -10,12 +10,16 @@ function runCli(options: { command: string; label?: string } & SpawnOptions) { const [name, ...args] = options.command.split(' ') const child = x(name!, args, { nodeOptions: options }).process! const label = `[${options.label ?? 'cli'}]` + let stdout = '' + let stderr = '' child.stdout!.on('data', (data) => { + stdout += stripVTControlCharacters(String(data)) if (process.env.TEST_DEBUG) { console.log(styleText('cyan', label), data.toString()) } }) child.stderr!.on('data', (data) => { + stderr += stripVTControlCharacters(String(data)) console.log(styleText('magenta', label), data.toString()) }) const done = new Promise((resolve) => { @@ -48,7 +52,14 @@ function runCli(options: { command: string; label?: string } & SpawnOptions) { } } - return { proc: child, done, findPort, kill } + return { + proc: child, + done, + findPort, + kill, + stdout: () => stdout, + stderr: () => stderr, + } } export type Fixture = ReturnType @@ -64,12 +75,13 @@ export function useFixture(options: { let baseURL!: string const cwd = path.resolve(options.root) + let proc!: ReturnType // TODO: `beforeAll` is called again on any test failure. // https://playwright.dev/docs/test-retries test.beforeAll(async () => { if (options.mode === 'dev') { - const proc = runCli({ + proc = runCli({ command: options.command ?? `pnpm dev`, label: `${options.root}:dev`, cwd, @@ -144,6 +156,7 @@ export function useFixture(options: { root: cwd, url: (url: string = './') => new URL(url, baseURL).href, createEditor, + proc: () => proc, } } diff --git a/packages/plugin-rsc/examples/basic/vite.config.ts b/packages/plugin-rsc/examples/basic/vite.config.ts index b0eec5511..d4ee08d3b 100644 --- a/packages/plugin-rsc/examples/basic/vite.config.ts +++ b/packages/plugin-rsc/examples/basic/vite.config.ts @@ -151,8 +151,6 @@ export default { fetch: handler }; }, ssr: { optimizeDeps: { - // TODO: this should be somehow auto inferred or at least show a warning - // to guide users to `optimizeDeps.include` include: [ '@vitejs/test-dep-transitive-cjs > use-sync-external-store/shim/index.js', ], diff --git a/packages/plugin-rsc/src/plugin.ts b/packages/plugin-rsc/src/plugin.ts index cc3861b52..c89213f94 100644 --- a/packages/plugin-rsc/src/plugin.ts +++ b/packages/plugin-rsc/src/plugin.ts @@ -20,7 +20,7 @@ import { normalizePath, parseAstAsync, } from 'vite' -import { crawlFrameworkPkgs } from 'vitefu' +import { crawlFrameworkPkgs, findClosestPkgJsonPath } from 'vitefu' import vitePluginRscCore from './core/plugin' import { type TransformWrapExportFilter, @@ -838,9 +838,42 @@ globalThis.AsyncLocalStorage = __viteRscAyncHooks.AsyncLocalStorage; ? [validateImportPlugin()] : []), scanBuildStripPlugin(), + detectNonOptimizedCjsPlugin(), ] } +function detectNonOptimizedCjsPlugin(): Plugin { + return { + name: 'rsc:detect-non-optimized-cjs', + apply: 'serve', + async transform(code, id) { + if ( + id.includes('/node_modules/') && + !id.startsWith(this.environment.config.cacheDir) && + /\b(require|exports)\b/.test(code) + ) { + id = parseIdQuery(id).filename + let isEsm = id.endsWith('.mjs') + if (id.endsWith('.js')) { + const pkgJsonPath = await findClosestPkgJsonPath(path.dirname(id)) + if (pkgJsonPath) { + const pkgJson = JSON.parse( + fs.readFileSync(pkgJsonPath, 'utf-8'), + ) as { type?: string } + isEsm = pkgJson.type === 'module' + } + } + if (!isEsm) { + this.warn( + `[vite-rsc] found non-optimized CJS dependency in '${this.environment.name}' environment. ` + + `It is recommended to manually add the dependency to 'environments.${this.environment.name}.optimizeDeps.include'.`, + ) + } + } + }, + } +} + function scanBuildStripPlugin(): Plugin { return { name: 'rsc:scan-strip', @@ -1900,7 +1933,7 @@ function evalValue(rawValue: string): T { // https://github.com/vitejs/vite-plugin-vue/blob/06931b1ea2b9299267374cb8eb4db27c0626774a/packages/plugin-vue/src/utils/query.ts#L13 function parseIdQuery(id: string) { if (!id.includes('?')) return { filename: id, query: {} } - const [filename, rawQuery] = id.split(`?`, 2) + const [filename, rawQuery] = id.split(`?`, 2) as [string, string] const query = Object.fromEntries(new URLSearchParams(rawQuery)) return { filename, query } }