|  | 
| 1 | 1 | import { test, expect } from '@playwright/test' | 
| 2 | 2 | import { setupInlineFixture, useFixture, type Fixture } from './fixture' | 
| 3 | 3 | import { x } from 'tinyexec' | 
| 4 |  | -import path from 'node:path' | 
| 5 | 4 | import { expectNoPageError, waitForHydration } from './helper' | 
| 6 | 5 | 
 | 
| 7 | 6 | test.describe('validate imports', () => { | 
| 8 | 7 |   test.describe('valid imports', () => { | 
| 9 |  | -    const root = 'examples/e2e/temp/validate-imports-server-only-client' | 
|  | 8 | +    const root = 'examples/e2e/temp/validate-imports' | 
| 10 | 9 |     test.beforeAll(async () => { | 
| 11 | 10 |       await setupInlineFixture({ | 
| 12 | 11 |         src: 'examples/starter', | 
| @@ -61,279 +60,101 @@ test.describe('validate imports', () => { | 
| 61 | 60 |     } | 
| 62 | 61 |   }) | 
| 63 | 62 | 
 | 
| 64 |  | -  test('should fail build when server-only is imported in client component', async () => { | 
| 65 |  | -    const root = 'examples/e2e/temp/validate-imports-server-only-client' | 
| 66 |  | - | 
| 67 |  | -    await setupInlineFixture({ | 
| 68 |  | -      src: 'examples/starter', | 
| 69 |  | -      dest: root, | 
| 70 |  | -      files: { | 
| 71 |  | -        'src/client.tsx': /* tsx */ ` | 
| 72 |  | -          "use client"; | 
| 73 |  | -          import 'server-only'; | 
| 74 |  | -           | 
| 75 |  | -          export function ClientComponent() { | 
| 76 |  | -            return <div>This should fail</div> | 
| 77 |  | -          } | 
| 78 |  | -        `, | 
| 79 |  | -        'src/root.tsx': /* tsx */ ` | 
| 80 |  | -          import { ClientComponent } from './client.tsx' | 
| 81 |  | -
 | 
| 82 |  | -          export function Root() { | 
| 83 |  | -            return ( | 
| 84 |  | -              <html lang="en"> | 
| 85 |  | -                <head> | 
| 86 |  | -                  <meta charSet="UTF-8" /> | 
| 87 |  | -                </head> | 
| 88 |  | -                <body> | 
| 89 |  | -                  <ClientComponent /> | 
| 90 |  | -                </body> | 
| 91 |  | -              </html> | 
| 92 |  | -            ) | 
| 93 |  | -          } | 
| 94 |  | -        `, | 
| 95 |  | -      }, | 
| 96 |  | -    }) | 
| 97 |  | - | 
| 98 |  | -    // Expect build to fail | 
| 99 |  | -    const result = await x('pnpm', ['build'], { | 
| 100 |  | -      throwOnError: false, | 
| 101 |  | -      nodeOptions: { | 
| 102 |  | -        cwd: root, | 
| 103 |  | -        stdio: 'pipe', | 
| 104 |  | -      }, | 
| 105 |  | -    }) | 
| 106 |  | - | 
| 107 |  | -    expect(result.exitCode).not.toBe(0) | 
| 108 |  | -    expect(result.stderr).toContain("'server-only' is included in client build") | 
| 109 |  | -  }) | 
| 110 |  | - | 
| 111 |  | -  test('should fail build when client-only is imported in server component', async () => { | 
| 112 |  | -    const root = 'examples/e2e/temp/validate-imports-client-only-server' | 
| 113 |  | - | 
| 114 |  | -    await setupInlineFixture({ | 
| 115 |  | -      src: 'examples/starter', | 
| 116 |  | -      dest: root, | 
| 117 |  | -      files: { | 
| 118 |  | -        'package.json': { | 
| 119 |  | -          edit: (content) => { | 
| 120 |  | -            const pkg = JSON.parse(content) | 
| 121 |  | -            // Add package.json overrides like setupIsolatedFixture | 
| 122 |  | -            const packagesDir = path.join(import.meta.dirname, '..', '..') | 
| 123 |  | -            const overrides = { | 
| 124 |  | -              '@vitejs/plugin-rsc': `file:${path.join(packagesDir, 'plugin-rsc')}`, | 
| 125 |  | -              '@vitejs/plugin-react': `file:${path.join(packagesDir, 'plugin-react')}`, | 
| 126 |  | -            } | 
| 127 |  | -            Object.assign(((pkg.pnpm ??= {}).overrides ??= {}), overrides) | 
| 128 |  | -            // Add external dependencies that are managed in examples/e2e/package.json | 
| 129 |  | -            pkg.dependencies = { | 
| 130 |  | -              ...pkg.dependencies, | 
| 131 |  | -              'client-only': '^0.0.1', | 
| 132 |  | -            } | 
| 133 |  | -            return JSON.stringify(pkg, null, 2) | 
| 134 |  | -          }, | 
| 135 |  | -        }, | 
| 136 |  | -        'src/server.tsx': /* tsx */ ` | 
| 137 |  | -          import 'client-only'; | 
| 138 |  | -           | 
| 139 |  | -          export function ServerComponent() { | 
| 140 |  | -            return <div>This should fail</div> | 
| 141 |  | -          } | 
| 142 |  | -        `, | 
| 143 |  | -        'src/root.tsx': /* tsx */ ` | 
| 144 |  | -          import { ServerComponent } from './server.tsx' | 
| 145 |  | -
 | 
| 146 |  | -          export function Root() { | 
| 147 |  | -            return ( | 
| 148 |  | -              <html lang="en"> | 
| 149 |  | -                <head> | 
| 150 |  | -                  <meta charSet="UTF-8" /> | 
| 151 |  | -                </head> | 
| 152 |  | -                <body> | 
| 153 |  | -                  <ServerComponent /> | 
| 154 |  | -                </body> | 
| 155 |  | -              </html> | 
| 156 |  | -            ) | 
| 157 |  | -          } | 
| 158 |  | -        `, | 
| 159 |  | -      }, | 
| 160 |  | -    }) | 
| 161 |  | - | 
| 162 |  | -    // Expect build to fail | 
| 163 |  | -    const result = await x('pnpm', ['build'], { | 
| 164 |  | -      throwOnError: false, | 
| 165 |  | -      nodeOptions: { | 
| 166 |  | -        cwd: root, | 
| 167 |  | -        stdio: 'pipe', | 
| 168 |  | -      }, | 
| 169 |  | -    }) | 
| 170 |  | - | 
| 171 |  | -    expect(result.exitCode).not.toBe(0) | 
| 172 |  | -    expect(result.stderr).toContain("'client-only' is included in server build") | 
| 173 |  | -  }) | 
| 174 |  | - | 
| 175 |  | -  test('should allow valid imports when validation is enabled', async () => { | 
| 176 |  | -    const root = 'examples/e2e/temp/validate-imports-valid' | 
| 177 |  | - | 
| 178 |  | -    await setupInlineFixture({ | 
| 179 |  | -      src: 'examples/starter', | 
| 180 |  | -      dest: root, | 
| 181 |  | -      files: { | 
| 182 |  | -        'package.json': { | 
| 183 |  | -          edit: (content) => { | 
| 184 |  | -            const pkg = JSON.parse(content) | 
| 185 |  | -            // Add package.json overrides like setupIsolatedFixture | 
| 186 |  | -            const packagesDir = path.join(import.meta.dirname, '..', '..') | 
| 187 |  | -            const overrides = { | 
| 188 |  | -              '@vitejs/plugin-rsc': `file:${path.join(packagesDir, 'plugin-rsc')}`, | 
| 189 |  | -              '@vitejs/plugin-react': `file:${path.join(packagesDir, 'plugin-react')}`, | 
|  | 63 | +  test.describe('server-only on client', () => { | 
|  | 64 | +    const root = 'examples/e2e/temp/validate-server-only' | 
|  | 65 | +    test.beforeAll(async () => { | 
|  | 66 | +      await setupInlineFixture({ | 
|  | 67 | +        src: 'examples/starter', | 
|  | 68 | +        dest: root, | 
|  | 69 | +        files: { | 
|  | 70 | +          'src/client.tsx': /* tsx */ ` | 
|  | 71 | +            "use client"; | 
|  | 72 | +            import 'server-only'; | 
|  | 73 | +             | 
|  | 74 | +            export function TestClient() { | 
|  | 75 | +              return <div>[test-client]</div> | 
| 190 | 76 |             } | 
| 191 |  | -            Object.assign(((pkg.pnpm ??= {}).overrides ??= {}), overrides) | 
| 192 |  | -            // Add external dependencies that are managed in examples/e2e/package.json | 
| 193 |  | -            pkg.dependencies = { | 
| 194 |  | -              ...pkg.dependencies, | 
| 195 |  | -              'server-only': '^0.0.1', | 
| 196 |  | -              'client-only': '^0.0.1', | 
|  | 77 | +          `, | 
|  | 78 | +          'src/root.tsx': /* tsx */ ` | 
|  | 79 | +            import { TestClient } from './client.tsx' | 
|  | 80 | +            import 'server-only'; | 
|  | 81 | +   | 
|  | 82 | +            export function Root() { | 
|  | 83 | +              return ( | 
|  | 84 | +                <html lang="en"> | 
|  | 85 | +                  <head> | 
|  | 86 | +                    <meta charSet="UTF-8" /> | 
|  | 87 | +                  </head> | 
|  | 88 | +                  <body> | 
|  | 89 | +                    <div>[test-server]</div> | 
|  | 90 | +                    <TestClient /> | 
|  | 91 | +                  </body> | 
|  | 92 | +                </html> | 
|  | 93 | +              ) | 
| 197 | 94 |             } | 
| 198 |  | -            return JSON.stringify(pkg, null, 2) | 
| 199 |  | -          }, | 
|  | 95 | +          `, | 
| 200 | 96 |         }, | 
| 201 |  | -        'src/client.tsx': /* tsx */ ` | 
| 202 |  | -          "use client"; | 
| 203 |  | -          import 'client-only'; | 
| 204 |  | -           | 
| 205 |  | -          export function ClientComponent() { | 
| 206 |  | -            return <div>Valid client import</div> | 
| 207 |  | -          } | 
| 208 |  | -        `, | 
| 209 |  | -        'src/server.tsx': /* tsx */ ` | 
| 210 |  | -          import 'server-only'; | 
| 211 |  | -           | 
| 212 |  | -          export function ServerComponent() { | 
| 213 |  | -            return <div>Valid server import</div> | 
| 214 |  | -          } | 
| 215 |  | -        `, | 
| 216 |  | -        'src/root.tsx': /* tsx */ ` | 
| 217 |  | -          import { ClientComponent } from './client.tsx' | 
| 218 |  | -          import { ServerComponent } from './server.tsx' | 
| 219 |  | -
 | 
| 220 |  | -          export function Root() { | 
| 221 |  | -            return ( | 
| 222 |  | -              <html lang="en"> | 
| 223 |  | -                <head> | 
| 224 |  | -                  <meta charSet="UTF-8" /> | 
| 225 |  | -                </head> | 
| 226 |  | -                <body> | 
| 227 |  | -                  <ServerComponent /> | 
| 228 |  | -                  <ClientComponent /> | 
| 229 |  | -                </body> | 
| 230 |  | -              </html> | 
| 231 |  | -            ) | 
| 232 |  | -          } | 
| 233 |  | -        `, | 
| 234 |  | -      }, | 
| 235 |  | -    }) | 
| 236 |  | - | 
| 237 |  | -    // Install dependencies | 
| 238 |  | -    await x('pnpm', ['i'], { | 
| 239 |  | -      throwOnError: true, | 
| 240 |  | -      nodeOptions: { | 
| 241 |  | -        cwd: root, | 
| 242 |  | -        stdio: 'ignore', | 
| 243 |  | -      }, | 
|  | 97 | +      }) | 
| 244 | 98 |     }) | 
| 245 | 99 | 
 | 
| 246 |  | -    // Expect build to succeed | 
| 247 |  | -    const result = await x('pnpm', ['build'], { | 
| 248 |  | -      throwOnError: false, | 
| 249 |  | -      nodeOptions: { | 
| 250 |  | -        cwd: root, | 
| 251 |  | -        stdio: 'pipe', | 
| 252 |  | -      }, | 
|  | 100 | +    test('build', async () => { | 
|  | 101 | +      const result = await x('pnpm', ['build'], { | 
|  | 102 | +        throwOnError: false, | 
|  | 103 | +        nodeOptions: { cwd: root }, | 
|  | 104 | +      }) | 
|  | 105 | +      expect(result.stderr).toContain( | 
|  | 106 | +        `'server-only' cannot be imported in client build`, | 
|  | 107 | +      ) | 
|  | 108 | +      expect(result.exitCode).not.toBe(0) | 
| 253 | 109 |     }) | 
| 254 |  | - | 
| 255 |  | -    expect(result.exitCode).toBe(0) | 
| 256 | 110 |   }) | 
| 257 | 111 | 
 | 
| 258 |  | -  test('should allow invalid imports when validation is disabled', async () => { | 
| 259 |  | -    const root = 'examples/e2e/temp/validate-imports-disabled' | 
| 260 |  | - | 
| 261 |  | -    await setupInlineFixture({ | 
| 262 |  | -      src: 'examples/starter', | 
| 263 |  | -      dest: root, | 
| 264 |  | -      files: { | 
| 265 |  | -        'package.json': { | 
| 266 |  | -          edit: (content) => { | 
| 267 |  | -            const pkg = JSON.parse(content) | 
| 268 |  | -            // Add package.json overrides like setupIsolatedFixture | 
| 269 |  | -            const packagesDir = path.join(import.meta.dirname, '..', '..') | 
| 270 |  | -            const overrides = { | 
| 271 |  | -              '@vitejs/plugin-rsc': `file:${path.join(packagesDir, 'plugin-rsc')}`, | 
| 272 |  | -              '@vitejs/plugin-react': `file:${path.join(packagesDir, 'plugin-react')}`, | 
|  | 112 | +  test.describe('client-only on server', () => { | 
|  | 113 | +    const root = 'examples/e2e/temp/validate-client-only' | 
|  | 114 | +    test.beforeAll(async () => { | 
|  | 115 | +      await setupInlineFixture({ | 
|  | 116 | +        src: 'examples/starter', | 
|  | 117 | +        dest: root, | 
|  | 118 | +        files: { | 
|  | 119 | +          'src/client.tsx': /* tsx */ ` | 
|  | 120 | +            "use client"; | 
|  | 121 | +            import 'client-only'; | 
|  | 122 | +             | 
|  | 123 | +            export function TestClient() { | 
|  | 124 | +              return <div>[test-client]</div> | 
| 273 | 125 |             } | 
| 274 |  | -            Object.assign(((pkg.pnpm ??= {}).overrides ??= {}), overrides) | 
| 275 |  | -            // Add external dependencies that are managed in examples/e2e/package.json | 
| 276 |  | -            pkg.dependencies = { | 
| 277 |  | -              ...pkg.dependencies, | 
| 278 |  | -              'server-only': '^0.0.1', | 
|  | 126 | +          `, | 
|  | 127 | +          'src/root.tsx': /* tsx */ ` | 
|  | 128 | +            import { TestClient } from './client.tsx' | 
|  | 129 | +            import 'client-only'; | 
|  | 130 | +   | 
|  | 131 | +            export function Root() { | 
|  | 132 | +              return ( | 
|  | 133 | +                <html lang="en"> | 
|  | 134 | +                  <head> | 
|  | 135 | +                    <meta charSet="UTF-8" /> | 
|  | 136 | +                  </head> | 
|  | 137 | +                  <body> | 
|  | 138 | +                    <div>[test-server]</div> | 
|  | 139 | +                    <TestClient /> | 
|  | 140 | +                  </body> | 
|  | 141 | +                </html> | 
|  | 142 | +              ) | 
| 279 | 143 |             } | 
| 280 |  | -            return JSON.stringify(pkg, null, 2) | 
| 281 |  | -          }, | 
| 282 |  | -        }, | 
| 283 |  | -        'vite.config.ts': { | 
| 284 |  | -          edit: (content) => { | 
| 285 |  | -            // Only modify the rsc plugin options to disable validation | 
| 286 |  | -            return content.replace( | 
| 287 |  | -              'rsc({', | 
| 288 |  | -              'rsc({\n      validateImports: false, // Disable validation', | 
| 289 |  | -            ) | 
| 290 |  | -          }, | 
|  | 144 | +          `, | 
| 291 | 145 |         }, | 
| 292 |  | -        'src/client.tsx': /* tsx */ ` | 
| 293 |  | -          "use client"; | 
| 294 |  | -          import 'server-only'; | 
| 295 |  | -           | 
| 296 |  | -          export function ClientComponent() { | 
| 297 |  | -            return <div>Invalid but allowed</div> | 
| 298 |  | -          } | 
| 299 |  | -        `, | 
| 300 |  | -        'src/root.tsx': /* tsx */ ` | 
| 301 |  | -          import { ClientComponent } from './client.tsx' | 
| 302 |  | -
 | 
| 303 |  | -          export function Root() { | 
| 304 |  | -            return ( | 
| 305 |  | -              <html lang="en"> | 
| 306 |  | -                <head> | 
| 307 |  | -                  <meta charSet="UTF-8" /> | 
| 308 |  | -                </head> | 
| 309 |  | -                <body> | 
| 310 |  | -                  <ClientComponent /> | 
| 311 |  | -                </body> | 
| 312 |  | -              </html> | 
| 313 |  | -            ) | 
| 314 |  | -          } | 
| 315 |  | -        `, | 
| 316 |  | -      }, | 
| 317 |  | -    }) | 
| 318 |  | - | 
| 319 |  | -    // Install dependencies | 
| 320 |  | -    await x('pnpm', ['i'], { | 
| 321 |  | -      throwOnError: true, | 
| 322 |  | -      nodeOptions: { | 
| 323 |  | -        cwd: root, | 
| 324 |  | -        stdio: 'ignore', | 
| 325 |  | -      }, | 
|  | 146 | +      }) | 
| 326 | 147 |     }) | 
| 327 | 148 | 
 | 
| 328 |  | -    // Expect build to succeed even with invalid import because validation is disabled | 
| 329 |  | -    const result = await x('pnpm', ['build'], { | 
| 330 |  | -      throwOnError: false, | 
| 331 |  | -      nodeOptions: { | 
| 332 |  | -        cwd: root, | 
| 333 |  | -        stdio: 'pipe', | 
| 334 |  | -      }, | 
|  | 149 | +    test('build', async () => { | 
|  | 150 | +      const result = await x('pnpm', ['build'], { | 
|  | 151 | +        throwOnError: false, | 
|  | 152 | +        nodeOptions: { cwd: root }, | 
|  | 153 | +      }) | 
|  | 154 | +      expect(result.stderr).toContain( | 
|  | 155 | +        `'client-only' cannot be imported in server build`, | 
|  | 156 | +      ) | 
|  | 157 | +      expect(result.exitCode).not.toBe(0) | 
| 335 | 158 |     }) | 
| 336 |  | - | 
| 337 |  | -    expect(result.exitCode).toBe(0) | 
| 338 | 159 |   }) | 
| 339 | 160 | }) | 
0 commit comments