From 0e63953e2a462163d7175746c41a098a355803bc Mon Sep 17 00:00:00 2001 From: Atharva Deosthale Date: Tue, 5 May 2026 00:47:33 +0530 Subject: [PATCH 1/4] perf(build): replace specs glob with fs reads The 11 import.meta.glob calls in specs.ts expanded into 58k lazy import statements, producing 49,123 server chunks (500 MB) and pushing peak build RSS to 7 GB. The .md and .json files were being treated as JS modules with full transform/sourcemap/chunk overhead, when they are just static text. Resolve @appwrite.io/specs at runtime via createRequire and load examples + OpenAPI specs with fs.readFile. Move the package from devDependencies to dependencies so it survives bun install --production and ships to the final image. Build wall: 192s -> 113s. Peak RSS: 7.0 GB -> 5.6 GB. Server chunks: 49,123 -> 3,194. Final image net ~125 MB smaller (build/server drops 421 MB, node_modules adds 296 MB). Drops vite-plugin-dynamic-import which is no longer needed. --- bun.lock | 6 +- package.json | 3 +- .../[version]/[platform]/[service]/specs.ts | 113 +++++------------- vite.config.ts | 8 -- 4 files changed, 35 insertions(+), 95 deletions(-) diff --git a/bun.lock b/bun.lock index 3ec792db019..8641b1f1393 100644 --- a/bun.lock +++ b/bun.lock @@ -1,10 +1,10 @@ { "lockfileVersion": 1, - "configVersion": 0, "workspaces": { "": { "name": "appwrite-website", "dependencies": { + "@appwrite.io/specs": "github:appwrite/specs#095d750d7036626d949279ddb7cd9510b902a13f", "@sentry/sveltekit": "^10.28.0", "@statsig/js-client": "^3.33.0", "@statsig/session-replay": "^3.33.0", @@ -17,7 +17,6 @@ "@appwrite.io/console": "^0.6.4", "@appwrite.io/pink": "~0.26.0", "@appwrite.io/pink-icons": "~0.26.0", - "@appwrite.io/specs": "github:appwrite/specs#095d750d7036626d949279ddb7cd9510b902a13f", "@eslint/compat": "^1.4.1", "@eslint/js": "^9.39.1", "@fingerprintjs/fingerprintjs": "^4.6.2", @@ -95,7 +94,6 @@ "typescript-eslint": "^8.48.1", "vaul-svelte": "1.0.0-next.7", "vite": "^7.2.7", - "vite-plugin-dynamic-import": "^1.6.0", "vite-plugin-image-optimizer": "^2.0.3", "vite-plugin-manifest-sri": "^0.2.0", "vitest": "^3.2.4", @@ -2102,8 +2100,6 @@ "vite-node": ["vite-node@3.2.4", "", { "dependencies": { "cac": "6.7.14", "debug": "4.4.1", "es-module-lexer": "1.7.0", "pathe": "2.0.3", "vite": "npm:rolldown-vite@7.1.19" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="], - "vite-plugin-dynamic-import": ["vite-plugin-dynamic-import@1.6.0", "", { "dependencies": { "acorn": "8.15.0", "es-module-lexer": "1.7.0", "fast-glob": "3.3.3", "magic-string": "0.30.18" } }, "sha512-TM0sz70wfzTIo9YCxVFwS8OA9lNREsh+0vMHGSkWDTZ7bgd1Yjs5RV8EgB634l/91IsXJReg0xtmuQqP0mf+rg=="], - "vite-plugin-image-optimizer": ["vite-plugin-image-optimizer@2.0.3", "", { "dependencies": { "ansi-colors": "^4.1.3", "pathe": "^2.0.3" }, "peerDependencies": { "sharp": ">=0.34.0", "svgo": ">=4", "vite": ">=5" }, "optionalPeers": ["sharp", "svgo"] }, "sha512-1vrFOTcpSvv6DCY7h8UXab4wqMAjTJB/ndOzG/Kmj1oDOuPF6mbjkNQoGzzCEYeWGe7qU93jc8oQqvoJ57al3A=="], "vite-plugin-manifest-sri": ["vite-plugin-manifest-sri@0.2.0", "", {}, "sha512-Zt5jt19xTIJ91LOuQTCtNG7rTFc5OziAjBz2H5NdCGqaOD1nxrWExLhcKW+W4/q8/jOPCg/n5ncYEQmqCxiGQQ=="], diff --git a/package.json b/package.json index 1481b6658df..b11170f4bbe 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "@statsig/session-replay": "^3.33.0", "@statsig/statsig-node-core": "^0.19.3", "@statsig/web-analytics": "^3.33.0", + "@appwrite.io/specs": "github:appwrite/specs#095d750d7036626d949279ddb7cd9510b902a13f", "h3": "^1.15.4", "sharp": "^0.34.5" }, @@ -39,7 +40,6 @@ "@appwrite.io/console": "^0.6.4", "@appwrite.io/pink": "~0.26.0", "@appwrite.io/pink-icons": "~0.26.0", - "@appwrite.io/specs": "github:appwrite/specs#095d750d7036626d949279ddb7cd9510b902a13f", "@eslint/compat": "^1.4.1", "@eslint/js": "^9.39.1", "@fingerprintjs/fingerprintjs": "^4.6.2", @@ -117,7 +117,6 @@ "typescript-eslint": "^8.48.1", "vaul-svelte": "1.0.0-next.7", "vite": "^7.2.7", - "vite-plugin-dynamic-import": "^1.6.0", "vite-plugin-image-optimizer": "^2.0.3", "vite-plugin-manifest-sri": "^0.2.0", "vitest": "^3.2.4", diff --git a/src/routes/docs/references/[version]/[platform]/[service]/specs.ts b/src/routes/docs/references/[version]/[platform]/[service]/specs.ts index 2bd2ef87fc3..338bec95a00 100644 --- a/src/routes/docs/references/[version]/[platform]/[service]/specs.ts +++ b/src/routes/docs/references/[version]/[platform]/[service]/specs.ts @@ -1,6 +1,13 @@ import { error } from '@sveltejs/kit'; import { OpenAPIV3 } from 'openapi-types'; -import { Platform, type ServiceValue, type Version } from '$lib/utils/references'; +import { Platform, type ServiceValue } from '$lib/utils/references'; +import { readFile } from 'node:fs/promises'; +import { createRequire } from 'node:module'; +import { dirname, join } from 'node:path'; + +const specsRoot = dirname( + createRequire(import.meta.url).resolve('@appwrite.io/specs/package.json') +); export type SDKMethod = { 'rate-limit': number; @@ -90,62 +97,15 @@ export const ModelType = { type ModelTypeType = keyof typeof ModelType; type ModelTypeValue = (typeof ModelType)[ModelTypeType]; -type ExampleVersion = Exclude; -type ExampleLoaders = Record Promise>; - -const examplesByVersion: Record = { - '0.15.x': import.meta.glob('/node_modules/@appwrite.io/specs/examples/0.15.x/**/*.md', { - query: '?raw', - import: 'default' - }), - '1.0.x': import.meta.glob('/node_modules/@appwrite.io/specs/examples/1.0.x/**/*.md', { - query: '?raw', - import: 'default' - }), - '1.1.x': import.meta.glob('/node_modules/@appwrite.io/specs/examples/1.1.x/**/*.md', { - query: '?raw', - import: 'default' - }), - '1.2.x': import.meta.glob('/node_modules/@appwrite.io/specs/examples/1.2.x/**/*.md', { - query: '?raw', - import: 'default' - }), - '1.3.x': import.meta.glob('/node_modules/@appwrite.io/specs/examples/1.3.x/**/*.md', { - query: '?raw', - import: 'default' - }), - '1.4.x': import.meta.glob('/node_modules/@appwrite.io/specs/examples/1.4.x/**/*.md', { - query: '?raw', - import: 'default' - }), - '1.5.x': import.meta.glob('/node_modules/@appwrite.io/specs/examples/1.5.x/**/*.md', { - query: '?raw', - import: 'default' - }), - '1.6.x': import.meta.glob('/node_modules/@appwrite.io/specs/examples/1.6.x/**/*.md', { - query: '?raw', - import: 'default' - }), - '1.7.x': import.meta.glob('/node_modules/@appwrite.io/specs/examples/1.7.x/**/*.md', { - query: '?raw', - import: 'default' - }), - '1.8.x': import.meta.glob('/node_modules/@appwrite.io/specs/examples/1.8.x/**/*.md', { - query: '?raw', - import: 'default' - }), - '1.9.x': import.meta.glob('/node_modules/@appwrite.io/specs/examples/1.9.x/**/*.md', { - query: '?raw', - import: 'default' - }) -}; - -function getExamples(version: string) { - if (!(version in examplesByVersion)) { - return undefined; +async function loadExample(relativePath: string): Promise { + try { + return await readFile(join(specsRoot, relativePath), 'utf8'); + } catch (e) { + if ((e as NodeJS.ErrnoException).code === 'ENOENT') { + return null; + } + throw e; } - - return examplesByVersion[version as ExampleVersion]; } function stripMarkdownCodeFence(content: string): string { @@ -396,23 +356,23 @@ export function getSchema(id: string, api: OpenAPIV3.Document): OpenAPIV3.Schema error(404, { message: `Not found` }); } -const specs = import.meta.glob('/node_modules/@appwrite.io/specs/specs/*/open-api3*.json', { - exhaustive: true -}); - export async function getApi(version: string, platform: string): Promise { const isClient = platform.startsWith('client-'); const mode = platform.startsWith('server-') ? 'server' : isClient ? 'client' : 'console'; const filename = `open-api3-${version}-${mode}.json`; - - const loader = Object.entries(specs).find(([key]) => key.endsWith(`/${filename}`))?.[1]; - - if (!loader) { - throw error(404, `Missing OpenAPI spec loader for ${filename}`); + const specPath = join(specsRoot, 'specs', version, filename); + + let raw: string; + try { + raw = await readFile(specPath, 'utf8'); + } catch (e) { + if ((e as NodeJS.ErrnoException).code === 'ENOENT') { + throw error(404, `Missing OpenAPI spec ${filename}`); + } + throw e; } - const loaded = (await loader()) as OpenAPIV3.Document | { default: OpenAPIV3.Document }; - return ('default' in loaded ? loaded.default : loaded) as OpenAPIV3.Document; + return JSON.parse(raw) as OpenAPIV3.Document; } const descriptions = import.meta.glob('./descriptions/*.md', { @@ -460,12 +420,6 @@ export async function getService( methods: [] }; - const examples = getExamples(version); - - if (!examples) { - return data; - } - for (const { method, value, url } of iterateAllMethods(api, service)) { const operation = value as AppwriteOperationObject; const parameters = getParameters(operation); @@ -507,22 +461,21 @@ export async function getService( } ); - const path = isAndroid - ? `/node_modules/@appwrite.io/specs/examples/${version}/${ + const examplePath = isAndroid + ? `examples/${version}/${ isAndroidServer ? 'server-kotlin' : 'client-android' }/${isAndroidJava ? 'java' : 'kotlin'}/${operation['x-appwrite']?.demo}` - : `/node_modules/@appwrite.io/specs/examples/${version}/${platform}/examples/${operation['x-appwrite']?.demo}`; + : `examples/${version}/${platform}/examples/${operation['x-appwrite']?.demo}`; - if (!(path in examples)) { + const demo = await loadExample(examplePath); + if (demo === null) { continue; } - const demo = (await examples[path]()) as unknown as string; - data.methods.push({ id: operation['x-appwrite'].method, group: operation['x-appwrite'].group, - demo: typeof demo === 'string' ? stripMarkdownCodeFence(demo) : '', + demo: stripMarkdownCodeFence(demo), title: operation.summary ?? '', description: operation.description ?? '', parameters: parameters ?? [], diff --git a/vite.config.ts b/vite.config.ts index bbc1820f9a5..5d6d5568758 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,6 +1,5 @@ import { enhancedImages } from '@sveltejs/enhanced-img'; import { sveltekit } from '@sveltejs/kit/vite'; -import dynamicImport from 'vite-plugin-dynamic-import'; import { ViteImageOptimizer } from 'vite-plugin-image-optimizer'; import manifestSRI from 'vite-plugin-manifest-sri'; import { defineConfig } from 'vitest/config'; @@ -21,13 +20,6 @@ export default defineConfig({ // }), enhancedImages(), sveltekit(), - dynamicImport({ - filter(id) { - if (id.includes('/node_modules/@appwrite.io/specs/examples')) { - return true; - } - } - }), ViteImageOptimizer({ include: ['**/*.png', '**/*.jpg', '**/*.jpeg', '**/*.gif', '**/*.svg'], exclude: ['**/*.avif', '**/*.webp'], From d4df7d7a4a013e0d98ba59bd3283158ce7859103 Mon Sep 17 00:00:00 2001 From: Atharva Deosthale Date: Tue, 5 May 2026 01:22:03 +0530 Subject: [PATCH 2/4] perf(specs): cache parsed APIs, parallelize example reads, validate URL params - Cache parsed OpenAPI documents in a module-scope Map keyed by version|platform. Restores the implicit caching the old import.meta.glob path got from Node's ESM module cache, avoids re-parsing multi-MB JSON on every request. - Replace the serial loadExample await loop with Promise.all over the prepared method list. Cuts I/O latency on services with many endpoints. - Validate version and platform against allowlists from references.ts before any path.join. URL segments shouldn't reach a filesystem read unguarded; a lone .. segment could escape examples/ otherwise. --- .../[version]/[platform]/[service]/specs.ts | 44 +++++++++++++++++-- 1 file changed, 40 insertions(+), 4 deletions(-) diff --git a/src/routes/docs/references/[version]/[platform]/[service]/specs.ts b/src/routes/docs/references/[version]/[platform]/[service]/specs.ts index 338bec95a00..ac48f56800f 100644 --- a/src/routes/docs/references/[version]/[platform]/[service]/specs.ts +++ b/src/routes/docs/references/[version]/[platform]/[service]/specs.ts @@ -1,6 +1,6 @@ import { error } from '@sveltejs/kit'; import { OpenAPIV3 } from 'openapi-types'; -import { Platform, type ServiceValue } from '$lib/utils/references'; +import { Platform, VALID_PLATFORMS, versions, type ServiceValue } from '$lib/utils/references'; import { readFile } from 'node:fs/promises'; import { createRequire } from 'node:module'; import { dirname, join } from 'node:path'; @@ -9,6 +9,24 @@ const specsRoot = dirname( createRequire(import.meta.url).resolve('@appwrite.io/specs/package.json') ); +// URL segments reach this module from SvelteKit route params, so anything +// interpolated into a filesystem path needs an explicit allowlist check. +const VALID_VERSIONS = new Set(versions as readonly string[]); + +function assertValidVersion(version: string): void { + if (!VALID_VERSIONS.has(version)) { + throw error(404, `Unknown spec version ${version}`); + } +} + +function assertValidPlatform(platform: string): void { + if (!VALID_PLATFORMS.has(platform as Platform)) { + throw error(404, `Unknown platform ${platform}`); + } +} + +const apiCache = new Map(); + export type SDKMethod = { 'rate-limit': number; 'rate-time': number; @@ -357,6 +375,15 @@ export function getSchema(id: string, api: OpenAPIV3.Document): OpenAPIV3.Schema } export async function getApi(version: string, platform: string): Promise { + assertValidVersion(version); + assertValidPlatform(platform); + + const cacheKey = `${version}|${platform}`; + const cached = apiCache.get(cacheKey); + if (cached) { + return cached; + } + const isClient = platform.startsWith('client-'); const mode = platform.startsWith('server-') ? 'server' : isClient ? 'client' : 'console'; const filename = `open-api3-${version}-${mode}.json`; @@ -372,7 +399,9 @@ export async function getApi(version: string, platform: string): Promise { const operation = value as AppwriteOperationObject; const parameters = getParameters(operation); const responses: SDKMethod['responses'] = Object.entries(operation.responses ?? {}).map( @@ -467,10 +496,17 @@ export async function getService( }/${isAndroidJava ? 'java' : 'kotlin'}/${operation['x-appwrite']?.demo}` : `examples/${version}/${platform}/examples/${operation['x-appwrite']?.demo}`; - const demo = await loadExample(examplePath); + return { method, value: operation, url, parameters, responses, examplePath }; + }); + + const demos = await Promise.all(prepared.map((p) => loadExample(p.examplePath))); + + for (let i = 0; i < prepared.length; i++) { + const demo = demos[i]; if (demo === null) { continue; } + const { method, value: operation, url, parameters, responses } = prepared[i]; data.methods.push({ id: operation['x-appwrite'].method, From 6ede4a8f535e7e26b5e8507659a29241a4985f04 Mon Sep 17 00:00:00 2001 From: Atharva Deosthale Date: Tue, 5 May 2026 01:59:12 +0530 Subject: [PATCH 3/4] fix(specs): move platform allowlist guard from getApi to getService MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit getApi only interpolates a derived literal mode (server/client/console) into the JSON path, never platform itself, so the platform allowlist there was rejecting valid internal callers (model-markdown.ts and models/[model]/+page.server.ts hardcode 'console-web' which isn't in the public Platform enum). Move the guard to getService where the raw platform value is actually interpolated into examples/${version}/${platform}/... — that's the real filesystem boundary. URL paths reaching getService still go through the [platform] route segment whose values come from the Platform enum, so the check stays correct there. Fixes 69 failing redirect tests for /docs/references/cloud/models/*. --- .../references/[version]/[platform]/[service]/specs.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/routes/docs/references/[version]/[platform]/[service]/specs.ts b/src/routes/docs/references/[version]/[platform]/[service]/specs.ts index ac48f56800f..8f42bc6296b 100644 --- a/src/routes/docs/references/[version]/[platform]/[service]/specs.ts +++ b/src/routes/docs/references/[version]/[platform]/[service]/specs.ts @@ -375,8 +375,10 @@ export function getSchema(id: string, api: OpenAPIV3.Document): OpenAPIV3.Schema } export async function getApi(version: string, platform: string): Promise { + // Only `version` reaches the filesystem path here; `platform` is collapsed to + // a fixed `mode` literal (server/client/console). Platform validation lives + // in `getService`, where the raw value is interpolated into the example path. assertValidVersion(version); - assertValidPlatform(platform); const cacheKey = `${version}|${platform}`; const cached = apiCache.get(cacheKey); @@ -430,6 +432,10 @@ export async function getService( }; methods: SDKMethod[]; }> { + // `platform` is interpolated into example paths below, so validate against + // the public allowlist before any filesystem access. + assertValidPlatform(platform); + /** * Exceptions for Android SDK. */ From c8fc2b46a0f6e1108d6dad7eae092e5a794b8b89 Mon Sep 17 00:00:00 2001 From: Atharva Deosthale Date: Tue, 5 May 2026 15:44:02 +0530 Subject: [PATCH 4/4] move specs out of node_modules so doesnt get pruned by sites --- scripts/build.js | 18 +++++++++ .../[version]/[platform]/[service]/specs.ts | 39 +++++++++++++++++-- 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/scripts/build.js b/scripts/build.js index 664d43a5a20..b0917286ddb 100644 --- a/scripts/build.js +++ b/scripts/build.js @@ -1,7 +1,25 @@ import { build } from 'vite'; +import { cp, mkdir } from 'node:fs/promises'; +import { createRequire } from 'node:module'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; async function main() { await build(); + + // Copy @appwrite.io/specs data into the build output so it ships with the + // deployment archive and is reachable at runtime even in environments that + // don't include node_modules (e.g. Appwrite Sites runtime, where only the + // build artifact is mounted). + const require = createRequire(import.meta.url); + const specsRoot = dirname(require.resolve('@appwrite.io/specs/package.json')); + const projectRoot = dirname(fileURLToPath(import.meta.url)).replace(/\/scripts$/, ''); + const target = resolve(projectRoot, 'build/_specs_data'); + + await mkdir(target, { recursive: true }); + await cp(resolve(specsRoot, 'specs'), resolve(target, 'specs'), { recursive: true }); + await cp(resolve(specsRoot, 'examples'), resolve(target, 'examples'), { recursive: true }); + console.log('[build] copied specs data to', target); } main(); diff --git a/src/routes/docs/references/[version]/[platform]/[service]/specs.ts b/src/routes/docs/references/[version]/[platform]/[service]/specs.ts index 8f42bc6296b..f52b36ec7b9 100644 --- a/src/routes/docs/references/[version]/[platform]/[service]/specs.ts +++ b/src/routes/docs/references/[version]/[platform]/[service]/specs.ts @@ -1,13 +1,44 @@ import { error } from '@sveltejs/kit'; import { OpenAPIV3 } from 'openapi-types'; import { Platform, VALID_PLATFORMS, versions, type ServiceValue } from '$lib/utils/references'; +import { existsSync } from 'node:fs'; import { readFile } from 'node:fs/promises'; import { createRequire } from 'node:module'; -import { dirname, join } from 'node:path'; +import { dirname, join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +// Spec data location. In environments where `@appwrite.io/specs` is installed +// (local dev, Docker prod with `bun install --production`), resolve via Node +// module resolution. In environments where only the build artifact ships +// (Appwrite Sites runtime), fall back to `_specs_data/` copied next to the +// server bundle by `scripts/build.js`. +function locateSpecsRoot(): string { + try { + const fromNodeModules = dirname( + createRequire(import.meta.url).resolve('@appwrite.io/specs/package.json') + ); + if (existsSync(join(fromNodeModules, 'specs'))) { + return fromNodeModules; + } + } catch { + // package not installed at runtime; fall through to bundled data + } + + const here = dirname(fileURLToPath(import.meta.url)); + const candidates = [ + resolve(here, '../../_specs_data'), + resolve(here, '../_specs_data'), + resolve(process.cwd(), '_specs_data') + ]; + for (const candidate of candidates) { + if (existsSync(join(candidate, 'specs'))) { + return candidate; + } + } + throw new Error('Unable to locate @appwrite.io/specs data'); +} -const specsRoot = dirname( - createRequire(import.meta.url).resolve('@appwrite.io/specs/package.json') -); +const specsRoot = locateSpecsRoot(); // URL segments reach this module from SvelteKit route params, so anything // interpolated into a filesystem path needs an explicit allowlist check.