diff --git a/.changeset/fix-starter-gitignore-packaging.md b/.changeset/fix-starter-gitignore-packaging.md new file mode 100644 index 0000000..1f59c09 --- /dev/null +++ b/.changeset/fix-starter-gitignore-packaging.md @@ -0,0 +1,5 @@ +--- +"@runablejs/cli": patch +--- + +Ensure every generated starter includes its framework-specific `.gitignore` file. diff --git a/.changeset/inherit-module-aliases.md b/.changeset/inherit-module-aliases.md new file mode 100644 index 0000000..789d2f9 --- /dev/null +++ b/.changeset/inherit-module-aliases.md @@ -0,0 +1,5 @@ +--- +"runable": patch +--- + +Inherit aliases declared by modules in application Vite and TypeScript resolution. diff --git a/packages/cli/src/commands/create/starter.ts b/packages/cli/src/commands/create/starter.ts index 2cfefe0..304c85a 100644 --- a/packages/cli/src/commands/create/starter.ts +++ b/packages/cli/src/commands/create/starter.ts @@ -1,4 +1,4 @@ -import { cp, mkdir, readFile, stat, writeFile } from "node:fs/promises"; +import { cp, mkdir, readFile, rename, stat, writeFile } from "node:fs/promises"; import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; @@ -75,6 +75,10 @@ export async function copyStarterTemplate( await mkdir(targetDir, { recursive: true }); await cp(sharedTemplateDir, targetDir, { recursive: true, force: true }); await cp(templateDir, targetDir, { recursive: true, force: true }); + await rename( + resolve(targetDir, "gitignore.template"), + resolve(targetDir, ".gitignore"), + ); const packageJsonPath = resolve(targetDir, "package.json"); const packageJson = JSON.parse(await readFile(packageJsonPath, "utf8")); diff --git a/packages/cli/starters/adonisjs/.gitignore b/packages/cli/starters/adonisjs/gitignore.template similarity index 97% rename from packages/cli/starters/adonisjs/.gitignore rename to packages/cli/starters/adonisjs/gitignore.template index 4a70e94..ae3f9fd 100644 --- a/packages/cli/starters/adonisjs/.gitignore +++ b/packages/cli/starters/adonisjs/gitignore.template @@ -26,4 +26,4 @@ yarn-error.log .DS_Store .app -.output \ No newline at end of file +.output diff --git a/packages/cli/starters/express/.gitignore b/packages/cli/starters/express/gitignore.template similarity index 100% rename from packages/cli/starters/express/.gitignore rename to packages/cli/starters/express/gitignore.template diff --git a/packages/cli/starters/fastify/.gitignore b/packages/cli/starters/fastify/gitignore.template similarity index 100% rename from packages/cli/starters/fastify/.gitignore rename to packages/cli/starters/fastify/gitignore.template diff --git a/packages/cli/starters/hono/.gitignore b/packages/cli/starters/hono/gitignore.template similarity index 100% rename from packages/cli/starters/hono/.gitignore rename to packages/cli/starters/hono/gitignore.template diff --git a/packages/cli/starters/koa/.gitignore b/packages/cli/starters/koa/gitignore.template similarity index 100% rename from packages/cli/starters/koa/.gitignore rename to packages/cli/starters/koa/gitignore.template diff --git a/packages/cli/starters/nestjs/.gitignore b/packages/cli/starters/nestjs/gitignore.template similarity index 100% rename from packages/cli/starters/nestjs/.gitignore rename to packages/cli/starters/nestjs/gitignore.template diff --git a/packages/runable/src/config/load.ts b/packages/runable/src/config/load.ts index 818f940..43dfef4 100644 --- a/packages/runable/src/config/load.ts +++ b/packages/runable/src/config/load.ts @@ -496,6 +496,21 @@ export interface ConfigGraph { all: ResolvedConfig[]; } +/** + * Combines aliases from the resolved config graph. Dependencies are applied + * first, then their parents, and the main application last, so the closest + * consumer wins when two configs declare the same alias. + */ +function mergeConfigAliases(configs: ResolvedConfig[]) { + const aliases: ResolvedConfig["alias"] = {}; + + for (const config of [...configs].reverse()) { + Object.assign(aliases, config.alias); + } + + return aliases; +} + /** * Resolves the full config graph for a project — the main config, every * module it transitively depends on, their options, and module `setup()` @@ -585,10 +600,12 @@ export async function resolveConfigGraph( resolved = await runConfigExtensions(resolved, pendingSetups); const main = resolved.__main!; + const all = Object.values(resolved).sort((a, b) => a._index - b._index); + + main.alias = mergeConfigAliases(all); await runSetups(pendingSetups, main); - const all = Object.values(resolved).sort((a, b) => a._index - b._index); return { main, all }; } diff --git a/packages/runable/src/utils/tsconfig.ts b/packages/runable/src/utils/tsconfig.ts index 7a80aa3..d941838 100644 --- a/packages/runable/src/utils/tsconfig.ts +++ b/packages/runable/src/utils/tsconfig.ts @@ -40,7 +40,7 @@ export function writeTsConfig() { Object.entries(alias ?? {}).forEach(([key, value]) => { if (key === "#build") return; - tsconfig.app.addAlias(key, normalizeDir(relative(process.cwd(), value))); + tsconfig.app.addAlias(key, normalizeDir(relative(output, value))); }); tsconfig.app.addAlias("#build", "./"); diff --git a/tests/integration/agents-md.test.ts b/tests/integration/agents-md.test.ts index fc23ac0..eee015d 100644 --- a/tests/integration/agents-md.test.ts +++ b/tests/integration/agents-md.test.ts @@ -76,6 +76,10 @@ describe("AGENTS.md is shipped to newly scaffolded Runable projects", () => { path.join(starterTemplateDir, "package.json"), JSON.stringify({ name: "fixture-starter", version: "1.0.0" }), ); + writeFileSync( + path.join(starterTemplateDir, "gitignore.template"), + "node_modules\n.app\n.output\n", + ); const { copyStarterTemplate } = await import( "../../packages/cli/dist/commands/create/starter.js" @@ -95,6 +99,10 @@ describe("AGENTS.md is shipped to newly scaffolded Runable projects", () => { // The starter template itself is still there too — copyAgentsFile // must not have clobbered anything from copyStarterTemplate. expect(existsSync(path.join(projectDir, "package.json"))).toBe(true); + expect(existsSync(path.join(projectDir, ".gitignore"))).toBe(true); + expect(existsSync(path.join(projectDir, "gitignore.template"))).toBe( + false, + ); } finally { rmSync(starterTemplateDir, { recursive: true, force: true }); cleanupFixtureDir(projectDir); diff --git a/tests/regressions/cli-starters.test.ts b/tests/regressions/cli-starters.test.ts index f515c24..f6c6565 100644 --- a/tests/regressions/cli-starters.test.ts +++ b/tests/regressions/cli-starters.test.ts @@ -17,7 +17,12 @@ describe("CLI starter templates", () => { it(`copies a complete ${framework} starter`, async () => { expect( existsSync( - join(process.cwd(), "packages/cli/starters", framework, ".gitignore"), + join( + process.cwd(), + "packages/cli/starters", + framework, + "gitignore.template", + ), ), ).toBe(true); @@ -32,6 +37,7 @@ describe("CLI starter templates", () => { expect(existsSync(join(target, "package.json"))).toBe(true); expect(existsSync(join(target, ".gitignore"))).toBe(true); + expect(existsSync(join(target, "gitignore.template"))).toBe(false); expect(existsSync(join(target, "runable.config.ts"))).toBe(true); expect(existsSync(join(target, "app/app.vue"))).toBe(true); expect(existsSync(join(target, "app/pages/index.vue"))).toBe(true); diff --git a/tests/regressions/module-aliases.test.ts b/tests/regressions/module-aliases.test.ts new file mode 100644 index 0000000..897dccc --- /dev/null +++ b/tests/regressions/module-aliases.test.ts @@ -0,0 +1,75 @@ +import { readFileSync } from "node:fs"; +import path from "node:path"; +import { afterEach, describe, expect, it, vi } from "vitest"; + +import { + cleanupFixtureDir, + createFixtureDir, + linkWorkspacePackage, + writeFixtureFile, +} from "../fixtures.js"; + +const originalCwd = process.cwd(); + +afterEach(() => { + process.chdir(originalCwd); +}); + +describe("module aliases", () => { + it("adds module aliases to the application with consumer precedence", async () => { + const directory = createFixtureDir("module-aliases-"); + + try { + linkWorkspacePackage(directory, "runable", "packages/runable"); + writeFixtureFile( + directory, + "runable.config.ts", + `import path from "node:path"; +import { defineConfig } from "runable"; + +export default defineConfig({ + modules: ["./module"], + alias: { "@shared": path.join(import.meta.dirname, "app/shared") }, +}); +`, + ); + writeFixtureFile( + directory, + "module/runable.config.ts", + `import path from "node:path"; +import { defineModule } from "runable"; + +export default defineModule({ + alias: { + "@module": path.join(import.meta.dirname, "app"), + "@shared": path.join(import.meta.dirname, "app/shared"), + }, +}); +`, + ); + + process.chdir(directory); + vi.resetModules(); + const { loadConfig, useConfig, writeTsConfig } = await import("runable"); + await loadConfig(); + writeTsConfig(); + + const config = useConfig(); + expect(config.alias["@module"]).toBe( + path.join(directory, "module/app"), + ); + expect(config.alias["@shared"]).toBe(path.join(directory, "app/shared")); + expect(config.alias["#build"]).toBe(path.join(directory, ".app")); + + const generated = JSON.parse( + readFileSync(path.join(directory, ".app/tsconfig.app.json"), "utf8"), + ); + expect(generated.compilerOptions.paths).toMatchObject({ + "@module": ["../module/app"], + "@shared": ["../app/shared"], + }); + } finally { + cleanupFixtureDir(directory); + } + }); +}); diff --git a/website/app/components/landing/Sponsors.vue b/website/app/components/landing/Sponsors.vue index 35b5e7f..ced95c1 100644 --- a/website/app/components/landing/Sponsors.vue +++ b/website/app/components/landing/Sponsors.vue @@ -1,9 +1,7 @@