diff --git a/CHANGES.md b/CHANGES.md index a7743bb98..c26e21692 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -22,8 +22,15 @@ Released on March 19, 2026. repository-relative path logic has been made safe for published JSR execution. [[#624], [#633]] + - Revived removed `fedify init` options. [[#632], [#638] by ChanHaeng Lee] + - `bare-bones` option for web framework. + - `in-memory` option for key-value store. + - `in-process` option for message queue. + [#624]: https://github.com/fedify-dev/fedify/issues/624 +[#632]: https://github.com/fedify-dev/fedify/issues/632 [#633]: https://github.com/fedify-dev/fedify/pull/633 +[#638]: https://github.com/fedify-dev/fedify/pull/638 ### @fedify/vocab-runtime diff --git a/docs/cli.md b/docs/cli.md index 35d87f350..5ec0fe7ec 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -5,6 +5,8 @@ description: >- features of the fedify command. --- + + `fedify`: CLI toolchain ======================= @@ -222,11 +224,12 @@ fedify init my-fedify-project The above command will start the interactive prompt to initialize a new Fedify project. It will ask you a few questions to set up the project: - - Web framework: [Hono], [Elysia], [Express], [Nitro], or [Next.js] + - Web framework: Bare-bones, [Hono], [Elysia], [Express], [Nitro], + or [Next.js] - Package manager: [Deno], [Bun], [npm], [pnpm], or [Yarn] - - Message queue: [Redis], [PostgreSQL], [AMQP] (e.g., [RabbitMQ]), + - Message queue: In-Process, [Redis], [PostgreSQL], [AMQP] (e.g., [RabbitMQ]), or [Deno KV] (if Deno) - - Key–value store: [Redis], [PostgreSQL], or [Deno KV] (if Deno) + - Key–value store: In-Memory, [Redis], [PostgreSQL], or [Deno KV] (if Deno) > [!TIP] > Projects created with `fedify init` automatically include [`@fedify/lint`] @@ -276,6 +279,8 @@ option. The available options are: You can specify the web framework to integrate with Fedify by using the `-w`/`--web-framework` option. The available options are: + - `bare-bones`: A minimal setup without any web framework integration, but in + Node.js, [Hono] is used for a simple adapter for a lightweight experience. - `hono`: [Hono] - `nitro`: [Nitro] - `next`: [Next.js] @@ -287,6 +292,8 @@ the `-w`/`--web-framework` option. The available options are: You can specify the key–value store to use by using the `-k`/`--kv-store` option. The available options are: + - `in-memory`: An in-memory key–value store that does not persist data across + restarts. This is useful for testing and development purposes. - `redis`: [Redis] - `postgres`: [PostgreSQL] - `denokv`: [Deno KV] (if Deno) @@ -296,6 +303,8 @@ option. The available options are: You can specify the message queue to use by using the `-m`/`--message-queue` option. The available options are: + - `in-process`: An in-process message queue that does not persist messages + across restarts. This is useful for testing and development purposes. - `redis`: [Redis] - `postgres`: [PostgreSQL] - `amqp`: [AMQP] (e.g., [RabbitMQ]) diff --git a/packages/init/README.md b/packages/init/README.md index 892fb2f50..443ddcbd3 100644 --- a/packages/init/README.md +++ b/packages/init/README.md @@ -25,10 +25,10 @@ Supported options The initializer supports the following project configurations: - - **Web frameworks**: [Hono], [Nitro], [Next.js], [Elysia], [Express] + - **Web frameworks**: Bare-bones, [Hono], [Nitro], [Next.js], [Elysia], [Express] - **Package managers**: Deno, pnpm, Bun, Yarn, npm - - **Key-value stores**: Deno KV, Redis, PostgreSQL - - **Message queues**: Deno KV, Redis, PostgreSQL, AMQP + - **Key-value stores**: In-Memory, Deno KV, Redis, PostgreSQL + - **Message queues**: In-Process, Deno KV, Redis, PostgreSQL, AMQP [Hono]: https://hono.dev/ [Nitro]: https://nitro.build/ diff --git a/packages/init/src/const.ts b/packages/init/src/const.ts index 0cbfec5b9..36a9636db 100644 --- a/packages/init/src/const.ts +++ b/packages/init/src/const.ts @@ -1,11 +1,21 @@ +import kv from "./json/kv.json" with { type: "json" }; +import mq from "./json/mq.json" with { type: "json" }; + +/** All supported package manager identifiers, in display order. */ export const PACKAGE_MANAGER = ["deno", "pnpm", "bun", "yarn", "npm"] as const; export const WEB_FRAMEWORK = [ + "bare-bones", "hono", "nitro", "next", "elysia", "express", ] as const; -export const MESSAGE_QUEUE = ["denokv", "redis", "postgres", "amqp"] as const; -export const KV_STORE = ["denokv", "redis", "postgres"] as const; + +/** All supported message queue backend identifiers. */ +export const MESSAGE_QUEUE = Object.keys(mq) as readonly (keyof typeof mq)[]; + +/** All supported key-value store backend identifiers. */ +export const KV_STORE = Object.keys(kv) as readonly (keyof typeof kv)[]; + export const DB_TO_CHECK = ["redis", "postgres", "amqp"] as const; diff --git a/packages/init/src/json/kv.json b/packages/init/src/json/kv.json index fbbb79230..fc79cd4a0 100644 --- a/packages/init/src/json/kv.json +++ b/packages/init/src/json/kv.json @@ -1,4 +1,12 @@ { + "in-memory": { + "label": "In-Memory", + "packageManagers": ["deno", "bun", "npm", "yarn", "pnpm"], + "imports": { + "@fedify/fedify": { "MemoryKvStore": "MemoryKvStore" } + }, + "object": "new MemoryKvStore()" + }, "redis": { "label": "Redis", "packageManagers": ["deno", "bun", "npm", "yarn", "pnpm"], diff --git a/packages/init/src/json/mq.json b/packages/init/src/json/mq.json index 2dd6143e8..a65bd5bf7 100644 --- a/packages/init/src/json/mq.json +++ b/packages/init/src/json/mq.json @@ -1,4 +1,20 @@ { + "in-process": { + "label": "In-Process", + "packageManagers": [ + "deno", + "bun", + "npm", + "yarn", + "pnpm" + ], + "imports": { + "@fedify/fedify": { + "InProcessMessageQueue": "InProcessMessageQueue" + } + }, + "object": "new InProcessMessageQueue()" + }, "redis": { "label": "Redis", "packageManagers": [ diff --git a/packages/init/src/lib.ts b/packages/init/src/lib.ts index f3b05f49f..f3dbcc57e 100644 --- a/packages/init/src/lib.ts +++ b/packages/init/src/lib.ts @@ -39,11 +39,15 @@ const addFedifyDeps = (json: T): T => key, toMerged(value, { dependencies: { - [`@fedify/${key}`]: PACKAGE_VERSION, + ...(NO_INTEGRATIONS.includes(key) ? {} : { + [`@fedify/${key}`]: PACKAGE_VERSION, + }), }, }), ]), ) as T; +const NO_INTEGRATIONS = ["in-memory", "in-process", "bare-bones"]; + export const kvStores = addFedifyDeps(kv as KvStores); export const messageQueues = addFedifyDeps(mq as MessageQueues); const toRegExp = (str: string): RegExp => new RegExp(str); diff --git a/packages/init/src/templates/bare-bones/main/bun.ts.tpl b/packages/init/src/templates/bare-bones/main/bun.ts.tpl new file mode 100644 index 000000000..d68ef5b54 --- /dev/null +++ b/packages/init/src/templates/bare-bones/main/bun.ts.tpl @@ -0,0 +1,16 @@ +import { behindProxy } from "x-forwarded-fetch"; +import federation from "./federation.ts"; +import "./logging.ts"; + +const server = Bun.serve({ + port: 8000, + fetch: behindProxy((req) => + new URL(req.url).pathname === "/" + ? new Response("Hello, this is a Fedify server!", { + headers: { "Content-Type": "text/plain" }, + }) + : federation.fetch(req, { contextData: undefined }) + ), +}); + +console.log("Server started at", server.url.href); diff --git a/packages/init/src/templates/bare-bones/main/deno.ts.tpl b/packages/init/src/templates/bare-bones/main/deno.ts.tpl new file mode 100644 index 000000000..7d1c08896 --- /dev/null +++ b/packages/init/src/templates/bare-bones/main/deno.ts.tpl @@ -0,0 +1,19 @@ +import "@std/dotenv/load"; +import { behindProxy } from "@hongminhee/x-forwarded-fetch"; +import federation from "./federation.ts"; +import "./logging.ts"; + +Deno.serve( + { + port: 8000, + onListen: ({ port, hostname }) => + console.log("Server started at http://" + hostname + ":" + port) + }, + behindProxy((req) => + new URL(req.url).pathname === "/" + ? new Response("Hello, this is a Fedify server!", { + headers: { "Content-Type": "text/plain" }, + }) + : federation.fetch(req, { contextData: undefined }) + ), +); diff --git a/packages/init/src/templates/bare-bones/main/node.ts.tpl b/packages/init/src/templates/bare-bones/main/node.ts.tpl new file mode 100644 index 000000000..fc65e76b1 --- /dev/null +++ b/packages/init/src/templates/bare-bones/main/node.ts.tpl @@ -0,0 +1,19 @@ +import { serve } from "@hono/node-server"; +import { behindProxy } from "x-forwarded-fetch"; +import federation from "./federation.ts"; +import "./logging.ts"; + +serve( + { + port: 8000, + fetch: behindProxy((req) => + new URL(req.url).pathname === "/" + ? new Response("Hello, this is a Fedify server!", { + headers: { "Content-Type": "text/plain" }, + }) + : federation.fetch(req, { contextData: undefined }) + ), + }, + (info) => + console.log("Server started at http://" + info.address + ":" + info.port) +); diff --git a/packages/init/src/test/lookup.ts b/packages/init/src/test/lookup.ts index c070c0ba6..f372ebbc7 100644 --- a/packages/init/src/test/lookup.ts +++ b/packages/init/src/test/lookup.ts @@ -6,7 +6,6 @@ import { createWriteStream } from "node:fs"; import { join, sep } from "node:path"; import process from "node:process"; import type Stream from "node:stream"; -import { printErrorMessage, printMessage, runSubCommand } from "../utils.ts"; import { getDevCommand } from "../lib.ts"; import type { KvStore, @@ -14,6 +13,7 @@ import type { PackageManager, WebFramework, } from "../types.ts"; +import { printErrorMessage, printMessage, runSubCommand } from "../utils.ts"; import webFrameworks from "../webframeworks.ts"; const HANDLE = "john"; @@ -80,6 +80,7 @@ async function testApp(dir: string): Promise { const result = await serverClosure( dir, getDevCommand(pm), + webFrameworks[wf].defaultPort, sendLookup, ); @@ -154,6 +155,7 @@ async function waitForServer(url: string, timeout: number): Promise { async function serverClosure( dir: string, cmd: string, + defaultPort: number, callback: (port: number) => Promise, ): Promise> { // Start the dev server using Node.js spawn @@ -172,7 +174,11 @@ async function serverClosure( serverProcess.stderr?.pipe(stderr); try { - const port = await determinePort(serverProcess); + const port = await determinePort(serverProcess).catch((err) => { + printErrorMessage`Failed to determine server port: ${err.message}`; + printErrorMessage`Use default port ${String(defaultPort)} for lookup.`; + return defaultPort; + }); return await callback(port); } finally { try { diff --git a/packages/init/src/webframeworks.ts b/packages/init/src/webframeworks.ts index 60b949cda..f77cd4dfa 100644 --- a/packages/init/src/webframeworks.ts +++ b/packages/init/src/webframeworks.ts @@ -12,6 +12,73 @@ import type { WebFrameworks } from "./types.ts"; import { replace } from "./utils.ts"; const webFrameworks: WebFrameworks = { + "bare-bones": { + label: "Bare-bones", + packageManagers: PACKAGE_MANAGER, + defaultPort: 8000, + init: async ({ packageManager: pm }) => ({ + dependencies: pm === "deno" + ? { + ...defaultDenoDependencies, + "@std/dotenv": "^0.225.2", + "@hongminhee/x-forwarded-fetch": "^0.2.0", + } + : pm === "bun" + ? { "npm:x-forwarded-fetch": "^0.2.0" } + : { + "npm:@dotenvx/dotenvx": "^1.14.1", + "npm:@hono/node-server": "^1.12.0", + "npm:tsx": "^4.17.0", + "npm:x-forwarded-fetch": "^0.2.0", + }, + devDependencies: { + ...defaultDevDependencies, + ...(pm === "bun" + ? { "@types/bun": "^1.1.6" } + : { "@types/node": "^18.0.0" }), + }, + federationFile: "src/federation.ts", + loggingFile: "src/logging.ts", + files: { + "src/main.ts": await readTemplate( + `bare-bones/main/${packageManagerToRuntime(pm)}.ts`, + ), + ...(pm !== "deno" + ? { + "eslint.config.ts": await readTemplate("defaults/eslint.config.ts"), + } + : {}), + }, + compilerOptions: (pm === "deno" + ? { + "jsx": "precompile", + "jsxImportSource": "hono/jsx", + } + : { + "lib": ["ESNext", "DOM"], + "target": "ESNext", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + "strict": true, + }) as Record, + tasks: { + "dev": pm === "deno" + ? "deno run -A --watch ./src/main.ts" + : pm === "bun" + ? "bun run --hot ./src/main.ts" + : "dotenvx run -- tsx watch ./src/main.ts", + "prod": pm === "deno" + ? "deno run -A ./src/main.ts" + : pm === "bun" + ? "bun run ./src/main.ts" + : "dotenvx run -- node --import tsx ./src/main.ts", + }, + instruction: getInstruction(pm, 8000), + }), + }, hono: { label: "Hono", packageManagers: PACKAGE_MANAGER,