From aa75090a9e7c1b783496ed8b0f585fdd86288dea Mon Sep 17 00:00:00 2001 From: "Alexis H. Munsayac" Date: Mon, 19 Oct 2026 12:00:55 +0800 Subject: [PATCH] ci: add a headless test script for vite-ecosystem-ci - Add `pnpm test:ecosystem`. It checks the built plugin against the Vite version installed in the repository: dev client and SSR transforms, HMR runtime injection, a client build, an SSR build, a dependency with a `solid` export condition, and no deprecation warnings. - It needs no browser, no network and no example installs, so vite-ecosystem-ci can run it after it replaces Vite. Refs #285 Co-Authored-By: Claude Opus 5.5 (1M context) --- package.json | 3 +- scripts/test-ecosystem.mjs | 172 +++++++++++++++++++++++++++++++++++++ 2 files changed, 174 insertions(+), 1 deletion(-) create mode 100644 scripts/test-ecosystem.mjs diff --git a/package.json b/package.json index a6a6caac..fad87588 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,8 @@ "release": "pnpm build && changeset publish", "check": "package-check", "test": "node scripts/test-examples.ts", - "test:unit": "vitest run" + "test:unit": "vitest run", + "test:ecosystem": "node scripts/test-ecosystem.mjs" }, "repository": { "type": "git", diff --git a/scripts/test-ecosystem.mjs b/scripts/test-ecosystem.mjs new file mode 100644 index 00000000..d50cc117 --- /dev/null +++ b/scripts/test-ecosystem.mjs @@ -0,0 +1,172 @@ +// Headless checks of the built plugin against the Vite version installed in +// this repository. vite-ecosystem-ci runs this script (`pnpm test:ecosystem`) +// after it replaces Vite with the build under test. It needs no browser and +// no network, so it only depends on Vite and the plugin. +import { mkdir, mkdtemp, readFile, readdir, realpath, rm, symlink, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; +import { fileURLToPath, pathToFileURL } from 'node:url'; +import { build, createLogger, createServer, version } from 'vite'; +import solid from '../dist/esm/index.mjs'; + +const repoRoot = fileURLToPath(new URL('..', import.meta.url)); +const failures = []; +const warnings = []; + +function check(name, ok, detail = '') { + console.log(`${ok ? 'PASS' : 'FAIL'} ${name}${ok || !detail ? '' : `: ${detail}`}`); + if (!ok) failures.push(name); +} + +function logger() { + const base = createLogger('warn', { allowClearScreen: false }); + return { + ...base, + warn(message, options) { + warnings.push(message); + base.warn(message, options); + }, + warnOnce(message, options) { + warnings.push(message); + base.warnOnce(message, options); + }, + }; +} + +async function write(file, content) { + await mkdir(path.dirname(file), { recursive: true }); + await writeFile(file, content); +} + +async function createFixture() { + const root = await realpath(await mkdtemp(path.join(tmpdir(), 'vite-plugin-solid-ecosystem-'))); + await write( + path.join(root, 'package.json'), + JSON.stringify({ + name: 'ecosystem-fixture', + private: true, + type: 'module', + dependencies: { 'solid-js': '*', '@solidjs/web': '*', 'fixture-solid-lib': '*' }, + }), + ); + await mkdir(path.join(root, 'node_modules')); + await mkdir(path.join(root, 'node_modules/@solidjs')); + for (const name of ['solid-js', '@solidjs/web']) { + await symlink( + await realpath(path.join(repoRoot, 'node_modules', name)), + path.join(root, 'node_modules', name), + 'dir', + ); + } + // A library that ships JSX source under the `solid` export condition. + const lib = path.join(root, 'node_modules/fixture-solid-lib'); + await write( + path.join(lib, 'package.json'), + JSON.stringify({ + name: 'fixture-solid-lib', + type: 'module', + exports: { '.': { solid: './index.jsx', default: './index.js' } }, + peerDependencies: { 'solid-js': '*', '@solidjs/web': '*' }, + }), + ); + await write(path.join(lib, 'index.jsx'), 'export const Badge = (props) => {props.children};\n'); + await write(path.join(lib, 'index.js'), 'throw new Error("the solid condition was not used");\n'); + + await write( + path.join(root, 'index.html'), + '
\n', + ); + await write( + path.join(root, 'src/App.tsx'), + [ + "import { createSignal } from 'solid-js';", + "import { Badge } from 'fixture-solid-lib';", + 'export default function App() {', + ' const [count, setCount] = createSignal(0);', + ' return
lib
;', + '}', + '', + ].join('\n'), + ); + await write( + path.join(root, 'src/entry-client.tsx'), + "import { hydrate } from '@solidjs/web';\nimport App from './App';\nhydrate(() => , document.getElementById('root')!);\n", + ); + await write( + path.join(root, 'src/entry-server.tsx'), + "import { renderToString } from '@solidjs/web';\nimport App from './App';\nexport const render = () => renderToString(() => );\n", + ); + return root; +} + +// Hydratable HTML with the app and the library component rendered on the server. +const isExpectedHtml = (html) => + typeof html === 'string' && + /^
0') && + /lib<\/b>/.test(html); + +async function main() { + console.log(`vite ${version}`); + const root = await createFixture(); + const base = { root, configFile: false, customLogger: logger() }; + try { + // Dev server: client and SSR transforms, HMR runtime, SSR rendering. + const server = await createServer({ + ...base, + appType: 'custom', + server: { middlewareMode: true, hmr: false, watch: null }, + optimizeDeps: { noDiscovery: true, include: [] }, + plugins: [solid({ ssr: true })], + }); + try { + const client = await server.transformRequest('/src/App.tsx'); + check('dev: client transform compiles JSX to DOM templates', /template/.test(client?.code ?? '')); + check('dev: client transform includes the HMR runtime', /\$\$refresh\(/.test(client?.code ?? '')); + const { render } = await server.ssrLoadModule('/src/entry-server.tsx'); + const html = render(); + check('dev: SSR renders hydratable HTML', isExpectedHtml(html), html); + } finally { + await server.close(); + } + + // Client build. + await build({ + ...base, + plugins: [solid({ ssr: true })], + build: { outDir: 'dist/client', minify: false }, + }); + const assets = path.join(root, 'dist/client/assets'); + const js = (await readdir(assets)).filter((file) => file.endsWith('.js')); + const code = (await Promise.all(js.map((file) => readFile(path.join(assets, file), 'utf8')))).join('\n'); + check('build: client bundle contains the app and the library', code.includes('badge') && code.includes('hydrate')); + check('build: client bundle has no HMR runtime', !code.includes('$$refresh(')); + + // SSR build. + await build({ + ...base, + plugins: [solid({ ssr: true })], + build: { ssr: 'src/entry-server.tsx', outDir: 'dist/server', minify: false }, + }); + const { render } = await import(pathToFileURL(path.join(root, 'dist/server/entry-server.js')).href); + const html = render(); + check('build: SSR bundle renders hydratable HTML', isExpectedHtml(html), html); + + const deprecations = warnings.filter((message) => /deprecat/i.test(message)); + check('no deprecation warnings', deprecations.length === 0, deprecations.join('\n')); + } finally { + await rm(root, { recursive: true, force: true }); + } +} + +try { + await main(); +} catch (error) { + console.error(error); + failures.push('unexpected error'); +} +if (failures.length) { + console.error(`\n${failures.length} check(s) failed`); + process.exit(1); +} +console.log('\nall checks passed');