diff --git a/.github/workflows/nuxflare-deploy.yml b/.github/workflows/nuxflare-deploy.yml index 63ba764..d5e3157 100644 --- a/.github/workflows/nuxflare-deploy.yml +++ b/.github/workflows/nuxflare-deploy.yml @@ -28,12 +28,6 @@ jobs: with: fetch-depth: 0 - - uses: actions/cache@v4 - with: - path: | - playground/.sst - key: ${{ runner.os }}-sst-playground - - name: Install pnpm uses: pnpm/action-setup@v4 with: @@ -48,6 +42,7 @@ jobs: - name: Build Nuxflare working-directory: . run: | + pnpm i pnpm run build - name: Install deps @@ -66,6 +61,7 @@ jobs: STAGE="${{ github.event.inputs.stage }}" else # Sanitize branch name: lower-case and replace non-alphanumeric chars with dashes + echo "${GITHUB_REF_NAME} and ${GITHUB_REF} and ${GITHUB_EVENT_NAME}" STAGE=$(echo "${GITHUB_REF_NAME}" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g') fi echo "Deploying to stage: ${STAGE}" @@ -73,4 +69,4 @@ jobs: fi echo "Running: ${DEPLOY_CMD}" - node ../dist/index.js ${DEPLOY_CMD} + ../dist/index.js ${DEPLOY_CMD} diff --git a/src/init/sst/.nuxflare/utils/builder.ts b/playground/nuxflare/builder.ts similarity index 100% rename from src/init/sst/.nuxflare/utils/builder.ts rename to playground/nuxflare/builder.ts diff --git a/src/init/sst/.nuxflare/utils/nuxt.ts b/playground/nuxflare/nuxt.ts similarity index 100% rename from src/init/sst/.nuxflare/utils/nuxt.ts rename to playground/nuxflare/nuxt.ts diff --git a/src/init/sst/.nuxflare/utils/semaphore.ts b/playground/nuxflare/semaphore.ts similarity index 100% rename from src/init/sst/.nuxflare/utils/semaphore.ts rename to playground/nuxflare/semaphore.ts diff --git a/playground/package.json b/playground/package.json index e80d6fb..685ea5c 100644 --- a/playground/package.json +++ b/playground/package.json @@ -32,7 +32,7 @@ "drizzle-kit": "^0.30.0", "eslint": "^9.16.0", "nuxthub-ratelimit": "^1.0.4", - "sst": "^3.9.26", + "sst": "^3.9.27", "vue-tsc": "^2.1.10", "wrangler": "^3.95.0", "zod": "^3.24.1" diff --git a/playground/pnpm-lock.yaml b/playground/pnpm-lock.yaml index ef1569b..9fe7ade 100644 --- a/playground/pnpm-lock.yaml +++ b/playground/pnpm-lock.yaml @@ -58,8 +58,8 @@ importers: specifier: ^1.0.4 version: 1.0.4(magicast@0.3.5)(rollup@4.28.1) sst: - specifier: ^3.9.26 - version: 3.9.26 + specifier: ^3.9.27 + version: 3.9.27 vue-tsc: specifier: ^2.1.10 version: 2.1.10(typescript@5.7.2) @@ -4969,33 +4969,33 @@ packages: resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==} engines: {node: '>=0.10.0'} - sst-darwin-arm64@3.9.26: - resolution: {integrity: sha512-78GSe3obviQV3PycSo4zUfF7k8IxjS4LmHJwunFlVc4z9AzCU7F2au12+9O5jxWraVK/EnYCN/MhRMi54qMqfA==} + sst-darwin-arm64@3.9.27: + resolution: {integrity: sha512-eDmSOFoW2EXzM8+CfqW0n+0bb5gFCSNkAS/wcwMT3iyAPPJANtWXAyFYUsXbLgXjIpg6uofMTbEGb4T3gEmpJg==} cpu: [arm64] os: [darwin] - sst-darwin-x64@3.9.26: - resolution: {integrity: sha512-sOQUZCz/CcCBm5wRyRdaCe7LnO+imrgxHSgq8x4zCWBD5snElDamRtbUe7kw+3zd2ckU/fp34bDP4KfzTKARGw==} + sst-darwin-x64@3.9.27: + resolution: {integrity: sha512-qRfXwBtCZfI1lHSlvPdt9wex3uTRCTE2e4tCQ749mtY3s0mdSYQ4ikdD1/7uxpjVxTwve705jfQBNpa4Pn6DIg==} cpu: [x64] os: [darwin] - sst-linux-arm64@3.9.26: - resolution: {integrity: sha512-IpUA7eLjSwAvKMx92P+CJEuIZPKsDo9t/khSCoAWwkKq1EN80hV9etPhm1/jQCdB0PERoggGXT2oL/IJTLtb2g==} + sst-linux-arm64@3.9.27: + resolution: {integrity: sha512-BbGM59rYcBdwHLUsp3++VkbtZABxon4eYmgrc7v8iP3CrpbeoWn1VCP1MnFoXdXvx9n6Nbmui3oATQUVeWgBFg==} cpu: [arm64] os: [linux] - sst-linux-x64@3.9.26: - resolution: {integrity: sha512-tMNDQBSFGAFcX9Zc/9W8hsd8n9cp2vx6rMjko2sV3wbF/K0dledKGjT5EhzDAlccP6bFS3XGiIzsUfhsgZW4Ag==} + sst-linux-x64@3.9.27: + resolution: {integrity: sha512-ZSBmYryaXkQVgVfbPx/ZlPqZXQTX2aGnbCAQ91vkKnhDx7kaF84zAx7bIvk9Jk/1ZbPg5HTvoKm0CWFgJjKKxg==} cpu: [x64] os: [linux] - sst-linux-x86@3.9.26: - resolution: {integrity: sha512-ll1koUZ4D6peMxR8Th5DSY5UZvLXaAWmCA7vC9la0KnQZz/odJBeY2CZRwUXyntpyoGRvFJXWKhecbOoaVyzhA==} + sst-linux-x86@3.9.27: + resolution: {integrity: sha512-aMw7b+SJjPrlSqasJjkIIf6Saz3HVFugo1XDoCW8rNIgwfzVX7/gT2ds87KefpCOYdMARICcKuCycQq1nT+4jg==} cpu: [x86] os: [linux] - sst@3.9.26: - resolution: {integrity: sha512-LWDRA+sYcc4cD+1sXp4RW5asVDCzrWVyqE0L1FdWxQ8vV/BN9/5TpEFulShTkom1s/CmuLh5H6+qOGO5Di7XqQ==} + sst@3.9.27: + resolution: {integrity: sha512-kt9Lw65MhqSwIJBywBalhZrj3FFLnKHn9KcVI4FuoLXpek+0tvQKW2mnwxsO9pqF96a8sYtlNRki6F1neb9qgg==} hasBin: true stable-hash@0.0.4: @@ -11473,32 +11473,32 @@ snapshots: speakingurl@14.0.1: {} - sst-darwin-arm64@3.9.26: + sst-darwin-arm64@3.9.27: optional: true - sst-darwin-x64@3.9.26: + sst-darwin-x64@3.9.27: optional: true - sst-linux-arm64@3.9.26: + sst-linux-arm64@3.9.27: optional: true - sst-linux-x64@3.9.26: + sst-linux-x64@3.9.27: optional: true - sst-linux-x86@3.9.26: + sst-linux-x86@3.9.27: optional: true - sst@3.9.26: + sst@3.9.27: dependencies: aws4fetch: 1.0.20 jose: 5.2.3 openid-client: 5.6.4 optionalDependencies: - sst-darwin-arm64: 3.9.26 - sst-darwin-x64: 3.9.26 - sst-linux-arm64: 3.9.26 - sst-linux-x64: 3.9.26 - sst-linux-x86: 3.9.26 + sst-darwin-arm64: 3.9.27 + sst-darwin-x64: 3.9.27 + sst-linux-arm64: 3.9.27 + sst-linux-x64: 3.9.27 + sst-linux-x86: 3.9.27 stable-hash@0.0.4: {} diff --git a/playground/sst.config.ts b/playground/sst.config.ts index 1392207..9087bff 100644 --- a/playground/sst.config.ts +++ b/playground/sst.config.ts @@ -1,15 +1,14 @@ /// -import Nuxt from "./.nuxflare/utils/nuxt"; +import Nuxt from "./nuxflare/nuxt"; -const prodDomain = "test.tanay.codes"; -const devDomain = "chat.tanay.codes"; +const prodDomain = "chat.tanay.codes"; +const devDomain = undefined; export default $config({ app(input) { return { name: "tanay-chat", removal: input?.stage === "production" ? "retain" : "remove", - protect: ["production"].includes(input?.stage), home: "cloudflare", providers: { cloudflare: true, @@ -23,8 +22,8 @@ export default $config({ $app.stage === "production" ? prodDomain || undefined : devDomain - ? `${$app.stage}.${devDomain}` - : undefined; + ? `${$app.stage}.${devDomain}` + : undefined; Nuxt("App", { dir: ".", domain, diff --git a/src/commands/deploy.ts b/src/commands/deploy.ts index e2b03b0..fbdbefa 100644 --- a/src/commands/deploy.ts +++ b/src/commands/deploy.ts @@ -40,6 +40,7 @@ async function displayProjectUrls(stage: string) { } } } catch (error) { + console.log("error project uurls", error) // If there's an error reading the directory, just skip URL display console.error("Unable to read deployment URLs"); } @@ -87,7 +88,7 @@ export async function deploy(options: DeployOptions = {}) { log.step(`Deploying to stage: ${deployStage}`); try { - await executeSST(["deploy", "--stage", deployStage], { + await executeSST(["deploy", "--stage", deployStage, "--verbose"], { stdio: "inherit", env: { NITRO_PRESET: "cloudflare-module", diff --git a/src/commands/init.ts b/src/commands/init.ts index 475c923..caab477 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -86,9 +86,16 @@ export async function init() { placeholder: "e.g., dev.example.codes (stages like 'dev' will deploy to 'dev.dev.example.codes')", }), - setupGithubActions: () => - p.confirm({ - message: "Would you like to setup GitHub Actions?", + githubActions: () => + p.select({ + message: "How would you like to setup GitHub Actions?", + options: [ + { value: "none", label: "Don't setup GitHub Actions" }, + { value: "manual", label: "Manual deployments only (via workflow dispatch)" }, + { value: "prod", label: "Automatic production deployments only (main branch)" }, + { value: "full", label: "Full setup (automatic prod and preview deployments for PRs)" }, + ], + initialValue: "none", }), }, { @@ -161,7 +168,7 @@ export async function init() { { title: "Setting up GitHub Actions", task: async () => { - if (!results.setupGithubActions) { + if (results.githubActions === "none") { return "GitHub Actions setup skipped"; } @@ -171,7 +178,10 @@ export async function init() { // Copy and render the action.yml template const engine = new Liquid() const template = engine.parse(await fs.readFile(path.join(initDir, "action.yml.liquid"), "utf8")); - const actionContent = await engine.render(template, { package_manager: results.packageManager }); + const actionContent = await engine.render(template, { + package_manager: results.packageManager, + github_action_type: results.githubActions + }); await fs.writeFile( path.join(githubDir, "nuxflare-deploy.yml"), @@ -217,21 +227,33 @@ export async function init() { log.success(chalk.green("✅ Successfully initialized Nuxflare!")); + const nextSteps = [ + `1. Run ${chalk.cyan( + "nuxflare deploy --stage ", + )} to do a preview deployment.`, + `2. Run ${chalk.cyan( + "nuxflare deploy --production", + )} to deploy to production.`, + `3. Run ${chalk.cyan( + "nuxflare dev --stage ", + )} to run a local dev server and connect to remote resources.`, + `4. Run ${chalk.cyan( + "nuxflare copy-env --stage --file .env", + )} to copy environment variables from a .env file to a stage.` + ]; + + if (results.githubActions !== "none") { + nextSteps.push( + `5. ${chalk.yellow("Important:")} Review your GitHub Actions workflow in ${chalk.cyan( + ".github/workflows/nuxflare-deploy.yml" + )} and add your ${chalk.cyan( + "CLOUDFLARE_API_TOKEN" + )} to your repository secrets by going to Settings > Secrets and variables > Actions > New repository secret.` + ); + } + p.note( - [ - `1. Run ${chalk.cyan( - "nuxflare deploy --stage ", - )} to do a preview deployment.`, - `2. Run ${chalk.cyan( - "nuxflare deploy --production", - )} to deploy to production.`, - `3. Run ${chalk.cyan( - "nuxflare dev --stage ", - )} to run a local dev server and connect to remote resources.`, - `4. Run ${chalk.cyan( - "nuxflare copy-env --stage --file .env", - )} to copy environment variables from a .env file to a stage.` - ].join("\n"), + nextSteps.join("\n"), "Next steps", ); } catch (error) { diff --git a/src/init/action.yml.liquid b/src/init/action.yml.liquid index 7b310c3..8da0df7 100644 --- a/src/init/action.yml.liquid +++ b/src/init/action.yml.liquid @@ -1,8 +1,14 @@ name: "Nuxflare Deploy" on: + {% if github_action_type == "full" %} push: pull_request: + {% elsif github_action_type == "prod" %} + push: + branches: + - main + {% endif %} workflow_dispatch: inputs: stage: @@ -15,7 +21,10 @@ concurrency: jobs: deploy: - if: github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request' || (github.event_name == 'push' && github.ref == 'refs/heads/main') + {% if github_action_type == "full" %}if: github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request' || (github.event_name == 'push' && github.ref == 'refs/heads/main') + {% elsif github_action_type == "prod" %}if: github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && github.ref == 'refs/heads/main') + {% else %}if: github.event_name == 'workflow_dispatch' + {% endif %} runs-on: ubuntu-latest permissions: contents: read @@ -42,6 +51,8 @@ jobs: {% elsif package_manager == "pnpm" %} - name: Install pnpm uses: pnpm/action-setup@v4 + with: + version: 9.15.0 - name: Set up Node.js uses: actions/setup-node@v4 diff --git a/src/init/sst/nuxflare/builder.ts b/src/init/sst/nuxflare/builder.ts new file mode 100644 index 0000000..74c974f --- /dev/null +++ b/src/init/sst/nuxflare/builder.ts @@ -0,0 +1,32 @@ +import * as path from "node:path"; +import { Semaphore } from "./semaphore"; + +const limiter = new Semaphore( + parseInt(process.env.SST_BUILD_CONCURRENCY_SITE || "1"), +); +export async function builder( + name: string, + { + dir, + env, + packageManager, + }: { dir: string; env?: Record; packageManager: string }, +): Promise { + return new Promise((res) => { + $resolve({ env }).apply(async ({ env }) => { + // acquire semaphore after only resolving environment and we are ready to build + await limiter.acquire(); + const cmd = new command.local.Command(`${name}Build`, { + dir: path.resolve(dir), + create: `[ -z "$SKIP_BUILD" ] && NITRO_PRESET=cloudflare-module ${packageManager} run build || ( [ -n "$SKIP_BUILD" ] && echo "Skipping build." )`, + update: `[ -z "$SKIP_BUILD" ] && NITRO_PRESET=cloudflare-module ${packageManager} run build || ( [ -n "$SKIP_BUILD" ] && echo "Skipping build." )`, + triggers: [new Date().toString()], + environment: env, + }); + cmd.urn.apply(() => { + limiter.release(); + res(cmd); + }); + }); + }); +} diff --git a/src/init/sst/nuxflare/nuxt.ts b/src/init/sst/nuxflare/nuxt.ts new file mode 100644 index 0000000..be8b5aa --- /dev/null +++ b/src/init/sst/nuxflare/nuxt.ts @@ -0,0 +1,390 @@ +import { writeFileSync, mkdirSync, existsSync } from "node:fs"; +import * as path from "node:path"; +import { loadNuxtConfig } from "nuxt/kit"; +import { builder } from "./builder"; + +const DEFAULT_CLOUDFLARE_COMPATIBILITY_DATE = "2024-12-05"; +const BINDINGS = { + ASSETS: "ASSETS", + DATABASE: "DB", + AI: "AI", + KV: "KV", + CACHE: "CACHE", + BLOB: "BLOB", +} as const; + +const PACKAGE_MANAGER_COMMANDS = { + pnpm: "pnpm exec", + yarn: "yarn exec", + npm: "npx", + bun: "bun x", +} as const; + +type DatabaseConfig = { id: string; name: string } | sst.cloudflare.D1; + +class WranglerConfigBuilder { + private config: Record = {}; + + constructor(name: string, outputDir: string, compatibilityDate?: string) { + this.config = { + name: name.toLowerCase(), + main: path.resolve(outputDir, "server", "index.mjs"), + compatibility_date: + compatibilityDate || DEFAULT_CLOUDFLARE_COMPATIBILITY_DATE, + compatibility_flags: ["nodejs_compat"], + observability: { enabled: true }, + }; + } + + addAssets(publicDir: string): this { + this.config.assets = { + directory: publicDir, + binding: BINDINGS.ASSETS, + }; + return this; + } + + addDomain(domain?: string): this { + if (domain) { + this.config.routes = [ + { + pattern: domain, + custom_domain: true, + }, + ]; + } + return this; + } + + addD1Database(database: DatabaseConfig, migrationsDir: string): this { + this.config.d1_databases ||= []; + if (database instanceof sst.cloudflare.D1) { + this.config.d1_databases.push({ + binding: BINDINGS.DATABASE, + database_name: database.nodes.database.name, + database_id: database.id, + migrations_dir: migrationsDir, + }); + } else { + this.config.d1_databases.push({ + binding: BINDINGS.DATABASE, + database_name: database.name, + database_id: database.id, + migrations_dir: migrationsDir, + }); + } + return this; + } + + addAI(): this { + this.config.ai = { + binding: BINDINGS.AI, + }; + return this; + } + + addKV(kv: sst.cloudflare.Kv, binding: string): this { + this.config.kv_namespaces ||= []; + this.config.kv_namespaces.push({ + binding, + id: kv.id, + }); + return this; + } + + addBucket(blob: sst.cloudflare.Bucket): this { + this.config.r2_buckets ||= []; + this.config.r2_buckets.push({ + binding: BINDINGS.BLOB, + bucket_name: blob.name, + }); + return this; + } + + addVectorize(name: string, indexName: any): this { + this.config.vectorize ||= []; + this.config.vectorize.push({ + binding: `VECTORIZE_${name.toUpperCase()}`, + index_name: indexName, + }); + return this; + } + + addVars( + environment: sst.Secret, + extraVars: Record, + nuxtHubSecret: typeof random.RandomUuid.prototype.result, + ): this { + this.config.vars = environment.value.apply((env) => { + let parsedEnv = {}; + try { + parsedEnv = JSON.parse(env); + } catch (error) { + console.warn("Failed to parse environment:", error); + } + return { + NUXT_HUB_PROJECT_SECRET_KEY: nuxtHubSecret, + ...parsedEnv, + ...extraVars, + }; + }); + return this; + } + + async transform( + transformFn?: (config: Record) => Promise | void, + ): Promise { + if (transformFn) { + await transformFn(this.config); + } + return this; + } + + build(): Record { + return this.config; + } +} + +/** + * Creates and configures a Nuxt.js application for Cloudflare Workers deployment + * + * @param name - Application name (must start with a capital letter, no hyphens) + * @param options - Configuration options for the deployment + * @param options.dir - Root directory of the Nuxt application + * @param options.domain - Optional domain/subdomain. If not specified, automatic workers.dev subdomain is used + * @param options.extraVars - Additional environment variables used in binding and building the Nuxt app + * @param options.transformWrangler - Optional function to modify the wrangler configuration before deployment + * @param options.packageManager - Package manager to use for building the app and running wrangler + * @param options.compatibilityDate - Optionally specify the Cloudflare Workers compatibility date + * @param options.database - Optional database configuration (existing or SST database) + * @returns Promise that resolves when deployment is configured + */ +async function Nuxt( + name: string, + { + dir, + domain, + extraVars = {}, + transformWrangler, + packageManager = "pnpm", + database, + compatibilityDate, + }: { + dir: string; + domain?: string; + extraVars?: Record; + transformWrangler?: ( + wrangler: Record, + ) => Promise | void; + packageManager?: keyof typeof PACKAGE_MANAGER_COMMANDS; + database?: DatabaseConfig; + compatibilityDate?: string; + }, +) { + const packageManagerX = PACKAGE_MANAGER_COMMANDS[packageManager]; + const projectPath = path.resolve(dir); + const environment = new sst.Secret("Env", "{}"); + const nuxtConfig = await loadNuxtConfig({ cwd: projectPath }); + const hubConfig = nuxtConfig.hub || {}; + const nuxtHubSecret = new random.RandomUuid(`${name}NuxtHubSecret`); + const buildOutputPath = path.resolve(projectPath, "dist"); + const migrationsPath = path.resolve( + buildOutputPath, + "database", + "migrations", + ); + + const wranglerBuilder = new WranglerConfigBuilder( + `${$app.name}-${$app.stage}-${name}`, + buildOutputPath, + compatibilityDate, + ) + .addAssets(path.resolve(buildOutputPath, "public")) + .addDomain(domain) + .addVars(environment, extraVars, nuxtHubSecret.result); + + const resources: any[] = []; + const databaseConfig: { name?: any } = {}; + + if (hubConfig.database) { + if (database instanceof sst.cloudflare.D1) { + resources.push(database); + wranglerBuilder.addD1Database(database, migrationsPath); + databaseConfig.name = database.nodes.database.name; + } else if (database) { + wranglerBuilder.addD1Database(database, migrationsPath); + databaseConfig.name = database.name; + } else { + const database = new sst.cloudflare.D1(BINDINGS.DATABASE); + resources.push(database); + wranglerBuilder.addD1Database(database, migrationsPath); + databaseConfig.name = database.nodes.database.name; + } + } + + if (hubConfig.ai) { + wranglerBuilder.addAI(); + } + + if (hubConfig.kv) { + const kv = new sst.cloudflare.Kv(BINDINGS.KV); + resources.push(kv); + wranglerBuilder.addKV(kv, BINDINGS.KV); + } + + if (hubConfig.cache) { + const cache = new sst.cloudflare.Kv(BINDINGS.CACHE); + resources.push(cache); + wranglerBuilder.addKV(cache, BINDINGS.CACHE); + } + + if (hubConfig.blob) { + const blob = new sst.cloudflare.Bucket(BINDINGS.BLOB); + resources.push(blob); + wranglerBuilder.addBucket(blob); + } + + if (hubConfig.vectorize) { + for (const [name, config] of Object.entries(hubConfig.vectorize)) { + const indexName = `${$app.name}-${$app.stage}-${name}`; + const index = new command.local.Command(`Vector${indexName}`, { + dir: projectPath, + create: `${packageManagerX} wrangler vectorize create ${indexName} --dimensions=${config.dimensions} --metric=${config.metric}`, + delete: `${packageManagerX} wrangler vectorize delete ${indexName} --force`, + }); + resources.push(index); + for (const [propertyName, type] of Object.entries( + config.metadataIndexes || {}, + )) { + new command.local.Command( + `MetadataIndex${indexName}${propertyName}`, + { + dir: projectPath, + create: `${packageManagerX} wrangler vectorize create-metadata-index ${indexName} --property-name=${propertyName} --type=${type}`, + delete: `${packageManagerX} wrangler vectorize delete-metadata-index ${indexName} --property-name=${propertyName}`, + }, + { + dependsOn: [index], + }, + ); + } + wranglerBuilder.addVectorize( + name, + index.stdout.apply(() => indexName), + ); + } + } + + await wranglerBuilder.transform(transformWrangler); + const wrangler = wranglerBuilder.build(); + + $resolve(wrangler).apply((wrangler) => { + const stateDir = path.resolve(`.nuxflare/state/${$app.stage}/${name}`); + const wranglerConfigPath = path.resolve(stateDir, "wrangler.json"); + + if (!existsSync(stateDir)) { + mkdirSync(stateDir, { recursive: true }); + } + + writeFileSync(wranglerConfigPath, JSON.stringify(wrangler, null, 2)); + + const build = builder(name, { + dir: projectPath, + env: { ...extraVars }, + packageManager, + }); + + const deploy = new command.local.Command( + `${name}WorkerVersion`, + { + dir: projectPath, + create: `${packageManagerX} wrangler deploy --config ${wranglerConfigPath}`, + triggers: [new Date().toString()], + logging: command.local.Logging.Stderr, + }, + { + dependsOn: [build, ...resources], + }, + ); + + $resolve([deploy.urn, nuxtHubSecret.result]).apply(async ([_, secret]) => { + let projectUrl; + if (domain) { + projectUrl = `https://${domain}`; + } else { + const apiToken = process.env.CLOUDFLARE_API_TOKEN; + if (!apiToken) { + console.error('CLOUDFLARE_API_TOKEN environment variable is required'); + return; + } + // First, get the account ID if not set + let accountId = process.env.CLOUDFLARE_DEFAULT_ACCOUNT_ID || process.env.CLOUDFLARE_ACCOUNT_ID; + if (!accountId) { + const accountResponse = await fetch('https://api.cloudflare.com/client/v4/accounts', { + headers: { + 'Authorization': `Bearer ${apiToken}`, + 'Content-Type': 'application/json' + } + }); + const accountData = await accountResponse.json(); + if (!accountData.success || !accountData.result?.[0]?.id) { + console.error('Failed to fetch Cloudflare account ID'); + return; + } + accountId = accountData.result[0].id; + } + // Then proceed with getting the workers subdomain + const response = await fetch(`https://api.cloudflare.com/client/v4/accounts/${accountId}/workers/subdomain`, { + headers: { + 'Authorization': `Bearer ${apiToken}`, + 'Content-Type': 'application/json' + } + }); + const data = await response.json(); + if (data.success) { + const workersDomain = data.result.subdomain; + projectUrl = `https://${wrangler.name}.${workersDomain}.workers.dev`; + } else { + console.error('Failed to fetch workers subdomain'); + projectUrl = ''; + } + } + const stateFile = path.resolve(stateDir, 'state.json'); + const stateData = { + projectUrl, + nuxtHubSecret: secret, + }; + writeFileSync(stateFile, JSON.stringify(stateData, null, 2)); + }); + + new command.local.Command( + `${name}Worker`, + { + dir: projectPath, + delete: `${packageManagerX} wrangler delete --name ${wrangler.name}`, + }, + { + dependsOn: [deploy], + }, + ); + + if (databaseConfig.name) { + $resolve([deploy.urn, databaseConfig.name]).apply(([_, name]) => { + new command.local.Command( + `${name}Migrations`, + { + dir: projectPath, + create: `${existsSync(migrationsPath) + ? `${packageManagerX} wrangler --config ${wranglerConfigPath} d1 migrations apply ${name} --remote` + : 'echo "Migrations directory not found, skipping."' + }`, + triggers: [new Date().toString()], + }, + { dependsOn: [deploy] }, + ); + }); + } + }); +} + +export default Nuxt; diff --git a/src/init/sst/nuxflare/semaphore.ts b/src/init/sst/nuxflare/semaphore.ts new file mode 100644 index 0000000..5e3a557 --- /dev/null +++ b/src/init/sst/nuxflare/semaphore.ts @@ -0,0 +1,29 @@ +export class Semaphore { + private current: number; + private queue: (() => void)[]; + + constructor(private max: number) { + this.current = 0; + this.queue = []; + } + + public async acquire(): Promise { + if (this.current < this.max) { + this.current++; + return Promise.resolve(); + } + + return new Promise((resolve) => { + this.queue.push(resolve); + }); + } + + public release(): void { + if (this.queue.length > 0) { + const next = this.queue.shift(); + next?.(); + return; + } + this.current--; + } +} diff --git a/src/init/sst/sst.config.ts b/src/init/sst/sst.config.ts index ff7a93d..30272f8 100644 --- a/src/init/sst/sst.config.ts +++ b/src/init/sst/sst.config.ts @@ -1,5 +1,5 @@ /// -import Nuxt from "./.nuxflare/utils/nuxt"; +import Nuxt from "./nuxflare/nuxt"; const prodDomain = "__PROD_DOMAIN__"; const devDomain = "__DEV_DOMAIN__"; @@ -9,7 +9,6 @@ export default $config({ return { name: "__PROJECT_NAME__", removal: input?.stage === "production" ? "retain" : "remove", - protect: ["production"].includes(input?.stage), home: "cloudflare", providers: { cloudflare: true, @@ -23,8 +22,8 @@ export default $config({ $app.stage === "production" ? prodDomain || undefined : devDomain - ? `${$app.stage}.${devDomain}` - : undefined; + ? `${$app.stage}.${devDomain}` + : undefined; Nuxt("App", { dir: ".", domain,