diff --git a/.plans/files-gateway-asset-index.md b/.plans/files-gateway-asset-index.md deleted file mode 100644 index 8cd0a3f6..00000000 --- a/.plans/files-gateway-asset-index.md +++ /dev/null @@ -1,450 +0,0 @@ -# Authenticated Files gateway and asset index - -## Status - -Implemented; awaiting review. - -## Objective - -Exercise one complete authenticated file lifecycle through the API application: - -1. an authenticated user uploads a file through Files SDK; -2. the object is written to the configured S3-compatible bucket; -3. a Files SDK `onAction` hook records the object's ownership and metadata in the - `storage.assets` table; -4. another user cannot access the object; and -5. deleting the object removes the asset record. - -Keep the Files SDK gateway at the versionless `/files` transport seam. Reserve -`/v1/assets` for a future application-owned HTTP interface over asset IDs, ownership, -organizations, metadata, and references. - -## Acceptance criteria - -- The Files SDK gateway is mounted at `/files`, not `/v1/files`. -- `apps/api/src/routes/files.ts` owns the Files SDK router and authorization policy. -- The existing Files SDK singleton in `apps/api/src/shared/files.ts` owns the inline - asset-index hook. -- The `/v1` router contains only application-owned HTTP routes. -- Hono's existing `contextStorage()` and `requireSession` middleware provide the - authenticated session, database, and logger to Files SDK callbacks. -- Shared `context()` and `AuthenticatedAppContext` definitions provide typed access to - that request state outside ordinary Hono handlers. -- The route does not perform a second Better Auth session lookup inside `authorize`. -- Successful upload completion creates or updates one `storage.assets` row owned by - the authenticated user. -- Successful deletion removes the matching asset row owned by the authenticated user. -- `onAction` dispatches successful events to local `handleUpload` and `handleDelete` - functions in the singleton module; there is no persistence service or custom Files - SDK plugin. -- Repeated upload-completion events are idempotent through the unique index on `key`. -- Database failures from the fire-and-forget hook are caught and logged without - producing unhandled promise rejections. -- The template contains exactly one baseline database migration generated from the - current schema. -- Formatting, static checks, dependency analysis, and relevant build verification - pass. - -## Decisions - -### Treat `/files` as transport infrastructure - -The Files SDK gateway's wire format is controlled by Files SDK. It is transport and -protocol plumbing, like Better Auth, tRPC, and Inngest, rather than an -application-owned REST resource. - -Use this route split: - -| Route | Ownership | -| ------------ | ------------------------------------------ | -| `/files` | Files SDK transport gateway | -| `/auth` | Better Auth transport gateway | -| `/trpc` | tRPC transport gateway | -| `/workflows` | Inngest transport gateway | -| `/v1/assets` | Future application-owned asset interface | -| `/v1/me` | Application-owned authenticated HTTP route | - -Do not add `/v1/assets` as part of this slice. Its route is reserved for later domain -behavior and should not proxy Files SDK operations. - -### Use the database as an eventually consistent asset index - -Object storage remains the source of truth for whether bytes exist. The assets table -tracks which authenticated user owns each stored object and provides a stable asset ID -for future references from other tables. - -Files SDK's `onAction` hook is observational and fire-and-forget: Files SDK does not -await a promise returned by the hook, and a hook failure cannot fail the storage -operation. Therefore: - -- the upload response does not guarantee that the asset row is already visible; -- every hook-owned promise must have an explicit rejection handler; -- stronger delivery guarantees or reconciliation are follow-up work, not hidden inside - this first slice. - -### Dispatch `onAction` events to local handlers - -Keep the persistence behavior local to the Files singleton. Do not introduce an -`assetPersistence` plugin, repository wrapper, or callback adapter. - -Use a switch to keep the hook concise. Single upload and head events call -`handleUpload`; delete events normalize their single or bulk keys and call -`handleDelete`. These handlers remain private functions in `shared/files.ts` and -execute the Drizzle operations directly. - -### Use a Files-SDK-shaped asset record - -The `storage.assets` table stores the stable asset ID, Files SDK metadata, and two -distinct user roles: - -- `key`, `name`, `size`, `type`, `etag`, `lastModified`, and `metadata` mirror the - stored file returned by Files SDK; -- `ownerId` records the user to whom the asset belongs; and -- `uploaderId` records the user who placed the asset in managed storage. - -The current upload flow assigns the authenticated user to both roles. Keeping them -separate preserves creation provenance if ownership changes later. The configured -Files singleton owns one bucket, so the storage key is the unique object identity and -bucket/provider columns are unnecessary. - -### Observe upload and head completion - -Handle successful `upload` events and successful `head` events. Direct/proxied uploads -can produce an upload result, while completion of a direct or presigned upload can be -confirmed through a follow-up `head`. The unique `key` conflict target makes both paths -idempotent. - -Do not support copy or move until their destination-ownership behavior is explicitly -defined. Do not build the index from list results. - -## Implementation plan - -### 1. Move the Files SDK gateway out of `/v1` - -Move: - -```text -apps/api/src/routes/v1/files.ts -``` - -to: - -```text -apps/api/src/routes/files.ts -``` - -Add the route to `apps/api/src/routes/index.ts`: - -```diff -+import filesRoutes from "#routes/files.ts" - import healthRoutes from "#routes/health.ts" - import trpcRoutes from "#routes/trpc.ts" - import v1Routes from "#routes/v1/index.ts" - import workflowRoutes from "#routes/workflows.ts" - - export const router = app - // Keep the existing root and OpenAPI handlers. - .route("/health", healthRoutes) -+ .route("/files", filesRoutes) - .route("/workflows", workflowRoutes) - .route("/trpc", trpcRoutes) - .route("/v1", v1Routes) -``` - -Remove the transport route from `apps/api/src/routes/v1/index.ts`: - -```diff --import filesRoutes from "#routes/v1/files.ts" - import { m } from "#shared/internationalization/messages.js" - - export default factory - .createApp() -- .route("/files", filesRoutes) - .get( - "/hello", - // Keep the existing handler unchanged. - ) - .get("/me", requireSession, (c) => c.json(c.var.session.user)) -``` - -### 2. Add shared typed context access - -Add the authenticated refinement to `apps/api/src/shared/types.ts`: - -```ts -export type AuthenticatedAppContext = DeepMerge -``` - -Restore the context-storage helper in `apps/api/src/shared/utils.ts`, allowing callers -to select a refined context while defaulting to `AppContext`: - -```ts -export function context() { - return getContext() -} -``` - -Make `requireSession` consume the shared authenticated type instead of declaring the -same merge inline: - -```ts -export const requireSession = createMiddleware(async (c, next) => { - // Keep the existing middleware implementation. -}) -``` - -### 3. Add `onAction` directly to the Files singleton - -Replace `apps/api/src/shared/files.ts` with this proposed implementation: - -```ts -import { assets, type UserId } from "@init/db/schema" -import * as z from "@init/utils/schema" -import { helpers } from "@init/db/helpers" -import type { DeleteManyResult, StoredFile, UploadResult } from "files-sdk" -import { createFiles } from "files-sdk" -import { bunS3 } from "files-sdk/bun-s3" -import { contentType } from "files-sdk/content-type" -import { signedUrlPolicy } from "files-sdk/signed-url-policy" -import { validation } from "files-sdk/validation" -import env from "#shared/env.ts" -import type { AuthenticatedAppContext } from "#shared/types.ts" -import { context } from "#shared/utils.ts" - -export const FILES_MAX_UPLOAD_SIZE = 10 * 1024 * 1024 -export const FILES_MAX_URL_AGE = 15 * 60 - -export const files = createFiles({ - adapter: bunS3({ - accessKeyId: env.S3_ACCESS_KEY_ID, - bucket: env.S3_BUCKET, - endpoint: env.S3_ENDPOINT, - region: env.S3_REGION, - secretAccessKey: env.S3_SECRET_ACCESS_KEY, - virtualHostedStyle: !env.S3_ENDPOINT, - }), - hooks: { - onAction(event) { - if (event.status !== "success") return - - switch (event.type) { - case "upload": - if (event.key) handleUpload(event.key, event.result as UploadResult) - break - case "head": - if (event.key) handleUpload(event.key, event.result as StoredFile) - break - case "delete": { - const keys = event.key ? [event.key] : (event.result as DeleteManyResult).deleted - - handleDelete(keys) - break - } - default: - break - } - }, - }, - plugins: [ - signedUrlPolicy({ - maxExpiresIn: FILES_MAX_URL_AGE, - maxUploadSize: FILES_MAX_UPLOAD_SIZE, - }), - validation({ - allowedTypes: ["image/*", "application/pdf"], - key: (key) => - z - .string() - .regex(/^[\w.-]+(?:\/[\w.-]+)*$/u) - .refine((value) => - value.split("/").every((segment) => segment !== "." && segment !== "..") - ) - .safeParse(key).success, - maxSize: FILES_MAX_UPLOAD_SIZE, - minSize: 1, - }), - contentType({ onMismatch: "reject" }), - ], -}) - -function handleUpload(key: string, file: UploadResult | StoredFile) { - const ctx = context() - const mimeType = "contentType" in file ? file.contentType : file.type - const name = "name" in file ? file.name : (key.split("/").at(-1) ?? key) - - void ctx.var.db - .insert(assets) - .values({ - etag: file.etag, - key, - lastModified: file.lastModified, - metadata: "metadata" in file ? file.metadata : undefined, - name, - ownerId: ctx.var.session.user.id as UserId, - size: file.size, - type: mimeType, - uploaderId: ctx.var.session.user.id as UserId, - }) - .onConflictDoUpdate({ - set: { - etag: file.etag, - lastModified: file.lastModified, - metadata: "metadata" in file ? file.metadata : undefined, - name, - size: file.size, - type: mimeType, - updatedAt: new Date(), - }, - target: assets.key, - }) - .catch((error: unknown) => { - ctx.var.logger.error(`Failed to record asset: ${String(error)}`) - }) -} - -function handleDelete(keys: string[]) { - if (keys.length === 0) return - - const ctx = context() - - void ctx.var.db - .delete(assets) - .where( - helpers.and( - helpers.inArray(assets.key, keys), - helpers.eq(assets.ownerId, ctx.var.session.user.id as UserId) - ) - ) - .catch((error: unknown) => { - ctx.var.logger.error(`Failed to delete asset records: ${String(error)}`) - }) -} -``` - -The singleton is still constructed once at module initialization. Only the `onAction` -callback reads Hono context, and it does so when Files SDK invokes the singleton during -an authenticated request. - -This intentionally makes action-producing use of this singleton request-bound: a -future caller that invokes it outside Hono context would not have a session to assign -as `ownerId` and `uploaderId` and must use a separately configured Files instance or -establish an equivalent ownership context. - -### 4. Implement the complete versionless gateway route - -Use this proposed implementation for `apps/api/src/routes/files.ts`: - -```ts -import { createFilesRouter } from "files-sdk/api" -import { createRouteHandler } from "files-sdk/hono" -import env from "#shared/env.ts" -import { files, FILES_MAX_UPLOAD_SIZE, FILES_MAX_URL_AGE } from "#shared/files.ts" -import { requireSession } from "#shared/middleware.ts" -import type { AuthenticatedAppContext } from "#shared/types.ts" -import { context, factory } from "#shared/utils.ts" - -const router = createFilesRouter({ - allowedOrigins: env.ALLOWED_API_ORIGINS, - authorize: () => { - const ctx = context() - - return { - disposition: "attachment", - keyPrefix: `users/${ctx.var.session.user.id}/`, - maxExpiresIn: FILES_MAX_URL_AGE, - maxResults: 100, - } - }, - files, - maxListLimit: 100, - maxSearchResults: 100, - maxUploadSize: FILES_MAX_UPLOAD_SIZE, - operations: [ - "capabilities", - "delete", - "download", - "exists", - "head", - "list", - "search", - "signedUploadUrl", - "upload", - "url", - ], - secret: env.FILES_API_SECRET, -}) - -export default factory.createApp().all("/", requireSession, createRouteHandler(router)) -``` - -The installed Files SDK router applies `keyPrefix` before invoking `files.upload`, -`files.head`, and `files.delete`. The hook therefore observes storage keys such as -`users//avatar.png`, while gateway responses remove the prefix for clients. - -The singleton's `event.key` guards intentionally ignore bulk head events because reads -are not the asset-creation seam. Bulk deletion uses `DeleteManyResult.deleted`, so only -keys that were successfully removed change status. - -`requireSession` runs before `createRouteHandler`, allowing both `authorize` and -`onAction` to read the session from Hono context storage. Remove the old direct `auth` -import, `auth.api.getSession()` call, and `FilesError` authorization branch. - -### 5. Expose Drizzle through the database package - -Keep Drizzle's package identity behind `@init/db` and expose its helpers as a namespace -from `packages/db/src/helpers.ts`: - -```ts -export * as helpers from "drizzle-orm" -``` - -The API imports `helpers` from `@init/db/helpers`; it does not declare a separate direct -Drizzle dependency. - -### 6. Limit the initial operation surface - -The concrete `operations` list above permits upload, direct/presigned upload, metadata, -listing, download, URL, and deletion. It excludes `copy` and `move` because the hook -does not update destination ownership, and excludes versioning and trash operations -because the configured Files instance does not install those plugins. - -### 7. Regenerate the baseline migration - -Delete the existing migration SQL and metadata, then run `bun run generate` from -`packages/db`. The template deliberately keeps one starting migration and does not -preserve migration history for databases created from earlier template revisions. - -### 8. Verify the change - -Run the repository-managed static checks: - -```sh -bun run format -bun run check -bun run analyze -bun run check:monorepo -``` - -The API workspace currently has no build task, so do not add or invoke one as part of -this change. - -## Explicitly out of scope - -- Implementing `/v1/assets`. -- Making the asset table the source of truth for object existence. -- Transactional guarantees between object storage and Postgres. -- A reconciliation worker for missing or stale asset rows. -- Copy and move ownership semantics. -- Organization-owned asset authorization. -- Asset labels, references, transformations, or processing pipelines. - -## Follow-up work - -- Design the application-owned `/v1/assets` interface around asset IDs rather than raw - storage keys. -- Decide whether ownership should expand from a User owner to a polymorphic owner or - separate user/organization ownership records. -- Add reconciliation if production requirements demand repair of missed hook writes. -- Define database behavior for copy, move, bulk operations, and failed uploads. -- Add domain references from future records to `assets.id` instead of duplicating - bucket keys. diff --git a/apps/api/.env.template b/apps/api/.env.template index 6413c99c..cc8888ea 100644 --- a/apps/api/.env.template +++ b/apps/api/.env.template @@ -1,8 +1,8 @@ # ===========PROJECT=========== # Add specific project environment variables here -BASE_URL="http://localhost:3000" -ALLOWED_API_ORIGINS="http://localhost:3001" +BASE_URL="https://api.init.localhost" +ALLOWED_API_ORIGINS="https://init.localhost" PORT="3000" # --- Files SDK / S3 --- @@ -18,7 +18,7 @@ S3_SECRET_ACCESS_KEY="minioadmin" # --- Auth (@init/env/auth) --- AUTH_SECRET="dev-secret" -AUTH_TRUSTED_ORIGINS="http://localhost:3001,http://localhost:3000" +AUTH_TRUSTED_ORIGINS="https://init.localhost,https://api.init.localhost" # --- Auth Providers (@init/env/auth/providers) --- # GitHub OAuth diff --git a/apps/api/package.json b/apps/api/package.json index 531389f2..a9563c99 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -13,7 +13,8 @@ "bump:deps": "bun update --interactive", "clean": "git clean -xdf .cache .turbo dist node_modules", "codegen": "paraglide-js compile --project ../../tooling/internationalization/project.inlang --outdir ./src/shared/internationalization", - "dev": "bun --hot src/index.ts", + "dev": "portless", + "dev:app": "bun --hot src/index.ts", "start": "bun --bun src/index.ts" }, "dependencies": { @@ -43,5 +44,9 @@ "@tooling/tsconfig": "workspace:*", "@types/bun": "1.3.14", "typescript": "7.0.2" + }, + "portless": { + "name": "api.init", + "script": "dev:app" } } diff --git a/apps/api/src/routes/files.ts b/apps/api/src/routes/files.ts index 364b6a41..58c5b3f2 100644 --- a/apps/api/src/routes/files.ts +++ b/apps/api/src/routes/files.ts @@ -4,10 +4,10 @@ import type { AuthenticatedAppContext } from "#shared/types.ts" import env from "#shared/env.ts" import { files, FILES_MAX_UPLOAD_SIZE, FILES_MAX_URL_AGE } from "#shared/files.ts" import { requireSession } from "#shared/middleware.ts" -import { context, factory } from "#shared/utils.ts" +import { allowedOrigins, context, factory } from "#shared/utils.ts" const router = createFilesRouter({ - allowedOrigins: env.ALLOWED_API_ORIGINS, + allowedOrigins, authorize: () => { const ctx = context() diff --git a/apps/api/src/routes/index.ts b/apps/api/src/routes/index.ts index 91d4b6e3..18ff83e7 100644 --- a/apps/api/src/routes/index.ts +++ b/apps/api/src/routes/index.ts @@ -14,11 +14,10 @@ import trpcRoutes from "#routes/trpc.ts" import v1Routes from "#routes/v1/index.ts" import workflowRoutes from "#routes/workflows.ts" import { auth } from "#shared/auth.ts" -import env from "#shared/env.ts" import { files } from "#shared/files.ts" import { LoggerCategory, logger } from "#shared/logger.ts" import { withLanguageDetection } from "#shared/middleware.ts" -import { factory } from "#shared/utils.ts" +import { allowedOrigins, factory } from "#shared/utils.ts" const app = factory.createApp() @@ -37,7 +36,7 @@ app.use( credentials: true, exposeHeaders: ["Content-Length"], maxAge: 600, - origin: env.ALLOWED_API_ORIGINS, + origin: allowedOrigins, }) ) diff --git a/apps/api/src/shared/auth.ts b/apps/api/src/shared/auth.ts index 7f69f99b..6e1ea7b0 100644 --- a/apps/api/src/shared/auth.ts +++ b/apps/api/src/shared/auth.ts @@ -10,12 +10,13 @@ import { database } from "@init/db/client" import { sendEmail } from "@init/email/client" import PasswordReset from "@init/email/templates/password-reset" import env from "#shared/env.ts" +import { allowedOrigins, baseUrl } from "#shared/utils.ts" export const auth = createAuth({ advanced: AUTH_ADVANCED_OPTIONS, appName: AUTH_APP_NAME, basePath: "/auth", - baseURL: env.BASE_URL, + baseURL: baseUrl, database: databaseAdapter(database()), emailAndPassword: { ...AUTH_EMAIL_AND_PASSWORD_OPTIONS, @@ -41,7 +42,7 @@ export const auth = createAuth({ enabled: true, }, }, - trustedOrigins: env.ALLOWED_API_ORIGINS, + trustedOrigins: allowedOrigins, }) export type Auth = typeof auth diff --git a/apps/api/src/shared/env.ts b/apps/api/src/shared/env.ts index bce5d53d..fc1f617d 100644 --- a/apps/api/src/shared/env.ts +++ b/apps/api/src/shared/env.ts @@ -1,5 +1,5 @@ import { createEnv } from "@init/env" -import { auth, db, inngest, kv, resend, s3, sentry } from "@init/env/presets" +import { auth, db, inngest, kv, portless, resend, s3, sentry } from "@init/env/presets" import * as z from "@init/utils/schema" import { isCI } from "std-env" @@ -11,6 +11,7 @@ export default createEnv({ auth.providers.google(), db(), kv(), + portless(), inngest(), resend(), s3(), diff --git a/apps/api/src/shared/utils.ts b/apps/api/src/shared/utils.ts index 728c22d0..65b4e7b5 100644 --- a/apps/api/src/shared/utils.ts +++ b/apps/api/src/shared/utils.ts @@ -1,6 +1,19 @@ import { getContext } from "hono/context-storage" import { createFactory } from "hono/factory" import type { AppContext } from "#shared/types.ts" +import env from "#shared/env.ts" + +export const baseUrl = env.PORTLESS_URL ?? env.BASE_URL + +export const allowedOrigins = env.PORTLESS_URL + ? env.ALLOWED_API_ORIGINS.map( + (origin) => + new URL( + env.PORTLESS_URL?.replace(new URL(env.BASE_URL).hostname, new URL(origin).hostname) ?? + origin + ).origin + ) + : env.ALLOWED_API_ORIGINS /** * A utility function to create Hono apps and middlewares with the correct context type. diff --git a/apps/app/.env.template b/apps/app/.env.template index 70aa393b..12839f92 100644 --- a/apps/app/.env.template +++ b/apps/app/.env.template @@ -2,11 +2,11 @@ # Add specific project environment variables here # Client-side variables (PUBLIC_ prefix for Vite) -PUBLIC_BASE_URL="http://localhost:3001" +PUBLIC_BASE_URL="https://init.localhost" # --- Auth (@init/env/auth) --- AUTH_SECRET="dev-secret" -AUTH_TRUSTED_ORIGINS="http://localhost:3001" +AUTH_TRUSTED_ORIGINS="https://init.localhost" # --- Auth Providers (@init/env/auth/providers) --- GITHUB_CLIENT_ID="dev-github-client-id" diff --git a/apps/app/package.json b/apps/app/package.json index 09be99d1..b2505761 100644 --- a/apps/app/package.json +++ b/apps/app/package.json @@ -11,7 +11,8 @@ "bump:deps": "bun update --interactive", "clean": "git clean -xdf .cache .turbo dist node_modules", "codegen": "paraglide-js compile --project ../../tooling/internationalization/project.inlang --outdir ./src/shared/internationalization", - "dev": "bun --bun vite dev", + "dev": "portless", + "dev:app": "bun --bun vite dev", "preview": "bun --bun vite preview", "start": "node .output/server/index.mjs" }, @@ -56,5 +57,9 @@ "tailwindcss": "4.3.3", "typescript": "7.0.2", "vite": "8.1.5" + }, + "portless": { + "name": "init", + "script": "dev:app" } } diff --git a/apps/app/src/features/auth/server/index.ts b/apps/app/src/features/auth/server/index.ts index aa252f5a..8d22223f 100644 --- a/apps/app/src/features/auth/server/index.ts +++ b/apps/app/src/features/auth/server/index.ts @@ -12,11 +12,15 @@ import { sendEmail } from "@init/email/client" import PasswordReset from "@init/email/templates/password-reset" import env from "#shared/env.ts" +const trustedOrigins = env.PORTLESS_URL + ? [...new Set([...env.AUTH_TRUSTED_ORIGINS, env.PORTLESS_URL])] + : env.AUTH_TRUSTED_ORIGINS + export const auth = createAuth({ advanced: AUTH_ADVANCED_OPTIONS, appName: AUTH_APP_NAME, basePath: "/api/auth", - baseURL: env.PUBLIC_BASE_URL, + baseURL: env.PORTLESS_URL ?? env.PUBLIC_BASE_URL, database: databaseAdapter(database()), emailAndPassword: { ...AUTH_EMAIL_AND_PASSWORD_OPTIONS, @@ -42,5 +46,5 @@ export const auth = createAuth({ enabled: true, }, }, - trustedOrigins: env.AUTH_TRUSTED_ORIGINS, + trustedOrigins, }) diff --git a/apps/app/src/shared/env.ts b/apps/app/src/shared/env.ts index 8d2f0471..f63b797a 100644 --- a/apps/app/src/shared/env.ts +++ b/apps/app/src/shared/env.ts @@ -1,5 +1,5 @@ import { createEnv, REACT_PUBLIC_ENV_PREFIX } from "@init/env" -import { auth, db, resend, sentry } from "@init/env/presets" +import { auth, db, portless, resend, sentry } from "@init/env/presets" import * as z from "@init/utils/schema" import { env, isCI } from "std-env" @@ -14,6 +14,7 @@ export default createEnv({ auth.providers.github(), auth.providers.google(), db(), + portless(), resend(), sentry.client(), ], diff --git a/apps/app/src/shared/utils.ts b/apps/app/src/shared/utils.ts index b5205f99..ddcbe475 100644 --- a/apps/app/src/shared/utils.ts +++ b/apps/app/src/shared/utils.ts @@ -1,10 +1,14 @@ import { createUrlBuilder } from "@init/utils/url" +import { hasWindow } from "std-env" import env from "#shared/env.ts" -const isProduction = import.meta.env.PROD +const baseUrl = hasWindow ? globalThis.location.origin : (env.PORTLESS_URL ?? env.PUBLIC_BASE_URL) -export const buildUrl = createUrlBuilder(env.PUBLIC_BASE_URL, isProduction ? "https" : "http") -export const buildApiUrl = createUrlBuilder( - env.PUBLIC_API_URL ?? `${env.PUBLIC_BASE_URL}/api`, - isProduction ? "https" : "http" -) +export const buildUrl = createUrlBuilder(baseUrl, getProtocol(baseUrl)) + +const apiUrl = env.PUBLIC_API_URL ?? `${baseUrl}/api` +export const buildApiUrl = createUrlBuilder(apiUrl, getProtocol(apiUrl)) + +function getProtocol(url: string) { + return new URL(url).protocol === "http:" ? "http" : "https" +} diff --git a/apps/app/vite.config.ts b/apps/app/vite.config.ts index a5d45555..7d840b42 100644 --- a/apps/app/vite.config.ts +++ b/apps/app/vite.config.ts @@ -31,7 +31,7 @@ export default defineConfig(async ({ mode }) => { }), ], server: { - port: 3001, + port: Number(process.env.PORT ?? 3001), }, } }) diff --git a/apps/desktop/package.json b/apps/desktop/package.json index b354ac85..3da83653 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -11,7 +11,8 @@ "bump:deps": "bun update --interactive", "clean": "git clean -xdf .cache .turbo dist node_modules", "codegen": "paraglide-js compile --project ../../tooling/internationalization/project.inlang --outdir ./src/shared/internationalization", - "dev": "tauri dev" + "dev": "portless", + "dev:app": "tauri dev" }, "dependencies": { "@init/env": "workspace:*", @@ -46,5 +47,9 @@ "babel-plugin-react-compiler": "1.0.0", "typescript": "7.0.2", "vite": "8.1.5" + }, + "portless": { + "name": "desktop.init", + "script": "dev:app" } } diff --git a/apps/desktop/src-tauri/tauri.conf.json b/apps/desktop/src-tauri/tauri.conf.json index 0ee0d9d9..f303e1d8 100644 --- a/apps/desktop/src-tauri/tauri.conf.json +++ b/apps/desktop/src-tauri/tauri.conf.json @@ -6,7 +6,7 @@ "build": { "beforeBuildCommand": "bun vite build", "beforeDevCommand": "bun vite dev", - "devUrl": "http://localhost:3003", + "devUrl": "https://desktop.init.localhost", "frontendDist": "../dist" }, "app": { diff --git a/apps/desktop/vite.config.ts b/apps/desktop/vite.config.ts index 4a065a65..e6613150 100644 --- a/apps/desktop/vite.config.ts +++ b/apps/desktop/vite.config.ts @@ -38,12 +38,11 @@ export default defineConfig(async ({ mode }) => { hmr: host ? { host, - port: 4003, protocol: "ws", } : undefined, host: host ?? false, - port: 3003, + port: Number(process.env.PORT ?? 3003), strictPort: true, watch: { ignored: ["**/src-tauri/**"], diff --git a/apps/docs/astro.config.ts b/apps/docs/astro.config.ts index 5ce9adfe..46133387 100644 --- a/apps/docs/astro.config.ts +++ b/apps/docs/astro.config.ts @@ -46,7 +46,7 @@ export default defineConfig({ }), ], server: { - port: 3004, + port: Number(process.env.PORT ?? 3004), }, vite: { plugins: [ diff --git a/apps/docs/package.json b/apps/docs/package.json index 98edf482..78612904 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -12,7 +12,8 @@ "bump:deps": "bun update --interactive", "clean": "git clean -xdf .cache .turbo dist node_modules", "codegen": "paraglide-js compile --project ../../tooling/internationalization/project.inlang --outdir ./src/shared/internationalization", - "dev": "astro dev", + "dev": "portless", + "dev:app": "astro dev", "preview": "astro preview", "typegen": "astro sync" }, @@ -29,5 +30,9 @@ "@tooling/tsconfig": "workspace:*", "tailwindcss": "4.3.3", "typescript": "7.0.2" + }, + "portless": { + "name": "docs.init", + "script": "dev:app" } } diff --git a/apps/extension/package.json b/apps/extension/package.json index 23d3bc1c..ffcf344a 100644 --- a/apps/extension/package.json +++ b/apps/extension/package.json @@ -12,7 +12,8 @@ "bump:deps": "bun update --interactive", "clean": "git clean -xdf .cache .turbo dist node_modules", "codegen": "paraglide-js compile --project ../../tooling/internationalization/project.inlang --outdir ./src/shared/internationalization", - "dev": "wxt", + "dev": "portless", + "dev:app": "wxt", "dev:firefox": "wxt -b firefox", "postinstall": "wxt prepare", "typegen": "wxt prepare", @@ -43,5 +44,9 @@ "vite": "8.1.5", "web-ext": "10.5.0", "wxt": "0.21.1" + }, + "portless": { + "name": "extension.init", + "script": "dev:app" } } diff --git a/apps/extension/wxt.config.ts b/apps/extension/wxt.config.ts index 4b313c05..3f1d505b 100644 --- a/apps/extension/wxt.config.ts +++ b/apps/extension/wxt.config.ts @@ -9,7 +9,7 @@ export default defineConfig({ baseIconPath: "shared/assets/icon.svg", }, dev: { - server: { port: 3005 }, + server: { port: Number(process.env.PORT ?? 3005) }, }, imports: false, modules: ["@wxt-dev/module-react", "@wxt-dev/auto-icons"], diff --git a/apps/mobile/package.json b/apps/mobile/package.json index 8a0c1b6b..52419880 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -11,8 +11,9 @@ "bump:deps": "bun update --interactive", "clean": "git clean -xdf .cache .turbo .expo dist node_modules", "codegen": "bun run scripts/codegen.ts", - "dev": "expo start --port 3002", + "dev": "portless", "dev:android": "expo start --android", + "dev:app": "expo start --port ${PORT:-3002}", "dev:ios": "expo start --ios", "doctor": "expo-doctor", "install:check": "expo install --check", @@ -71,5 +72,9 @@ "babel-preset-expo": "~54.0.0", "expo-doctor": "1.17.14", "typescript": "7.0.2" + }, + "portless": { + "name": "mobile.init", + "script": "dev:app" } } diff --git a/apps/web/.env.template b/apps/web/.env.template index e0d74ee2..289cf6f0 100644 --- a/apps/web/.env.template +++ b/apps/web/.env.template @@ -2,7 +2,7 @@ # Add specific project environment variables here # Client-side variables (PUBLIC_ prefix for Astro) -PUBLIC_SITE_URL="http://localhost:3006" +PUBLIC_SITE_URL="https://web.init.localhost" # ===========OPTIONAL=========== # Optional packages that may be used in the future diff --git a/apps/web/astro.config.ts b/apps/web/astro.config.ts index c7ac7b22..53709834 100644 --- a/apps/web/astro.config.ts +++ b/apps/web/astro.config.ts @@ -12,7 +12,7 @@ const { default: env } = await import("./src/shared/env.ts") export default defineConfig({ output: "static", server: { - port: 3006, + port: Number(process.env.PORT ?? 3006), }, site: env.PUBLIC_SITE_URL ?? "https://init.now", diff --git a/apps/web/package.json b/apps/web/package.json index 50014c0f..cc1f4fcb 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -12,7 +12,8 @@ "bump:deps": "bun update --interactive", "clean": "git clean -xdf .cache .turbo dist node_modules", "codegen": "paraglide-js compile --project ../../tooling/internationalization/project.inlang --outdir ./src/shared/internationalization", - "dev": "astro dev", + "dev": "portless", + "dev:app": "astro dev", "preview": "astro preview", "typegen": "astro sync" }, @@ -40,5 +41,9 @@ "@types/react-dom": "19.1.9", "tailwindcss": "4.3.3", "typescript": "7.0.2" + }, + "portless": { + "name": "web.init", + "script": "dev:app" } } diff --git a/bun.lock b/bun.lock index 949414a1..41e6b3f4 100644 --- a/bun.lock +++ b/bun.lock @@ -15,6 +15,7 @@ "oxfmt": "0.60.0", "oxlint": "1.75.0", "oxlint-tsgolint": "7.0.2001", + "portless": "0.15.5", "sherif": "1.13.0", "turbo": "2.10.7", "typescript": "7.0.2", @@ -518,6 +519,7 @@ }, "devDependencies": { "@tooling/tsconfig": "workspace:*", + "inngest-cli": "1.40.0", "typescript": "7.0.2", }, }, @@ -546,10 +548,11 @@ }, }, "trustedDependencies": [ + "@sentry/cli", "@tailwindcss/oxide", "extension", "docs", - "@sentry/cli", + "inngest-cli", ], "overrides": { "@types/react": "19.1.13", @@ -3304,6 +3307,8 @@ "inngest": ["inngest@3.49.3", "", { "dependencies": { "@bufbuild/protobuf": "^2.2.3", "@inngest/ai": "^0.1.3", "@jpwilliams/waitgroup": "^2.1.1", "@opentelemetry/api": "^1.9.0", "@opentelemetry/auto-instrumentations-node": ">=0.66.0 <1.0.0", "@opentelemetry/context-async-hooks": ">=2.0.0 <3.0.0", "@opentelemetry/exporter-trace-otlp-http": ">=0.200.0 <0.300.0", "@opentelemetry/instrumentation": ">=0.200.0 <0.300.0", "@opentelemetry/resources": ">=2.0.0 <3.0.0", "@opentelemetry/sdk-trace-base": ">=2.0.0 <3.0.0", "@standard-schema/spec": "^1.0.0", "@types/debug": "^4.1.12", "@types/ms": "~2.1.0", "canonicalize": "^1.0.8", "chalk": "^4.1.2", "cross-fetch": "^4.0.0", "debug": "^4.3.4", "hash.js": "^1.1.7", "json-stringify-safe": "^5.0.1", "ms": "^2.1.3", "serialize-error-cjs": "^0.1.3", "strip-ansi": "^5.2.0", "temporal-polyfill": "^0.2.5", "ulid": "^2.3.0", "zod": "^3.25.0" }, "peerDependencies": { "@sveltejs/kit": ">=1.27.3", "@vercel/node": ">=2.15.9", "aws-lambda": ">=1.0.7", "express": ">=4.19.2", "fastify": ">=4.21.0", "h3": ">=1.8.1", "hono": ">=4.2.7", "koa": ">=2.14.2", "next": ">=12.0.0", "typescript": ">=5.8.0" }, "optionalPeers": ["@sveltejs/kit", "@vercel/node", "aws-lambda", "express", "fastify", "h3", "hono", "koa", "next", "typescript"] }, "sha512-JH4VBcxmBh7J0QIk28yYNSlBs1q2wnlds20Sj4a1m8RXRSfDh+z6+Lq+WVpaHH0XolsPYwkRwUA9Gf540AcBmg=="], + "inngest-cli": ["inngest-cli@1.40.0", "", { "dependencies": { "adm-zip": "^0.5.10", "debug": "^4.3.4", "node-fetch": "^2.6.7", "tar": "^7.5.3" }, "bin": { "inngest-cli": "bin/inngest", "inngest": "bin/inngest" } }, "sha512-65dtJcvxq/XR9PuVab4gNC0ZdyAeyNf7mYe5A/L1WV2SgbqEaqQz3cd9ukOSWSlEP8684C1vwCM/wlZp7/eD5g=="], + "input-otp": ["input-otp@1.4.2", "", { "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-l3jWwYNvrEa6NTCt7BECfCm48GvwuZzkoeG3gBL2w4CHeOXW3eKFmf9UNYkNfYc3mxMrthMnxjIE07MT0zLBQA=="], "internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="], @@ -3966,6 +3971,8 @@ "pngjs": ["pngjs@3.4.0", "", {}, "sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w=="], + "portless": ["portless@0.15.5", "", { "os": [ "linux", "win32", "darwin", ], "bin": { "portless": "dist/cli.js" } }, "sha512-zmJu4Q8/fY54oVUT/5NnmF4Ih8wTdCvCf6JCN783dRYl9mXkJBzXSckX2lztGCLIbM70varDjCudAbGKT73XPg=="], + "postcss": ["postcss@8.5.23", "", { "dependencies": { "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg=="], "postcss-nested": ["postcss-nested@6.2.0", "", { "dependencies": { "postcss-selector-parser": "^6.1.1" }, "peerDependencies": { "postcss": "^8.2.14" } }, "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ=="], @@ -4750,7 +4757,7 @@ "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], - "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + "yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], "yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="], @@ -5756,8 +5763,6 @@ "tar/minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], - "tar/yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], - "terser/acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], "terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], @@ -5856,6 +5861,8 @@ "@babel/helper-annotate-as-pure/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + "@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + "@babel/helper-create-class-features-plugin/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.28.6", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q=="], "@babel/helper-create-class-features-plugin/@babel/traverse/@babel/generator": ["@babel/generator@7.28.6", "", { "dependencies": { "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw=="], @@ -7032,6 +7039,8 @@ "@babel/helper-create-class-features-plugin/@babel/traverse/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + "@babel/helper-define-polyfill-provider/@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + "@babel/helper-member-expression-to-functions/@babel/traverse/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], "@babel/helper-module-imports/@babel/traverse/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], @@ -7064,6 +7073,8 @@ "@babel/plugin-transform-async-generator-functions/@babel/traverse/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + "@babel/plugin-transform-classes/@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + "@babel/plugin-transform-classes/@babel/traverse/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], "@babel/plugin-transform-classes/@babel/traverse/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], @@ -7082,6 +7093,8 @@ "@babel/plugin-transform-destructuring/@babel/traverse/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + "@babel/plugin-transform-function-name/@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + "@babel/plugin-transform-function-name/@babel/traverse/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], "@babel/plugin-transform-function-name/@babel/traverse/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], @@ -7100,6 +7113,8 @@ "@babel/plugin-transform-modules-commonjs/@babel/helper-module-transforms/@babel/traverse/@babel/types": ["@babel/types@7.28.6", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg=="], + "@babel/plugin-transform-object-rest-spread/@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + "@babel/plugin-transform-object-rest-spread/@babel/traverse/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], "@babel/plugin-transform-object-rest-spread/@babel/traverse/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], @@ -7450,6 +7465,8 @@ "@expo/cli/ora/cli-cursor/restore-cursor/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + "@expo/metro-config/@babel/core/@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + "@expo/package-manager/ora/chalk/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], "@expo/package-manager/ora/chalk/supports-color/has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="], @@ -7462,6 +7479,12 @@ "@istanbuljs/load-nyc-config/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], + "@jest/transform/@babel/core/@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + + "@react-native/babel-preset/@babel/core/@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + + "@react-native/codegen/@babel/core/@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + "@react-native/codegen/yargs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "@react-native/dev-middleware/chrome-launcher/lighthouse-logger/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], @@ -7470,10 +7493,24 @@ "addons-linter/yargs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + "babel-dead-code-elimination/@babel/core/@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + + "istanbul-lib-instrument/@babel/core/@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + + "metro-babel-transformer/@babel/core/@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + + "metro-transform-plugins/@babel/core/@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + + "metro-transform-worker/@babel/core/@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + + "metro/@babel/core/@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + "metro/yargs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "react-native/yargs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + "shadcn/@babel/core/@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + "web-ext/yargs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "@expo/cli/ora/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], diff --git a/docs/development.md b/docs/development.md index bd012e2b..fddba9c5 100644 --- a/docs/development.md +++ b/docs/development.md @@ -11,20 +11,20 @@ See `docs/getting-started.md` for the authoritative versions. In short: These match the root `package.json` scripts. -| Command | Description | -| ---------------------- | -------------------------------------------- | -| `bun run dev` | Start all workspaces in development mode | -| `bun run dev:apps` | Start app workspaces in development mode | -| `bun run dev:packages` | Start package workspaces in development mode | -| `bun run build` | Build all workspaces | -| `bun run clean` | Clean build artifacts | -| `bun run check` | Run Adamantite checks | -| `bun run fix` | Auto-fix issues with Adamantite | -| `bun run format` | Format code with Adamantite | -| `bun run test` | Run the test suite | -| `bun run docker:up` | Start local services | -| `bun run docker:down` | Stop local services | -| `bun run boundaries` | Generate dependency boundaries report | +| Command | Description | +| ---------------------- | ------------------------------------------ | +| `bun run dev` | Start all workspaces with named HTTPS URLs | +| `bun run dev:apps` | Start application workspaces with Portless | +| `bun run dev:packages` | Start package workspaces with Portless | +| `bun run build` | Build all workspaces | +| `bun run clean` | Clean build artifacts | +| `bun run check` | Run Adamantite checks | +| `bun run fix` | Auto-fix issues with Adamantite | +| `bun run format` | Format code with Adamantite | +| `bun run test` | Run the test suite | +| `bun run docker:up` | Start local services | +| `bun run docker:down` | Stop local services | +| `bun run boundaries` | Generate dependency boundaries report | If you want to run a command for a specific workspace, you can use the following syntax: @@ -32,6 +32,35 @@ If you want to run a command for a specific workspace, you can use the following bun run --filter ``` +## Portless + +Every HTTP-serving workspace uses `portless` as its `dev` script and keeps its +underlying framework command in `dev:app`, following Portless's +[recommended Turborepo configuration](https://portless.sh/configuration). This means +both root commands such as `bun run dev` and workspace-local commands such as +`cd apps/api && bun run dev` use named HTTPS URLs. The normalized npm scope owns the +hostname suffix. For a scope named `example`, the App uses +`https://example.localhost`, the API uses `https://api.example.localhost`, and other +HTTP development servers follow the same `.example.localhost` convention. + +Each HTTP-serving workspace's package-local `portless` object is the source of truth +for its project-qualified name and `dev:app` script. Portless assigns an available +upstream port at runtime, so multiple projects can run concurrently without sharing +application ports. Separate projects still need distinct npm scopes—and therefore +distinct Portless names—while Git worktrees receive automatic route prefixes. + +Drizzle Studio, React Email, and the Inngest dev server use the same package-local +pattern. Inngest runs from `packages/workflows`; Docker Compose is reserved for data +infrastructure. + +The first run may request administrator access to trust Portless's local certificate +authority and bind port 443. Use `portless doctor` to inspect proxy, certificate, DNS, +and route health. + +Portless routes Mobile, Desktop, and Extension development servers, but it does not +replace Expo, Tauri, or browser-extension launch behavior. `.localhost` resolves only +on the development machine; physical devices require Portless LAN mode and `.local`. + ## Managing Dependencies - `bun run bump:deps` - Update dependencies interactively diff --git a/docs/generators.md b/docs/generators.md index 77a7016b..d609496f 100644 --- a/docs/generators.md +++ b/docs/generators.md @@ -136,8 +136,8 @@ Generate the optional client in an app that consumes the API: ```bash bun run generate files-client -bun run generate files-client --args app http://localhost:3000/files -bun run generate files-client --args web http://localhost:3000/files +bun run generate files-client --args app https://api.init.localhost/files +bun run generate files-client --args web https://api.init.localhost/files ``` The generator can target any workspace under `apps/`. It creates diff --git a/docs/getting-started.md b/docs/getting-started.md index beeeda58..dd3092e9 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -78,50 +78,60 @@ bun run codegen bun run docker:up ``` -5. Start the development server: +5. Start the development servers through Portless: ```bash -bun run dev # or bun run dev --filter to start a specific workspace +bun run dev ``` +Portless serves local HTTP development servers over named HTTPS URLs. On first use it +creates a local certificate authority, asks the operating system to trust it, and +starts its proxy on port 443. The project scope determines the hostname suffix; the +default template uses: + +- App: `https://init.localhost` +- API: `https://api.init.localhost` +- Web: `https://web.init.localhost` +- Docs: `https://docs.init.localhost` +- Mobile server: `https://mobile.init.localhost` +- Desktop frontend: `https://desktop.init.localhost` +- Extension server: `https://extension.init.localhost` +- Drizzle Studio: `https://db.init.localhost` +- Email preview: `https://email.init.localhost` +- Inngest: `https://workflows.init.localhost` + +Running `bun template setup` or a root `bun template rename` changes `init` in these +hostnames to the normalized project scope. The App owns the bare project hostname. + +Every HTTP-serving workspace uses `portless` as its `dev` script and keeps its +framework command in `dev:app`. Running `bun run dev` inside `apps/api`, for example, +serves `https://api.init.localhost`. At the repository root, the same command runs all +workspaces through Turbo. Package-local Portless configuration keeps both entry points +on the same names. + ### First Run Checklist - Run `bun template setup` - Generate source files and types with `bun run codegen` - Start services with `bun run docker:up` -- Start a workspace with `bun run dev --filter ` - -### Ports - -#### Apps - -Apps run in the 3000-3999 range. - -- API: `3000` -- App: `3001` -- Mobile: `3002` -- Desktop: `3003` -- Docs: `3004` -- Extension: `3005` -- Web: `3006` - -#### Packages +- Start the named HTTPS development topology with `bun run dev` +- Run `portless doctor` if a local URL or certificate is unavailable -Packages run in the 4000-4999 range. +### Port Allocation -- Email: `4000` -- Convex: `4001` -- Convex site: `4002` -- Desktop HMR: `4003` +Portless assigns an available upstream port to each application and package UI when it +starts. The public HTTPS names stay stable even when two projects run simultaneously. +Separate projects must use different npm scopes so their public names do not collide; +Git worktrees receive automatic route prefixes. Use `portless list` to inspect the +current assignments. -#### Infra +#### Infrastructure Ports -Infra runs in the 8000-8999 range. +Docker infrastructure retains fixed host ports and can still conflict across projects: - Redis: `8000` - Database: `8001` - Minio: `8002` (S3), `8003` (console) -- Workflows (Inngest): `8004` ### Troubleshooting @@ -129,3 +139,7 @@ Infra runs in the 8000-8999 range. - Node version mismatch: install Node.js `>=24` with your version manager. - Docker services not running: check `docker ps`, then run `bun run docker:up`. - Missing env variables: compare `.env.local` with each `.env.template`. +- Portless trust or DNS problems: run `portless doctor`, then follow its suggested + `portless trust` or `portless hosts sync` command. +- `.localhost` is available only on the development machine. Expo on a physical device + requires Portless LAN mode and a `.local` hostname. diff --git a/docs/template-commands.md b/docs/template-commands.md index c5b93459..6f850eac 100644 --- a/docs/template-commands.md +++ b/docs/template-commands.md @@ -11,6 +11,7 @@ Configure a newly created project. This command: - Prompts for the apps and packages to keep - Sets the project name, which is also used as the package scope - Rewrites `@init/` references with the project name +- Sets package-local Portless route names from the normalized package scope - Stamps `.template.json` with the source template, commit, and creation time ```bash @@ -19,7 +20,8 @@ bun template setup ### `bun template rename` -Rename the project and rewrite its `@init/` package scope references. +Rename the project, rewrite its package scope references, and update its Portless +hostnames. ```bash bun template rename --name [--scope ] @@ -27,7 +29,8 @@ bun template rename --name [--scope ] ### `bun template add app ` -Copy an app workspace from the template using Turbo generators, then apply the project's package scope. +Copy an app workspace from the template using Turbo generators, apply the project's +package scope, and restore its Portless route configuration. ```bash bun template add app web @@ -35,7 +38,8 @@ bun template add app web ### `bun template add package ` -Copy a package workspace from the template using Turbo generators, then apply the project's package scope. +Copy a package workspace from the template using Turbo generators, apply the project's +package scope, and restore its Portless route configuration when it owns a local UI. ```bash bun template add package auth diff --git a/infra/local/docker-compose.yml b/infra/local/docker-compose.yml index 8171e279..32bb595d 100644 --- a/infra/local/docker-compose.yml +++ b/infra/local/docker-compose.yml @@ -57,14 +57,3 @@ services: - > mc alias set local http://minio:9000 minioadmin minioadmin && mc mb --ignore-existing local/assets - - inngest: - image: inngest/inngest:v1.16.0 - command: "inngest dev -u http://host.docker.internal:3000/workflows --poll-interval 60" # Check for new workflows every 60 seconds - ports: - - "8004:8288" - healthcheck: - test: ["CMD", "curl", "--fail", "--silent", "http://localhost:8288/health"] - interval: 5s - timeout: 3s - retries: 20 diff --git a/package.json b/package.json index acedb85f..da272b30 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "dev:apps": "turbo run dev --filter=!@init/*", "dev:packages": "turbo run dev --filter=@init/*", "docker:down": "docker compose -f infra/local/docker-compose.yml down", - "docker:up": "docker compose -f infra/local/docker-compose.yml up -d", + "docker:up": "docker compose -f infra/local/docker-compose.yml up -d --remove-orphans", "fix": "adamantite fix", "fix:monorepo": "adamantite monorepo --fix", "format": "adamantite format", @@ -48,6 +48,7 @@ "oxfmt": "0.60.0", "oxlint": "1.75.0", "oxlint-tsgolint": "7.0.2001", + "portless": "0.15.5", "sherif": "1.13.0", "turbo": "2.10.7", "typescript": "7.0.2", @@ -80,6 +81,7 @@ "@sentry/cli", "@tailwindcss/oxide", "docs", - "extension" + "extension", + "inngest-cli" ] } diff --git a/packages/db/package.json b/packages/db/package.json index be75b0b3..930b1de2 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -15,7 +15,8 @@ "scripts": { "bump:deps": "bun update --interactive", "clean": "git clean -xdf .cache .turbo dist node_modules", - "dev": "bun --bun drizzle-kit studio", + "dev": "portless", + "dev:app": "bun --bun drizzle-kit studio --port ${PORT:-4983}", "generate": "bun --bun drizzle-kit generate", "migrate": "bun --bun drizzle-kit migrate", "push": "bun --bun drizzle-kit push", @@ -35,5 +36,9 @@ "drizzle-seed": "0.3.1", "postgres": "3.4.8", "typescript": "7.0.2" + }, + "portless": { + "name": "db.init", + "script": "dev:app" } } diff --git a/packages/email/package.json b/packages/email/package.json index ac717a9a..8e6cb4f7 100644 --- a/packages/email/package.json +++ b/packages/email/package.json @@ -11,7 +11,8 @@ "build": "email build --dir src/templates", "bump:deps": "bun update --interactive", "clean": "git clean -xdf .cache .turbo dist node_modules", - "dev": "email dev --dir src/templates --port 4000", + "dev": "portless", + "dev:app": "email dev --dir src/templates --port ${PORT:-4000}", "export": "email export --dir src/templates" }, "dependencies": { @@ -32,5 +33,9 @@ "react": "19.1.0", "react-email": "6.9.1", "typescript": "7.0.2" + }, + "portless": { + "name": "email.init", + "script": "dev:app" } } diff --git a/packages/env/src/presets.ts b/packages/env/src/presets.ts index f0f5a85a..b951bdb4 100644 --- a/packages/env/src/presets.ts +++ b/packages/env/src/presets.ts @@ -114,6 +114,15 @@ export const kv = () => skipValidation: isCI, }) +export const portless = () => + createEnv({ + runtimeEnv: env, + server: { + PORTLESS_URL: z.url().optional(), + }, + skipValidation: isCI, + }) + export const resend = () => createEnv({ runtimeEnv: env, diff --git a/packages/workflows/package.json b/packages/workflows/package.json index 6ff05261..1ee06872 100644 --- a/packages/workflows/package.json +++ b/packages/workflows/package.json @@ -14,7 +14,9 @@ }, "scripts": { "bump:deps": "bun update --interactive", - "clean": "git clean -xdf .cache .turbo dist node_modules" + "clean": "git clean -xdf .cache .turbo dist node_modules", + "dev": "portless", + "dev:app": "inngest dev --port ${PORT:-8288} --connect-executor-grpc-port $(( ${PORT:-8288} + 10000 )) --connect-gateway-grpc-port $(( ${PORT:-8288} + 11000 )) --connect-gateway-port $(( ${PORT:-8288} + 12000 )) -u ${INNGEST_API_URL:-https://api.init.localhost/workflows} --poll-interval 60" }, "dependencies": { "@init/observability": "workspace:*", @@ -23,6 +25,11 @@ }, "devDependencies": { "@tooling/tsconfig": "workspace:*", + "inngest-cli": "1.40.0", "typescript": "7.0.2" + }, + "portless": { + "name": "workflows.init", + "script": "dev:app" } } diff --git a/scripts/template/add.ts b/scripts/template/add.ts index ac7f6877..d4183c0d 100644 --- a/scripts/template/add.ts +++ b/scripts/template/add.ts @@ -2,6 +2,7 @@ import { join } from "node:path" import consola from "consola" import { defineCommand } from "../utils" +import { restorePortlessWorkspaces } from "./portless" import { renameProject } from "./rename" import { getDependencyNames, @@ -159,6 +160,7 @@ export default defineCommand({ } const copiedPaths = await addWorkspaces(rootDir, scope, [target], new Set([targetPath])) + await restorePortlessWorkspaces(rootDir, copiedPaths, scope) consola.success(`Added ${copiedPaths.join(", ")}. Run bun install to link the new workspaces.`) }, }) diff --git a/scripts/template/portless.ts b/scripts/template/portless.ts new file mode 100644 index 00000000..5726b438 --- /dev/null +++ b/scripts/template/portless.ts @@ -0,0 +1,66 @@ +import { join, relative } from "node:path" + +import { getWorkspaces, readJson, writeJson } from "./shared" + +export function getWorkspaceRouteName(workspacePath: string, projectName: string) { + const normalizedWorkspacePath = workspacePath.replaceAll("\\", "/") + if (normalizedWorkspacePath === "apps/app") return projectName + + const workspaceName = normalizedWorkspacePath.split("/").at(-1) + return workspaceName ? `${workspaceName}.${projectName}` : projectName +} + +async function updateWorkspacePackageName( + rootDir: string, + workspacePath: string, + projectName: string +) { + const packageJsonPath = join(rootDir, workspacePath, "package.json") + if (!(await Bun.file(packageJsonPath).exists())) return null + + const packageJson = await readJson(packageJsonPath) + const portless = packageJson.portless as { name?: string } | undefined + if (!portless) return null + + const routeName = getWorkspaceRouteName(workspacePath, projectName) + if (portless.name === routeName) return null + + portless.name = routeName + await writeJson(packageJsonPath, packageJson) + + return packageJsonPath +} + +async function updateWorkspaceNames( + rootDir: string, + workspacePaths: string[], + projectName: string +) { + const changedFiles = await Promise.all( + workspacePaths.map((workspacePath) => + updateWorkspacePackageName(rootDir, workspacePath, projectName) + ) + ) + + return changedFiles.filter((path): path is string => path !== null) +} + +export async function updatePortlessProjectName(rootDir: string, projectName: string) { + const workspaces = await Promise.all([ + getWorkspaces(rootDir, "app"), + getWorkspaces(rootDir, "package"), + ]) + const workspacePaths = workspaces + .flat() + .map((workspace) => relative(rootDir, workspace.directory)) + + return updateWorkspaceNames(rootDir, workspacePaths, projectName) +} + +export async function restorePortlessWorkspaces( + rootDir: string, + workspacePaths: string[], + projectName: string +) { + return updateWorkspaceNames(rootDir, workspacePaths, projectName) +} diff --git a/scripts/template/rename.ts b/scripts/template/rename.ts index e3df3e06..ab6f07ef 100644 --- a/scripts/template/rename.ts +++ b/scripts/template/rename.ts @@ -2,6 +2,7 @@ import { join, resolve } from "node:path" import consola from "consola" import { defineCommand } from "../utils" +import { updatePortlessProjectName } from "./portless" import { checkIsPathWithinRoot, getProjectScope, @@ -32,23 +33,35 @@ export async function renameProject({ }: RenameOptions): Promise { const normalizedScope = normalizeScope(scope) const normalizedSourceScope = normalizeScope(sourceScope) - const changedFiles = await replaceTextInFiles( - rootDir, - getScopePrefix(normalizedSourceScope), - getScopePrefix(normalizedScope) + const changedFiles = new Set( + await replaceTextInFiles( + rootDir, + getScopePrefix(normalizedSourceScope), + getScopePrefix(normalizedScope) + ) ) - if (!projectName) return { changedFiles } + for (const path of await replaceTextInFiles( + rootDir, + `${normalizedSourceScope}.localhost`, + `${normalizedScope}.localhost` + )) + changedFiles.add(path) + + if (!projectName) return { changedFiles: [...changedFiles] } + + for (const path of await updatePortlessProjectName(rootDir, normalizedScope)) + changedFiles.add(path) const packageJsonPath = join(rootDir, "package.json") const packageJson = await readJson(packageJsonPath) - if (packageJson.name === projectName) return { changedFiles } - - packageJson.name = projectName - await writeJson(packageJsonPath, packageJson) - changedFiles.push(packageJsonPath) + if (packageJson.name !== projectName) { + packageJson.name = projectName + await writeJson(packageJsonPath, packageJson) + changedFiles.add(packageJsonPath) + } - return { changedFiles } + return { changedFiles: [...changedFiles] } } export default defineCommand({ diff --git a/turbo/generators/commands/files-client.ts b/turbo/generators/commands/files-client.ts index 8c55f83b..91260781 100644 --- a/turbo/generators/commands/files-client.ts +++ b/turbo/generators/commands/files-client.ts @@ -103,7 +103,7 @@ export function registerFilesClientGenerator(plop: PlopTypes.NodePlopAPI): void type: "list", }, { - default: "http://localhost:3000/files", + default: "https://api.init.localhost/files", message: "What is the Files SDK endpoint?", name: "endpoint", type: "input", diff --git a/turbo/generators/templates/backend-clients/hono/app/env.hbs b/turbo/generators/templates/backend-clients/hono/app/env.hbs index 32bb380d..9e4d7bb1 100644 --- a/turbo/generators/templates/backend-clients/hono/app/env.hbs +++ b/turbo/generators/templates/backend-clients/hono/app/env.hbs @@ -1,2 +1,2 @@ # --- Hono API --- -PUBLIC_API_URL="http://localhost:3000" +PUBLIC_API_URL="https://api.init.localhost" diff --git a/turbo/generators/templates/backend-clients/hono/desktop/env.hbs b/turbo/generators/templates/backend-clients/hono/desktop/env.hbs index 32bb380d..9e4d7bb1 100644 --- a/turbo/generators/templates/backend-clients/hono/desktop/env.hbs +++ b/turbo/generators/templates/backend-clients/hono/desktop/env.hbs @@ -1,2 +1,2 @@ # --- Hono API --- -PUBLIC_API_URL="http://localhost:3000" +PUBLIC_API_URL="https://api.init.localhost" diff --git a/turbo/generators/templates/backend-clients/hono/mobile/env.hbs b/turbo/generators/templates/backend-clients/hono/mobile/env.hbs index fd08f176..b0cfa272 100644 --- a/turbo/generators/templates/backend-clients/hono/mobile/env.hbs +++ b/turbo/generators/templates/backend-clients/hono/mobile/env.hbs @@ -1,2 +1,2 @@ # --- Hono API --- -EXPO_PUBLIC_API_URL="http://localhost:3000" +EXPO_PUBLIC_API_URL="https://api.init.localhost"