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,