import assert from "assert"; import { promises as fs } from "fs"; import path from "path"; import { RealAppTest } from "./test_utils/test-on-real-app.js"; // Helper function to ensure sealgen configuration exists async function ensureSealgenConfig(test_app: RealAppTest): Promise { const packageJsonPath = path.join(test_app.app_path, "package.json"); let packageJsonContent = await fs.readFile(packageJsonPath, "utf-8"); let packageJson = JSON.parse(packageJsonContent); if (!packageJson.sealgen) { packageJson.sealgen = {}; } await fs.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2)); } // Helper function to create mock external module async function createMockExternalModule( test_app: RealAppTest, moduleName: string, subdirectory: string, files: Array<{ name: string; content: string }> ): Promise { const externalModulePath = path.join(test_app.app_path, "node_modules", moduleName); const targetPath = path.join(externalModulePath, subdirectory); await fs.mkdir(targetPath, { recursive: true }); for (const file of files) { await fs.writeFile(path.join(targetPath, file.name), file.content); // If the file is a .ts controller, also create a .js file for build import if (file.name.endsWith(".ts") && file.name.includes("stimulus")) { const jsName = file.name.replace(/\.ts$/, ".js"); const exportName = file.name.replace(/\.stimulus\.ts$/, ""); await fs.writeFile( path.join(targetPath, jsName), `export default { connect() { console.log('Dummy JS for ${exportName}'); } }` ); } } } describe("register-external CLI commands", () => { describe("package.json configuration", () => { it("registers external controllers and verifies package.json is updated", async function () { const test_app = await RealAppTest.init(); await ensureSealgenConfig(test_app); const moduleName = "test-controller-module"; const subdirectory = "controllers"; await test_app.runSealgenCommand( `register-external-controllers ${moduleName} ${subdirectory}` ); // Verify package.json was updated correctly const packageJsonPath = path.join(test_app.app_path, "package.json"); const packageJsonContent = await fs.readFile(packageJsonPath, "utf-8"); const packageJson = JSON.parse(packageJsonContent); assert(packageJson.sealgen, "sealgen configuration should exist"); assert(packageJson.sealgen.controllerDirs, "controllerDirs should exist"); assert( Array.isArray(packageJson.sealgen.controllerDirs), "controllerDirs should be an array" ); const expectedPath = `node_modules/${moduleName}/${subdirectory}`; assert( packageJson.sealgen.controllerDirs.includes(expectedPath), `controllerDirs should contain ${expectedPath}` ); await test_app.close(); }).timeout(100 * 1000); it("registers external styles and verifies package.json is updated", async function () { const test_app = await RealAppTest.init(); await ensureSealgenConfig(test_app); const moduleName = "test-style-module"; const subdirectory = "styles"; await test_app.runSealgenCommand( `register-external-styles ${moduleName} ${subdirectory}` ); // Verify package.json was updated correctly const packageJsonPath = path.join(test_app.app_path, "package.json"); const packageJsonContent = await fs.readFile(packageJsonPath, "utf-8"); const packageJson = JSON.parse(packageJsonContent); assert(packageJson.sealgen, "sealgen configuration should exist"); assert(packageJson.sealgen.styleDirs, "styleDirs should exist"); assert(Array.isArray(packageJson.sealgen.styleDirs), "styleDirs should be an array"); const expectedPath = `node_modules/${moduleName}/${subdirectory}`; assert( packageJson.sealgen.styleDirs.includes(expectedPath), `styleDirs should contain ${expectedPath}` ); await test_app.close(); }).timeout(100 * 1000); it("registers both controllers and styles in the same project", async function () { const test_app = await RealAppTest.init(); await ensureSealgenConfig(test_app); const controllerModule = "test-controller-module"; const controllerSubdir = "controllers"; const styleModule = "test-style-module"; const styleSubdir = "styles"; await test_app.runSealgenCommand( `register-external-controllers ${controllerModule} ${controllerSubdir}` ); await test_app.runSealgenCommand( `register-external-styles ${styleModule} ${styleSubdir}` ); // Verify package.json was updated correctly for both const packageJsonPath = path.join(test_app.app_path, "package.json"); const packageJsonContent = await fs.readFile(packageJsonPath, "utf-8"); const packageJson = JSON.parse(packageJsonContent); assert(packageJson.sealgen, "sealgen configuration should exist"); assert(packageJson.sealgen.controllerDirs, "controllerDirs should exist"); assert(packageJson.sealgen.styleDirs, "styleDirs should exist"); const expectedControllerPath = `node_modules/${controllerModule}/${controllerSubdir}`; const expectedStylePath = `node_modules/${styleModule}/${styleSubdir}`; assert( packageJson.sealgen.controllerDirs.includes(expectedControllerPath), `controllerDirs should contain ${expectedControllerPath}` ); assert( packageJson.sealgen.styleDirs.includes(expectedStylePath), `styleDirs should contain ${expectedStylePath}` ); await test_app.close(); }).timeout(100 * 1000); it("handles duplicate registrations gracefully", async function () { const test_app = await RealAppTest.init(); await ensureSealgenConfig(test_app); const moduleName = "test-duplicate-module"; const subdirectory = "controllers"; // Register the same module twice await test_app.runSealgenCommand( `register-external-controllers ${moduleName} ${subdirectory}` ); await test_app.runSealgenCommand( `register-external-controllers ${moduleName} ${subdirectory}` ); // Verify package.json was updated correctly (should not have duplicates) const packageJsonPath = path.join(test_app.app_path, "package.json"); const packageJsonContent = await fs.readFile(packageJsonPath, "utf-8"); const packageJson = JSON.parse(packageJsonContent); assert(packageJson.sealgen, "sealgen configuration should exist"); assert(packageJson.sealgen.controllerDirs, "controllerDirs should exist"); const expectedPath = `node_modules/${moduleName}/${subdirectory}`; const controllerDirs = packageJson.sealgen.controllerDirs as string[]; // Should only appear once const occurrences = controllerDirs.filter((dir) => dir === expectedPath).length; assert( occurrences === 1, `controllerDirs should contain ${expectedPath} exactly once, found ${occurrences} times` ); await test_app.close(); }).timeout(100 * 1000); it("preserves existing sealgen configuration when adding new entries", async function () { const test_app = await RealAppTest.init(); // First, manually add some existing configuration to package.json const packageJsonPath = path.join(test_app.app_path, "package.json"); const packageJsonContent = await fs.readFile(packageJsonPath, "utf-8"); const packageJson = JSON.parse(packageJsonContent); // Add existing sealgen configuration packageJson.sealgen = { controllerDirs: ["node_modules/existing-controller/controllers"], styleDirs: ["node_modules/existing-style/styles"], }; await fs.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2)); // Now register new external modules const newControllerModule = "new-controller-module"; const newStyleModule = "new-style-module"; await test_app.runSealgenCommand( `register-external-controllers ${newControllerModule} controllers` ); await test_app.runSealgenCommand(`register-external-styles ${newStyleModule} styles`); // Verify both old and new configurations are preserved const updatedPackageJsonContent = await fs.readFile(packageJsonPath, "utf-8"); const updatedPackageJson = JSON.parse(updatedPackageJsonContent); assert(updatedPackageJson.sealgen, "sealgen configuration should exist"); assert(updatedPackageJson.sealgen.controllerDirs, "controllerDirs should exist"); assert(updatedPackageJson.sealgen.styleDirs, "styleDirs should exist"); // Check that existing entries are preserved assert( updatedPackageJson.sealgen.controllerDirs.includes( "node_modules/existing-controller/controllers" ), "Existing controller directory should be preserved" ); assert( updatedPackageJson.sealgen.styleDirs.includes("node_modules/existing-style/styles"), "Existing style directory should be preserved" ); // Check that new entries are added assert( updatedPackageJson.sealgen.controllerDirs.includes( `node_modules/${newControllerModule}/controllers` ), "New controller directory should be added" ); assert( updatedPackageJson.sealgen.styleDirs.includes( `node_modules/${newStyleModule}/styles` ), "New style directory should be added" ); await test_app.close(); }).timeout(100 * 1000); }); describe("build integration", () => { it("registers external controllers and verifies they are included in build", async function () { const test_app = await RealAppTest.init(); await ensureSealgenConfig(test_app); // Create a mock external module with controllers const externalModuleName = "test-external-controllers"; await createMockExternalModule(test_app, externalModuleName, "controllers", [ { name: "test-external.stimulus.ts", content: ` import { Controller } from "stimulus"; export default class TestExternalController extends Controller { static targets = ["output"]; connect() { console.log("External controller connected!"); } } `, }, ]); // Register the external controllers await test_app.runSealgenCommand( `register-external-controllers ${externalModuleName} controllers` ); // Run the build process await test_app.runSealgenCommand("build"); // Verify the controllers are included in the build const controllersFilePath = path.join(test_app.app_path, "src/front/controllers.ts"); const controllersContent = await fs.readFile(controllersFilePath, "utf-8"); // Check that the external controller is imported assert( controllersContent.includes( `import { default as TestExternal } from "./../../node_modules/${externalModuleName}/controllers/test-external.stimulus.js"` ), "External controller should be imported in generated controllers file" ); // Check that the controller is registered assert( controllersContent.includes(`application.register("test-external", TestExternal);`), "External controller should be registered in generated controllers file" ); // Verify the build output exists const bundlePath = path.join(test_app.app_path, "public/dist/bundle.js"); assert( await fs .stat(bundlePath) .then(() => true) .catch(() => false), "Bundle should be generated" ); await test_app.close(); }).timeout(100 * 1000); it("registers external styles and verifies they are included in build", async function () { const test_app = await RealAppTest.init(); await ensureSealgenConfig(test_app); // Create a mock external module with styles const externalModuleName = "test-external-styles"; await createMockExternalModule(test_app, externalModuleName, "styles", [ { name: "external.css", content: ` .external-style { color: red; background: blue; } `, }, ]); // Register the external styles await test_app.runSealgenCommand( `register-external-styles ${externalModuleName} styles` ); // Run the build process await test_app.runSealgenCommand("build"); // Verify the styles are included in the build const styleEntrypointPath = path.join( test_app.app_path, "src/style-entrypoints/default.entrypoint.css" ); const styleEntrypointContent = await fs.readFile(styleEntrypointPath, "utf-8"); // Check that the external CSS is imported assert( styleEntrypointContent.includes( `@import "../../node_modules/${externalModuleName}/styles/external.css"` ), "External CSS should be imported in generated style entrypoint" ); // Verify the CSS build output exists const cssOutputPath = path.join( test_app.app_path, "public/dist/default.entrypoint.css" ); assert( await fs .stat(cssOutputPath) .then(() => true) .catch(() => false), "CSS should be generated" ); await test_app.close(); }).timeout(100 * 1000); it("registers both external controllers and styles and verifies complete build integration", async function () { const test_app = await RealAppTest.init(); await ensureSealgenConfig(test_app); // Create mock external modules const controllerModuleName = "test-external-controllers"; const styleModuleName = "test-external-styles"; // Create controller module await createMockExternalModule(test_app, controllerModuleName, "controllers", [ { name: "combined-test.stimulus.ts", content: ` import { Controller } from "stimulus"; export default class CombinedTestController extends Controller { static targets = ["output"]; connect() { console.log("Combined test controller connected!"); } } `, }, ]); // Create style module await createMockExternalModule(test_app, styleModuleName, "styles", [ { name: "combined-external.css", content: ` .combined-external-style { color: green; background: yellow; } `, }, ]); // Register both external modules await test_app.runSealgenCommand( `register-external-controllers ${controllerModuleName} controllers` ); await test_app.runSealgenCommand(`register-external-styles ${styleModuleName} styles`); // Run the build process await test_app.runSealgenCommand("build"); // Verify controllers are included const controllersFilePath = path.join(test_app.app_path, "src/front/controllers.ts"); const controllersContent = await fs.readFile(controllersFilePath, "utf-8"); assert( controllersContent.includes( `import { default as CombinedTest } from "./../../node_modules/${controllerModuleName}/controllers/combined-test.stimulus.js"` ), "Combined test controller should be imported" ); assert( controllersContent.includes(`application.register("combined-test", CombinedTest);`), "Combined test controller should be registered" ); // Verify styles are included const styleEntrypointPath = path.join( test_app.app_path, "src/style-entrypoints/default.entrypoint.css" ); const styleEntrypointContent = await fs.readFile(styleEntrypointPath, "utf-8"); assert( styleEntrypointContent.includes( `@import "../../node_modules/${styleModuleName}/styles/combined-external.css"` ), "Combined external CSS should be imported" ); // Verify build outputs exist const bundlePath = path.join(test_app.app_path, "public/dist/bundle.js"); const cssOutputPath = path.join( test_app.app_path, "public/dist/default.entrypoint.css" ); assert( await fs .stat(bundlePath) .then(() => true) .catch(() => false), "Bundle should be generated" ); assert( await fs .stat(cssOutputPath) .then(() => true) .catch(() => false), "CSS should be generated" ); await test_app.close(); }).timeout(100 * 1000); }); });