diff --git a/lib/cmd/vite/generators.test.js b/lib/cmd/vite/generators.test.js
new file mode 100644
index 0000000..b99a20e
--- /dev/null
+++ b/lib/cmd/vite/generators.test.js
@@ -0,0 +1,192 @@
+/* eslint-env jest */
+'use strict';
+
+const fs = require('fs-extra');
+const os = require('os');
+const path = require('path');
+
+let cwdSpy, tmpDir;
+
+async function setupTmpDir(prefix) {
+ tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), prefix));
+ cwdSpy = jest.spyOn(process, 'cwd').mockReturnValue(tmpDir);
+ jest.resetModules();
+}
+
+async function cleanupTmpDir() {
+ if (cwdSpy) cwdSpy.mockRestore();
+ if (tmpDir) await fs.remove(tmpDir);
+ cwdSpy = null;
+ tmpDir = null;
+ jest.resetModules();
+}
+
+describe('generate-vite env/bootstrap/kiln generators', () => {
+ afterEach(async () => {
+ await cleanupTmpDir();
+ jest.dontMock('./generate-env-init');
+ jest.dontMock('../../config-file-helpers');
+ });
+
+ describe('generate-env-init', () => {
+ it('writes .clay/_env-init.js with hydration runtime', async () => {
+ await setupTmpDir('claycli-vite-env-');
+
+ const { generateViteEnvInit, ENV_INIT_FILE } = require('./generate-env-init');
+ const writtenPath = await generateViteEnvInit();
+ const content = await fs.readFile(ENV_INIT_FILE, 'utf8');
+
+ expect(writtenPath).toBe(ENV_INIT_FILE);
+ expect(await fs.pathExists(ENV_INIT_FILE)).toBe(true);
+ expect(content).toContain('AUTO-GENERATED');
+ expect(content).toContain('window.kiln.preloadData._envVars');
+ expect(content).toContain('window.process.env = Object.assign');
+ });
+ });
+
+ describe('generate-globals-init', () => {
+ it('returns null when global/js has no non-test files', async () => {
+ await setupTmpDir('claycli-vite-globals-empty-');
+
+ await fs.ensureDir(path.join(tmpDir, 'global', 'js'));
+ await fs.writeFile(path.join(tmpDir, 'global', 'js', 'foo.test.js'), 'module.exports = {};');
+
+ const { generateViteGlobalsInit, GLOBALS_INIT_FILE } = require('./generate-globals-init');
+ const writtenPath = await generateViteGlobalsInit();
+
+ expect(writtenPath).toBeNull();
+ expect(await fs.pathExists(GLOBALS_INIT_FILE)).toBe(false);
+ });
+
+ it('writes _globals-init.js with imports for every non-test global script', async () => {
+ await setupTmpDir('claycli-vite-globals-');
+
+ await fs.ensureDir(path.join(tmpDir, 'global', 'js'));
+ await fs.writeFile(path.join(tmpDir, 'global', 'js', 'a.js'), 'window.a = true;');
+ await fs.writeFile(path.join(tmpDir, 'global', 'js', 'b.js'), 'window.b = true;');
+ await fs.writeFile(path.join(tmpDir, 'global', 'js', 'b.test.js'), 'window.bt = true;');
+
+ const { generateViteGlobalsInit, GLOBALS_INIT_FILE } = require('./generate-globals-init');
+ const writtenPath = await generateViteGlobalsInit();
+ const content = await fs.readFile(GLOBALS_INIT_FILE, 'utf8');
+
+ expect(writtenPath).toBe(GLOBALS_INIT_FILE);
+ expect(content).toContain("import './../global/js/a.js';");
+ expect(content).toContain("import './../global/js/b.js';");
+ expect(content).not.toContain('b.test.js');
+ });
+ });
+
+ describe('generate-bootstrap', () => {
+ it('builds bootstrap with env/globals imports, sticky shim and module map', async () => {
+ await setupTmpDir('claycli-vite-bootstrap-');
+
+ await fs.ensureDir(path.join(tmpDir, '.clay'));
+ await fs.writeFile(path.join(tmpDir, '.clay', '_globals-init.js'), '// globals');
+ await fs.ensureDir(path.join(tmpDir, 'components', 'article'));
+ await fs.writeFile(path.join(tmpDir, 'components', 'article', 'client.js'), 'module.exports = function() {};');
+ await fs.ensureDir(path.join(tmpDir, 'layouts', 'homepage'));
+ await fs.writeFile(path.join(tmpDir, 'layouts', 'homepage', 'client.js'), 'module.exports = function() {};');
+
+ const generateEnvInitMock = jest.fn().mockResolvedValue(path.join(tmpDir, '.clay', '_env-init.js'));
+
+ jest.doMock('./generate-env-init', () => ({
+ generateViteEnvInit: generateEnvInitMock,
+ }));
+ jest.doMock('../../config-file-helpers', () => ({
+ getConfigValue: jest.fn().mockReturnValue(['auth:init']),
+ }));
+
+ const { generateViteBootstrap, VITE_BOOTSTRAP_FILE, VITE_BOOTSTRAP_KEY } = require('./generate-bootstrap');
+ const writtenPath = await generateViteBootstrap();
+ const content = await fs.readFile(VITE_BOOTSTRAP_FILE, 'utf8');
+
+ expect(generateEnvInitMock).toHaveBeenCalledTimes(1);
+ expect(writtenPath).toBe(VITE_BOOTSTRAP_FILE);
+ expect(VITE_BOOTSTRAP_KEY).toBe('.clay/vite-bootstrap');
+ expect(content).toContain("import './_env-init.js';");
+ expect(content).toContain("import './_globals-init.js';");
+ expect(content).toContain('window.modules = window.modules || {};');
+ expect(content).toContain('"components/article/client.js": () => import("../components/article/client.js")');
+ expect(content).toContain('"layouts/homepage/client.js": () => import("../layouts/homepage/client.js")');
+ expect(content).toContain('fired["auth:init"] = ev.detail;');
+ expect(content).toContain('mountComponentModules().catch(console.error);');
+ });
+
+ it('omits sticky shim and globals import when not configured', async () => {
+ await setupTmpDir('claycli-vite-bootstrap-min-');
+ await fs.ensureDir(path.join(tmpDir, 'components', 'article'));
+ await fs.writeFile(path.join(tmpDir, 'components', 'article', 'client.js'), 'module.exports = function() {};');
+
+ jest.doMock('./generate-env-init', () => ({
+ generateViteEnvInit: jest.fn().mockResolvedValue(path.join(tmpDir, '.clay', '_env-init.js')),
+ }));
+ jest.doMock('../../config-file-helpers', () => ({
+ getConfigValue: jest.fn().mockReturnValue(undefined),
+ }));
+
+ const { generateViteBootstrap, VITE_BOOTSTRAP_FILE } = require('./generate-bootstrap');
+
+ await generateViteBootstrap();
+ const content = await fs.readFile(VITE_BOOTSTRAP_FILE, 'utf8');
+
+ expect(content).toContain('// no global/js — skipping _globals-init');
+ expect(content).not.toContain('clayViteStickyEvents');
+ });
+ });
+
+ describe('generate-kiln-edit', () => {
+ it('builds kiln edit entry with models, kilnjs, and optional kiln plugin', async () => {
+ await setupTmpDir('claycli-vite-kiln-');
+
+ await fs.ensureDir(path.join(tmpDir, 'components', 'article'));
+ await fs.writeFile(path.join(tmpDir, 'components', 'article', 'model.js'), 'module.exports = {};');
+ await fs.writeFile(path.join(tmpDir, 'components', 'article', 'kiln.js'), 'module.exports = function() {};');
+ await fs.ensureDir(path.join(tmpDir, 'layouts', 'homepage'));
+ await fs.writeFile(path.join(tmpDir, 'layouts', 'homepage', 'model.js'), 'module.exports = {};');
+ await fs.ensureDir(path.join(tmpDir, 'services', 'kiln'));
+ await fs.writeFile(path.join(tmpDir, 'services', 'kiln', 'index.js'), 'module.exports = function() {};');
+
+ const generateEnvInitMock = jest.fn().mockResolvedValue(path.join(tmpDir, '.clay', '_env-init.js'));
+
+ jest.doMock('./generate-env-init', () => ({
+ generateViteEnvInit: generateEnvInitMock,
+ }));
+
+ const { generateViteKilnEditEntry, KILN_EDIT_ENTRY_FILE, KILN_EDIT_ENTRY_KEY } = require('./generate-kiln-edit');
+ const writtenPath = await generateViteKilnEditEntry();
+ const content = await fs.readFile(KILN_EDIT_ENTRY_FILE, 'utf8');
+
+ expect(generateEnvInitMock).toHaveBeenCalledTimes(1);
+ expect(writtenPath).toBe(KILN_EDIT_ENTRY_FILE);
+ expect(KILN_EDIT_ENTRY_KEY).toBe('.clay/vite-kiln-edit-init');
+ expect(content).toContain('import \'./_env-init.js\';');
+ expect(content).toContain('import * as _m0 from "../components/article/model.js";');
+ expect(content).toContain('import * as _m1 from "../layouts/homepage/model.js";');
+ expect(content).toContain('import * as _k0 from "../components/article/kiln.js";');
+ expect(content).toContain('import * as _kilnPluginNs from "../services/kiln/index.js";');
+ expect(content).toContain('window.kiln.componentModels["article"] = _resolveDefault(_m0);');
+ expect(content).toContain('window.kiln.componentKilnjs["article"] = _resolveDefault(_k0);');
+ expect(content).toContain('if (typeof _initKilnPlugins === "function") _initKilnPlugins();');
+ });
+
+ it('does not import kiln plugin when services/kiln/index.js is missing', async () => {
+ await setupTmpDir('claycli-vite-kiln-noplugin-');
+
+ await fs.ensureDir(path.join(tmpDir, 'components', 'article'));
+ await fs.writeFile(path.join(tmpDir, 'components', 'article', 'model.js'), 'module.exports = {};');
+
+ jest.doMock('./generate-env-init', () => ({
+ generateViteEnvInit: jest.fn().mockResolvedValue(path.join(tmpDir, '.clay', '_env-init.js')),
+ }));
+
+ const { generateViteKilnEditEntry, KILN_EDIT_ENTRY_FILE } = require('./generate-kiln-edit');
+
+ await generateViteKilnEditEntry();
+ const content = await fs.readFile(KILN_EDIT_ENTRY_FILE, 'utf8');
+
+ expect(content).not.toContain('_kilnPluginNs');
+ expect(content).not.toContain('_initKilnPlugins');
+ });
+ });
+});
diff --git a/lib/cmd/vite/plugins/browser-compat.test.js b/lib/cmd/vite/plugins/browser-compat.test.js
index 0354d50..c78b1f8 100644
--- a/lib/cmd/vite/plugins/browser-compat.test.js
+++ b/lib/cmd/vite/plugins/browser-compat.test.js
@@ -2,6 +2,9 @@
'use strict';
+const fs = require('fs-extra');
+const os = require('os');
+const path = require('path');
const viteBrowserCompatPlugin = require('./browser-compat');
// ── helpers ───────────────────────────────────────────────────────────────────
@@ -196,6 +199,71 @@ describe('viteBrowserCompatPlugin', () => {
});
});
+ describe('browser field false mappings', () => {
+ let tmpDir;
+
+ beforeEach(async () => {
+ tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'clay-browser-field-'));
+ });
+
+ afterEach(async () => {
+ if (tmpDir) await fs.remove(tmpDir);
+ tmpDir = null;
+ });
+
+ it('stubs bare imports mapped to false in package.json browser field', async () => {
+ const pkgDir = path.join(tmpDir, 'pkg');
+ const importer = path.join(pkgDir, 'lib', 'index.js');
+
+ await fs.ensureDir(path.dirname(importer));
+ await fs.writeFile(path.join(pkgDir, 'package.json'), JSON.stringify({
+ name: 'pkg',
+ browser: { fs: false },
+ }));
+ await fs.writeFile(importer, '// importer');
+
+ const plugin = viteBrowserCompatPlugin();
+ const resolved = plugin.resolveId('fs', importer);
+
+ expect(resolved).toContain('simple:fs');
+ expect(plugin.load(resolved)).toBe('export default {}; export {};');
+ });
+
+ it('stubs relative imports mapped to false in package.json browser field', async () => {
+ const pkgDir = path.join(tmpDir, 'pkg');
+ const importer = path.join(pkgDir, 'lib', 'css-syntax-error.js');
+
+ await fs.ensureDir(path.join(pkgDir, 'lib'));
+ await fs.writeFile(path.join(pkgDir, 'package.json'), JSON.stringify({
+ name: 'pkg',
+ browser: { './lib/terminal-highlight': false },
+ }));
+ await fs.writeFile(importer, '// importer');
+
+ const plugin = viteBrowserCompatPlugin();
+ const resolved = plugin.resolveId('./terminal-highlight', importer);
+
+ expect(resolved).toContain('simple:./terminal-highlight');
+ expect(plugin.load(resolved)).toBe('export default {}; export {};');
+ });
+ });
+
+ describe('lenient externalize mode', () => {
+ it('replaces Vite browser-external proxy ids with empty module when enabled', () => {
+ const plugin = viteBrowserCompatPlugin({}, { lenientExternalize: true });
+
+ expect(plugin.load('__vite-browser-external')).toBe('export default {}; export {};');
+ expect(plugin.load('__vite-browser-external:fs')).toBe('export default {}; export {};');
+ });
+
+ it('does not intercept Vite browser-external ids when disabled', () => {
+ const plugin = viteBrowserCompatPlugin({}, { lenientExternalize: false });
+
+ expect(plugin.load('__vite-browser-external')).toBeNull();
+ expect(plugin.load('__vite-browser-external:fs')).toBeNull();
+ });
+ });
+
describe('plugin metadata', () => {
it('has the correct plugin name', () => {
const plugin = viteBrowserCompatPlugin();
diff --git a/lib/cmd/vite/plugins/vue2.test.js b/lib/cmd/vite/plugins/vue2.test.js
new file mode 100644
index 0000000..d6bc5f7
--- /dev/null
+++ b/lib/cmd/vite/plugins/vue2.test.js
@@ -0,0 +1,122 @@
+/* eslint-env jest */
+'use strict';
+
+const fs = require('fs-extra');
+const os = require('os');
+const path = require('path');
+
+let tmpDir, cwdSpy;
+
+async function setupTmp(prefix) {
+ tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), prefix));
+ cwdSpy = jest.spyOn(process, 'cwd').mockReturnValue(tmpDir);
+ jest.resetModules();
+}
+
+async function cleanupTmp() {
+ if (cwdSpy) cwdSpy.mockRestore();
+ if (tmpDir) await fs.remove(tmpDir);
+ cwdSpy = null;
+ tmpDir = null;
+ jest.resetModules();
+}
+
+describe('vite vue2 plugin', () => {
+ afterEach(async () => {
+ await cleanupTmp();
+ jest.dontMock('@vue/component-compiler-utils');
+ jest.dontMock('vue-template-compiler');
+ });
+
+ it('returns null for non-vue files', async () => {
+ await setupTmp('claycli-vue2-nonvue-');
+
+ const pluginFactory = require('./vue2');
+ const plugin = pluginFactory();
+ const result = await plugin.transform.call({ warn: jest.fn(), error: jest.fn() }, 'const x = 1;', '/tmp/a.js');
+
+ expect(result).toBeNull();
+ });
+
+ it('transforms vue SFC and writes kiln css on closeBundle', async () => {
+ await setupTmp('claycli-vue2-transform-');
+
+ const vuePath = path.join(tmpDir, 'components', 'foo', 'client.vue');
+
+ await fs.ensureDir(path.dirname(vuePath));
+ await fs.writeFile(vuePath, [
+ 'hello
',
+ '',
+ '',
+ ].join('\n'));
+
+ const pluginFactory = require('./vue2');
+ const plugin = pluginFactory();
+ const ctx = {
+ warn: jest.fn(),
+ error: jest.fn((msg) => {
+ throw new Error(msg);
+ }),
+ };
+
+ const out = await plugin.transform.call(ctx, '', vuePath);
+
+ expect(out).toBeTruthy();
+ expect(out.code).toContain('const __sfc__ =');
+ expect(out.code).toContain('__sfc__.render = render;');
+ expect(out.code).toContain('__sfc__._scopeId');
+ expect(out.code).toContain('document.createElement("style")');
+ expect(out.code).toContain('export default __sfc__;');
+
+ await plugin.closeBundle();
+
+ const kilnCssPath = path.join(tmpDir, 'public', 'css', '_kiln-plugins.css');
+ const kilnCss = await fs.readFile(kilnCssPath, 'utf8');
+
+ expect(kilnCss).toContain('.foo');
+ });
+
+ it('warns and returns null if vue file cannot be read', async () => {
+ await setupTmp('claycli-vue2-missing-file-');
+
+ const pluginFactory = require('./vue2');
+ const plugin = pluginFactory();
+ const ctx = {
+ warn: jest.fn(),
+ error: jest.fn((msg) => {
+ throw new Error(msg);
+ }),
+ };
+
+ const result = await plugin.transform.call(ctx, '', path.join(tmpDir, 'missing.vue'));
+
+ expect(result).toBeNull();
+ expect(ctx.warn).toHaveBeenCalled();
+ });
+
+ it('errors with install guidance when vue compilers are unavailable', async () => {
+ await setupTmp('claycli-vue2-missing-compiler-');
+
+ await fs.ensureDir(path.join(tmpDir, 'components', 'foo'));
+ const vuePath = path.join(tmpDir, 'components', 'foo', 'client.vue');
+
+ await fs.writeFile(vuePath, '');
+
+ jest.doMock('@vue/component-compiler-utils', () => {
+ throw new Error('module missing');
+ });
+
+ const pluginFactory = require('./vue2');
+ const plugin = pluginFactory();
+ const ctx = {
+ warn: jest.fn(),
+ error: jest.fn((msg) => {
+ throw new Error(msg);
+ }),
+ };
+
+ await expect(plugin.transform.call(ctx, '', vuePath)).rejects.toThrow(
+ /Vue 2 SFC support requires @vue\/component-compiler-utils and vue-template-compiler/
+ );
+ });
+});
diff --git a/lib/cmd/vite/scripts.test.js b/lib/cmd/vite/scripts.test.js
new file mode 100644
index 0000000..21a1eec
--- /dev/null
+++ b/lib/cmd/vite/scripts.test.js
@@ -0,0 +1,636 @@
+/* eslint-env jest */
+'use strict';
+
+const fs = require('fs-extra');
+const os = require('os');
+const path = require('path');
+
+let tmpDir, cwdSpy;
+
+async function setupTmp(prefix) {
+ tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), prefix));
+ cwdSpy = jest.spyOn(process, 'cwd').mockReturnValue(tmpDir);
+ jest.resetModules();
+}
+
+async function cleanupTmp() {
+ if (cwdSpy) cwdSpy.mockRestore();
+ if (tmpDir) await fs.remove(tmpDir);
+ cwdSpy = null;
+ tmpDir = null;
+ jest.resetModules();
+}
+
+function setStdoutTTY(value) {
+ const previous = Object.getOwnPropertyDescriptor(process.stdout, 'isTTY');
+
+ Object.defineProperty(process.stdout, 'isTTY', {
+ configurable: true,
+ value,
+ });
+
+ return () => {
+ if (previous) {
+ Object.defineProperty(process.stdout, 'isTTY', previous);
+ } else {
+ delete process.stdout.isTTY;
+ }
+ };
+}
+
+describe('vite scripts', () => {
+ afterEach(async () => {
+ jest.useRealTimers();
+ await cleanupTmp();
+ jest.dontMock('vite');
+ jest.dontMock('./generate-bootstrap');
+ jest.dontMock('./generate-kiln-edit');
+ jest.dontMock('./generate-globals-init');
+ jest.dontMock('./plugins/client-env');
+ jest.dontMock('./plugins/browser-compat');
+ jest.dontMock('./plugins/service-rewrite');
+ jest.dontMock('./plugins/missing-module');
+ jest.dontMock('./plugins/vue2');
+ jest.dontMock('./plugins/manual-chunks');
+ jest.dontMock('./styles');
+ jest.dontMock('./fonts');
+ jest.dontMock('./templates');
+ jest.dontMock('./vendor');
+ jest.dontMock('./media');
+ jest.dontMock('chokidar');
+ jest.dontMock('../../config-file-helpers');
+ });
+
+ it('getViteConfig merges bundlerConfig customizer output', async () => {
+ await setupTmp('claycli-vite-scripts-config-');
+
+ jest.doMock('../../config-file-helpers', () => ({
+ getConfigValue: jest.fn().mockImplementation(key => {
+ if (key !== 'bundlerConfig') return undefined;
+ return (cfg) => ({ ...cfg, minify: true, kilnSplit: true, extraEntries: ['x.js'] });
+ }),
+ }));
+
+ const { getViteConfig } = require('./scripts');
+ const cfg = getViteConfig({ minify: false });
+
+ expect(cfg.minify).toBe(true);
+ expect(cfg.kilnSplit).toBe(true);
+ expect(cfg.extraEntries).toEqual(['x.js']);
+ expect(cfg.sourcemap).toBe(true);
+ });
+
+ it('buildJS runs split builds, writes manifest and client-env', async () => {
+ await setupTmp('claycli-vite-scripts-build-');
+
+ const clayDir = path.join(tmpDir, '.clay');
+ const destDir = path.join(tmpDir, 'public', 'js');
+ const bootstrapFile = path.join(clayDir, 'vite-bootstrap.js');
+ const kilnFile = path.join(clayDir, 'vite-kiln-edit-init.js');
+ const envFile = path.join(tmpDir, 'client-env.json');
+
+ const viewResult = {
+ output: [
+ {
+ type: 'chunk',
+ isEntry: true,
+ facadeModuleId: bootstrapFile,
+ fileName: '.clay/vite-bootstrap-a1b2c3d4.js',
+ imports: ['chunks/shared-111.js'],
+ },
+ ],
+ };
+ const kilnResult = {
+ output: [
+ {
+ type: 'chunk',
+ isEntry: true,
+ facadeModuleId: kilnFile,
+ fileName: '.clay/vite-kiln-edit-init-z9y8x7w6.js',
+ imports: [],
+ },
+ ],
+ };
+
+ const viteBuild = jest.fn()
+ .mockResolvedValueOnce(viewResult)
+ .mockResolvedValueOnce(kilnResult);
+ const envCollector = {
+ plugin: jest.fn().mockReturnValue({ name: 'env-collector' }),
+ write: jest.fn().mockResolvedValue(undefined),
+ };
+
+ jest.doMock('vite', () => ({ build: viteBuild }));
+ jest.doMock('./plugins/client-env', () => ({
+ createClientEnvCollector: jest.fn().mockReturnValue(envCollector),
+ }));
+ jest.doMock('./generate-globals-init', () => ({
+ generateViteGlobalsInit: jest.fn().mockResolvedValue(null),
+ }));
+ jest.doMock('./generate-kiln-edit', () => ({
+ generateViteKilnEditEntry: jest.fn().mockResolvedValue(kilnFile),
+ KILN_EDIT_ENTRY_FILE: kilnFile,
+ KILN_EDIT_ENTRY_KEY: '.clay/vite-kiln-edit-init',
+ }));
+ jest.doMock('./generate-bootstrap', () => ({
+ generateViteBootstrap: jest.fn().mockImplementation(async () => {
+ await fs.ensureDir(clayDir);
+ await fs.writeFile(bootstrapFile, '// bootstrap');
+ return bootstrapFile;
+ }),
+ VITE_BOOTSTRAP_FILE: bootstrapFile,
+ VITE_BOOTSTRAP_KEY: '.clay/vite-bootstrap',
+ }));
+ jest.doMock('./plugins/browser-compat', () => jest.fn(() => ({ name: 'browser-compat' })));
+ jest.doMock('./plugins/service-rewrite', () => jest.fn(() => ({ name: 'service-rewrite' })));
+ jest.doMock('./plugins/missing-module', () => jest.fn(() => ({ name: 'missing-module' })));
+ jest.doMock('./plugins/vue2', () => jest.fn(() => ({ name: 'vue2' })));
+ jest.doMock('./plugins/manual-chunks', () => jest.fn(() => 'manual-chunks'));
+ jest.doMock('./styles', () => ({ buildStyles: jest.fn(), SRC_GLOBS: [] }));
+ jest.doMock('./fonts', () => ({ buildFonts: jest.fn(), FONTS_SRC_GLOB: '' }));
+ jest.doMock('./templates', () => ({ buildTemplates: jest.fn(), TEMPLATE_GLOB_PATTERN: '*.hbs' }));
+ jest.doMock('./vendor', () => ({ copyVendor: jest.fn() }));
+ jest.doMock('./media', () => ({ copyMedia: jest.fn() }));
+ jest.doMock('../../config-file-helpers', () => ({
+ getConfigValue: jest.fn().mockReturnValue(undefined),
+ }));
+
+ const { buildJS } = require('./scripts');
+
+ await buildJS();
+
+ const manifestPath = path.join(destDir, '_manifest.json');
+ const manifest = await fs.readJson(manifestPath);
+
+ expect(viteBuild).toHaveBeenCalledTimes(2);
+ expect(envCollector.plugin).toHaveBeenCalledTimes(1);
+ expect(envCollector.write).toHaveBeenCalledTimes(1);
+ expect(path.dirname(envFile)).toBe(tmpDir);
+ expect(manifest['.clay/vite-bootstrap'].file).toBe('/js/.clay/vite-bootstrap-a1b2c3d4.js');
+ expect(manifest['.clay/vite-bootstrap'].imports).toEqual(['/js/chunks/shared-111.js']);
+ expect(manifest['.clay/vite-kiln-edit-init'].file).toBe('/js/.clay/vite-kiln-edit-init-z9y8x7w6.js');
+ });
+
+ it('buildAll runs media first then parallel asset steps', async () => {
+ await setupTmp('claycli-vite-scripts-buildall-');
+
+ const order = [];
+
+ jest.doMock('./media', () => ({
+ copyMedia: jest.fn().mockImplementation(async () => {
+ order.push('media');
+ }),
+ }));
+ jest.doMock('./styles', () => ({
+ buildStyles: jest.fn().mockImplementation(async () => {
+ order.push('styles');
+ }),
+ SRC_GLOBS: [],
+ }));
+ jest.doMock('./fonts', () => ({
+ buildFonts: jest.fn().mockImplementation(async () => {
+ order.push('fonts');
+ }),
+ FONTS_SRC_GLOB: '',
+ }));
+ jest.doMock('./templates', () => ({
+ buildTemplates: jest.fn().mockImplementation(async () => {
+ order.push('templates');
+ }),
+ TEMPLATE_GLOB_PATTERN: '*.hbs',
+ }));
+ jest.doMock('./vendor', () => ({
+ copyVendor: jest.fn().mockImplementation(async () => {
+ order.push('vendor');
+ }),
+ }));
+ jest.doMock('vite', () => ({
+ build: jest.fn().mockResolvedValue({ output: [] }),
+ }));
+ jest.doMock('./plugins/client-env', () => ({
+ createClientEnvCollector: jest.fn().mockReturnValue({
+ plugin: jest.fn().mockReturnValue({ name: 'env-collector' }),
+ write: jest.fn().mockResolvedValue(undefined),
+ }),
+ }));
+ jest.doMock('./generate-globals-init', () => ({
+ generateViteGlobalsInit: jest.fn().mockResolvedValue(null),
+ }));
+ jest.doMock('./generate-kiln-edit', () => ({
+ generateViteKilnEditEntry: jest.fn().mockResolvedValue(path.join(tmpDir, '.clay', 'vite-kiln-edit-init.js')),
+ KILN_EDIT_ENTRY_FILE: path.join(tmpDir, '.clay', 'vite-kiln-edit-init.js'),
+ KILN_EDIT_ENTRY_KEY: '.clay/vite-kiln-edit-init',
+ }));
+ jest.doMock('./generate-bootstrap', () => ({
+ generateViteBootstrap: jest.fn().mockImplementation(async () => {
+ const file = path.join(tmpDir, '.clay', 'vite-bootstrap.js');
+
+ await fs.ensureDir(path.dirname(file));
+ await fs.writeFile(file, '// bootstrap');
+ return file;
+ }),
+ VITE_BOOTSTRAP_FILE: path.join(tmpDir, '.clay', 'vite-bootstrap.js'),
+ VITE_BOOTSTRAP_KEY: '.clay/vite-bootstrap',
+ }));
+ jest.doMock('./plugins/browser-compat', () => jest.fn(() => ({ name: 'browser-compat' })));
+ jest.doMock('./plugins/service-rewrite', () => jest.fn(() => ({ name: 'service-rewrite' })));
+ jest.doMock('./plugins/missing-module', () => jest.fn(() => ({ name: 'missing-module' })));
+ jest.doMock('./plugins/vue2', () => jest.fn(() => ({ name: 'vue2' })));
+ jest.doMock('./plugins/manual-chunks', () => jest.fn(() => 'manual-chunks'));
+ jest.doMock('../../config-file-helpers', () => ({
+ getConfigValue: jest.fn().mockReturnValue(undefined),
+ }));
+
+ const restoreTTY = setStdoutTTY(false);
+ const { buildAll } = require('./scripts');
+
+ await buildAll();
+
+ restoreTTY();
+ expect(order[0]).toBe('media');
+ expect(order).toEqual(expect.arrayContaining(['styles', 'fonts', 'templates', 'vendor']));
+ });
+
+ it('buildJS supports kilnSplit single-pass mode with extra entries', async () => {
+ await setupTmp('claycli-vite-scripts-kilnsplit-');
+
+ const clayDir = path.join(tmpDir, '.clay');
+ const destDir = path.join(tmpDir, 'public', 'js');
+ const bootstrapFile = path.join(clayDir, 'vite-bootstrap.js');
+ const kilnFile = path.join(clayDir, 'vite-kiln-edit-init.js');
+ const extraFile = path.join(tmpDir, 'components', 'foo', 'client.js');
+
+ await fs.ensureDir(path.dirname(extraFile));
+ await fs.writeFile(extraFile, 'module.exports = function() {};');
+
+ const viewResult = {
+ output: [
+ {
+ type: 'chunk',
+ isEntry: true,
+ facadeModuleId: bootstrapFile,
+ fileName: '.clay/vite-bootstrap-aa.js',
+ imports: [],
+ },
+ {
+ type: 'chunk',
+ isEntry: true,
+ facadeModuleId: kilnFile,
+ fileName: '.clay/vite-kiln-edit-init-bb.js',
+ imports: [],
+ },
+ {
+ type: 'chunk',
+ isEntry: true,
+ facadeModuleId: extraFile,
+ fileName: 'components/foo/client-cc.js',
+ imports: [],
+ },
+ ],
+ };
+
+ const viteBuild = jest.fn().mockResolvedValue(viewResult);
+ const envCollector = {
+ plugin: jest.fn().mockReturnValue({ name: 'env-collector' }),
+ write: jest.fn().mockResolvedValue(undefined),
+ };
+
+ jest.doMock('vite', () => ({ build: viteBuild }));
+ jest.doMock('./plugins/client-env', () => ({
+ createClientEnvCollector: jest.fn().mockReturnValue(envCollector),
+ }));
+ jest.doMock('./generate-globals-init', () => ({
+ generateViteGlobalsInit: jest.fn().mockResolvedValue(null),
+ }));
+ jest.doMock('./generate-kiln-edit', () => ({
+ generateViteKilnEditEntry: jest.fn().mockResolvedValue(kilnFile),
+ KILN_EDIT_ENTRY_FILE: kilnFile,
+ KILN_EDIT_ENTRY_KEY: '.clay/vite-kiln-edit-init',
+ }));
+ jest.doMock('./generate-bootstrap', () => ({
+ generateViteBootstrap: jest.fn().mockImplementation(async () => {
+ await fs.ensureDir(clayDir);
+ await fs.writeFile(bootstrapFile, '// bootstrap');
+ return bootstrapFile;
+ }),
+ VITE_BOOTSTRAP_FILE: bootstrapFile,
+ VITE_BOOTSTRAP_KEY: '.clay/vite-bootstrap',
+ }));
+ jest.doMock('./plugins/browser-compat', () => jest.fn(() => ({ name: 'browser-compat' })));
+ jest.doMock('./plugins/service-rewrite', () => jest.fn(() => ({ name: 'service-rewrite' })));
+ jest.doMock('./plugins/missing-module', () => jest.fn(() => ({ name: 'missing-module' })));
+ jest.doMock('./plugins/vue2', () => jest.fn(() => ({ name: 'vue2' })));
+ jest.doMock('./plugins/manual-chunks', () => jest.fn(() => 'manual-chunks'));
+ jest.doMock('./styles', () => ({ buildStyles: jest.fn(), SRC_GLOBS: [] }));
+ jest.doMock('./fonts', () => ({ buildFonts: jest.fn(), FONTS_SRC_GLOB: '' }));
+ jest.doMock('./templates', () => ({ buildTemplates: jest.fn(), TEMPLATE_GLOB_PATTERN: '*.hbs' }));
+ jest.doMock('./vendor', () => ({ copyVendor: jest.fn() }));
+ jest.doMock('./media', () => ({ copyMedia: jest.fn() }));
+ jest.doMock('../../config-file-helpers', () => ({
+ getConfigValue: jest.fn().mockImplementation(key => {
+ if (key !== 'bundlerConfig') return undefined;
+ return cfg => ({ ...cfg, kilnSplit: true, extraEntries: [extraFile] });
+ }),
+ }));
+
+ const { buildJS } = require('./scripts');
+
+ await buildJS();
+
+ const manifest = await fs.readJson(path.join(destDir, '_manifest.json'));
+
+ expect(viteBuild).toHaveBeenCalledTimes(1);
+ expect(manifest['.clay/vite-bootstrap'].file).toBe('/js/.clay/vite-bootstrap-aa.js');
+ expect(manifest['.clay/vite-kiln-edit-init'].file).toBe('/js/.clay/vite-kiln-edit-init-bb.js');
+ expect(manifest['components/foo/client'].file).toBe('/js/components/foo/client-cc.js');
+ });
+
+ it('buildJS fails clearly when bootstrap file was not generated', async () => {
+ await setupTmp('claycli-vite-scripts-missing-bootstrap-');
+
+ const clayDir = path.join(tmpDir, '.clay');
+ const bootstrapFile = path.join(clayDir, 'vite-bootstrap.js');
+ const kilnFile = path.join(clayDir, 'vite-kiln-edit-init.js');
+
+ jest.doMock('vite', () => ({ build: jest.fn() }));
+ jest.doMock('./plugins/client-env', () => ({
+ createClientEnvCollector: jest.fn().mockReturnValue({
+ plugin: jest.fn().mockReturnValue({ name: 'env-collector' }),
+ write: jest.fn().mockResolvedValue(undefined),
+ }),
+ }));
+ jest.doMock('./generate-globals-init', () => ({
+ generateViteGlobalsInit: jest.fn().mockResolvedValue(null),
+ }));
+ jest.doMock('./generate-kiln-edit', () => ({
+ generateViteKilnEditEntry: jest.fn().mockResolvedValue(kilnFile),
+ KILN_EDIT_ENTRY_FILE: kilnFile,
+ KILN_EDIT_ENTRY_KEY: '.clay/vite-kiln-edit-init',
+ }));
+ jest.doMock('./generate-bootstrap', () => ({
+ generateViteBootstrap: jest.fn().mockResolvedValue(bootstrapFile),
+ VITE_BOOTSTRAP_FILE: bootstrapFile,
+ VITE_BOOTSTRAP_KEY: '.clay/vite-bootstrap',
+ }));
+ jest.doMock('./plugins/browser-compat', () => jest.fn(() => ({ name: 'browser-compat' })));
+ jest.doMock('./plugins/service-rewrite', () => jest.fn(() => ({ name: 'service-rewrite' })));
+ jest.doMock('./plugins/missing-module', () => jest.fn(() => ({ name: 'missing-module' })));
+ jest.doMock('./plugins/vue2', () => jest.fn(() => ({ name: 'vue2' })));
+ jest.doMock('./plugins/manual-chunks', () => jest.fn(() => 'manual-chunks'));
+ jest.doMock('./styles', () => ({ buildStyles: jest.fn(), SRC_GLOBS: [] }));
+ jest.doMock('./fonts', () => ({ buildFonts: jest.fn(), FONTS_SRC_GLOB: '' }));
+ jest.doMock('./templates', () => ({ buildTemplates: jest.fn(), TEMPLATE_GLOB_PATTERN: '*.hbs' }));
+ jest.doMock('./vendor', () => ({ copyVendor: jest.fn() }));
+ jest.doMock('./media', () => ({ copyMedia: jest.fn() }));
+ jest.doMock('../../config-file-helpers', () => ({
+ getConfigValue: jest.fn().mockReturnValue(undefined),
+ }));
+
+ const { buildJS } = require('./scripts');
+
+ await expect(buildJS()).rejects.toThrow('clay vite: missing .clay/vite-bootstrap.js after prepare.');
+ });
+
+ it('buildAll reports failed steps with aggregate error', async () => {
+ await setupTmp('claycli-vite-scripts-buildall-fail-');
+
+ jest.doMock('./media', () => ({ copyMedia: jest.fn().mockResolvedValue(undefined) }));
+ jest.doMock('./styles', () => ({
+ buildStyles: jest.fn().mockRejectedValue(new Error('styles failed')),
+ SRC_GLOBS: [],
+ }));
+ jest.doMock('./fonts', () => ({
+ buildFonts: jest.fn().mockResolvedValue(undefined),
+ FONTS_SRC_GLOB: '',
+ }));
+ jest.doMock('./templates', () => ({
+ buildTemplates: jest.fn().mockResolvedValue(undefined),
+ TEMPLATE_GLOB_PATTERN: '*.hbs',
+ }));
+ jest.doMock('./vendor', () => ({ copyVendor: jest.fn().mockResolvedValue(undefined) }));
+ jest.doMock('vite', () => ({
+ build: jest.fn().mockResolvedValue({ output: [] }),
+ }));
+ jest.doMock('./plugins/client-env', () => ({
+ createClientEnvCollector: jest.fn().mockReturnValue({
+ plugin: jest.fn().mockReturnValue({ name: 'env-collector' }),
+ write: jest.fn().mockResolvedValue(undefined),
+ }),
+ }));
+ jest.doMock('./generate-globals-init', () => ({
+ generateViteGlobalsInit: jest.fn().mockResolvedValue(null),
+ }));
+ jest.doMock('./generate-kiln-edit', () => ({
+ generateViteKilnEditEntry: jest.fn().mockResolvedValue(path.join(tmpDir, '.clay', 'vite-kiln-edit-init.js')),
+ KILN_EDIT_ENTRY_FILE: path.join(tmpDir, '.clay', 'vite-kiln-edit-init.js'),
+ KILN_EDIT_ENTRY_KEY: '.clay/vite-kiln-edit-init',
+ }));
+ jest.doMock('./generate-bootstrap', () => ({
+ generateViteBootstrap: jest.fn().mockImplementation(async () => {
+ const file = path.join(tmpDir, '.clay', 'vite-bootstrap.js');
+
+ await fs.ensureDir(path.dirname(file));
+ await fs.writeFile(file, '// bootstrap');
+ return file;
+ }),
+ VITE_BOOTSTRAP_FILE: path.join(tmpDir, '.clay', 'vite-bootstrap.js'),
+ VITE_BOOTSTRAP_KEY: '.clay/vite-bootstrap',
+ }));
+ jest.doMock('./plugins/browser-compat', () => jest.fn(() => ({ name: 'browser-compat' })));
+ jest.doMock('./plugins/service-rewrite', () => jest.fn(() => ({ name: 'service-rewrite' })));
+ jest.doMock('./plugins/missing-module', () => jest.fn(() => ({ name: 'missing-module' })));
+ jest.doMock('./plugins/vue2', () => jest.fn(() => ({ name: 'vue2' })));
+ jest.doMock('./plugins/manual-chunks', () => jest.fn(() => 'manual-chunks'));
+ jest.doMock('../../config-file-helpers', () => ({
+ getConfigValue: jest.fn().mockReturnValue(undefined),
+ }));
+
+ const restoreTTY = setStdoutTTY(false);
+ const { buildAll } = require('./scripts');
+
+ await expect(buildAll()).rejects.toThrow(/Build failed: 1 step\(s\) failed — styles failed/);
+ restoreTTY();
+ });
+
+ it('watch wires rollup/chokidar and dispose closes all watchers', async () => {
+ await setupTmp('claycli-vite-scripts-watch-');
+
+ const jsRollupHandlers = {};
+ const kilnRollupHandlers = {};
+ const chokidarWatchers = [];
+ const rollupWatchers = [];
+
+ function createRollupWatcher(handlerBag) {
+ const watcher = {
+ on: jest.fn((evt, cb) => {
+ handlerBag[evt] = cb;
+ return watcher;
+ }),
+ close: jest.fn(),
+ };
+
+ return watcher;
+ }
+
+ function createChokidarWatcher() {
+ const handlers = {};
+ const watcher = {
+ on: jest.fn((evt, cb) => {
+ handlers[evt] = cb;
+ return watcher;
+ }),
+ once: jest.fn((evt, cb) => {
+ if (evt === 'ready') setImmediate(cb);
+ return watcher;
+ }),
+ close: jest.fn().mockResolvedValue(undefined),
+ };
+
+ watcher.__handlers = handlers;
+ chokidarWatchers.push(watcher);
+ return watcher;
+ }
+
+ const viteBuild = jest.fn().mockImplementation(async () => {
+ const watcher = rollupWatchers.length === 0
+ ? createRollupWatcher(kilnRollupHandlers)
+ : createRollupWatcher(jsRollupHandlers);
+
+ rollupWatchers.push(watcher);
+ return watcher;
+ });
+
+ jest.doMock('vite', () => ({ build: viteBuild }));
+ jest.doMock('chokidar', () => ({
+ watch: jest.fn(() => createChokidarWatcher()),
+ }));
+ jest.doMock('./plugins/client-env', () => ({
+ createClientEnvCollector: jest.fn().mockReturnValue({
+ plugin: jest.fn().mockReturnValue({ name: 'env-collector' }),
+ write: jest.fn().mockResolvedValue(undefined),
+ }),
+ }));
+ jest.doMock('./generate-globals-init', () => ({
+ generateViteGlobalsInit: jest.fn().mockResolvedValue(null),
+ }));
+ jest.doMock('./generate-kiln-edit', () => ({
+ generateViteKilnEditEntry: jest.fn().mockResolvedValue(path.join(tmpDir, '.clay', 'vite-kiln-edit-init.js')),
+ KILN_EDIT_ENTRY_FILE: path.join(tmpDir, '.clay', 'vite-kiln-edit-init.js'),
+ KILN_EDIT_ENTRY_KEY: '.clay/vite-kiln-edit-init',
+ }));
+ jest.doMock('./generate-bootstrap', () => ({
+ generateViteBootstrap: jest.fn().mockImplementation(async () => {
+ const file = path.join(tmpDir, '.clay', 'vite-bootstrap.js');
+
+ await fs.ensureDir(path.dirname(file));
+ await fs.writeFile(file, '// bootstrap');
+ return file;
+ }),
+ VITE_BOOTSTRAP_FILE: path.join(tmpDir, '.clay', 'vite-bootstrap.js'),
+ VITE_BOOTSTRAP_KEY: '.clay/vite-bootstrap',
+ }));
+ jest.doMock('./plugins/browser-compat', () => jest.fn(() => ({ name: 'browser-compat' })));
+ jest.doMock('./plugins/service-rewrite', () => jest.fn(() => ({ name: 'service-rewrite' })));
+ jest.doMock('./plugins/missing-module', () => jest.fn(() => ({ name: 'missing-module' })));
+ jest.doMock('./plugins/vue2', () => jest.fn(() => ({ name: 'vue2' })));
+ jest.doMock('./plugins/manual-chunks', () => jest.fn(() => 'manual-chunks'));
+ const buildStylesMock = jest.fn().mockResolvedValue(undefined);
+ const buildFontsMock = jest.fn().mockResolvedValue(undefined);
+ const buildTemplatesMock = jest.fn().mockResolvedValue(undefined);
+
+ jest.doMock('./styles', () => ({ buildStyles: buildStylesMock, SRC_GLOBS: [] }));
+ jest.doMock('./fonts', () => ({ buildFonts: buildFontsMock, FONTS_SRC_GLOB: '' }));
+ jest.doMock('./templates', () => ({ buildTemplates: buildTemplatesMock, TEMPLATE_GLOB_PATTERN: '*.hbs' }));
+ jest.doMock('./vendor', () => ({ copyVendor: jest.fn().mockResolvedValue(undefined) }));
+ jest.doMock('./media', () => ({ copyMedia: jest.fn().mockResolvedValue(undefined) }));
+ jest.doMock('../../config-file-helpers', () => ({
+ getConfigValue: jest.fn().mockReturnValue(undefined),
+ }));
+
+ const restoreTTY = setStdoutTTY(false);
+ const onRebuild = jest.fn();
+ const onReady = jest.fn();
+ const { watch } = require('./scripts');
+ const session = await watch({ onRebuild, onReady });
+
+ expect(viteBuild).toHaveBeenCalledTimes(2);
+ expect(jsRollupHandlers.event).toBeDefined();
+ expect(kilnRollupHandlers.event).toBeDefined();
+
+ const kilnCfg = viteBuild.mock.calls[0][0];
+ const jsCfg = viteBuild.mock.calls[1][0];
+ const kilnCapture = kilnCfg.plugins.find(p => p.name === 'clay-capture-kiln-output');
+ const jsCapture = jsCfg.plugins.find(p => p.name === 'clay-capture-watch-output');
+ const jsResult = { close: jest.fn() };
+ const kilnResult = { close: jest.fn() };
+
+ expect(kilnCapture).toBeTruthy();
+ expect(jsCapture).toBeTruthy();
+
+ jsRollupHandlers.event({ code: 'BUNDLE_START' });
+ jsCapture.writeBundle(null, {
+ '.clay/vite-bootstrap-a.js': {
+ type: 'chunk',
+ isEntry: true,
+ facadeModuleId: path.join(tmpDir, '.clay', 'vite-bootstrap.js'),
+ fileName: '.clay/vite-bootstrap-a.js',
+ imports: [],
+ },
+ });
+ await jsRollupHandlers.event({ code: 'BUNDLE_END', duration: 12, result: jsResult });
+
+ kilnRollupHandlers.event({ code: 'BUNDLE_START' });
+ kilnCapture.writeBundle(null, {
+ '.clay/vite-kiln-edit-init-a.js': {
+ type: 'chunk',
+ isEntry: true,
+ facadeModuleId: path.join(tmpDir, '.clay', 'vite-kiln-edit-init.js'),
+ fileName: '.clay/vite-kiln-edit-init-a.js',
+ imports: [],
+ },
+ });
+ await kilnRollupHandlers.event({ code: 'BUNDLE_END', duration: 9, result: kilnResult });
+
+ // Trigger chokidar add/change paths to exercise debounced rebuild handlers.
+ const jsWatcher = chokidarWatchers[0];
+ const cssWatcher = chokidarWatchers[1];
+ const fontWatcher = chokidarWatchers[2];
+ const templateWatcher = chokidarWatchers[3];
+
+ jsWatcher.__handlers.add(path.join(tmpDir, 'components', 'foo', 'client.js'));
+ jsWatcher.__handlers.add(path.join(tmpDir, 'global', 'js', 'site.js'));
+ jsWatcher.__handlers.add(path.join(tmpDir, 'components', 'foo', 'model.js'));
+ jsWatcher.__handlers.unlink(path.join(tmpDir, 'components', 'foo', 'client.js'));
+ jsWatcher.__handlers.change(path.join(tmpDir, 'components', 'foo', 'client.js'));
+ cssWatcher.__handlers.change(path.join(tmpDir, 'styleguides', 'sg', 'components', 'nav.css'));
+ fontWatcher.__handlers.change(path.join(tmpDir, 'fonts', 'a.woff'));
+ templateWatcher.__handlers.change(path.join(tmpDir, 'components', 'foo', 'template.hbs'));
+ await new Promise(resolve => setTimeout(resolve, 350));
+
+ buildStylesMock.mockRejectedValueOnce(new Error('style watch fail'));
+ buildFontsMock.mockRejectedValueOnce(new Error('font watch fail'));
+ buildTemplatesMock.mockRejectedValueOnce(new Error('template watch fail'));
+ cssWatcher.__handlers.change(path.join(tmpDir, 'styleguides', 'sg', 'components', 'nav.css'));
+ fontWatcher.__handlers.change(path.join(tmpDir, 'fonts', 'a.woff'));
+ templateWatcher.__handlers.change(path.join(tmpDir, 'components', 'foo', 'template.hbs'));
+ await new Promise(resolve => setTimeout(resolve, 350));
+
+ expect(onRebuild).toHaveBeenCalled();
+ expect(onReady).toHaveBeenCalled();
+ expect(jsResult.close).toHaveBeenCalled();
+ expect(kilnResult.close).toHaveBeenCalled();
+ expect(buildStylesMock).toHaveBeenCalled();
+ expect(buildFontsMock).toHaveBeenCalled();
+ expect(buildTemplatesMock).toHaveBeenCalled();
+
+ await session.dispose();
+ restoreTTY();
+
+ expect(rollupWatchers[0].close).toHaveBeenCalled();
+ expect(rollupWatchers[1].close).toHaveBeenCalled();
+ expect(chokidarWatchers).toHaveLength(4);
+ chokidarWatchers.forEach((w) => {
+ expect(w.close).toHaveBeenCalled();
+ });
+ });
+});