From 7e855d607d156e9de1db5955e23d81a0e0452f37 Mon Sep 17 00:00:00 2001 From: Mamadou Talla DIA Date: Sun, 20 Sep 2026 00:06:44 +0100 Subject: [PATCH 1/2] =?UTF-8?q?feat(cli):=20am=C3=A9liorer=20la=20cr=C3=A9?= =?UTF-8?q?ation=20de=20modules=20avec=20des=20options=20de=20configuratio?= =?UTF-8?q?n=20suppl=C3=A9mentaires=20et=20mise=20=C3=A0=20jour=20de=20la?= =?UTF-8?q?=20documentation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/cli/src/commands/create/module.ts | 196 +++++++++++------- packages/cli/src/commands/create/shared.ts | 4 +- .../src/app/components/runable-welcome.vue | 1 + tests/regressions/cli-create-module.test.ts | 23 +- website/content/docs/en/guide/cli/create.md | 18 +- 5 files changed, 148 insertions(+), 94 deletions(-) diff --git a/packages/cli/src/commands/create/module.ts b/packages/cli/src/commands/create/module.ts index 0291518..1566120 100644 --- a/packages/cli/src/commands/create/module.ts +++ b/packages/cli/src/commands/create/module.ts @@ -1,4 +1,4 @@ -import { cp, mkdir, writeFile } from "node:fs/promises"; +import { cp, mkdir, readFile, writeFile } from "node:fs/promises"; import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; @@ -6,19 +6,24 @@ import * as p from "@clack/prompts"; import { consola } from "consola"; import { + askFramework, + askInstallDeps, + askPackageManager, + copyAgentsFile, + createPackageJson, exitOnCancel, - type BaseProjectAnswers, - handleSharedAnswers, - afterAnswer, - copyServerEntry, getCliPackageVersion, - writeRunableConfig, + installDependenciesIfWanted, } from "./shared.js"; +import { copyStarterTemplate } from "./starter.js"; /** Answers collected for the "create a Runable module" flow: the shared answers plus the module's own identity (name and `configKey`). */ -export interface ModuleProjectAnswers extends BaseProjectAnswers { +export interface ModuleProjectAnswers { moduleName: string; configKey: string; + framework: string; + packageManager: string; + installDeps: boolean; } /** Prompts for the module name, enforcing the same format as a valid (optionally scoped) npm package name. */ @@ -51,10 +56,7 @@ function printSummary(answers: ModuleProjectAnswers): void { consola.success("Configuration collected:"); consola.info(` Module name: ${answers.moduleName}`); consola.info(` Config key: ${answers.configKey}`); - consola.info(` appDir: ${answers.appDir}`); - consola.info(` outputDir: ${answers.outputDir}`); - consola.info(` distDir: ${answers.distDir}`); - consola.info(` publicDir: ${answers.publicDir}`); + consola.info(` Framework: ${answers.framework}`); consola.info(` packageManager: ${answers.packageManager}`); consola.info(` installDeps: ${answers.installDeps ? "yes" : "no"}`); } @@ -65,69 +67,111 @@ export async function createModulePlayground( options: { moduleName: string; framework: string; - createServerEntry: boolean; }, ) { const __dirname = dirname(fileURLToPath(import.meta.url)); const sharedStarterDir = resolve(__dirname, "../../../starters/_shared"); const playgroundDir = resolve(moduleDir, "playground"); - await mkdir(playgroundDir, { recursive: true }); - await cp(resolve(sharedStarterDir, "app"), resolve(playgroundDir, "app"), { - recursive: true, - force: true, - }); + if (options.framework !== "other") { + await copyStarterTemplate(options.framework, playgroundDir); + } else { + await mkdir(playgroundDir, { recursive: true }); + await cp(sharedStarterDir, playgroundDir, { + recursive: true, + force: true, + }); + + const version = await getCliPackageVersion(); + await writeFile( + resolve(playgroundDir, "package.json"), + `${JSON.stringify( + { + name: "playground", + private: true, + type: "module", + scripts: { + prepare: "runable prepare", + build: "runable build", + typecheck: "tsc --noEmit", + }, + dependencies: { + runable: version, + vue: "^3.5.0", + "vue-router": "^5.2.0", + }, + devDependencies: { + "@runablejs/cli": version, + "@types/node": "^24.13.3", + typescript: "^6.0.3", + }, + }, + null, + 2, + )}\n`, + ); + } + + const packageJsonPath = resolve(playgroundDir, "package.json"); + const packageJson = JSON.parse(await readFile(packageJsonPath, "utf8")); + packageJson.name = `${options.moduleName.replace(/^@/, "").replace("/", "-")}-playground`; + packageJson.private = true; + packageJson.scripts = packageJson.scripts ?? {}; + packageJson.scripts.prepare = "runable prepare"; + delete packageJson.scripts.preprepare; + delete packageJson.scripts.prebuild; + delete packageJson.scripts["app:prepare"]; + delete packageJson.scripts["app:build"]; + await writeFile(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`); + + await writeFile( + resolve(playgroundDir, "runable.config.ts"), + `import { defineConfig } from "runable"; + +export default defineConfig({ + output: "../.app", + distdir: "../.output", + modules: [".."], +}); +`, + ); +} + +/** Creates the publishable module package at the project root. */ +export async function createModuleRoot( + moduleDir: string, + options: { + moduleName: string; + configKey: string; + packageManager: string; + }, +) { + const __dirname = dirname(fileURLToPath(import.meta.url)); + const sharedStarterDir = resolve(__dirname, "../../../starters/_shared"); + + await mkdir(moduleDir, { recursive: true }); + await copyAgentsFile(moduleDir); + await writeFile( + resolve(moduleDir, "runable.config.ts"), + `import { defineModule } from "runable"; + +export default defineModule({ + configKey: ${JSON.stringify(options.configKey)}, +}); +`, + ); await cp( resolve(sharedStarterDir, "tsconfig.json"), - resolve(playgroundDir, "tsconfig.json"), + resolve(moduleDir, "tsconfig.json"), ); await cp( resolve(sharedStarterDir, "tsconfig.node.json"), - resolve(playgroundDir, "tsconfig.node.json"), - ); - await writeRunableConfig({ modules: [".."] }, { cwd: playgroundDir }); - await copyServerEntry( - options.createServerEntry, - options.framework, - playgroundDir, + resolve(moduleDir, "tsconfig.node.json"), ); - - const version = await getCliPackageVersion(); - const packageJson = { - name: `${options.moduleName.replace(/^@/, "").replace("/", "-")}-playground`, - private: true, - type: "module", - scripts: { - ...(options.createServerEntry - ? { dev: "runable prepare && tsx watch server.ts" } - : {}), - prepare: "runable prepare", - build: "runable build", - typecheck: "tsc --noEmit", - }, - dependencies: { - [options.moduleName]: "*", - ...(options.framework === "express" && options.createServerEntry - ? { express: "^5.2.1" } - : {}), - runable: version, - vue: "^3.5.0", - "vue-router": "^5.2.0", - }, - devDependencies: { - "@runablejs/cli": version, - ...(options.framework === "express" && options.createServerEntry - ? { "@types/express": "^5.0.6" } - : {}), - "@types/node": "^24.13.3", - tsx: "^4.23.12", - typescript: "^6.0.3", - }, - }; - - await writeFile( - resolve(playgroundDir, "package.json"), - `${JSON.stringify(packageJson, null, 2)}\n`, + await createPackageJson( + moduleDir, + options.moduleName, + options.packageManager, ); } @@ -145,28 +189,22 @@ export async function handleModuleProject() { consola.info(`Selected module: ${moduleName}`); consola.info(`Config key: ${configKey}`); - const answers = await handleSharedAnswers(); + const framework = await askFramework(); + const packageManager = await askPackageManager(); + const installDeps = await askInstallDeps(); // Unlike the "existing project" flow, a module gets its own fresh // directory (named after it) rather than being added to `process.cwd()`. const moduleDir = resolve(process.cwd(), moduleName); - await mkdir(moduleDir, { recursive: true }); - - // Record the config key alongside the other shared config values so it - // ends up in the module's generated runable.config. - Object.assign(answers._config, { configKey }); - - await createModulePlayground(moduleDir, { moduleName, ...answers }); - - // `isModule: true` tells `afterAnswer` to scaffold module-specific output - // (e.g. `defineModule` instead of `defineConfig`) rather than a regular project. - await afterAnswer( - moduleDir, - { ...answers, createServerEntry: false }, - { moduleName, isModule: true }, - ); + await createModuleRoot(moduleDir, { + moduleName, + configKey, + packageManager, + }); + await createModulePlayground(moduleDir, { moduleName, framework }); + await installDependenciesIfWanted(packageManager, installDeps, moduleDir); // printSummary(answers); - return answers; + return { moduleName, configKey, framework, packageManager, installDeps }; } diff --git a/packages/cli/src/commands/create/shared.ts b/packages/cli/src/commands/create/shared.ts index 1b97045..01c2e7a 100644 --- a/packages/cli/src/commands/create/shared.ts +++ b/packages/cli/src/commands/create/shared.ts @@ -436,6 +436,7 @@ export async function writeRunableConfig( export async function createPackageJson( targetDir: string, moduleName: string, + packageManager = "pnpm", ): Promise { const pkgPath = resolve(targetDir, "package.json"); @@ -477,9 +478,10 @@ export async function createPackageJson( workspaces: ["playground"], scripts: { build: "runable build", - "app:prepare": "runable prepare", + prepare: "runable prepare", "playground:prepare": "cd playground && runable prepare", "playground:build": "cd playground && runable build", + "playground:dev": `cd playground && ${packageManager} run dev`, }, devDependencies: { "@runablejs/cli": version, diff --git a/packages/runable/src/app/components/runable-welcome.vue b/packages/runable/src/app/components/runable-welcome.vue index 0a9956f..1cf1dee 100644 --- a/packages/runable/src/app/components/runable-welcome.vue +++ b/packages/runable/src/app/components/runable-welcome.vue @@ -153,6 +153,7 @@ const links = [ background: var(--welcome-background); font-family: Inter, ui-sans-serif, system-ui, sans-serif; -webkit-font-smoothing: antialiased; + margin: -8px; } .welcome-shell ::selection { diff --git a/tests/regressions/cli-create-module.test.ts b/tests/regressions/cli-create-module.test.ts index 05dc4d9..54e4e71 100644 --- a/tests/regressions/cli-create-module.test.ts +++ b/tests/regressions/cli-create-module.test.ts @@ -31,18 +31,21 @@ describe("runable create --module", () => { const directory = createFixtureDir("cli-module-playground-"); try { - const { createModulePlayground } = await import( + const { createModulePlayground, createModuleRoot } = await import( "../../packages/cli/dist/commands/create/module.js" ); - const { configurePnpmBuilds, createPackageJson } = await import( + const { configurePnpmBuilds } = await import( "../../packages/cli/dist/commands/create/shared.js" ); + await createModuleRoot(directory, { + moduleName: "test-module", + configKey: "testModule", + packageManager: "pnpm", + }); await createModulePlayground(directory, { moduleName: "test-module", - framework: "other", - createServerEntry: false, + framework: "fastify", }); - await createPackageJson(directory, "test-module"); await configurePnpmBuilds("pnpm", directory); expect(existsSync(path.join(directory, "playground/app/app.vue"))).toBe( @@ -69,9 +72,14 @@ describe("runable create --module", () => { ); expect(packageJson.workspaces).toEqual(["playground"]); expect(packageJson.scripts).toMatchObject({ + prepare: "runable prepare", "playground:prepare": "cd playground && runable prepare", "playground:build": "cd playground && runable build", + "playground:dev": "cd playground && pnpm run dev", }); + expect(existsSync(path.join(directory, "tsconfig.json"))).toBe(true); + expect(existsSync(path.join(directory, "tsconfig.node.json"))).toBe(true); + expect(existsSync(path.join(directory, "app"))).toBe(false); const playgroundPackageJson = JSON.parse( readFileSync(path.join(directory, "playground/package.json"), "utf8"), @@ -80,7 +88,7 @@ describe("runable create --module", () => { name: "test-module-playground", private: true, dependencies: { - "test-module": "*", + fastify: expect.any(String), runable: expect.any(String), vue: expect.any(String), "vue-router": expect.any(String), @@ -90,6 +98,9 @@ describe("runable create --module", () => { typescript: expect.any(String), }, }); + expect(playgroundPackageJson.dependencies).not.toHaveProperty( + "test-module", + ); expect( readFileSync(path.join(directory, "pnpm-workspace.yaml"), "utf8"), ).toContain("- playground"); diff --git a/website/content/docs/en/guide/cli/create.md b/website/content/docs/en/guide/cli/create.md index 96fda59..7521ea7 100644 --- a/website/content/docs/en/guide/cli/create.md +++ b/website/content/docs/en/guide/cli/create.md @@ -98,20 +98,22 @@ The module workflow asks for: 1. **Module name** — a valid lowercase npm package name, optionally scoped. 2. **Config key** — the property consumers use in `runable.config.ts`; it defaults to the module name. -3. **Directories** — `appDir`, `outputDir`, `distDir`, and `publicDir`. +3. **Backend framework** — used by the generated playground application. 4. **Package manager** — detected from the current project when possible. 5. **Install dependencies now?** It creates a **new directory** named after the module. The generated package -contains the application template, `AGENTS.md`, a publishable `package.json`, -and a `runable.config.ts` defined with `defineModule()` instead of -`defineConfig()`. Because these packages are only needed to develop and build -the module, `runable`, `vue`, and `vue-router` are added to `devDependencies` -rather than its runtime dependencies. +contains `AGENTS.md`, TypeScript configuration, a publishable `package.json`, +and a minimal `runable.config.ts` defined with `defineModule()`. Application +files and backend code live in the playground rather than the module root. +Because the framework packages are only needed to develop and build the module, +`runable`, `vue`, and `vue-router` are added to root `devDependencies` rather +than runtime dependencies. It also creates a `playground/` Runable application for developing the module -locally. Its `runable.config.ts` declares `modules: [".."]`, so it always loads -the module from the project root without requiring the package to be published. +locally using the selected backend starter. Its `runable.config.ts` writes +generated output to the module root and declares `modules: [".."]`, so it +always loads the module locally without requiring the package to be published. Use `playground:prepare` and `playground:build` from the module root to run the corresponding Runable commands against that application. The playground has its own private `package.json` and is registered as a workspace of the module. From 04b01a44dfc8fb564ec476d532edaa2dd4784605 Mon Sep 17 00:00:00 2001 From: Mamadou Talla DIA Date: Sun, 20 Sep 2026 12:40:46 +0100 Subject: [PATCH 2/2] feat(tsconfig): ajouter l'inclusion de fichiers de configuration dans writeTsConfig --- packages/runable/src/utils/tsconfig.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/runable/src/utils/tsconfig.ts b/packages/runable/src/utils/tsconfig.ts index 014affb..da8bb95 100644 --- a/packages/runable/src/utils/tsconfig.ts +++ b/packages/runable/src/utils/tsconfig.ts @@ -60,6 +60,12 @@ export function writeTsConfig() { tsconfig.app.addInclude( normalizeDir(join(relative(output, config.appDir), "**/*")), ); + + if (config._configFile) { + tsconfig.app.addInclude( + normalizeDir(relative(output, config._configFile)), + ); + } } const obj = tsconfig.app.toObject();