From 4498a08b43f784863dd6edd7a33084e1bc2b32b0 Mon Sep 17 00:00:00 2001 From: Nico Schett Date: Tue, 18 Feb 2025 22:52:17 +0100 Subject: [PATCH 001/138] integrate pylon-builder into pylon-dev and implement pm2 for process management - Removed the pylon-builder package and utilized esbuild watch mode for faster builds. - Added pm2 for automatic server restarts and logging. - Introduced cross-environment client builds using gqty. - Breaking change: removed client generation feature; users should now utilize GQty CLI for client generation. --- .changeset/gold-boxes-switch.md | 17 + packages/pylon-builder/CHANGELOG.md | 13 - packages/pylon-builder/package.json | 33 - packages/pylon-builder/src/bundler/bundler.ts | 203 ---- .../pylon-builder/src/load-package-json.ts | 15 - packages/pylon-builder/tsconfig.json | 5 - packages/pylon-dev/package.json | 16 +- .../pylon-dev/src/builder/build-client.ts | 156 +++ .../pylon-dev/src/builder/bundler/bundler.ts | 82 ++ .../src/builder}/bundler/index.ts | 0 .../bundler/plugins/inject-code-plugin.ts | 107 ++ .../builder/bundler/plugins/notify-plugin.ts | 91 ++ .../src => pylon-dev/src/builder}/index.ts | 14 +- .../src/builder}/schema/builder.ts | 0 .../src/builder}/schema/schema-parser.ts | 0 .../schema/type-definition-builder.ts | 0 .../src/builder}/schema/types-helper.ts | 0 .../src/builder/update-file-if-changed.ts | 15 + packages/pylon-dev/src/index.ts | 301 ++--- pnpm-lock.yaml | 1079 +++++++++++++++-- 20 files changed, 1570 insertions(+), 577 deletions(-) create mode 100644 .changeset/gold-boxes-switch.md delete mode 100644 packages/pylon-builder/CHANGELOG.md delete mode 100644 packages/pylon-builder/package.json delete mode 100644 packages/pylon-builder/src/bundler/bundler.ts delete mode 100644 packages/pylon-builder/src/load-package-json.ts delete mode 100644 packages/pylon-builder/tsconfig.json create mode 100644 packages/pylon-dev/src/builder/build-client.ts create mode 100644 packages/pylon-dev/src/builder/bundler/bundler.ts rename packages/{pylon-builder/src => pylon-dev/src/builder}/bundler/index.ts (100%) create mode 100644 packages/pylon-dev/src/builder/bundler/plugins/inject-code-plugin.ts create mode 100644 packages/pylon-dev/src/builder/bundler/plugins/notify-plugin.ts rename packages/{pylon-builder/src => pylon-dev/src/builder}/index.ts (77%) rename packages/{pylon-builder/src => pylon-dev/src/builder}/schema/builder.ts (100%) rename packages/{pylon-builder/src => pylon-dev/src/builder}/schema/schema-parser.ts (100%) rename packages/{pylon-builder/src => pylon-dev/src/builder}/schema/type-definition-builder.ts (100%) rename packages/{pylon-builder/src => pylon-dev/src/builder}/schema/types-helper.ts (100%) create mode 100644 packages/pylon-dev/src/builder/update-file-if-changed.ts diff --git a/.changeset/gold-boxes-switch.md b/.changeset/gold-boxes-switch.md new file mode 100644 index 0000000..ac1b39c --- /dev/null +++ b/.changeset/gold-boxes-switch.md @@ -0,0 +1,17 @@ +--- +'@getcronit/pylon-dev': major +--- + +- Integrated `@getcronit/pylon-builder` directly into `@getcronit/pylon-dev`. + - Removed the `pylon-builder` package. + - The builder now utilizes the `esbuild` watch mode for development. This is a much faster and more efficient way to build the project. +- Implemented `pm2` for process management: + - `pm2` is now used to manage the `pylon-dev` server. After files are built, the server is restarted automatically. + - The stdout and stderr logs are logged directly with `consola`. +- Now builds a cross-environment client in `.pylon/client` using `gqty`. This will be used for pylon/pages. + +### Breaking Change: Removed Client Generation Feature + +- **What**: The client generation feature has been removed. +- **Why**: We have decided to use `gqty` directly to streamline the development process and reduce complexity. +- **How to Update**: Consumers should now use the [GQty CLI](https://gqty.dev/api-reference/cli#basic-usage) directly to generate their clients. Update your build scripts and development workflows to integrate `gqty` as described in the GQty documentation. diff --git a/packages/pylon-builder/CHANGELOG.md b/packages/pylon-builder/CHANGELOG.md deleted file mode 100644 index 7937753..0000000 --- a/packages/pylon-builder/CHANGELOG.md +++ /dev/null @@ -1,13 +0,0 @@ -# @getcronit/pylon-builder - -## 2.6.7 - -### Patch Changes - -- [#67](https://github.com/getcronit/pylon/pull/67) [`eb85d99`](https://github.com/getcronit/pylon/commit/eb85d9920235e0322f39f46576e1098526e871b5) Thanks [@schettn](https://github.com/schettn)! - Missing build before release lead to broken packages - -## 2.6.6 - -### Patch Changes - -- [#62](https://github.com/getcronit/pylon/pull/62) [`084df6d`](https://github.com/getcronit/pylon/commit/084df6daa53ccfe575db1aacbd1a07adebf8a716) Thanks [@schettn](https://github.com/schettn)! - Replace bun with pnpm, replace bun build with esbuild and replace semantic-release with changesets. diff --git a/packages/pylon-builder/package.json b/packages/pylon-builder/package.json deleted file mode 100644 index 2094798..0000000 --- a/packages/pylon-builder/package.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "name": "@getcronit/pylon-builder", - "version": "2.6.7", - "type": "module", - "main": "dist/index.js", - "types": "dist/index.d.ts", - "scripts": { - "build": "rimraf ./dist && esbuild ./src/index.ts --bundle --platform=node --target=node18 --format=esm --minify --outdir=./dist --sourcemap=linked --packages=external && pnpm run build:declarations", - "build:declarations": "tsc --declaration --emitDeclarationOnly --outDir ./dist" - }, - "files": [ - "dist" - ], - "author": "Nico Schett ", - "license": "Apache-2.0", - "repository": { - "type": "git", - "url": "https://github.com/getcronit/pylon.git", - "directory": "packages/pylon-builder" - }, - "homepage": "https://pylon.cronit.io", - "dependencies": { - "chokidar": "^3.5.3", - "consola": "^3.2.3", - "esbuild": "^0.23.1", - "esbuild-plugin-tsc": "^0.4.0", - "source-map-support": "^0.5.21", - "typescript": "^5.0.0" - }, - "engines": { - "node": ">=18.0.0" - } -} diff --git a/packages/pylon-builder/src/bundler/bundler.ts b/packages/pylon-builder/src/bundler/bundler.ts deleted file mode 100644 index 7f41e63..0000000 --- a/packages/pylon-builder/src/bundler/bundler.ts +++ /dev/null @@ -1,203 +0,0 @@ -// bundler.ts -import fs from 'fs' -import chokidar from 'chokidar' -import {Plugin, build} from 'esbuild' -import esbuildPluginTsc from 'esbuild-plugin-tsc' - -import path from 'path' -import consola from 'consola' - -export interface BundlerBuildOptions { - getBuildDefs: () => { - typeDefs: string - resolvers: Record< - string, - { - __resolveType?: (obj: any) => string - } - > - } - watch?: boolean - onWatch?: (output: { - totalFiles: number - totalSize: number - schemaChanged: boolean - duration: number - }) => void -} - -export class Bundler { - sfiFilePath: string - outputDir: string - - private cachedTypeDefs: string | null = null - - constructor(sfiFilePath: string, outputDir: string = './.pylon') { - this.sfiFilePath = sfiFilePath - this.outputDir = outputDir - } - - public async build(options: BundlerBuildOptions) { - const buildOnce = async () => { - const startTime = Date.now() - - const {typeDefs, resolvers} = options.getBuildDefs() - - const preparedResolvers = prepareObjectInjection(resolvers) - - const injectCodePlugin: Plugin = { - name: 'inject-code', - setup(build) { - build.onLoad( - {filter: /src[\/\\]index\.ts$/, namespace: 'file'}, - async args => { - // Convert to relative path to ensure we match `src/index.ts` at root - const relativePath = path.relative(process.cwd(), args.path) - - if (relativePath !== path.join('src', 'index.ts')) { - return - } - - const contents = await fs.promises.readFile(args.path, 'utf-8') - - return { - loader: 'ts', - contents: - contents + - ` - import {handler as __internalPylonHandler} from "@getcronit/pylon" - - let __internalPylonConfig = undefined - - try { - __internalPylonConfig = config - } catch { - // config is not declared, pylonConfig remains undefined - } - - app.use(__internalPylonHandler({ - typeDefs: ${JSON.stringify(typeDefs)}, - graphql, - resolvers: ${preparedResolvers}, - config: __internalPylonConfig - })) - ` - } - } - ) - } - } - - const inputPath = path.join(process.cwd(), this.sfiFilePath) - const dir = path.join(process.cwd(), this.outputDir) - - const output = await build({ - logLevel: 'silent', - metafile: true, - entryPoints: [inputPath], - outdir: dir, - bundle: true, - format: 'esm', - sourcemap: 'inline', - packages: 'external', - plugins: [ - injectCodePlugin, - esbuildPluginTsc({ - tsconfigPath: path.join(process.cwd(), 'tsconfig.json') - }) - ] - }) - - if (output.errors.length > 0) { - for (const error of output.errors) { - consola.error(error) - } - - throw new Error('Failed to build Pylon') - } - - if (output.warnings.length > 0) { - for (const warning of output.warnings) { - consola.warn(warning) - } - } - - const schemaChanged = this.cachedTypeDefs !== typeDefs - - this.cachedTypeDefs = typeDefs - - const duration = Date.now() - startTime - - const totalFiles = Object.keys(output.metafile.inputs).length - const totalSize = Object.values(output.metafile.outputs).reduce( - (acc, output) => acc + output.bytes, - 0 - ) - - // Write the typeDefs to a file - const typeDefsPath = path.join( - process.cwd(), - this.outputDir, - 'schema.graphql' - ) - - await fs.promises.writeFile(typeDefsPath, typeDefs) - - // Write base resolvers to a file - - const resolversPath = path.join( - process.cwd(), - this.outputDir, - 'resolvers.js' - ) - - await fs.promises.writeFile( - resolversPath, - `export const resolvers = ${preparedResolvers}` - ) - - return { - totalFiles, - totalSize, - schemaChanged, - duration - } - } - - if (options.watch) { - const folder = path.dirname(this.sfiFilePath) - - chokidar.watch(folder).on('change', async () => { - try { - const output = await buildOnce() - - if (options.onWatch) { - options.onWatch(output) - } - } catch (e) { - consola.error(e) - } - }) - } - - return await buildOnce() - } -} - -function prepareObjectInjection(obj: object) { - const entries = Object.entries(obj).map(([key, value]) => { - if (value === undefined) { - return undefined - } else if (typeof value === 'string') { - return `${key}:${value}` - } else if (typeof value === 'function') { - return `${key}:${value.toString()}` - } else if (typeof value === 'object' && !Array.isArray(value)) { - return `${key}:${prepareObjectInjection(value)}` - } else { - return `${key}:${JSON.stringify(value)}` - } - }) - - return `{${entries.join(',')}}` -} diff --git a/packages/pylon-builder/src/load-package-json.ts b/packages/pylon-builder/src/load-package-json.ts deleted file mode 100644 index 104a6ad..0000000 --- a/packages/pylon-builder/src/load-package-json.ts +++ /dev/null @@ -1,15 +0,0 @@ -import path from 'path' -import {readFile} from 'fs/promises' - -export async function loadPackageJson(): Promise<{ - baseURL?: string - pylon?: { - external?: string[] - } -}> { - const packageJsonPath = path.resolve(process.cwd(), 'package.json') - - const file = await readFile(packageJsonPath) - const packageJson = JSON.parse(file.toString()) - return packageJson -} diff --git a/packages/pylon-builder/tsconfig.json b/packages/pylon-builder/tsconfig.json deleted file mode 100644 index a49984e..0000000 --- a/packages/pylon-builder/tsconfig.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "include": ["src/**/*"], - "exclude": ["node_modules/**/*", "**/*.test.ts", "**/*.test.tsx", "**/__tests__/**/*"] -} \ No newline at end of file diff --git a/packages/pylon-dev/package.json b/packages/pylon-dev/package.json index d2b367a..f4793c4 100644 --- a/packages/pylon-dev/package.json +++ b/packages/pylon-dev/package.json @@ -21,18 +21,26 @@ }, "homepage": "https://pylon.cronit.io", "dependencies": { - "@getcronit/pylon-builder": "workspace:^", - "@getcronit/pylon-telemetry": "workspace:^", - "@gqty/cli": "^4.2.0", + "@getcronit/pylon-telemetry": "workspace:^1.0.0", + "@gqty/cli": "^4.2.5", "commander": "^12.1.0", "consola": "^3.2.3", "dotenv": "^16.4.5", - "treekill": "^1.0.0" + "esbuild": "^0.23.1", + "esbuild-plugin-tsc": "^0.4.0", + "pm2": "^5.4.3" }, "publishConfig": { "access": "public" }, "engines": { "node": ">=18.0.0" + }, + "devDependencies": { + "typescript": "^5.7.3" + }, + "peerDependencies": { + "@getcronit/pylon": "workspace:^2.0.0", + "graphql": "^16.9.0" } } diff --git a/packages/pylon-dev/src/builder/build-client.ts b/packages/pylon-dev/src/builder/build-client.ts new file mode 100644 index 0000000..e3e644d --- /dev/null +++ b/packages/pylon-dev/src/builder/build-client.ts @@ -0,0 +1,156 @@ +import path from 'path' +import fs from 'fs/promises' +import {generateClient} from '@gqty/cli' +import {buildSchema} from 'graphql' +import {updateFileIfChanged} from './update-file-if-changed' + +const PYLON_SCHEMA_PATH = path.join(process.cwd(), '.pylon/schema.graphql') +const PYLON_CLIENT_PATH = path.join(process.cwd(), '.pylon/client/index.ts') + +export interface BuildClientOptions { + /** + * Client will be generated if the schema has changed or if the client does not exist + */ + schemaChanged: boolean +} + +export const buildClient = async ({schemaChanged}: BuildClientOptions) => { + // Check if the schema exists + + try { + await fs.access(PYLON_SCHEMA_PATH) + } catch (e) { + throw new Error( + 'Schema not found. Please run `pylon build` or `pylon dev` first.' + ) + } + + // Check if the client exists + if (!schemaChanged) { + // If the schema has not changed, we need to check if the client exists + try { + await fs.access(PYLON_CLIENT_PATH) + return + } catch (e) { + // If the client does not exist, we need to generate it + } + } + + const schema = await fs.readFile(PYLON_SCHEMA_PATH, 'utf-8') + + const schemaObj = buildSchema(schema) + + // Write the custom client index file because the default one is not compatible with Pylon + await fs.mkdir(path.dirname(PYLON_CLIENT_PATH), {recursive: true}) + await updateFileIfChanged(PYLON_CLIENT_PATH, customClientIndex) + + await generateClient(schemaObj, { + endpoint: 'will-be-overwritten', + frameworks: ['react'], + destination: PYLON_CLIENT_PATH, + react: true, + scalarTypes: { + Number: 'number', + Object: 'Record' + } + }) +} + +const customClientIndex = `/** + * GQty: You can safely modify this file based on your needs. + */ + +import {createReactClient} from '@gqty/react' +import { + Cache, + createClient, + defaultResponseHandler, + type QueryFetcher +} from 'gqty' +import { + generatedSchema, + scalarsEnumsHash, + type GeneratedSchema +} from './schema.generated' + +const queryFetcher: QueryFetcher = async function ( + {query, variables, operationName}, + fetchOptions +) { + let browserOrInternalFetch: typeof fetch | typeof app.request = fetch + + try { + const moduleNameToPreventBundling = '@getcronit/pylon' + const {app} = await import(moduleNameToPreventBundling) + + browserOrInternalFetch = app.request + } catch (error) { + // Pylon is not found. Maybe we are running in a different environment. + } + + const response = await browserOrInternalFetch('/graphql', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + query, + variables, + operationName + }), + mode: 'cors', + ...fetchOptions + }) + + return await defaultResponseHandler(response) +} + +const cache = new Cache( + undefined, + /** + * Default option is immediate cache expiry but keep it for 5 minutes, + * allowing soft refetches in background. + */ + { + maxAge: Infinity, + staleWhileRevalidate: 5 * 60 * 1000, + normalization: true + } +) + +export const client = createClient({ + schema: generatedSchema, + scalars: scalarsEnumsHash, + cache, + fetchOptions: { + fetcher: queryFetcher + } +}) + +// Core functions +export const {resolve, subscribe, schema} = client + +// Legacy functions +export const {query, mutation, mutate, subscription, resolved, refetch, track} = + client + +export const { + graphql, + useQuery, + usePaginatedQuery, + useTransactionQuery, + useLazyQuery, + useRefetch, + useMutation, + useMetaState, + prepareReactRender, + useHydrateCache, + prepareQuery +} = createReactClient(client, { + defaults: { + // Enable Suspense, you can override this option for each hook. + suspense: false + } +}) + +export * from './schema.generated'` diff --git a/packages/pylon-dev/src/builder/bundler/bundler.ts b/packages/pylon-dev/src/builder/bundler/bundler.ts new file mode 100644 index 0000000..5d218cf --- /dev/null +++ b/packages/pylon-dev/src/builder/bundler/bundler.ts @@ -0,0 +1,82 @@ +// bundler.ts +import esbuild, {context} from 'esbuild' +import esbuildPluginTsc from 'esbuild-plugin-tsc' + +import path from 'path' +import fs from 'fs/promises' +import { + InjectCodePluginOptions, + injectCodePlugin +} from './plugins/inject-code-plugin' +import {NotifyPluginOptions, notifyPlugin} from './plugins/notify-plugin' +import {updateFileIfChanged} from '../update-file-if-changed' + +export interface BundlerBuildOptions { + getBuildDefs: InjectCodePluginOptions['getBuildDefs'] + onBuild?: NotifyPluginOptions['onBuild'] +} + +export class Bundler { + sfiFilePath: string + outputDir: string + + constructor(sfiFilePath: string, outputDir: string = './.pylon') { + this.sfiFilePath = sfiFilePath + this.outputDir = outputDir + } + + public async build(options: BundlerBuildOptions) { + const inputPath = path.join(process.cwd(), this.sfiFilePath) + const dir = path.join(process.cwd(), this.outputDir) + + // Create directory if it doesn't exist + await fs.mkdir(dir, {recursive: true}) + + const writeOnEndPlugin: esbuild.Plugin = { + name: 'write-on-end', + setup(build) { + build.onEnd(async result => { + await Promise.all( + result.outputFiles!.map(async file => { + await fs.mkdir(path.dirname(file.path), {recursive: true}) + await updateFileIfChanged(file.path, file.text) + }) + ) + }) + } + } + + const ctx = await context({ + write: false, + logLevel: 'silent', + metafile: true, + entryPoints: [inputPath], + outdir: dir, + bundle: true, + format: 'esm', + sourcemap: 'inline', + packages: 'external', + + plugins: [ + notifyPlugin({ + dir, + onBuild: async output => { + await options.onBuild?.(output) + } + }), + injectCodePlugin({ + getBuildDefs: options.getBuildDefs, + outputDir: this.outputDir + }), + esbuildPluginTsc({ + tsconfigPath: path.join(process.cwd(), 'tsconfig.json') + }), + writeOnEndPlugin + ] + }) + + await ctx.rebuild() + + return ctx + } +} diff --git a/packages/pylon-builder/src/bundler/index.ts b/packages/pylon-dev/src/builder/bundler/index.ts similarity index 100% rename from packages/pylon-builder/src/bundler/index.ts rename to packages/pylon-dev/src/builder/bundler/index.ts diff --git a/packages/pylon-dev/src/builder/bundler/plugins/inject-code-plugin.ts b/packages/pylon-dev/src/builder/bundler/plugins/inject-code-plugin.ts new file mode 100644 index 0000000..add9ad7 --- /dev/null +++ b/packages/pylon-dev/src/builder/bundler/plugins/inject-code-plugin.ts @@ -0,0 +1,107 @@ +import {Plugin} from 'esbuild' +import path from 'path' +import fs from 'fs/promises' +import {updateFileIfChanged} from '../../update-file-if-changed' + +export interface InjectCodePluginOptions { + getBuildDefs: () => { + typeDefs: string + resolvers: Record< + string, + { + __resolveType?: (obj: any) => string + } + > + } + outputDir: string +} + +export const injectCodePlugin = ({ + getBuildDefs, + outputDir +}: InjectCodePluginOptions): Plugin => ({ + name: 'inject-code', + setup(build) { + build.onLoad( + {filter: /src[\/\\]index\.ts$/, namespace: 'file'}, + async args => { + // Convert to relative path to ensure we match `src/index.ts` at root + const relativePath = path.relative(process.cwd(), args.path) + + if (relativePath !== path.join('src', 'index.ts')) { + return + } + + const {typeDefs, resolvers} = getBuildDefs() + + const preparedResolvers = prepareObjectInjection(resolvers) + + const contents = await fs.readFile(args.path, 'utf-8') + + // Write the typeDefs to a file + const typeDefsPath = path.join( + process.cwd(), + outputDir, + 'schema.graphql' + ) + + await updateFileIfChanged(typeDefsPath, typeDefs) + + // Write base resolvers to a file + + const resolversPath = path.join( + process.cwd(), + outputDir, + 'resolvers.js' + ) + + await updateFileIfChanged( + resolversPath, + `export const resolvers = ${preparedResolvers}` + ) + + return { + loader: 'ts', + contents: + contents + + ` + import {handler as __internalPylonHandler} from "@getcronit/pylon" + + let __internalPylonConfig = undefined + + try { + __internalPylonConfig = config + } catch { + // config is not declared, pylonConfig remains undefined + } + + app.use(__internalPylonHandler({ + typeDefs: ${JSON.stringify(typeDefs)}, + graphql, + resolvers: ${preparedResolvers}, + config: __internalPylonConfig + })) + ` + } + } + ) + } +}) + +function prepareObjectInjection(obj: object) { + const entries = Object.entries(obj).map(([key, value]) => { + if (value === undefined) { + return undefined + } else if (typeof value === 'string') { + return `${key}:${value}` + } else if (typeof value === 'function') { + return `${key}:${value.toString()}` + } else if (typeof value === 'object' && !Array.isArray(value)) { + return `${key}:${prepareObjectInjection(value)}` + } else { + return `${key}:${JSON.stringify(value)}` + } + }) + + return `{${entries.join(',')}}` +} diff --git a/packages/pylon-dev/src/builder/bundler/plugins/notify-plugin.ts b/packages/pylon-dev/src/builder/bundler/plugins/notify-plugin.ts new file mode 100644 index 0000000..c9b51ea --- /dev/null +++ b/packages/pylon-dev/src/builder/bundler/plugins/notify-plugin.ts @@ -0,0 +1,91 @@ +import {Plugin} from 'esbuild' +import path from 'path' +import fs from 'fs/promises' +import consola from 'consola' + +export interface NotifyPluginOptions { + onBuild?: (output: { + totalFiles: number + totalSize: number + schemaChanged: boolean + duration: number + }) => Promise | void + dir: string +} + +export const notifyPlugin = ({dir, onBuild}: NotifyPluginOptions): Plugin => ({ + name: 'notify', + async setup(build) { + const loadSchema = async () => { + const schemaPath = path.join(dir, 'schema.graphql') + + try { + await fs.access(schemaPath) + } catch { + return null + } + + return await fs.readFile(schemaPath, 'utf-8') + } + + let cachedSchema: string | null = await loadSchema() + + let startTime = Date.now() + build.onStart(async () => { + startTime = Date.now() + consola.start('[Pylon]: Building...') + }) + + build.onEnd(async result => { + if (result.errors.length > 0) { + for (const error of result.errors) { + consola.error(`[Pylon]: ${error.text} +${ + error.location + ? `at ${error.location.file}:${error.location.line}:${error.location.column}` + : '' +} +${error.detail ? error.detail : ''}`) + } + + throw new Error('Failed to build Pylon') + } + + if (result.warnings.length > 0) { + for (const warning of result.warnings) { + consola.warn(warning) + } + } + + const duration = Date.now() - startTime + + const totalFiles = Object.keys(result.metafile!.inputs).length + + const totalSize = Object.values(result.metafile!.outputs).reduce( + (acc, output) => acc + output.bytes, + 0 + ) + + const latestSchema = await loadSchema() + + consola.success(`[Pylon]: Built in ${duration}ms`) + + const schemaChanged = latestSchema !== cachedSchema + + if (schemaChanged) { + consola.info('[Pylon]: Schema updated') + + cachedSchema = latestSchema + } + + if (onBuild) { + await onBuild({ + totalFiles, + totalSize, + schemaChanged, + duration + }) + } + }) + } +}) diff --git a/packages/pylon-builder/src/index.ts b/packages/pylon-dev/src/builder/index.ts similarity index 77% rename from packages/pylon-builder/src/index.ts rename to packages/pylon-dev/src/builder/index.ts index df5e4d1..ad0a69a 100644 --- a/packages/pylon-builder/src/index.ts +++ b/packages/pylon-dev/src/builder/index.ts @@ -5,8 +5,7 @@ import {SchemaBuilder} from './schema/builder.js' export interface BuildOptions { sfiFilePath: string outputFilePath: string - watch?: boolean - onWatch?: (output: { + onBuild?: (output: { totalFiles: number totalSize: number schemaChanged: boolean @@ -19,12 +18,12 @@ export {SchemaBuilder} export const build = async (options: BuildOptions) => { const bundler = new Bundler(options.sfiFilePath, options.outputFilePath) + const builder = new SchemaBuilder( + path.join(process.cwd(), options.sfiFilePath) + ) + return await bundler.build({ getBuildDefs: () => { - const builder = new SchemaBuilder( - path.join(process.cwd(), options.sfiFilePath) - ) - const built = builder.build() const typeDefs = built.typeDefs @@ -34,7 +33,6 @@ export const build = async (options: BuildOptions) => { resolvers: built.resolvers } }, - watch: options.watch, - onWatch: options.onWatch + onBuild: options.onBuild }) } diff --git a/packages/pylon-builder/src/schema/builder.ts b/packages/pylon-dev/src/builder/schema/builder.ts similarity index 100% rename from packages/pylon-builder/src/schema/builder.ts rename to packages/pylon-dev/src/builder/schema/builder.ts diff --git a/packages/pylon-builder/src/schema/schema-parser.ts b/packages/pylon-dev/src/builder/schema/schema-parser.ts similarity index 100% rename from packages/pylon-builder/src/schema/schema-parser.ts rename to packages/pylon-dev/src/builder/schema/schema-parser.ts diff --git a/packages/pylon-builder/src/schema/type-definition-builder.ts b/packages/pylon-dev/src/builder/schema/type-definition-builder.ts similarity index 100% rename from packages/pylon-builder/src/schema/type-definition-builder.ts rename to packages/pylon-dev/src/builder/schema/type-definition-builder.ts diff --git a/packages/pylon-builder/src/schema/types-helper.ts b/packages/pylon-dev/src/builder/schema/types-helper.ts similarity index 100% rename from packages/pylon-builder/src/schema/types-helper.ts rename to packages/pylon-dev/src/builder/schema/types-helper.ts diff --git a/packages/pylon-dev/src/builder/update-file-if-changed.ts b/packages/pylon-dev/src/builder/update-file-if-changed.ts new file mode 100644 index 0000000..d85ad34 --- /dev/null +++ b/packages/pylon-dev/src/builder/update-file-if-changed.ts @@ -0,0 +1,15 @@ +import fs from 'fs/promises' + +export async function updateFileIfChanged(path: string, newContent: string) { + try { + const currentContent = await fs.readFile(path, 'utf8') + if (currentContent === newContent) { + return false // No update needed + } + } catch (err: any) { + if (err.code !== 'ENOENT') throw err // Ignore file not found error + } + + await fs.writeFile(path, newContent, 'utf8') + return true // File created or updated +} diff --git a/packages/pylon-dev/src/index.ts b/packages/pylon-dev/src/index.ts index 9346c95..e231c1e 100644 --- a/packages/pylon-dev/src/index.ts +++ b/packages/pylon-dev/src/index.ts @@ -1,15 +1,14 @@ #!/usr/bin/env node -import {build} from '@getcronit/pylon-builder' -import {fetchSchema, generateClient} from '@gqty/cli' +import * as telemetry from '@getcronit/pylon-telemetry' import {program, type Command} from 'commander' import consola from 'consola' -import path from 'path' -import {version} from '../package.json' -import {ChildProcess, spawn} from 'child_process' -import kill from 'treekill' -import * as telemetry from '@getcronit/pylon-telemetry' import dotenv from 'dotenv' +import pm2 from 'pm2' + +import {version} from '../package.json' +import {build} from './builder' +import {buildClient} from './builder/build-client' dotenv.config() @@ -19,21 +18,23 @@ program .command('build') .description('Build the Pylon Schema') .action(async () => { - consola.start('[Pylon]: Building schema') - - const {totalFiles, totalSize, duration} = await build({ + const ctx = await build({ sfiFilePath: './src/index.ts', - outputFilePath: './.pylon' - }) + outputFilePath: './.pylon', + onBuild: async ({totalFiles, totalSize, duration, schemaChanged}) => { + await telemetry.sendBuildEvent({ + duration, + totalFiles, + totalSize, + isDevelopment: false + }) - await telemetry.sendBuildEvent({ - duration: duration, - totalFiles, - totalSize, - isDevelopment: false + await buildClient({schemaChanged}) + } }) - consola.success('[Pylon]: Schema built') + await ctx.rebuild() + await ctx.dispose() }) program @@ -43,18 +44,6 @@ program 'Command to run the server', 'bun run .pylon/index.js' ) - .option('--client', "Generate the client from the server's schema") - .option('--test', 'Test') - .option( - '--client-path ', - 'Path to generate the client to', - 'gqty/index.ts' - ) - .option( - '--client-port ', - 'Port of the pylon server to generate the client from', - '3000' - ) .action(main) type ArgOptions = { @@ -64,216 +53,78 @@ type ArgOptions = { clientPort: string } -const start = Date.now() - async function main(options: ArgOptions, command: Command) { - consola.log(`[Pylon]: ${command.name()} version ${command.version()}`) - - let currentProc: ChildProcess | null = null - - let serve = async (shouldGenerateClient: boolean = false) => { - if (currentProc) { - // Remove all listeners to prevent the pylon dev server from crashing - currentProc.removeAllListeners() - - kill(currentProc.pid, 'SIGINT', err => { - if (err) { - consola.error(err) - } - }) - } - - const [commandName, ...args] = options.command.split(' ') - - currentProc = spawn(commandName, args, { - shell: true, - stdio: 'inherit', - env: { - ...process.env, - NODE_ENV: 'development' - } - }) - - currentProc.on('exit', code => { - // if (code === 143 || code === null) { - // return - // } - - if (code === 0) { - consola.success('Pylon server stopped') - process.exit(0) - } - - consola.error( - `Pylon exited with code ${code}, fix the error and save the file to restart the server` - ) - }) - - if ( - shouldGenerateClient && - options.client && - options.clientPath && - options.clientPort - ) { - const clientPath = path.resolve(process.cwd(), options.clientPath) - - const endpoint = `http://localhost:${options.clientPort}/graphql` - - console.log('Generating client...', endpoint) - - const generate = async () => { - consola.start('[Pylon]: Fetching schema from server') - - const schema = await fetchSchema(endpoint, { - silent: true - }) - - consola.success('[Pylon]: Schema fetched') - - consola.start('[Pylon]: Generating client') - - await generateClient(schema, { - endpoint, - destination: clientPath, - react: true, - scalarTypes: { - Number: 'number', - Object: 'Record' - } - }) - - consola.success('[Pylon]: Client generated') - } - - let retries = 0 - - const generateWithRetry = async () => { - try { - await generate() - } catch (e) { - retries++ - - if (retries < 5) { - setTimeout(() => { - generateWithRetry() - }, 1000) - } - } - } - - generateWithRetry() + pm2.connect(async function (err) { + if (err) { + consola.error(err) + process.exit(1) } - } - - consola.start('[Pylon]: Building schema') - try { - const {duration, totalFiles, totalSize} = await build({ + const ctx = await build({ sfiFilePath: './src/index.ts', outputFilePath: `./.pylon`, - watch: true, - onWatch: async ({schemaChanged, totalFiles, totalSize, duration}) => { - const isServerRunning = currentProc !== null - - if (isServerRunning) { - consola.start('[Pylon]: Reloading server') - } else { - consola.start('[Pylon]: Starting server') - } - - await serve(schemaChanged) - - if (isServerRunning) { - consola.ready('[Pylon]: Server reloaded') - } else { - consola.ready('[Pylon]: Server started') - - consola.box(` - Pylon is up and running! - - Press \`Ctrl + C\` to stop the server. - - Encounter any issues? Report them here: - https://github.com/getcronit/pylon/issues - - We value your feedback—help us make Pylon even better! - `) - } - - if (schemaChanged) { - consola.info('[Pylon]: Schema updated') - - await telemetry.sendBuildEvent({ - duration, - totalFiles, - totalSize, - isDevelopment: true - }) - } + onBuild: async ({schemaChanged, totalFiles, totalSize, duration}) => { + await buildClient({schemaChanged}) } }) - await telemetry.sendBuildEvent({ - duration, - totalFiles, - totalSize, - isDevelopment: true - }) - - consola.success('[Pylon]: Schema built') - - consola.start('[Pylon]: Starting server') - await serve(true) - consola.ready('[Pylon]: Server started') + await ctx.watch() - consola.box(` - Pylon is up and running! - - Press \`Ctrl + C\` to stop the server. - - Encounter any issues? Report them here: - https://github.com/getcronit/pylon/issues - - We value your feedback—help us make Pylon even better! - `) - } catch (e) { - consola.error("[Pylon]: Couldn't build schema", e) - - // Kill the server if it's running - const proc = currentProc as ChildProcess | null - if (proc) { - proc.removeAllListeners() + pm2.launchBus((err, bus) => { + if (err) { + consola.error(err) + return + } - kill(proc.pid, 'SIGINT', err => { - if (err) { - consola.error(err) - } + bus.on('log:out', data => { + consola.log(data.data.trim()) }) - } - } - process.on('SIGINT', async code => { - try { - if (currentProc) { - currentProc.removeAllListeners() + bus.on('log:err', data => { + consola.error(data.data) + }) + }) - kill(currentProc.pid, 'SIGINT', err => { - if (err) { - consola.error(err) - } - }) + pm2.start( + { + name: 'pylon-dev', + script: options.command, + // args: args, + exec_mode: 'fork', + instances: 1, + autorestart: true, + watch: ['./.pylon'], + restart_delay: 1000, + watch_delay: 1000 as any, + ignore_watch: ['node_modules'], + env: { + ...process.env, + NODE_ENV: 'development' + } + } as any, + function (err, apps) { + // Check if it is a duplicate start + if (err) throw err + + consola.box(` +Pylon is up and running! + +Press \`Ctrl + C\` to stop the server. + +Encounter any issues? Report them here: +https://github.com/getcronit/pylon/issues + +We value your feedback—help us make Pylon even better!`) } - } catch { - // Ignore - } finally { - await telemetry.sendDevEvent({ - duration: Date.now() - start, - clientPath: options.clientPath, - clientPort: parseInt(options.clientPort) - }) + ) - process.exit(0) - } + process.on('SIGINT', async code => { + await ctx.cancel() + pm2.delete('pylon-dev', function (err) { + pm2.disconnect() + process.exit(0) + }) + }) }) } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 057acff..9618d1d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -213,38 +213,17 @@ importers: specifier: ^8.54.0 version: 8.54.0 - packages/pylon-builder: - dependencies: - chokidar: - specifier: ^3.5.3 - version: 3.6.0 - consola: - specifier: ^3.2.3 - version: 3.4.0 - esbuild: - specifier: ^0.23.1 - version: 0.23.1 - esbuild-plugin-tsc: - specifier: ^0.4.0 - version: 0.4.0(typescript@5.7.3) - source-map-support: - specifier: ^0.5.21 - version: 0.5.21 - typescript: - specifier: ^5.0.0 - version: 5.7.3 - packages/pylon-dev: dependencies: - '@getcronit/pylon-builder': - specifier: workspace:^ - version: link:../pylon-builder + '@getcronit/pylon': + specifier: workspace:^2.0.0 + version: link:../pylon '@getcronit/pylon-telemetry': - specifier: workspace:^ + specifier: workspace:^1.0.0 version: link:../pylon-telemetry '@gqty/cli': - specifier: ^4.2.0 - version: 4.2.2(@babel/core@7.26.7)(typescript@5.7.3) + specifier: ^4.2.5 + version: 4.2.5(@babel/core@7.26.7)(@types/node@22.13.1)(typescript@5.7.3) commander: specifier: ^12.1.0 version: 12.1.0 @@ -254,9 +233,22 @@ importers: dotenv: specifier: ^16.4.5 version: 16.4.7 - treekill: - specifier: ^1.0.0 - version: 1.0.0 + esbuild: + specifier: ^0.23.1 + version: 0.23.1 + esbuild-plugin-tsc: + specifier: ^0.4.0 + version: 0.4.0(typescript@5.7.3) + graphql: + specifier: ^16.9.0 + version: 16.10.0 + pm2: + specifier: ^5.4.3 + version: 5.4.3 + devDependencies: + typescript: + specifier: ^5.7.3 + version: 5.7.3 packages/pylon-telemetry: dependencies: @@ -678,10 +670,10 @@ packages: resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==} engines: {node: '>=0.1.90'} - '@commander-js/extra-typings@12.1.0': - resolution: {integrity: sha512-wf/lwQvWAA0goIghcb91dQYpkLBcyhOhQNqG/VgWhnKzgt+UOMvra7EX/2fv70arm5RW+PUHoQHHDa6/p77Eqg==} + '@commander-js/extra-typings@13.1.0': + resolution: {integrity: sha512-q5P52BYb1hwVWE6dtID7VvuJWrlfbCv4klj7BjUUOqMz4jbSZD4C9fJ9lRjL2jnBGTg+gDDlaXN51rkWcLk4fg==} peerDependencies: - commander: ~12.1.0 + commander: ~13.1.0 '@cspotcode/source-map-support@0.8.1': resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} @@ -1407,11 +1399,11 @@ packages: resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==} engines: {node: '>=14'} - '@gqty/cli@4.2.2': - resolution: {integrity: sha512-fpNvl8GZ82oTLYcvc6UmhnsKQvKLxLy79RGzhDePkTFMFYFUMnmc5bvuMCsP9M/gn4Z0WfTuMxOwMgMRkanrzQ==} + '@gqty/cli@4.2.5': + resolution: {integrity: sha512-TXlJvoTq1jrJ2fZzmO/K7thOQVeiM3L8vCNHlnCwUi3gGxz6iF7xiHUL/6k2xUJOy7tYPsPBH8k1BjIwCXbUqA==} hasBin: true peerDependencies: - trading-signals: ^5.0.4 + trading-signals: ^6.0.1 peerDependenciesMeta: trading-signals: optional: true @@ -1492,6 +1484,12 @@ packages: peerDependencies: graphql: ^16.9.0 + '@graphql-tools/utils@10.8.1': + resolution: {integrity: sha512-fI5NNuqeEAHyp7NuCDjvxWR5PTUXM4AqY9BoC59ZcX4nePAJje27ZsFHbAMS6EKDosY1K/D4ADxsO0P5+FH07A==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^16.9.0 + '@graphql-tools/wrap@10.0.29': resolution: {integrity: sha512-kQdosPBo6EvFhQV5s0XpN6+N0YN+31mCZTV7uwZisaUwwroAT19ujs2Zxz8Zyw4H9XRCsueLT0wqmSupjIFibQ==} engines: {node: '>=18.0.0'} @@ -1530,10 +1528,37 @@ packages: resolution: {integrity: sha512-sMgdETOfi2dUHT8r7TT1BTKOwNvdDGFDXYWtQ2J69SvlYNntk9I/gJe7r5yvMwwsuKnYbuRs3pNhx4tgNck5aA==} engines: {node: '>=18'} + '@inquirer/checkbox@4.1.2': + resolution: {integrity: sha512-PL9ixC5YsPXzXhAZFUPmkXGxfgjkdfZdPEPPmt4kFwQ4LBMDG9n/nHXYRGGZSKZJs+d1sGKWgS2GiPzVRKUdtQ==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@inquirer/confirm@3.2.0': resolution: {integrity: sha512-oOIwPs0Dvq5220Z8lGL/6LHRTEr9TgLHmiI99Rj1PJ1p1czTys+olrgBqZk4E2qC0YTzeHprxSQmoHioVdJ7Lw==} engines: {node: '>=18'} + '@inquirer/confirm@5.1.6': + resolution: {integrity: sha512-6ZXYK3M1XmaVBZX6FCfChgtponnL0R6I7k8Nu+kaoNkT828FVZTcca1MqmWQipaW2oNREQl5AaPCUOOCVNdRMw==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/core@10.1.7': + resolution: {integrity: sha512-AA9CQhlrt6ZgiSy6qoAigiA1izOa751ugX6ioSjqgJ+/Gd+tEN/TORk5sUYNjXuHWfW0r1n/a6ak4u/NqHHrtA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@inquirer/core@9.2.1': resolution: {integrity: sha512-F2VBt7W/mwqEU4bL0RnHNZmC/OxzNx9cOYxHqnXX3MP6ruYvZUZAW9imgN9+h/uBT/oP8Gh888J2OZSbjSeWcg==} engines: {node: '>=18'} @@ -1542,10 +1567,28 @@ packages: resolution: {integrity: sha512-9KHOpJ+dIL5SZli8lJ6xdaYLPPzB8xB9GZItg39MBybzhxA16vxmszmQFrRwbOA918WA2rvu8xhDEg/p6LXKbw==} engines: {node: '>=18'} + '@inquirer/editor@4.2.7': + resolution: {integrity: sha512-gktCSQtnSZHaBytkJKMKEuswSk2cDBuXX5rxGFv306mwHfBPjg5UAldw9zWGoEyvA9KpRDkeM4jfrx0rXn0GyA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@inquirer/expand@2.3.0': resolution: {integrity: sha512-qnJsUcOGCSG1e5DTOErmv2BPQqrtT6uzqn1vI/aYGiPKq+FgslGZmtdnXbhuI7IlT7OByDoEEqdnhUnVR2hhLw==} engines: {node: '>=18'} + '@inquirer/expand@4.0.9': + resolution: {integrity: sha512-Xxt6nhomWTAmuSX61kVgglLjMEFGa+7+F6UUtdEUeg7fg4r9vaFttUUKrtkViYYrQBA5Ia1tkOJj2koP9BuLig==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@inquirer/figures@1.0.10': resolution: {integrity: sha512-Ey6176gZmeqZuY/W/nZiUyvmb1/qInjcpiZjXWi6nON+nxJpD1bxtSoBxNliGISae32n6OwbY+TSXPZ1CfS4bw==} engines: {node: '>=18'} @@ -1554,30 +1597,93 @@ packages: resolution: {integrity: sha512-XfnpCStx2xgh1LIRqPXrTNEEByqQWoxsWYzNRSEUxJ5c6EQlhMogJ3vHKu8aXuTacebtaZzMAHwEL0kAflKOBw==} engines: {node: '>=18'} + '@inquirer/input@4.1.6': + resolution: {integrity: sha512-1f5AIsZuVjPT4ecA8AwaxDFNHny/tSershP/cTvTDxLdiIGTeILNcKozB0LaYt6mojJLUbOYhpIxicaYf7UKIQ==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@inquirer/number@1.1.0': resolution: {integrity: sha512-ilUnia/GZUtfSZy3YEErXLJ2Sljo/mf9fiKc08n18DdwdmDbOzRcTv65H1jjDvlsAuvdFXf4Sa/aL7iw/NanVA==} engines: {node: '>=18'} + '@inquirer/number@3.0.9': + resolution: {integrity: sha512-iN2xZvH3tyIYXLXBvlVh0npk1q/aVuKXZo5hj+K3W3D4ngAEq/DkLpofRzx6oebTUhBvOgryZ+rMV0yImKnG3w==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@inquirer/password@2.2.0': resolution: {integrity: sha512-5otqIpgsPYIshqhgtEwSspBQE40etouR8VIxzpJkv9i0dVHIpyhiivbkH9/dGiMLdyamT54YRdGJLfl8TFnLHg==} engines: {node: '>=18'} + '@inquirer/password@4.0.9': + resolution: {integrity: sha512-xBEoOw1XKb0rIN208YU7wM7oJEHhIYkfG7LpTJAEW913GZeaoQerzf5U/LSHI45EVvjAdgNXmXgH51cUXKZcJQ==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@inquirer/prompts@5.5.0': resolution: {integrity: sha512-BHDeL0catgHdcHbSFFUddNzvx/imzJMft+tWDPwTm3hfu8/tApk1HrooNngB2Mb4qY+KaRWF+iZqoVUPeslEog==} engines: {node: '>=18'} + '@inquirer/prompts@7.3.2': + resolution: {integrity: sha512-G1ytyOoHh5BphmEBxSwALin3n1KGNYB6yImbICcRQdzXfOGbuJ9Jske/Of5Sebk339NSGGNfUshnzK8YWkTPsQ==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@inquirer/rawlist@2.3.0': resolution: {integrity: sha512-zzfNuINhFF7OLAtGHfhwOW2TlYJyli7lOUoJUXw/uyklcwalV6WRXBXtFIicN8rTRK1XTiPWB4UY+YuW8dsnLQ==} engines: {node: '>=18'} + '@inquirer/rawlist@4.0.9': + resolution: {integrity: sha512-+5t6ebehKqgoxV8fXwE49HkSF2Rc9ijNiVGEQZwvbMI61/Q5RcD+jWD6Gs1tKdz5lkI8GRBL31iO0HjGK1bv+A==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@inquirer/search@1.1.0': resolution: {integrity: sha512-h+/5LSj51dx7hp5xOn4QFnUaKeARwUCLs6mIhtkJ0JYPBLmEYjdHSYh7I6GrLg9LwpJ3xeX0FZgAG1q0QdCpVQ==} engines: {node: '>=18'} + '@inquirer/search@3.0.9': + resolution: {integrity: sha512-DWmKztkYo9CvldGBaRMr0ETUHgR86zE6sPDVOHsqz4ISe9o1LuiWfgJk+2r75acFclA93J/lqzhT0dTjCzHuoA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@inquirer/select@2.5.0': resolution: {integrity: sha512-YmDobTItPP3WcEI86GvPo+T2sRHkxxOq/kXmsBjHS5BVXUgvgZ5AfJjkvQvZr03T81NnI3KrrRuMzeuYUQRFOA==} engines: {node: '>=18'} + '@inquirer/select@4.0.9': + resolution: {integrity: sha512-BpJyJe7Dkhv2kz7yG7bPSbJLQuu/rqyNlF1CfiiFeFwouegfH+zh13KDyt6+d9DwucKo7hqM3wKLLyJxZMO+Xg==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@inquirer/type@1.5.5': resolution: {integrity: sha512-MzICLu4yS7V8AA61sANROZ9vT1H3ooca5dSmI1FjZkzq7o/koMsRfQSzRtFo+F3Ao4Sf1C0bpLKejpKB/+j6MA==} engines: {node: '>=18'} @@ -1586,6 +1692,15 @@ packages: resolution: {integrity: sha512-XvJRx+2KR3YXyYtPUUy+qd9i7p+GO9Ko6VIIpWlBrpWwXDv8WLFeHTxz35CfQFUiBMLXlGHhGzys7lqit9gWag==} engines: {node: '>=18'} + '@inquirer/type@3.0.4': + resolution: {integrity: sha512-2MNFrDY8jkFYc9Il9DgLsHhMzuHnOYM1+CUYVWbzu9oT0hC7V7EcYvdCKeoll/Fcci04A+ERZ9wcc7cQ8lTkIA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -1891,6 +2006,20 @@ packages: peerDependencies: '@opentelemetry/api': ^1.1.0 + '@pm2/agent@2.0.4': + resolution: {integrity: sha512-n7WYvvTJhHLS2oBb1PjOtgLpMhgImOq8sXkPBw6smeg9LJBWZjiEgPKOpR8mn9UJZsB5P3W4V/MyvNnp31LKeA==} + + '@pm2/io@6.0.1': + resolution: {integrity: sha512-KiA+shC6sULQAr9mGZ1pg+6KVW9MF8NpG99x26Lf/082/Qy8qsTCtnJy+HQReW1A9Rdf0C/404cz0RZGZro+IA==} + engines: {node: '>=6.0'} + + '@pm2/js-api@0.8.0': + resolution: {integrity: sha512-nmWzrA/BQZik3VBz+npRcNIu01kdBhWL0mxKmP1ciF/gTcujPTQqt027N9fc1pK9ERM8RipFhymw7RcmCyOEYA==} + engines: {node: '>=4.0'} + + '@pm2/pm2-version-check@1.0.4': + resolution: {integrity: sha512-SXsM27SGH3yTWKc2fKR4SYNxsmnvuBQ9dd6QHtEWmiZ/VqaOYPAIlS8+vMcn27YLtAEBGvNRSh3TPNvtjZgfqA==} + '@pnpm/config.env-replace@1.1.0': resolution: {integrity: sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==} engines: {node: '>=12.22.0'} @@ -2099,6 +2228,9 @@ packages: resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} engines: {node: '>=18'} + '@tootallnate/quickjs-emscripten@0.23.0': + resolution: {integrity: sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==} + '@types/connect@3.4.36': resolution: {integrity: sha512-P63Zd/JUGq+PdrM1lv0Wv5SBYeA2+CORvbrXbngriYY0jzLUWfQMQQxOhjONEz/wlHOAxOdY7CY65rgQdTjq2w==} @@ -2211,6 +2343,12 @@ packages: resolution: {integrity: sha512-gOsf2YwSlleG6IjRYG2A7k0HmBMEo6qVNk9Bp/EaLgAJT5ngH6PXbqa4ItvnEwCm/velL5jAnQgsHsWnjhGmvw==} engines: {node: '>=18'} + amp-message@0.1.2: + resolution: {integrity: sha512-JqutcFwoU1+jhv7ArgW38bqrE+LQdcRv4NxNw0mp0JHQyB6tXesWRjtYKlDgHRY2o3JE5UTaBGUK8kSWUdxWUg==} + + amp@0.3.1: + resolution: {integrity: sha512-OwIuC4yZaRogHKiuU5WlMR5Xk/jAcpPtawWL05Gj8Lvm2F6mwoJt4O/bHI+DHwG79vWd+8OFYM4/BzYqyRd3qw==} + ansi-colors@4.1.3: resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} engines: {node: '>=6'} @@ -2279,6 +2417,13 @@ packages: assertion-error@1.1.0: resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} + ast-types@0.13.4: + resolution: {integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==} + engines: {node: '>=4'} + + async@2.6.4: + resolution: {integrity: sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==} + async@3.2.6: resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} @@ -2297,6 +2442,10 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + basic-ftp@5.0.5: + resolution: {integrity: sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==} + engines: {node: '>=10.0.0'} + before-after-hook@3.0.2: resolution: {integrity: sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==} @@ -2314,6 +2463,14 @@ packages: blake3-wasm@2.1.5: resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} + blessed@0.1.81: + resolution: {integrity: sha512-LoF5gae+hlmfORcG1M5+5XZi4LBmvlXTzwJWzUlPryN/SJdSflZvROM2TwkT0GMpq7oqT48NRd4GS7BiVBc5OQ==} + engines: {node: '>= 0.8.0'} + hasBin: true + + bodec@0.1.0: + resolution: {integrity: sha512-Ylo+MAo5BDUq1KA3f3R/MFhh+g8cnHmo8bz3YPGhI1znrMaf77ol1sfvYJzsw3nTE+Y2GryfDxBaR+AqpAkEHQ==} + bottleneck@2.19.5: resolution: {integrity: sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==} @@ -2376,6 +2533,10 @@ packages: resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} engines: {node: '>=4'} + chalk@3.0.0: + resolution: {integrity: sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==} + engines: {node: '>=8'} + chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -2397,6 +2558,9 @@ packages: chardet@0.7.0: resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} + charm@0.1.2: + resolution: {integrity: sha512-syedaZ9cPe7r3hoQA9twWYKu5AIyCswN5+szkmPBe9ccdLrj4bYaCnLVPTLd2kgVRc7+zoX4tyPgRnFKCj5YjQ==} + check-error@1.0.3: resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==} @@ -2428,6 +2592,10 @@ packages: resolution: {integrity: sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==} engines: {node: 10.* || >= 12.*} + cli-tableau@2.0.1: + resolution: {integrity: sha512-he+WTicka9cl0Fg/y+YyxcN6/bfQ/1O3QmgxRXDhABKqLzvoOSM4fMzp39uMyLBulAFuywD2N7UaoQE7WaADxQ==} + engines: {node: '>=8.10.0'} + cli-width@4.1.0: resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} engines: {node: '>= 12'} @@ -2465,6 +2633,13 @@ packages: resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} engines: {node: '>=18'} + commander@13.1.0: + resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==} + engines: {node: '>=18'} + + commander@2.15.1: + resolution: {integrity: sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==} + common-tags@1.8.2: resolution: {integrity: sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==} engines: {node: '>=4.0.0'} @@ -2529,6 +2704,9 @@ packages: typescript: optional: true + croner@4.1.97: + resolution: {integrity: sha512-/f6gpQuxDaqXu+1kwQYSckUglPaOrHdbIlBAu0YuW8/Cdb45XwXYNUBXg3r/9Mo6n540Kn/smKcZWko5x99KrQ==} + cross-fetch@3.2.0: resolution: {integrity: sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==} @@ -2551,9 +2729,16 @@ packages: resolution: {integrity: sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA==} engines: {node: '>=12'} + culvert@0.1.2: + resolution: {integrity: sha512-yi1x3EAWKjQTreYWeSd98431AV+IEE0qoDyOoaHJ7KJ21gv6HtBXHVLX74opVSGqcR8/AbjJBHAHpcOy2bj5Gg==} + data-uri-to-buffer@2.0.2: resolution: {integrity: sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==} + data-uri-to-buffer@6.0.2: + resolution: {integrity: sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==} + engines: {node: '>= 14'} + dataloader@1.4.0: resolution: {integrity: sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw==} @@ -2563,9 +2748,32 @@ packages: date-fns@3.6.0: resolution: {integrity: sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==} + dayjs@1.11.13: + resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==} + + dayjs@1.8.36: + resolution: {integrity: sha512-3VmRXEtw7RZKAf+4Tv1Ym9AGeo8r8+CjDi26x+7SYQil1UqtqdaokhzoEJohqlzt0m5kacJSDhJQkG/LWhpRBw==} + debounce-microtasks@0.1.8: resolution: {integrity: sha512-7mbJYntoO1FFIuOgwX2vnVwZMfbRpN6s16zTUHm/a1xmpZTGxSKvT6nOpAs1kHWQn7Ly2DGlCKeKdQrKMLBslw==} + debug@3.2.7: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.3.7: + resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + debug@4.4.0: resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} engines: {node: '>=6.0'} @@ -2586,6 +2794,10 @@ packages: defu@6.1.4: resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + degenerator@5.0.1: + resolution: {integrity: sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==} + engines: {node: '>= 14'} + del@6.1.1: resolution: {integrity: sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg==} engines: {node: '>=10'} @@ -2745,6 +2957,10 @@ packages: enabled@2.0.0: resolution: {integrity: sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==} + enquirer@2.3.6: + resolution: {integrity: sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==} + engines: {node: '>=8.6'} + enquirer@2.4.1: resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} engines: {node: '>=8.6'} @@ -2815,17 +3031,39 @@ packages: resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} engines: {node: '>=12'} + escodegen@2.1.0: + resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==} + engines: {node: '>=6.0'} + hasBin: true + esprima@4.0.1: resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} engines: {node: '>=4'} hasBin: true + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + estree-walker@0.6.1: resolution: {integrity: sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==} estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + eventemitter2@0.4.14: + resolution: {integrity: sha512-K7J4xq5xAD5jHsGM5ReWXRTFa3JRGofHiMcVgQ8PRwgWxzjHpMWCIzsmyf60+mh8KLsqYPcjUMa0AC4hd6lPyQ==} + + eventemitter2@5.0.1: + resolution: {integrity: sha512-5EM1GHXycJBS6mauYAbVKT1cVs7POKWb2NXD4Vyt8dDqeZa7LaDK1/sjtL+Zb0lzTpSNil4596Dyu97hz37QLg==} + + eventemitter2@6.4.9: + resolution: {integrity: sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg==} + execa@5.1.1: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} engines: {node: '>=10'} @@ -2849,6 +3087,9 @@ packages: resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} engines: {node: '>=4'} + extrareqp2@1.0.0: + resolution: {integrity: sha512-Gum0g1QYb6wpPJCVypWP3bbIuaibcFiJcpuPM10YSXp/tzqi84x9PJageob+eN4xVRIOto4wjSGNLyMD54D2xA==} + fast-content-type-parse@2.0.1: resolution: {integrity: sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q==} @@ -2856,6 +3097,9 @@ packages: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} + fast-json-patch@3.1.1: + resolution: {integrity: sha512-vf6IHUX2SBcA+5/+4883dsIjpBTqmfBjmYiWK1savxQmFk4JfBMLa7ynTYOs1Rolp/T1betJxHiGD3g1Mn8lUQ==} + fastq@1.19.0: resolution: {integrity: sha512-7SFSRCNjBQIZH/xZR3iy5iQYR8aGBE0h3VG6/cwlbrpdciNYBMotQav8c1XI3HjHH+NikUpP53nPdlZSdWmFzA==} @@ -2868,6 +3112,9 @@ packages: fbjs@3.0.5: resolution: {integrity: sha512-ztsSx77JBtkuMrEypfhgc3cI0+0h+svqeie7xHbh1k/IKdcydnvadp/mUaGgjAOXQmQSxsqgaRhS3q9fy+1kxg==} + fclone@1.0.11: + resolution: {integrity: sha512-GDqVQezKzRABdeqflsgMr7ktzgF9CyS+p2oe0jJqUY6izSSbhPIQJDpoU4PtGcD7VPM9xh/dVrTu6z1nwgmEGw==} + fecha@4.2.3: resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==} @@ -2913,6 +3160,15 @@ packages: fn.name@1.1.0: resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==} + follow-redirects@1.15.9: + resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + foreground-child@3.3.0: resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} engines: {node: '>=14'} @@ -2990,9 +3246,24 @@ packages: get-tsconfig@4.10.0: resolution: {integrity: sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==} + get-uri@6.0.4: + resolution: {integrity: sha512-E1b1lFFLvLgak2whF2xDBcOy6NLVGZBqqjJjsIhvopKfWWEi64pLVTWWehV8KlLerZkfNTA95sTe2OdJKm1OzQ==} + engines: {node: '>= 14'} + git-log-parser@1.2.1: resolution: {integrity: sha512-PI+sPDvHXNPl5WNOErAK05s3j0lgwUzMN6o8cyQrDaKfT3qd7TmNJKeXX+SknI5I0QhG5fVPAEwSY4tRGDtYoQ==} + git-node-fs@1.0.0: + resolution: {integrity: sha512-bLQypt14llVXBg0S0u8q8HmU7g9p3ysH+NvVlae5vILuUvs759665HvmR5+wb04KjHyjFcDRxdYb4kyNnluMUQ==} + peerDependencies: + js-git: ^0.7.8 + peerDependenciesMeta: + js-git: + optional: true + + git-sha1@0.1.2: + resolution: {integrity: sha512-2e/nZezdVlyCopOCYHeW0onkbZg7xP1Ad6pndPy1rCygeRykefUS6r7oA5cJRGEFvseiaz5a/qUHFVX1dd6Isg==} + glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -3021,8 +3292,8 @@ packages: resolution: {integrity: sha512-s3Fq41ZVh7vbbe2PN3nrW7yC7U7MFVc5c98/iTl9c2GawNMKx/J648KQRW6WKkuU8GIbbh2IXfIRQjOZnXcTnw==} engines: {node: '>=18'} - gqty@3.3.0: - resolution: {integrity: sha512-5ky4L771GcW2LDPR/k/5lYxn8f440K3oiB4Gglf29mqTgVNjD7V+K+4eiaJRGvduR7gSiaBD6hkFMGoLaYshnw==} + gqty@3.4.1: + resolution: {integrity: sha512-rd/FdqykHaGBfbFnmXQwN02QFz7p/MygGh/QBdTeLm2fP4JzoEvpIQyL4zt7j8b1ZGWv2OMbNotI4udWBsKFvQ==} engines: {node: ^12.20.0 || >=14.13.0} peerDependencies: graphql: ^16.9.0 @@ -3194,6 +3465,10 @@ packages: invariant@2.2.4: resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} + ip-address@9.0.5: + resolution: {integrity: sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==} + engines: {node: '>= 12'} + is-absolute@1.0.0: resolution: {integrity: sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==} engines: {node: '>=0.10.0'} @@ -3303,6 +3578,9 @@ packages: jose@4.15.9: resolution: {integrity: sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==} + js-git@0.7.8: + resolution: {integrity: sha512-+E5ZH/HeRnoc/LW0AmAyhU+mNcWBzAKE+30+IDMLSLbbK+Tdt02AdkOKq9u15rlJsDEGFqtgckc8ZM59LhhiUA==} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -3317,6 +3595,9 @@ packages: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} hasBin: true + jsbn@1.1.0: + resolution: {integrity: sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==} + jsesc@3.1.0: resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} engines: {node: '>=6'} @@ -3328,6 +3609,9 @@ packages: json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + json-stringify-safe@5.0.1: + resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} + json5@2.2.3: resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} engines: {node: '>=6'} @@ -3367,6 +3651,10 @@ packages: kuler@2.0.0: resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==} + lazy@1.0.11: + resolution: {integrity: sha512-Y+CjUfLmIpoUCCRl0ub4smrYtGGr5AOa2AKOaWelGHOGz33X/Y/KizefGqbkwfz44+cnq/+9habclf8vOmu2LA==} + engines: {node: '>=0.2.0'} + lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} @@ -3460,6 +3748,10 @@ packages: resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} engines: {node: '>=10'} + lru-cache@7.18.3: + resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} + engines: {node: '>=12'} + magic-string@0.25.9: resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==} @@ -3538,6 +3830,11 @@ packages: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} + mkdirp@1.0.4: + resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} + engines: {node: '>=10'} + hasBin: true + mlly@1.7.4: resolution: {integrity: sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==} @@ -3558,10 +3855,17 @@ packages: resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==} hasBin: true + mute-stream@0.0.8: + resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} + mute-stream@1.0.0: resolution: {integrity: sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + mute-stream@2.0.0: + resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} + engines: {node: ^18.17.0 || >=20.5.0} + mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} @@ -3570,12 +3874,21 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + needle@2.4.0: + resolution: {integrity: sha512-4Hnwzr3mi5L97hMYeNl8wRW/Onhy4nUKR/lVemJ8gJedxxUyBLm9kkrDColJvoSfwi0jCNhD+xCdOtiGDQiRZg==} + engines: {node: '>= 4.4.x'} + hasBin: true + neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} nerf-dart@1.0.0: resolution: {integrity: sha512-EZSPZB70jiVsivaBLYDCyntd5eH8NTSMOn3rB+HxwdmKThGELLdYv8qVIMWvZEFy9w8ZZpW9h9OB32l1rGtj7g==} + netmask@2.0.2: + resolution: {integrity: sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==} + engines: {node: '>= 0.4.0'} + no-case@3.0.4: resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} @@ -3703,6 +4016,10 @@ packages: - which - write-file-atomic + nssocket@0.6.0: + resolution: {integrity: sha512-a9GSOIql5IqgWJR3F/JXG4KpJTA3Z53Cj0MeMvGpglytB1nxE4PdFNC0jINe27CS7cGivoynwc054EzCcT3M3w==} + engines: {node: '>= 0.10.x'} + nullthrows@1.1.1: resolution: {integrity: sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==} @@ -3714,10 +4031,6 @@ packages: resolution: {integrity: sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==} engines: {node: '>= 6'} - object-hash@3.0.0: - resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} - engines: {node: '>= 6'} - ohash@1.1.4: resolution: {integrity: sha512-FlDryZAahJmEF3VR3w1KogSEdWX3WhA5GPakFx4J81kEAiHyLMpdLLElS8n8dfNadMgAne/MywcvmogzscVt4g==} @@ -3825,12 +4138,23 @@ packages: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} + pac-proxy-agent@7.2.0: + resolution: {integrity: sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==} + engines: {node: '>= 14'} + + pac-resolver@7.0.1: + resolution: {integrity: sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==} + engines: {node: '>= 14'} + package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} package-manager-detector@0.2.9: resolution: {integrity: sha512-+vYvA/Y31l8Zk8dwxHhL3JfTuHPm6tlxM2A3GeQyl7ovYnSp1+mzAxClxaOr0qO1TtPxbQxetI7v5XqKLJZk7Q==} + pako@0.2.9: + resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==} + param-case@3.0.4: resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==} @@ -3946,6 +4270,14 @@ packages: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} + pidusage@2.0.21: + resolution: {integrity: sha512-cv3xAQos+pugVX+BfXpHsbyz/dLzX+lr44zNMsYiGxUw+kV5sgQCIcLd1z+0vq+KyC7dJ+/ts2PsfgWfSC3WXA==} + engines: {node: '>=8'} + + pidusage@3.0.2: + resolution: {integrity: sha512-g0VU+y08pKw5M8EZ2rIGiEBaB8wrQMjYGFfW2QVIfyT8V+fq8YFLkvlz4bz5ljvFDJYNFCWT3PWqcRr2FKO81w==} + engines: {node: '>=10'} + pify@3.0.0: resolution: {integrity: sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==} engines: {node: '>=4'} @@ -3965,6 +4297,29 @@ packages: resolution: {integrity: sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==} engines: {node: '>=8'} + pm2-axon-rpc@0.7.1: + resolution: {integrity: sha512-FbLvW60w+vEyvMjP/xom2UPhUN/2bVpdtLfKJeYM3gwzYhoTEEChCOICfFzxkxuoEleOlnpjie+n1nue91bDQw==} + engines: {node: '>=5'} + + pm2-axon@4.0.1: + resolution: {integrity: sha512-kES/PeSLS8orT8dR5jMlNl+Yu4Ty3nbvZRmaAtROuVm9nYYGiaoXqqKQqQYzWQzMYWUKHMQTvBlirjE5GIIxqg==} + engines: {node: '>=5'} + + pm2-deploy@1.0.2: + resolution: {integrity: sha512-YJx6RXKrVrWaphEYf++EdOOx9EH18vM8RSZN/P1Y+NokTKqYAca/ejXwVLyiEpNju4HPZEk3Y2uZouwMqUlcgg==} + engines: {node: '>=4.0.0'} + + pm2-multimeter@0.1.2: + resolution: {integrity: sha512-S+wT6XfyKfd7SJIBqRgOctGxaBzUOmVQzTAS+cg04TsEUObJVreha7lvCfX8zzGVr871XwCSnHUU7DQQ5xEsfA==} + + pm2-sysmonit@1.2.8: + resolution: {integrity: sha512-ACOhlONEXdCTVwKieBIQLSi2tQZ8eKinhcr9JpZSUAL8Qy0ajIgRtsLxG/lwPOW3JEKqPyw/UaHmTWhUzpP4kA==} + + pm2@5.4.3: + resolution: {integrity: sha512-4/I1htIHzZk1Y67UgOCo4F1cJtas1kSds31N8zN0PybO230id1nigyjGuGFzUnGmUFPmrJ0On22fO1ChFlp7VQ==} + engines: {node: '>=12.0.0'} + hasBin: true + postcss@8.5.1: resolution: {integrity: sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==} engines: {node: ^10 || ^12 || >=14} @@ -4007,9 +4362,19 @@ packages: promise@7.3.1: resolution: {integrity: sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==} + promptly@2.2.0: + resolution: {integrity: sha512-aC9j+BZsRSSzEsXBNBwDnAxujdx19HycZoKgRgzWnS8eOHg1asuf9heuLprfbe739zY3IdUQx+Egv6Jn135WHA==} + proto-list@1.2.4: resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} + proxy-agent@6.3.1: + resolution: {integrity: sha512-Rb5RVBy1iyqOtNl15Cw/llpeLH8bsb37gM1FUfKQ+Wck6xHlbAhWGUFiTRHtkjqGTA5pSHz6+0hrPW/oECihPQ==} + engines: {node: '>= 14'} + + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -4039,6 +4404,10 @@ packages: resolution: {integrity: sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==} engines: {node: '>=6'} + read@1.0.7: + resolution: {integrity: sha512-rSOKNYUmaxy0om1BNjMN4ezNT6VKK+2xF4GBhc81mkH7L60i6dp8qPYrkndNLT3QPphoII3maL9PVC9XmhHwVQ==} + engines: {node: '>=0.8'} + readable-stream@2.3.8: resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} @@ -4064,6 +4433,10 @@ packages: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} + require-in-the-middle@5.2.0: + resolution: {integrity: sha512-efCx3b+0Z69/LGJmm9Yvi4cqEdxnoGnxYxGxBghkkTTFeXRtTCmmhO0AnAfHz59k957uTSuy8WaHqOs8wbYUWg==} + engines: {node: '>=6'} + require-in-the-middle@7.5.0: resolution: {integrity: sha512-/Tvpny/RVVicqlYTKwt/GtpZRsPG1CmJNhxVKGz+Sy/4MONfXCVNK69MFgGKdUt0/324q3ClI2dICcPgISrC8g==} engines: {node: '>=8.6.0'} @@ -4120,9 +4493,15 @@ packages: run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + run-series@1.1.9: + resolution: {integrity: sha512-Arc4hUN896vjkqCYrUXquBFtRZdv1PfLbTYP71efP6butxyQ0kWpiNJyAgsxscmQg1cqvHY32/UCBzXedTpU2g==} + safe-buffer@5.1.2: resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + safe-stable-stringify@2.5.0: resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} engines: {node: '>=10'} @@ -4130,6 +4509,9 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + sax@1.4.1: + resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==} + selfsigned@2.4.1: resolution: {integrity: sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==} engines: {node: '>=10'} @@ -4165,6 +4547,11 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true + semver@7.5.4: + resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==} + engines: {node: '>=10'} + hasBin: true + semver@7.7.1: resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==} engines: {node: '>=10'} @@ -4219,9 +4606,21 @@ packages: resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==} engines: {node: '>=14.16'} + smart-buffer@4.2.0: + resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} + engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} + snake-case@3.0.4: resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==} + socks-proxy-agent@8.0.5: + resolution: {integrity: sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==} + engines: {node: '>= 14'} + + socks@2.8.4: + resolution: {integrity: sha512-D3YaD0aRxR3mEcqnidIs7ReYJFVzWdd6fXJYUM8ixcQcJRGTka/b3saV0KflYhyVJXKhb947GndU35SxYNResQ==} + engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -4264,6 +4663,12 @@ packages: sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + sprintf-js@1.1.2: + resolution: {integrity: sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==} + + sprintf-js@1.1.3: + resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} + stack-trace@0.0.10: resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==} @@ -4356,6 +4761,12 @@ packages: swap-case@2.0.2: resolution: {integrity: sha512-kc6S2YS/2yXbtkSMunBtKdah4VFETZ8Oh6ONSmSd9bRxhqTrtARUCBUiWXH3xVPpvR7tz2CSnkuXVE42EcGnMw==} + systeminformation@5.25.11: + resolution: {integrity: sha512-jI01fn/t47rrLTQB0FTlMCC+5dYx8o0RRF+R4BPiUNsvg5OdY0s9DKMFmJGrx5SwMZQ4cag0Gl6v8oycso9b/g==} + engines: {node: '>=8.0.0'} + os: [darwin, linux, win32, freebsd, openbsd, netbsd, sunos, android] + hasBin: true + temp-dir@2.0.0: resolution: {integrity: sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==} engines: {node: '>=8'} @@ -4425,21 +4836,26 @@ packages: resolution: {integrity: sha512-aXJDbk6SnumuaZSANd21XAo15ucCDE38H4fkqiGsc3MhCK+wOlZvLP9cB/TvpHT0mOyWgC4Z8EwRlzqYSUzdsA==} engines: {node: '>= 0.4'} - treekill@1.0.0: - resolution: {integrity: sha512-yRk5h+uZ6oFKQWf88pzuOSujKvpU8wqo9nuxCMUvWU55sC6A9J0EzjmEwpTjHydhBhBDDZKgA6992aIEWTxDkw==} - engines: {node: '>= 0.10.0'} - deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. - triple-beam@1.4.1: resolution: {integrity: sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==} engines: {node: '>= 14.0.0'} + tslib@1.9.3: + resolution: {integrity: sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==} + tslib@2.6.3: resolution: {integrity: sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==} tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tv4@1.3.0: + resolution: {integrity: sha512-afizzfpJgvPr+eDkREK4MxJ/+r8nEEHcmitwgnPUqpaP+FpwQyadnxNoSACbgc/b1LsZYtODGoPiFxQrgJgjvw==} + engines: {node: '>= 0.8.0'} + + tx2@1.0.5: + resolution: {integrity: sha512-sJ24w0y03Md/bxzK4FU8J8JveYYUbSs2FViLJ2D/8bytSiyPRbuE3DyL/9UKYXTZlV3yXq0L8GLlhobTnekCVg==} + type-detect@4.1.0: resolution: {integrity: sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==} engines: {node: '>=4'} @@ -4626,6 +5042,10 @@ packages: jsdom: optional: true + vizion@2.2.1: + resolution: {integrity: sha512-sfAcO2yeSU0CSPFI/DmZp3FsFE9T+8913nv1xWBOyzODv13fwkn6Vl7HqxGpkr9F608M+8SuFId3s+BlZqfXww==} + engines: {node: '>=4.0'} + webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} @@ -4698,6 +5118,18 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + ws@7.5.10: + resolution: {integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==} + engines: {node: '>=8.3.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + ws@8.18.0: resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} engines: {node: '>=10.0.0'} @@ -5330,9 +5762,9 @@ snapshots: '@colors/colors@1.6.0': {} - '@commander-js/extra-typings@12.1.0(commander@12.1.0)': + '@commander-js/extra-typings@13.1.0(commander@13.1.0)': dependencies: - commander: 12.1.0 + commander: 13.1.0 '@cspotcode/source-map-support@0.8.1': dependencies: @@ -5719,27 +6151,27 @@ snapshots: '@fastify/busboy@2.1.1': {} - '@gqty/cli@4.2.2(@babel/core@7.26.7)(typescript@5.7.3)': + '@gqty/cli@4.2.5(@babel/core@7.26.7)(@types/node@22.13.1)(typescript@5.7.3)': dependencies: - '@commander-js/extra-typings': 12.1.0(commander@12.1.0) + '@commander-js/extra-typings': 13.1.0(commander@13.1.0) '@graphql-codegen/core': 4.0.2(graphql@16.10.0) '@graphql-codegen/typescript': 4.1.3(@babel/core@7.26.7)(graphql@16.10.0) - '@graphql-tools/delegate': 10.2.11(graphql@16.10.0) - '@graphql-tools/utils': 10.7.2(graphql@16.10.0) + '@graphql-tools/utils': 10.8.1(graphql@16.10.0) '@graphql-tools/wrap': 10.0.29(graphql@16.10.0) - '@inquirer/prompts': 5.5.0 - chalk: 4.1.2 - commander: 12.1.0 + '@inquirer/prompts': 7.3.2(@types/node@22.13.1) + chalk: 5.4.1 + commander: 13.1.0 cosmiconfig: 9.0.0(typescript@5.7.3) cross-fetch: 4.1.0 fast-glob: 3.3.3 - gqty: 3.3.0(graphql@16.10.0) + gqty: 3.4.1(graphql@16.10.0) graphql: 16.10.0 lodash-es: 4.17.21 micromatch: 4.0.8 prettier: 2.8.8 transitivePeerDependencies: - '@babel/core' + - '@types/node' - encoding - graphql-sse - graphql-ws @@ -5750,13 +6182,13 @@ snapshots: dependencies: '@graphql-codegen/plugin-helpers': 5.1.0(graphql@16.10.0) '@graphql-tools/schema': 10.0.16(graphql@16.10.0) - '@graphql-tools/utils': 10.7.2(graphql@16.10.0) + '@graphql-tools/utils': 10.8.1(graphql@16.10.0) graphql: 16.10.0 tslib: 2.6.3 '@graphql-codegen/plugin-helpers@5.1.0(graphql@16.10.0)': dependencies: - '@graphql-tools/utils': 10.7.2(graphql@16.10.0) + '@graphql-tools/utils': 10.8.1(graphql@16.10.0) change-case-all: 1.0.15 common-tags: 1.8.2 graphql: 16.10.0 @@ -5767,7 +6199,7 @@ snapshots: '@graphql-codegen/schema-ast@4.1.0(graphql@16.10.0)': dependencies: '@graphql-codegen/plugin-helpers': 5.1.0(graphql@16.10.0) - '@graphql-tools/utils': 10.7.2(graphql@16.10.0) + '@graphql-tools/utils': 10.8.1(graphql@16.10.0) graphql: 16.10.0 tslib: 2.6.3 @@ -5789,7 +6221,7 @@ snapshots: '@graphql-codegen/plugin-helpers': 5.1.0(graphql@16.10.0) '@graphql-tools/optimize': 2.0.0(graphql@16.10.0) '@graphql-tools/relay-operation-optimizer': 7.0.12(@babel/core@7.26.7)(graphql@16.10.0) - '@graphql-tools/utils': 10.7.2(graphql@16.10.0) + '@graphql-tools/utils': 10.8.1(graphql@16.10.0) auto-bind: 4.0.0 change-case-all: 1.0.15 dependency-graph: 0.11.0 @@ -5804,7 +6236,7 @@ snapshots: '@graphql-tools/batch-execute@9.0.11(graphql@16.10.0)': dependencies: - '@graphql-tools/utils': 10.7.2(graphql@16.10.0) + '@graphql-tools/utils': 10.8.1(graphql@16.10.0) dataloader: 2.2.3 graphql: 16.10.0 tslib: 2.8.1 @@ -5814,7 +6246,7 @@ snapshots: '@graphql-tools/batch-execute': 9.0.11(graphql@16.10.0) '@graphql-tools/executor': 1.3.12(graphql@16.10.0) '@graphql-tools/schema': 10.0.16(graphql@16.10.0) - '@graphql-tools/utils': 10.7.2(graphql@16.10.0) + '@graphql-tools/utils': 10.8.1(graphql@16.10.0) '@repeaterjs/repeater': 3.0.6 dataloader: 2.2.3 dset: 3.1.4 @@ -5845,7 +6277,7 @@ snapshots: '@graphql-tools/relay-operation-optimizer@7.0.12(@babel/core@7.26.7)(graphql@16.10.0)': dependencies: '@ardatan/relay-compiler': 12.0.1(@babel/core@7.26.7)(graphql@16.10.0) - '@graphql-tools/utils': 10.7.2(graphql@16.10.0) + '@graphql-tools/utils': 10.8.1(graphql@16.10.0) graphql: 16.10.0 tslib: 2.8.1 transitivePeerDependencies: @@ -5869,11 +6301,19 @@ snapshots: graphql: 16.10.0 tslib: 2.8.1 + '@graphql-tools/utils@10.8.1(graphql@16.10.0)': + dependencies: + '@graphql-typed-document-node/core': 3.2.0(graphql@16.10.0) + cross-inspect: 1.0.1 + dset: 3.1.4 + graphql: 16.10.0 + tslib: 2.8.1 + '@graphql-tools/wrap@10.0.29(graphql@16.10.0)': dependencies: '@graphql-tools/delegate': 10.2.11(graphql@16.10.0) '@graphql-tools/schema': 10.0.16(graphql@16.10.0) - '@graphql-tools/utils': 10.7.2(graphql@16.10.0) + '@graphql-tools/utils': 10.8.1(graphql@16.10.0) graphql: 16.10.0 tslib: 2.8.1 @@ -5914,11 +6354,41 @@ snapshots: ansi-escapes: 4.3.2 yoctocolors-cjs: 2.1.2 + '@inquirer/checkbox@4.1.2(@types/node@22.13.1)': + dependencies: + '@inquirer/core': 10.1.7(@types/node@22.13.1) + '@inquirer/figures': 1.0.10 + '@inquirer/type': 3.0.4(@types/node@22.13.1) + ansi-escapes: 4.3.2 + yoctocolors-cjs: 2.1.2 + optionalDependencies: + '@types/node': 22.13.1 + '@inquirer/confirm@3.2.0': dependencies: '@inquirer/core': 9.2.1 '@inquirer/type': 1.5.5 + '@inquirer/confirm@5.1.6(@types/node@22.13.1)': + dependencies: + '@inquirer/core': 10.1.7(@types/node@22.13.1) + '@inquirer/type': 3.0.4(@types/node@22.13.1) + optionalDependencies: + '@types/node': 22.13.1 + + '@inquirer/core@10.1.7(@types/node@22.13.1)': + dependencies: + '@inquirer/figures': 1.0.10 + '@inquirer/type': 3.0.4(@types/node@22.13.1) + ansi-escapes: 4.3.2 + cli-width: 4.1.0 + mute-stream: 2.0.0 + signal-exit: 4.1.0 + wrap-ansi: 6.2.0 + yoctocolors-cjs: 2.1.2 + optionalDependencies: + '@types/node': 22.13.1 + '@inquirer/core@9.2.1': dependencies: '@inquirer/figures': 1.0.10 @@ -5940,12 +6410,28 @@ snapshots: '@inquirer/type': 1.5.5 external-editor: 3.1.0 + '@inquirer/editor@4.2.7(@types/node@22.13.1)': + dependencies: + '@inquirer/core': 10.1.7(@types/node@22.13.1) + '@inquirer/type': 3.0.4(@types/node@22.13.1) + external-editor: 3.1.0 + optionalDependencies: + '@types/node': 22.13.1 + '@inquirer/expand@2.3.0': dependencies: '@inquirer/core': 9.2.1 '@inquirer/type': 1.5.5 yoctocolors-cjs: 2.1.2 + '@inquirer/expand@4.0.9(@types/node@22.13.1)': + dependencies: + '@inquirer/core': 10.1.7(@types/node@22.13.1) + '@inquirer/type': 3.0.4(@types/node@22.13.1) + yoctocolors-cjs: 2.1.2 + optionalDependencies: + '@types/node': 22.13.1 + '@inquirer/figures@1.0.10': {} '@inquirer/input@2.3.0': @@ -5953,17 +6439,39 @@ snapshots: '@inquirer/core': 9.2.1 '@inquirer/type': 1.5.5 + '@inquirer/input@4.1.6(@types/node@22.13.1)': + dependencies: + '@inquirer/core': 10.1.7(@types/node@22.13.1) + '@inquirer/type': 3.0.4(@types/node@22.13.1) + optionalDependencies: + '@types/node': 22.13.1 + '@inquirer/number@1.1.0': dependencies: '@inquirer/core': 9.2.1 '@inquirer/type': 1.5.5 + '@inquirer/number@3.0.9(@types/node@22.13.1)': + dependencies: + '@inquirer/core': 10.1.7(@types/node@22.13.1) + '@inquirer/type': 3.0.4(@types/node@22.13.1) + optionalDependencies: + '@types/node': 22.13.1 + '@inquirer/password@2.2.0': dependencies: '@inquirer/core': 9.2.1 '@inquirer/type': 1.5.5 ansi-escapes: 4.3.2 + '@inquirer/password@4.0.9(@types/node@22.13.1)': + dependencies: + '@inquirer/core': 10.1.7(@types/node@22.13.1) + '@inquirer/type': 3.0.4(@types/node@22.13.1) + ansi-escapes: 4.3.2 + optionalDependencies: + '@types/node': 22.13.1 + '@inquirer/prompts@5.5.0': dependencies: '@inquirer/checkbox': 2.5.0 @@ -5977,12 +6485,35 @@ snapshots: '@inquirer/search': 1.1.0 '@inquirer/select': 2.5.0 + '@inquirer/prompts@7.3.2(@types/node@22.13.1)': + dependencies: + '@inquirer/checkbox': 4.1.2(@types/node@22.13.1) + '@inquirer/confirm': 5.1.6(@types/node@22.13.1) + '@inquirer/editor': 4.2.7(@types/node@22.13.1) + '@inquirer/expand': 4.0.9(@types/node@22.13.1) + '@inquirer/input': 4.1.6(@types/node@22.13.1) + '@inquirer/number': 3.0.9(@types/node@22.13.1) + '@inquirer/password': 4.0.9(@types/node@22.13.1) + '@inquirer/rawlist': 4.0.9(@types/node@22.13.1) + '@inquirer/search': 3.0.9(@types/node@22.13.1) + '@inquirer/select': 4.0.9(@types/node@22.13.1) + optionalDependencies: + '@types/node': 22.13.1 + '@inquirer/rawlist@2.3.0': dependencies: '@inquirer/core': 9.2.1 '@inquirer/type': 1.5.5 yoctocolors-cjs: 2.1.2 + '@inquirer/rawlist@4.0.9(@types/node@22.13.1)': + dependencies: + '@inquirer/core': 10.1.7(@types/node@22.13.1) + '@inquirer/type': 3.0.4(@types/node@22.13.1) + yoctocolors-cjs: 2.1.2 + optionalDependencies: + '@types/node': 22.13.1 + '@inquirer/search@1.1.0': dependencies: '@inquirer/core': 9.2.1 @@ -5990,6 +6521,15 @@ snapshots: '@inquirer/type': 1.5.5 yoctocolors-cjs: 2.1.2 + '@inquirer/search@3.0.9(@types/node@22.13.1)': + dependencies: + '@inquirer/core': 10.1.7(@types/node@22.13.1) + '@inquirer/figures': 1.0.10 + '@inquirer/type': 3.0.4(@types/node@22.13.1) + yoctocolors-cjs: 2.1.2 + optionalDependencies: + '@types/node': 22.13.1 + '@inquirer/select@2.5.0': dependencies: '@inquirer/core': 9.2.1 @@ -5998,6 +6538,16 @@ snapshots: ansi-escapes: 4.3.2 yoctocolors-cjs: 2.1.2 + '@inquirer/select@4.0.9(@types/node@22.13.1)': + dependencies: + '@inquirer/core': 10.1.7(@types/node@22.13.1) + '@inquirer/figures': 1.0.10 + '@inquirer/type': 3.0.4(@types/node@22.13.1) + ansi-escapes: 4.3.2 + yoctocolors-cjs: 2.1.2 + optionalDependencies: + '@types/node': 22.13.1 + '@inquirer/type@1.5.5': dependencies: mute-stream: 1.0.0 @@ -6006,6 +6556,10 @@ snapshots: dependencies: mute-stream: 1.0.0 + '@inquirer/type@3.0.4(@types/node@22.13.1)': + optionalDependencies: + '@types/node': 22.13.1 + '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -6403,6 +6957,57 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@pm2/agent@2.0.4': + dependencies: + async: 3.2.6 + chalk: 3.0.0 + dayjs: 1.8.36 + debug: 4.3.7 + eventemitter2: 5.0.1 + fast-json-patch: 3.1.1 + fclone: 1.0.11 + nssocket: 0.6.0 + pm2-axon: 4.0.1 + pm2-axon-rpc: 0.7.1 + proxy-agent: 6.3.1 + semver: 7.5.4 + ws: 7.5.10 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@pm2/io@6.0.1': + dependencies: + async: 2.6.4 + debug: 4.3.7 + eventemitter2: 6.4.9 + require-in-the-middle: 5.2.0 + semver: 7.5.4 + shimmer: 1.2.1 + signal-exit: 3.0.7 + tslib: 1.9.3 + transitivePeerDependencies: + - supports-color + + '@pm2/js-api@0.8.0': + dependencies: + async: 2.6.4 + debug: 4.3.7 + eventemitter2: 6.4.9 + extrareqp2: 1.0.0(debug@4.3.7) + ws: 7.5.10 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@pm2/pm2-version-check@1.0.4': + dependencies: + debug: 4.4.0 + transitivePeerDependencies: + - supports-color + '@pnpm/config.env-replace@1.1.0': {} '@pnpm/network.ca-file@1.0.2': @@ -6654,6 +7259,8 @@ snapshots: '@sindresorhus/merge-streams@4.0.0': {} + '@tootallnate/quickjs-emscripten@0.23.0': {} + '@types/connect@3.4.36': dependencies: '@types/node': 18.19.75 @@ -6786,6 +7393,12 @@ snapshots: clean-stack: 5.2.0 indent-string: 5.0.0 + amp-message@0.1.2: + dependencies: + amp: 0.3.1 + + amp@0.3.1: {} + ansi-colors@4.1.3: {} ansi-escapes@4.3.2: @@ -6839,6 +7452,14 @@ snapshots: assertion-error@1.1.0: {} + ast-types@0.13.4: + dependencies: + tslib: 2.8.1 + + async@2.6.4: + dependencies: + lodash: 4.17.21 + async@3.2.6: {} auto-bind@4.0.0: {} @@ -6880,6 +7501,8 @@ snapshots: balanced-match@1.0.2: {} + basic-ftp@5.0.5: {} + before-after-hook@3.0.2: {} better-path-resolve@1.0.0: @@ -6892,6 +7515,10 @@ snapshots: blake3-wasm@2.1.5: {} + blessed@0.1.81: {} + + bodec@0.1.0: {} + bottleneck@2.19.5: {} brace-expansion@1.1.11: @@ -6971,6 +7598,11 @@ snapshots: escape-string-regexp: 1.0.5 supports-color: 5.5.0 + chalk@3.0.0: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 @@ -7010,6 +7642,8 @@ snapshots: chardet@0.7.0: {} + charm@0.1.2: {} + check-error@1.0.3: dependencies: get-func-name: 2.0.2 @@ -7051,6 +7685,10 @@ snapshots: optionalDependencies: '@colors/colors': 1.5.0 + cli-tableau@2.0.1: + dependencies: + chalk: 3.0.0 + cli-width@4.1.0: {} cliui@7.0.4: @@ -7094,6 +7732,10 @@ snapshots: commander@12.1.0: {} + commander@13.1.0: {} + + commander@2.15.1: {} + common-tags@1.8.2: {} compare-func@2.0.0: @@ -7153,6 +7795,8 @@ snapshots: optionalDependencies: typescript: 5.7.3 + croner@4.1.97: {} + cross-fetch@3.2.0: dependencies: node-fetch: 2.7.0 @@ -7181,16 +7825,32 @@ snapshots: dependencies: type-fest: 1.4.0 + culvert@0.1.2: {} + data-uri-to-buffer@2.0.2: {} + data-uri-to-buffer@6.0.2: {} + dataloader@1.4.0: {} dataloader@2.2.3: {} date-fns@3.6.0: {} + dayjs@1.11.13: {} + + dayjs@1.8.36: {} + debounce-microtasks@0.1.8: {} + debug@3.2.7: + dependencies: + ms: 2.1.3 + + debug@4.3.7: + dependencies: + ms: 2.1.3 + debug@4.4.0: dependencies: ms: 2.1.3 @@ -7203,6 +7863,12 @@ snapshots: defu@6.1.4: {} + degenerator@5.0.1: + dependencies: + ast-types: 0.13.4 + escodegen: 2.1.0 + esprima: 4.0.1 + del@6.1.1: dependencies: globby: 11.1.0 @@ -7277,6 +7943,10 @@ snapshots: enabled@2.0.0: {} + enquirer@2.3.6: + dependencies: + ansi-colors: 4.1.3 + enquirer@2.4.1: dependencies: ansi-colors: 4.1.3 @@ -7444,14 +8114,32 @@ snapshots: escape-string-regexp@5.0.0: {} + escodegen@2.1.0: + dependencies: + esprima: 4.0.1 + estraverse: 5.3.0 + esutils: 2.0.3 + optionalDependencies: + source-map: 0.6.1 + esprima@4.0.1: {} + estraverse@5.3.0: {} + estree-walker@0.6.1: {} estree-walker@3.0.3: dependencies: '@types/estree': 1.0.6 + esutils@2.0.3: {} + + eventemitter2@0.4.14: {} + + eventemitter2@5.0.1: {} + + eventemitter2@6.4.9: {} + execa@5.1.1: dependencies: cross-spawn: 7.0.6 @@ -7501,6 +8189,12 @@ snapshots: iconv-lite: 0.4.24 tmp: 0.0.33 + extrareqp2@1.0.0(debug@4.3.7): + dependencies: + follow-redirects: 1.15.9(debug@4.3.7) + transitivePeerDependencies: + - debug + fast-content-type-parse@2.0.1: {} fast-glob@3.3.3: @@ -7511,6 +8205,8 @@ snapshots: merge2: 1.4.1 micromatch: 4.0.8 + fast-json-patch@3.1.1: {} + fastq@1.19.0: dependencies: reusify: 1.0.4 @@ -7533,6 +8229,8 @@ snapshots: transitivePeerDependencies: - encoding + fclone@1.0.11: {} + fecha@4.2.3: {} figures@2.0.0: @@ -7573,6 +8271,10 @@ snapshots: fn.name@1.1.0: {} + follow-redirects@1.15.9(debug@4.3.7): + optionalDependencies: + debug: 4.3.7 + foreground-child@3.3.0: dependencies: cross-spawn: 7.0.6 @@ -7646,6 +8348,14 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 + get-uri@6.0.4: + dependencies: + basic-ftp: 5.0.5 + data-uri-to-buffer: 6.0.2 + debug: 4.4.0 + transitivePeerDependencies: + - supports-color + git-log-parser@1.2.1: dependencies: argv-formatter: 1.0.0 @@ -7655,6 +8365,12 @@ snapshots: through2: 2.0.5 traverse: 0.6.8 + git-node-fs@1.0.0(js-git@0.7.8): + optionalDependencies: + js-git: 0.7.8 + + git-sha1@0.1.2: {} + glob-parent@5.1.2: dependencies: is-glob: 4.0.3 @@ -7699,7 +8415,7 @@ snapshots: slash: 5.1.0 unicorn-magic: 0.1.0 - gqty@3.3.0(graphql@16.10.0): + gqty@3.4.1(graphql@16.10.0): dependencies: debounce-microtasks: 0.1.8 flatted: 3.3.2 @@ -7710,7 +8426,7 @@ snapshots: just-safe-get: 4.2.0 just-safe-set: 4.2.1 multidict: 1.0.9 - object-hash: 3.0.0 + ohash: 1.1.4 p-defer: 3.0.0 optionalDependencies: graphql: 16.10.0 @@ -7869,6 +8585,11 @@ snapshots: dependencies: loose-envify: 1.4.0 + ip-address@9.0.5: + dependencies: + jsbn: 1.1.0 + sprintf-js: 1.1.3 + is-absolute@1.0.0: dependencies: is-relative: 1.0.0 @@ -7954,6 +8675,13 @@ snapshots: jose@4.15.9: {} + js-git@0.7.8: + dependencies: + bodec: 0.1.0 + culvert: 0.1.2 + git-sha1: 0.1.2 + pako: 0.2.9 + js-tokens@4.0.0: {} js-tokens@9.0.1: {} @@ -7967,12 +8695,17 @@ snapshots: dependencies: argparse: 2.0.1 + jsbn@1.1.0: {} + jsesc@3.1.0: {} json-parse-better-errors@1.0.2: {} json-parse-even-better-errors@2.3.1: {} + json-stringify-safe@5.0.1: + optional: true + json5@2.2.3: {} jsonfile@4.0.0: @@ -8021,6 +8754,8 @@ snapshots: kuler@2.0.0: {} + lazy@1.0.11: {} + lines-and-columns@1.2.4: {} load-json-file@4.0.0: @@ -8112,6 +8847,8 @@ snapshots: dependencies: yallist: 4.0.0 + lru-cache@7.18.3: {} + magic-string@0.25.9: dependencies: sourcemap-codec: 1.4.8 @@ -8202,6 +8939,8 @@ snapshots: minipass@7.1.2: {} + mkdirp@1.0.4: {} + mlly@1.7.4: dependencies: acorn: 8.14.0 @@ -8219,8 +8958,12 @@ snapshots: mustache@4.2.0: {} + mute-stream@0.0.8: {} + mute-stream@1.0.0: {} + mute-stream@2.0.0: {} + mz@2.7.0: dependencies: any-promise: 1.3.0 @@ -8229,10 +8972,20 @@ snapshots: nanoid@3.3.8: {} + needle@2.4.0: + dependencies: + debug: 3.2.7 + iconv-lite: 0.4.24 + sax: 1.4.1 + transitivePeerDependencies: + - supports-color + neo-async@2.6.2: {} nerf-dart@1.0.0: {} + netmask@2.0.2: {} + no-case@3.0.4: dependencies: lower-case: 2.0.2 @@ -8287,14 +9040,17 @@ snapshots: npm@10.9.2: {} + nssocket@0.6.0: + dependencies: + eventemitter2: 0.4.14 + lazy: 1.0.11 + nullthrows@1.1.1: {} object-assign@4.1.1: {} object-hash@2.2.0: {} - object-hash@3.0.0: {} - ohash@1.1.4: {} oidc-token-hash@5.0.3: {} @@ -8384,10 +9140,30 @@ snapshots: p-try@2.2.0: {} + pac-proxy-agent@7.2.0: + dependencies: + '@tootallnate/quickjs-emscripten': 0.23.0 + agent-base: 7.1.3 + debug: 4.4.0 + get-uri: 6.0.4 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + pac-resolver: 7.0.1 + socks-proxy-agent: 8.0.5 + transitivePeerDependencies: + - supports-color + + pac-resolver@7.0.1: + dependencies: + degenerator: 5.0.1 + netmask: 2.0.2 + package-json-from-dist@1.0.1: {} package-manager-detector@0.2.9: {} + pako@0.2.9: {} + param-case@3.0.4: dependencies: dot-case: 3.0.4 @@ -8492,6 +9268,15 @@ snapshots: picomatch@2.3.1: {} + pidusage@2.0.21: + dependencies: + safe-buffer: 5.2.1 + optional: true + + pidusage@3.0.2: + dependencies: + safe-buffer: 5.2.1 + pify@3.0.0: {} pify@4.0.1: {} @@ -8511,6 +9296,79 @@ snapshots: dependencies: find-up: 3.0.0 + pm2-axon-rpc@0.7.1: + dependencies: + debug: 4.4.0 + transitivePeerDependencies: + - supports-color + + pm2-axon@4.0.1: + dependencies: + amp: 0.3.1 + amp-message: 0.1.2 + debug: 4.4.0 + escape-string-regexp: 4.0.0 + transitivePeerDependencies: + - supports-color + + pm2-deploy@1.0.2: + dependencies: + run-series: 1.1.9 + tv4: 1.3.0 + + pm2-multimeter@0.1.2: + dependencies: + charm: 0.1.2 + + pm2-sysmonit@1.2.8: + dependencies: + async: 3.2.6 + debug: 4.4.0 + pidusage: 2.0.21 + systeminformation: 5.25.11 + tx2: 1.0.5 + transitivePeerDependencies: + - supports-color + optional: true + + pm2@5.4.3: + dependencies: + '@pm2/agent': 2.0.4 + '@pm2/io': 6.0.1 + '@pm2/js-api': 0.8.0 + '@pm2/pm2-version-check': 1.0.4 + async: 3.2.6 + blessed: 0.1.81 + chalk: 3.0.0 + chokidar: 3.6.0 + cli-tableau: 2.0.1 + commander: 2.15.1 + croner: 4.1.97 + dayjs: 1.11.13 + debug: 4.4.0 + enquirer: 2.3.6 + eventemitter2: 5.0.1 + fclone: 1.0.11 + js-yaml: 4.1.0 + mkdirp: 1.0.4 + needle: 2.4.0 + pidusage: 3.0.2 + pm2-axon: 4.0.1 + pm2-axon-rpc: 0.7.1 + pm2-deploy: 1.0.2 + pm2-multimeter: 0.1.2 + promptly: 2.2.0 + semver: 7.7.1 + source-map-support: 0.5.21 + sprintf-js: 1.1.2 + vizion: 2.2.1 + optionalDependencies: + pm2-sysmonit: 1.2.8 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + postcss@8.5.1: dependencies: nanoid: 3.3.8 @@ -8547,8 +9405,27 @@ snapshots: dependencies: asap: 2.0.6 + promptly@2.2.0: + dependencies: + read: 1.0.7 + proto-list@1.2.4: {} + proxy-agent@6.3.1: + dependencies: + agent-base: 7.1.3 + debug: 4.4.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + lru-cache: 7.18.3 + pac-proxy-agent: 7.2.0 + proxy-from-env: 1.1.0 + socks-proxy-agent: 8.0.5 + transitivePeerDependencies: + - supports-color + + proxy-from-env@1.1.0: {} + queue-microtask@1.2.3: {} ramda@0.27.2: {} @@ -8590,6 +9467,10 @@ snapshots: pify: 4.0.1 strip-bom: 3.0.0 + read@1.0.7: + dependencies: + mute-stream: 0.0.8 + readable-stream@2.3.8: dependencies: core-util-is: 1.0.3 @@ -8626,6 +9507,14 @@ snapshots: require-directory@2.1.1: {} + require-in-the-middle@5.2.0: + dependencies: + debug: 4.4.0 + module-details-from-path: 1.0.3 + resolve: 1.22.10 + transitivePeerDependencies: + - supports-color + require-in-the-middle@7.5.0: dependencies: debug: 4.4.0 @@ -8702,12 +9591,18 @@ snapshots: dependencies: queue-microtask: 1.2.3 + run-series@1.1.9: {} + safe-buffer@5.1.2: {} + safe-buffer@5.2.1: {} + safe-stable-stringify@2.5.0: {} safer-buffer@2.1.2: {} + sax@1.4.1: {} + selfsigned@2.4.1: dependencies: '@types/node-forge': 1.3.11 @@ -8781,6 +9676,10 @@ snapshots: semver@6.3.1: {} + semver@7.5.4: + dependencies: + lru-cache: 6.0.0 + semver@7.7.1: {} sentence-case@3.0.4: @@ -8825,11 +9724,26 @@ snapshots: slash@5.1.0: {} + smart-buffer@4.2.0: {} + snake-case@3.0.4: dependencies: dot-case: 3.0.4 tslib: 2.8.1 + socks-proxy-agent@8.0.5: + dependencies: + agent-base: 7.1.3 + debug: 4.4.0 + socks: 2.8.4 + transitivePeerDependencies: + - supports-color + + socks@2.8.4: + dependencies: + ip-address: 9.0.5 + smart-buffer: 4.2.0 + source-map-js@1.2.1: {} source-map-support@0.5.21: @@ -8872,6 +9786,10 @@ snapshots: sprintf-js@1.0.3: {} + sprintf-js@1.1.2: {} + + sprintf-js@1.1.3: {} + stack-trace@0.0.10: {} stackback@0.0.2: {} @@ -8956,6 +9874,9 @@ snapshots: dependencies: tslib: 2.8.1 + systeminformation@5.25.11: + optional: true + temp-dir@2.0.0: {} temp-dir@3.0.0: {} @@ -9024,14 +9945,21 @@ snapshots: traverse@0.6.8: {} - treekill@1.0.0: {} - triple-beam@1.4.1: {} + tslib@1.9.3: {} + tslib@2.6.3: {} tslib@2.8.1: {} + tv4@1.3.0: {} + + tx2@1.0.5: + dependencies: + json-stringify-safe: 5.0.1 + optional: true + type-detect@4.1.0: {} type-fest@0.16.0: {} @@ -9188,6 +10116,13 @@ snapshots: - supports-color - terser + vizion@2.2.1: + dependencies: + async: 2.6.4 + git-node-fs: 1.0.0(js-git@0.7.8) + ini: 1.3.8 + js-git: 0.7.8 + webidl-conversions@3.0.1: {} whatwg-url@5.0.0: @@ -9308,6 +10243,8 @@ snapshots: wrappy@1.0.2: {} + ws@7.5.10: {} + ws@8.18.0: {} xtend@4.0.2: {} From a3a7c45380f62dc0c416599b84a97679674be459 Mon Sep 17 00:00:00 2001 From: Nico Schett Date: Tue, 18 Feb 2025 23:09:40 +0100 Subject: [PATCH 002/138] extend plugin system with setup, middleware, and build functions --- .changeset/popular-pugs-serve.md | 40 +++++++++ .../pylon-dev/src/builder/bundler/bundler.ts | 84 ++++++++++++++++++- .../src/app/handler/graphql-viewer-handler.ts | 64 -------------- packages/pylon/src/app/index.ts | 22 +++-- .../src/app/{handler => }/pylon-handler.ts | 48 +++++++++-- packages/pylon/src/index.ts | 26 +++++- .../{app/envelop => plugins}/use-sentry.ts | 4 +- packages/pylon/src/plugins/use-viewer.ts | 76 +++++++++++++++++ 8 files changed, 278 insertions(+), 86 deletions(-) create mode 100644 .changeset/popular-pugs-serve.md delete mode 100644 packages/pylon/src/app/handler/graphql-viewer-handler.ts rename packages/pylon/src/app/{handler => }/pylon-handler.ts (75%) rename packages/pylon/src/{app/envelop => plugins}/use-sentry.ts (99%) create mode 100644 packages/pylon/src/plugins/use-viewer.ts diff --git a/.changeset/popular-pugs-serve.md b/.changeset/popular-pugs-serve.md new file mode 100644 index 0000000..2c83f93 --- /dev/null +++ b/.changeset/popular-pugs-serve.md @@ -0,0 +1,40 @@ +--- +'@getcronit/pylon': minor +'@getcronit/pylon-dev': minor +--- + +Extend plugin system with setup, middleware, and build functions. +The viewer is now integrated via a built-in `useViewer` plugin. + +Custom plugins can now access the app instance and register routes, middleware, and custom build steps. + +```ts +import {Plugin} from '@getcronit/pylon' + +export function myPlugin(): Plugin { + return { + setup(app) { + app.use((req, res, next) => { + console.log('Request:', req.url) + next() + }) + + app.get('/hello', (req, res) => { + res.send('Hello, World!') + }) + }, + middleware: (c, next) => { + // This middleware will be inserted higher in the middleware stack + console.log('Middleware:', c.req.url) + next() + }, + build: async () => { + // Custom esbuild build + const ctx = await esbuild.context(...) + + // Must return the context + return ctx + } + } +} +``` diff --git a/packages/pylon-dev/src/builder/bundler/bundler.ts b/packages/pylon-dev/src/builder/bundler/bundler.ts index 5d218cf..608f103 100644 --- a/packages/pylon-dev/src/builder/bundler/bundler.ts +++ b/packages/pylon-dev/src/builder/bundler/bundler.ts @@ -1,6 +1,7 @@ // bundler.ts import esbuild, {context} from 'esbuild' import esbuildPluginTsc from 'esbuild-plugin-tsc' +import type {PylonConfig, Plugin} from '@getcronit/pylon' import path from 'path' import fs from 'fs/promises' @@ -25,6 +26,33 @@ export class Bundler { this.outputDir = outputDir } + private async initBuildPlugins(args: {onBuild: () => void}) { + const configPath = path.join(process.cwd(), this.outputDir, 'index.js') + + let config: PylonConfig | undefined + try { + let configModule = await import(configPath) + + config = configModule.config + } catch (e) { + console.error('Error loading config', e) + } + + const buildContexts: ReturnType>[] = [] + + const plugins = config?.plugins || [] + + for (const plugin of plugins) { + if (plugin.build) { + const ctx = plugin.build({onBuild: args.onBuild}) + + buildContexts.push(ctx) + } + } + + return buildContexts + } + public async build(options: BundlerBuildOptions) { const inputPath = path.join(process.cwd(), this.sfiFilePath) const dir = path.join(process.cwd(), this.outputDir) @@ -77,6 +105,60 @@ export class Bundler { await ctx.rebuild() - return ctx + const pluginCtxs = await this.initBuildPlugins({ + onBuild: () => { + options.onBuild?.({ + totalFiles: 0, + totalSize: 0, + schemaChanged: false, + duration: 0 + }) + } + }) + + await Promise.all( + pluginCtxs.map(async c => { + await (await c).rebuild() + }) + ) + + return { + watch: async () => { + for (const ctx of pluginCtxs) { + const c = await ctx + + await c.watch() + } + + return await ctx.watch() + }, + rebuild: async () => { + for (const ctx of pluginCtxs) { + const c = await ctx + + await c.rebuild() + } + + await ctx.rebuild() + }, + dispose: async () => { + for (const ctx of pluginCtxs) { + const c = await ctx + + await c.dispose() + } + + await ctx.dispose() + }, + cancel: async () => { + for (const ctx of pluginCtxs) { + const c = await ctx + + await c.cancel() + } + + await ctx.cancel() + } + } } } diff --git a/packages/pylon/src/app/handler/graphql-viewer-handler.ts b/packages/pylon/src/app/handler/graphql-viewer-handler.ts deleted file mode 100644 index 9d0e972..0000000 --- a/packages/pylon/src/app/handler/graphql-viewer-handler.ts +++ /dev/null @@ -1,64 +0,0 @@ -import type {MiddlewareHandler} from 'hono' -import {html} from 'hono/html' - -export const graphqlViewerHandler: MiddlewareHandler = async (c, next) => { - return c.html(html` - - - - Pylon Viewer - - - - - - - - -
Loading...
- - - - `) -} diff --git a/packages/pylon/src/app/index.ts b/packages/pylon/src/app/index.ts index c54403f..af0449a 100644 --- a/packages/pylon/src/app/index.ts +++ b/packages/pylon/src/app/index.ts @@ -1,9 +1,7 @@ -import {Hono} from 'hono' -import {logger} from 'hono/logger' import {sentry} from '@hono/sentry' +import {Hono, MiddlewareHandler} from 'hono' import {asyncContext, Env} from '../context' -import {graphqlViewerHandler} from './handler/graphql-viewer-handler' export const app = new Hono() @@ -21,12 +19,24 @@ app.use('*', async (c, next) => { }) }) -app.use('*', logger()) - app.use((c, next) => { // @ts-ignore c.req.id = crypto.randomUUID() return next() }) -app.get('/viewer', graphqlViewerHandler) +export const pluginsMiddleware: MiddlewareHandler[] = [] + +const pluginsMiddlewareLoader: MiddlewareHandler = async (c, next) => { + for (const middleware of pluginsMiddleware) { + const response = await middleware(c, async () => {}) + + if (response) { + return response + } + } + + return next() +} + +app.use(pluginsMiddlewareLoader) diff --git a/packages/pylon/src/app/handler/pylon-handler.ts b/packages/pylon/src/app/pylon-handler.ts similarity index 75% rename from packages/pylon/src/app/handler/pylon-handler.ts rename to packages/pylon/src/app/pylon-handler.ts index fa019c6..d04cf9c 100644 --- a/packages/pylon/src/app/handler/pylon-handler.ts +++ b/packages/pylon/src/app/pylon-handler.ts @@ -7,12 +7,14 @@ import { JSONResolver } from 'graphql-scalars' -import {useSentry} from '../envelop/use-sentry' -import {Context} from '../../context' -import {resolversToGraphQLResolvers} from '../../define-pylon' -import {PylonConfig} from '../..' +import {useSentry} from '../plugins/use-sentry' +import {Context} from '../context' +import {resolversToGraphQLResolvers} from '../define-pylon' +import {Plugin, PylonConfig} from '..' import {readFileSync} from 'fs' import path from 'path' +import {app, pluginsMiddleware} from '.' +import {useViewer} from '../plugins/use-viewer' interface PylonHandlerOptions { graphql: { @@ -23,12 +25,40 @@ interface PylonHandlerOptions { config?: PylonConfig } +type MaybeLazyObject = T | (() => T) + +const resolveLazyObject = (obj: MaybeLazyObject): T => { + return typeof obj === 'function' ? (obj as () => T)() : obj +} + export const handler = (options: PylonHandlerOptions) => { - let {typeDefs, resolvers, graphql, config} = - options as PylonHandlerOptions & { - typeDefs?: string - resolvers?: Record + let { + typeDefs, + resolvers, + graphql: graphql$, + config: config$ + } = options as PylonHandlerOptions & { + typeDefs?: string + resolvers?: Record + } + + const loadPluginsMiddleware = (plugins: Plugin[]) => { + for (const plugin of plugins) { + plugin.setup?.(app) + + if (plugin.middleware) { + pluginsMiddleware.push(plugin.middleware) + } } + } + + const graphql = resolveLazyObject(graphql$) + + const config = resolveLazyObject(config$) + + const plugins = [useSentry(), useViewer(), ...(config?.plugins || [])] + + loadPluginsMiddleware(plugins) if (!typeDefs) { // Try to read the schema from the default location @@ -113,7 +143,7 @@ export const handler = (options: PylonHandlerOptions) => { }, graphqlEndpoint: '/graphql', ...config, - plugins: [useSentry(), ...(config?.plugins || [])], + plugins, schema }) diff --git a/packages/pylon/src/index.ts b/packages/pylon/src/index.ts index 3a1980d..593cb38 100644 --- a/packages/pylon/src/index.ts +++ b/packages/pylon/src/index.ts @@ -1,5 +1,5 @@ import {YogaServerOptions} from 'graphql-yoga' -import {Context} from './context.js' +import {Context, Env} from './context.js' export {ServiceError} from './define-pylon.js' export * from './auth/index.js' @@ -12,14 +12,32 @@ export { getContext, setContext } from './context.js' -export {app} from './app/index.js' -export {handler} from './app/handler/pylon-handler.js' +import {app as pylonApp} from './app/index.js' +export {pylonApp as app} +export {handler} from './app/pylon-handler.js' export {getEnv} from './get-env.js' export {createDecorator} from './create-decorator.js' export {createPubSub as experimentalCreatePubSub} from 'graphql-yoga' -export type PylonConfig = Pick, 'plugins'> +import type {Plugin as YogaPlugin} from 'graphql-yoga' +import {MiddlewareHandler} from 'hono' +import {BuildContext, BuildOptions} from 'esbuild' +export type Plugin< + PluginContext extends Record = {}, + TServerContext extends Record = {}, + TUserContext = {} +> = YogaPlugin & { + middleware?: MiddlewareHandler + setup?: (app: typeof pylonApp) => void + build?: (args: { + onBuild: () => void + }) => Promise, 'serve'>> +} + +export type PylonConfig = { + plugins?: Plugin[] +} export type ID = string & {readonly brand?: unique symbol} export type Int = number & {readonly brand?: unique symbol} export type Float = number & {readonly brand?: unique symbol} diff --git a/packages/pylon/src/app/envelop/use-sentry.ts b/packages/pylon/src/plugins/use-sentry.ts similarity index 99% rename from packages/pylon/src/app/envelop/use-sentry.ts rename to packages/pylon/src/plugins/use-sentry.ts index 557fb37..eed8977 100644 --- a/packages/pylon/src/app/envelop/use-sentry.ts +++ b/packages/pylon/src/plugins/use-sentry.ts @@ -4,11 +4,11 @@ import { handleStreamOrSingleExecutionResult, isOriginalGraphQLError, OnExecuteDoneHookResultOnNextHook, - TypedExecutionArgs, - type Plugin + TypedExecutionArgs } from '@envelop/core' import * as Sentry from '@sentry/node' import type {Span, TraceparentData} from '@sentry/types' +import {Plugin} from '..' export type SentryPluginOptions> = { /** diff --git a/packages/pylon/src/plugins/use-viewer.ts b/packages/pylon/src/plugins/use-viewer.ts new file mode 100644 index 0000000..2d73338 --- /dev/null +++ b/packages/pylon/src/plugins/use-viewer.ts @@ -0,0 +1,76 @@ +import {html} from 'hono/html' +import {getContext, type Plugin} from '../index' + +export function useViewer(): Plugin { + return { + onRequest: async ({request, fetchAPI, endResponse, url}) => { + const c = getContext() + + if (request.method === 'GET' && url.pathname === '/viewer') { + endResponse( + c.html( + await html` + + + + Pylon Viewer + + + + + + + + +
Loading...
+ + + + ` + ) + ) + } + } + } +} From fbe26986db15dc5fa297c4e6fd5cfc2fcc8bc36b Mon Sep 17 00:00:00 2001 From: Nico Schett Date: Tue, 18 Feb 2025 23:11:35 +0100 Subject: [PATCH 003/138] add fallback page for landing and unhandled routes; configurable via Pylon config --- .changeset/shy-countries-help.md | 13 ++ packages/pylon/src/app/pylon-handler.ts | 9 + packages/pylon/src/index.ts | 2 + .../pylon/src/plugins/use-unhandled-route.ts | 186 ++++++++++++++++++ 4 files changed, 210 insertions(+) create mode 100644 .changeset/shy-countries-help.md create mode 100644 packages/pylon/src/plugins/use-unhandled-route.ts diff --git a/.changeset/shy-countries-help.md b/.changeset/shy-countries-help.md new file mode 100644 index 0000000..1ade958 --- /dev/null +++ b/.changeset/shy-countries-help.md @@ -0,0 +1,13 @@ +--- +'@getcronit/pylon': minor +--- + +Show a fallback page for the landing page and unhandled routes / 404s. + +This behavior can be disabled via the pylon config: + +```ts +export const config: PylonConfig = { + landingPage: false +} +``` diff --git a/packages/pylon/src/app/pylon-handler.ts b/packages/pylon/src/app/pylon-handler.ts index d04cf9c..21d38ae 100644 --- a/packages/pylon/src/app/pylon-handler.ts +++ b/packages/pylon/src/app/pylon-handler.ts @@ -15,6 +15,7 @@ import {readFileSync} from 'fs' import path from 'path' import {app, pluginsMiddleware} from '.' import {useViewer} from '../plugins/use-viewer' +import {useUnhandledRoute} from '../plugins/use-unhandled-route' interface PylonHandlerOptions { graphql: { @@ -58,6 +59,14 @@ export const handler = (options: PylonHandlerOptions) => { const plugins = [useSentry(), useViewer(), ...(config?.plugins || [])] + if (config?.landingPage ?? true) { + plugins.push( + useUnhandledRoute({ + graphqlEndpoint: '/graphql' + }) + ) + } + loadPluginsMiddleware(plugins) if (!typeDefs) { diff --git a/packages/pylon/src/index.ts b/packages/pylon/src/index.ts index 593cb38..b726a63 100644 --- a/packages/pylon/src/index.ts +++ b/packages/pylon/src/index.ts @@ -36,8 +36,10 @@ export type Plugin< } export type PylonConfig = { + landingPage?: boolean plugins?: Plugin[] } + export type ID = string & {readonly brand?: unique symbol} export type Int = number & {readonly brand?: unique symbol} export type Float = number & {readonly brand?: unique symbol} diff --git a/packages/pylon/src/plugins/use-unhandled-route.ts b/packages/pylon/src/plugins/use-unhandled-route.ts new file mode 100644 index 0000000..22a12b1 --- /dev/null +++ b/packages/pylon/src/plugins/use-unhandled-route.ts @@ -0,0 +1,186 @@ +import {getVersions} from '@getcronit/pylon-telemetry' +import {html} from 'hono/html' +import {type Plugin} from '../index' + +export function useUnhandledRoute(args: {graphqlEndpoint: string}): Plugin { + const versions = getVersions() + + return { + setup: app => { + app.use(async (c, next) => { + if (c.req.method === 'GET' && c.req.path !== args.graphqlEndpoint) { + return c.html( + await html` + + + + Welcome to Pylon + + + + +
+
+ +

Enables TypeScript developers to easily build GraphQL APIs

+ +
+
+

Not the page you are looking for? 👀

+

+ This page is shown be default whenever a 404 is hit.
You can disable this by behavior + via the landingPage option in the Pylon config. Edit the src/index.ts file + and add the following code: +

+
+    
+  export const config: PylonConfig = {
+    landingPage: false
+  }
+    
+  
+ +

+ When you define a route, this page will no longer be shown. For example, the following code + will show a "Hello, world!" message at the root of your app: +

+
+    
+  import {app} from '@getcronit/pylon'
+  
+  app.get("/", c => {
+    return c.text("Hello, world!")
+  })
+    
+  
+
+
+ + `, + 404 + ) + } + + return next() + }) + } + } +} From 798f3f4c977db504513d6686c5bb72d895b4e8b1 Mon Sep 17 00:00:00 2001 From: Nico Schett Date: Tue, 18 Feb 2025 23:18:49 +0100 Subject: [PATCH 004/138] overhaul authentication system with new useAuth plugin and role-based access control --- .changeset/soft-goats-run.md | 36 +++ ...lt-in-authentication-and-authorization.mdx | 128 ++++---- packages/pylon/package.json | 2 +- .../pylon/src/auth/decorators/requireAuth.ts | 52 --- packages/pylon/src/auth/index.ts | 306 ------------------ packages/pylon/src/context.ts | 4 +- packages/pylon/src/define-pylon.ts | 13 +- packages/pylon/src/index.ts | 5 +- .../src/plugins/use-auth/auth-require.ts | 95 ++++++ .../plugins/use-auth/import-private-key.ts | 61 ++++ packages/pylon/src/plugins/use-auth/index.ts | 3 + packages/pylon/src/plugins/use-auth/types.ts | 9 + .../pylon/src/plugins/use-auth/use-auth.ts | 253 +++++++++++++++ pnpm-lock.yaml | 39 +-- 14 files changed, 553 insertions(+), 453 deletions(-) create mode 100644 .changeset/soft-goats-run.md delete mode 100644 packages/pylon/src/auth/decorators/requireAuth.ts delete mode 100644 packages/pylon/src/auth/index.ts create mode 100644 packages/pylon/src/plugins/use-auth/auth-require.ts create mode 100644 packages/pylon/src/plugins/use-auth/import-private-key.ts create mode 100644 packages/pylon/src/plugins/use-auth/index.ts create mode 100644 packages/pylon/src/plugins/use-auth/types.ts create mode 100644 packages/pylon/src/plugins/use-auth/use-auth.ts diff --git a/.changeset/soft-goats-run.md b/.changeset/soft-goats-run.md new file mode 100644 index 0000000..c50cebd --- /dev/null +++ b/.changeset/soft-goats-run.md @@ -0,0 +1,36 @@ +--- +'@getcronit/pylon': major +--- + +**Summary:** +This changeset introduces a major overhaul to the built-in authentication system. The new implementation automatically sets up `/auth/login`, `/auth/callback`, and `/auth/logout` routes, injects an `auth` object into the context, and manages token cookies. Role-based route protection is now enhanced via `authMiddleware` and the updated `requireAuth` decorator, configurable through the streamlined `useAuth` plugin. + +--- + +**Breaking Changes:** + +- **WHAT:** + The authentication configuration has been completely revamped. The previous manual setup is replaced by the `useAuth` plugin. Custom authentication route definitions are no longer necessary, and existing middleware or decorator usage may require adjustments. + +- **WHY:** + This change was implemented to simplify authentication setup, reduce boilerplate, improve security by automating context and cookie management, and offer better role-based access control. + +- **HOW:** + Consumers should: + 1. Remove any custom authentication route setups. + 2. Update their configuration to use the new `useAuth` plugin as shown below: + ```typescript + export const config: PylonConfig = { + plugins: [ + useAuth({ + issuer: 'https://test-0o6zvq.zitadel.cloud', + endpoint: '/auth', + keyPath: 'key.json' + }) + ] + } + ``` + 3. Replace previous authentication middleware or decorators with the updated `requireAuth` and `authMiddleware` APIs. + 4. Test the new authentication endpoints (`/auth/login`, `/auth/callback`, and `/auth/logout`) to ensure proper integration. + +Ensure you update your code accordingly to avoid disruptions in your authentication flow. diff --git a/docs/pages/docs/core-concepts/built-in-authentication-and-authorization.mdx b/docs/pages/docs/core-concepts/built-in-authentication-and-authorization.mdx index 5f8be38..7badc04 100644 --- a/docs/pages/docs/core-concepts/built-in-authentication-and-authorization.mdx +++ b/docs/pages/docs/core-concepts/built-in-authentication-and-authorization.mdx @@ -2,37 +2,59 @@ import {Callout} from '@components/callout' # Built-in Authentication and Authorization -Discover how Pylon simplifies user authentication and authorization with its comprehensive built-in features, empowering you to secure your web services effortlessly. +Pylon now offers an enhanced, streamlined authentication system. With this update, the auth endpoint automatically creates routes for **/auth/login**, **/auth/callback**, and **/auth/logout**. When a user authenticates, Pylon sets an `auth` object in the context variables and automatically manages a cookie with the token—simplifying session management and ensuring a secure experience. + +--- ## General Setup -Before diving into authentication and authorization with Pylon, it's essential to set up your environment and configure the necessary components. Pylon's built-in authentication system follows the OIDC standard and is currently tightly integrated with ZITADEL for user management and access control. +Before you begin, configure your environment to integrate with your authentication provider (e.g., ZITADEL). The new configuration uses the `useAuth` plugin to initialize authentication routes and settings. -1. **Environment Variables:** - Ensure you have the required environment variables set up in your project: +```typescript +import { + app, + PylonConfig, + requireAuth, + useAuth, + authMiddleware +} from '@getcronit/pylon' + +export const config: PylonConfig = { + plugins: [ + useAuth({ + issuer: 'https://test-0o6zvq.zitadel.cloud', + endpoint: '/auth', // optional, default is '/auth' + keyPath: 'key.json' // optional, default is 'key.json' + }) + ] +} +``` - ``` - AUTH_ISSUER=https://test-0o6zvq.zitadel.cloud - AUTH_PROJECT_ID= - ``` +**How it works:** -2. **Integration with ZITADEL:** - To enable Pylon to authenticate users and manage access control, you need to integrate it with ZITADEL. Follow the documentation provided by ZITADEL to set up projects, applications, keys, and roles. - [ZITADEL Projects Documentation](https://zitadel.com/docs/guides/manage/console/projects) +- **Auth Routes:** + The plugin automatically creates routes for: + + - `/auth/login` + - `/auth/callback` + - `/auth/logout` + +- **Context & Cookie:** + After authentication, an `auth` object is added to your context, and a cookie containing the token is set for subsequent requests. - Pylon requires a **API** application with the **Private JWT Key** type to - authenticate users and manage access control. + Ensure that your API application is configured to use a **Private JWT Key** + type for secure token management. +--- + ## Authentication Example -Pylon makes authentication seamless by providing a straightforward integration with ZITADEL. Here's how you can set up authentication in your Pylon project: +To protect sensitive data, use the `requireAuth` decorator. In the example below, any user trying to access the data must be authenticated: ```typescript -import {app, auth, requireAuth} from '@getcronit/pylon' - -// Define your sensitive data service +// Define a service for sensitive data class SensitiveData { @requireAuth() static async getData() { @@ -40,27 +62,26 @@ class SensitiveData { } } +// Expose the resolver via GraphQL export const graphql = { Query: { sensitiveData: SensitiveData.getData } } -app.use('*', auth.initialize()) - export default app ``` -In this example, the `requireAuth()` decorator ensures that users are authenticated before accessing sensitive data. You can also specify roles to restrict access to certain data based on user permissions. +In this setup, the `@requireAuth()` decorator ensures that only authenticated users can access the `getData` method. If the user is not authenticated, they will be redirected to the login flow at `/auth/login`. --- ## Authorization Example -Authorization in Pylon allows you to control access to specific resources based on user roles and permissions. Here's how you can implement authorization in your Pylon project: +If you need to restrict access based on roles, you can pass a roles array to the `requireAuth` decorator. For instance, the following example limits access to users with the `"admin"` role: ```typescript -// Define your sensitive data service +// Define a service for admin-only data class SensitiveData { @requireAuth({ roles: ['admin'] @@ -70,72 +91,63 @@ class SensitiveData { } } -// Define your GraphQL schema +// Expose the resolver via GraphQL export const graphql = { Query: { sensitiveAdminData: SensitiveData.getAdminData } } -app.use('*', auth.initialize()) - export default app ``` -In this example, the `requireAuth()` decorator ensures that only authenticated users with the "admin" role can access the `getAdminData()` function. You can customize roles and permissions according to your application's requirements. +Only authenticated users who have the `"admin"` role will be allowed to access `getAdminData()`. Roles should be managed in your authentication provider (e.g., ZITADEL) for centralized control over permissions. -Roles can be defined in ZITADEL and assigned to users to control access to specific resources. By integrating Pylon with ZITADEL, you can easily manage roles and permissions for your application. -For more information on setting up roles in ZITADEL, refer to the [ZITADEL Roles Documentation](https://zitadel.com/docs/guides/manage/console/roles). +--- -## Securing Routes +## Securing Routes with Middleware -Securing routes in Pylon involves enforcing authentication and, optionally, authorization for specific endpoints or routes. Here's how you can secure a route in your Pylon project: +In addition to securing individual resolvers, you can enforce authentication and authorization for entire routes using the new `authMiddleware`. For example, to secure a specific REST endpoint: ```typescript -import {auth, requireAuth} from '@getcronit/pylon' - -// Define your sensitive data service -class SensitiveData { - static async getData() { - return 'Sensitive Data' - } +import {authMiddleware} from '@getcronit/pylon' - @requireAuth({ +// Secure all routes under /admin to only allow users with the 'admin' role +app.use( + '/admin', + authMiddleware({ roles: ['admin'] }) - static async getAdminData() { - return 'Admin Data' - } -} +) -export const graphql = { - Query: { - sensitiveData: SensitiveData.getData, - sensitiveAdminData: SensitiveData.getAdminData +// Secure specific route to only allow users with the 'admin' role +app.get( + '/secure', + authMiddleware({ + roles: ['admin'] + }), + c => { + return c.json({data: 'sensitive'}) } -} - -// Enforce authentication for all routes -app.use('*', auth.initialize()) - -// Secure a specific route with authentication and authorization -app.use('/admin', auth.requireAuth({roles: ['admin']})) - +) export default app ``` -In this example, we're securing the `/admin` route to ensure that only authenticated users with the "admin" role can access it. By using the `requireAuth()` middleware from Pylon's authentication module, we enforce both authentication and authorization for this specific route. +In this case, any request to the `/admin` route will first pass through `authMiddleware`, ensuring that the user is authenticated and has the required `"admin"` role. +The same applies to the `/secure` route, which is secured with the `authMiddleware` middleware. -You can customize the route and the required roles according to your application's requirements. This ensures that sensitive endpoints are protected, providing a secure environment for your users' data and resources. +--- ## Further Resources -For detailed instructions on setting up projects, applications, keys, and roles in ZITADEL, refer to the ZITADEL documentation: +For additional guidance on integrating with your authentication provider, please refer to the following resources: - [ZITADEL Projects Documentation](https://zitadel.com/docs/guides/manage/console/projects) - [ZITADEL Applications Documentation](https://zitadel.com/docs/guides/manage/console/applications#api) - [ZITADEL Roles Documentation](https://zitadel.com/docs/guides/manage/console/roles) +--- + ## Conclusion -With Pylon's built-in authentication and authorization features, you can easily secure your web services and control access to sensitive data, providing a seamless and secure user experience. +With the new built-in authentication and authorization features, Pylon makes securing your web services simpler than ever. The automatic route creation, context management, and cookie handling streamline the login flow, while decorators and middleware give you granular control over access to your application’s data and routes. Enjoy a secure and seamless user experience with minimal configuration! diff --git a/packages/pylon/package.json b/packages/pylon/package.json index d6d10a4..3d54ce2 100644 --- a/packages/pylon/package.json +++ b/packages/pylon/package.json @@ -32,7 +32,7 @@ "graphql-yoga": "^5.6.2", "hono": "^4.0.8", "jsonwebtoken": "^9.0.2", - "openid-client": "^5.6.4", + "openid-client": "^6.1.7", "toucan-js": "^4.1.0", "winston": "^3.8.2" }, diff --git a/packages/pylon/src/auth/decorators/requireAuth.ts b/packages/pylon/src/auth/decorators/requireAuth.ts deleted file mode 100644 index 427a08c..0000000 --- a/packages/pylon/src/auth/decorators/requireAuth.ts +++ /dev/null @@ -1,52 +0,0 @@ -import {sendFunctionEvent} from '@getcronit/pylon-telemetry' -import {HTTPException} from 'hono/http-exception' - -import {AuthRequireChecks, auth} from '..' -import {getContext} from '../../context' -import {ServiceError} from '../../define-pylon' -import {createDecorator} from '../../create-decorator' - -export function requireAuth(checks?: AuthRequireChecks) { - sendFunctionEvent({ - name: 'requireAuth', - duration: 0 - }).then(() => {}) - - const checkAuth = async (c: any) => { - const ctx = await c - - try { - await auth.require(checks)(ctx, async () => {}) - } catch (e) { - if (e instanceof HTTPException) { - if (e.status === 401) { - throw new ServiceError(e.message, { - statusCode: 401, - code: 'AUTH_REQUIRED' - }) - } else if (e.status === 403) { - const res = e.getResponse() - - throw new ServiceError(res.statusText, { - statusCode: res.status, - code: 'AUTHORIZATION_REQUIRED', - details: { - missingRoles: res.headers.get('Missing-Roles')?.split(','), - obtainedRoles: res.headers.get('Obtained-Roles')?.split(',') - } - }) - } else { - throw e - } - } - - throw e - } - } - - return createDecorator(async () => { - const ctx = getContext() - - await checkAuth(ctx) - }) -} diff --git a/packages/pylon/src/auth/index.ts b/packages/pylon/src/auth/index.ts deleted file mode 100644 index 73cc380..0000000 --- a/packages/pylon/src/auth/index.ts +++ /dev/null @@ -1,306 +0,0 @@ -import {MiddlewareHandler} from 'hono' -import jwt from 'jsonwebtoken' -import type {IdTokenClaims, IntrospectionResponse} from 'openid-client' -import path from 'path' -import {HTTPException} from 'hono/http-exception' -import {ContentfulStatusCode} from 'hono/utils/http-status' -import {env} from 'hono/adapter' -import * as Sentry from '@sentry/bun' -import {existsSync, readFileSync} from 'fs' -import {sendFunctionEvent} from '@getcronit/pylon-telemetry' - -export type AuthState = IntrospectionResponse & - IdTokenClaims & { - roles: string[] - } - -const authInitialize = () => { - // Load private key file from cwd - const authKeyFilePath = path.join(process.cwd(), 'key.json') - - // Load private key file from cwd - let API_PRIVATE_KEY_FILE: - | { - type: 'application' - keyId: string - key: string - appId: string - clientId: string - } - | undefined = undefined - - if (existsSync(authKeyFilePath)) { - try { - API_PRIVATE_KEY_FILE = JSON.parse(readFileSync(authKeyFilePath, 'utf-8')) - } catch (error) { - throw new Error( - 'Error while reading key file. Make sure it is valid JSON' - ) - } - } - - const middleware: MiddlewareHandler<{ - Variables: { - auth: AuthState - } - }> = Sentry.startSpan( - { - name: 'AuthMiddleware', - op: 'auth' - }, - () => - async function (ctx, next) { - const AUTH_ISSUER = env(ctx).AUTH_ISSUER - - if (!AUTH_ISSUER) { - throw new Error('AUTH_ISSUER is not set') - } - - if (!API_PRIVATE_KEY_FILE) { - // If the private key file is not loaded, try to load it from the environment - const AUTH_KEY = env(ctx).AUTH_KEY as string | undefined - - API_PRIVATE_KEY_FILE = AUTH_KEY ? JSON.parse(AUTH_KEY) : undefined - } - - if (!API_PRIVATE_KEY_FILE) { - throw new Error( - 'You have initialized the auth middleware without a private key file' - ) - } - - const AUTH_PROJECT_ID = env(ctx).AUTH_PROJECT_ID - - const ZITADEL_INTROSPECTION_URL = `${AUTH_ISSUER}/oauth/v2/introspect` - - async function getRolesFromToken(tokenString: string) { - const response = await fetch( - `${AUTH_ISSUER}/auth/v1/usergrants/me/_search`, - { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${tokenString}` - } - } - ) - - const data = (await response.json()) as any - - const userRoles = (data.result?.map((grant: any) => { - return (grant.roles || []).map((role: any) => { - return `${grant.projectId}:${role}` - }) - }) || []) as string[][] - - const projectScopedRoles = userRoles.flat() - - const rolesSet = new Set(projectScopedRoles) - - // Add unscoped roles based on project id - // This is useful so that it is not necessary to specify the project id for every role check - if (AUTH_PROJECT_ID) { - for (const role of projectScopedRoles) { - const [projectId, ...roleNameParts] = role.split(':') - - const roleName = roleNameParts.join(':') - - if (projectId === AUTH_PROJECT_ID) { - rolesSet.add(roleName) - } - } - } - - return Array.from(rolesSet) - } - - async function introspectToken( - tokenString: string - ): Promise { - if (!API_PRIVATE_KEY_FILE) { - throw new Error('Internal error: API_PRIVATE_KEY_FILE is not set') - } - - // Create JWT for client assertion - const payload = { - iss: API_PRIVATE_KEY_FILE.clientId, - sub: API_PRIVATE_KEY_FILE.clientId, - aud: AUTH_ISSUER, - exp: Math.floor(Date.now() / 1000) + 60 * 60, // Expires in 1 hour - iat: Math.floor(Date.now() / 1000) - } - - const headers = { - alg: 'RS256', - kid: API_PRIVATE_KEY_FILE.keyId - } - const jwtToken = jwt.sign(payload, API_PRIVATE_KEY_FILE.key, { - algorithm: 'RS256', - header: headers - }) - - const scopeSet = new Set() - - scopeSet.add('openid') - scopeSet.add('profile') - scopeSet.add('email') - - if (AUTH_PROJECT_ID) { - scopeSet.add( - `urn:zitadel:iam:org:project:id:${AUTH_PROJECT_ID}:aud` - ) - } - - const scope = Array.from(scopeSet).join(' ') - - // Send introspection request - const body = new URLSearchParams({ - client_assertion_type: - 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer', - client_assertion: jwtToken, - token: tokenString, - scope - }).toString() - - try { - const response = await fetch(ZITADEL_INTROSPECTION_URL, { - method: 'POST', - headers: {'Content-Type': 'application/x-www-form-urlencoded'}, - body - }) - - if (!response.ok) { - throw new Error('Network response was not ok') - } - - const tokenData = (await response.json()) as IntrospectionResponse - - const roles = await getRolesFromToken(tokenString) - - const state = { - ...tokenData, - roles - } as AuthState - - return state - } catch (error) { - console.error('Error while introspecting token', error) - throw new Error('Token introspection failed') - } - } - - let token: string | undefined = undefined - - if (ctx.req.header('Authorization')) { - const authHeader = ctx.req.header('Authorization') - - if (authHeader) { - const parts = authHeader.split(' ') - - if (parts.length === 2 && parts[0] === 'Bearer') { - token = parts[1] - } - } - } - - if (!token) { - const queryToken = ctx.req.query('token') - - if (queryToken) { - token = queryToken - } - } - - if (token) { - const auth = await introspectToken(token) - - if (auth.active) { - ctx.set('auth', auth) - - Sentry.setUser({ - id: auth.sub, - username: auth.preferred_username, - email: auth.email, - details: auth - }) - } - } - - return next() - } - ) - - sendFunctionEvent({ - name: 'authInitialize', - duration: 0 - }).then(() => {}) - - return middleware -} - -export type AuthRequireChecks = { - roles?: string[] -} - -const authRequire = (checks: AuthRequireChecks = {}) => { - sendFunctionEvent({ - name: 'authRequire', - duration: 0 - }).then(() => {}) - - const middleware: MiddlewareHandler<{ - Variables: { - auth?: AuthState - } - }> = async (ctx, next) => { - const AUTH_PROJECT_ID = env(ctx).AUTH_PROJECT_ID - - // Check if user is authenticated - const auth = ctx.get('auth') - - if (!auth) { - throw new HTTPException(401, { - message: 'Authentication required' - }) - } - - if (checks.roles) { - const roles = auth.roles - - const hasRole = checks.roles.some(role => { - return ( - roles.includes(role) || roles.includes(`${AUTH_PROJECT_ID}:${role}`) - ) - }) - - if (!hasRole) { - const resError = new Response('Forbidden', { - status: 403, - statusText: 'Forbidden', - headers: { - 'Missing-Roles': checks.roles.join(','), - 'Obtained-Roles': roles.join(',') - } - }) - - throw new HTTPException(resError.status as ContentfulStatusCode, {res: resError}) - } - } - - return next() - } - - sendFunctionEvent({ - name: 'authRequire', - duration: 0 - }).then(() => {}) - - return middleware -} - -export const auth = { - initialize: authInitialize, - require: authRequire -} - -export {requireAuth} from './decorators/requireAuth' diff --git a/packages/pylon/src/context.ts b/packages/pylon/src/context.ts index a268cd6..ce7c55a 100644 --- a/packages/pylon/src/context.ts +++ b/packages/pylon/src/context.ts @@ -1,10 +1,10 @@ import {Context as HonoContext} from 'hono' import type {Toucan} from 'toucan-js' -import {AuthState} from './auth' +import type {AuthState} from './plugins/use-auth' import {AsyncLocalStorage} from 'async_hooks' import {sendFunctionEvent} from '@getcronit/pylon-telemetry' import {env} from 'hono/adapter' -import type { GraphQLResolveInfo } from 'graphql' +import type {GraphQLResolveInfo} from 'graphql' export interface Bindings { NODE_ENV: string diff --git a/packages/pylon/src/define-pylon.ts b/packages/pylon/src/define-pylon.ts index c1a9456..f2a0f7e 100644 --- a/packages/pylon/src/define-pylon.ts +++ b/packages/pylon/src/define-pylon.ts @@ -178,23 +178,22 @@ export const resolversToGraphQLResolvers = ( return Sentry.withScope(async scope => { const ctx = asyncContext.getStore() - if (!ctx) { consola.warn( 'Context is not defined. Make sure AsyncLocalStorage is supported in your environment.' ) } - ctx?.set("graphqlResolveInfo", info) + ctx?.set('graphqlResolveInfo', info) const auth = ctx?.get('auth') - if (auth?.active) { + if (auth?.user) { scope.setUser({ - id: auth.sub, - username: auth.preferred_username, - email: auth.email, - details: auth + id: auth.user.sub, + username: auth.user.preferred_username, + email: auth.user.email, + details: auth.user }) } diff --git a/packages/pylon/src/index.ts b/packages/pylon/src/index.ts index b726a63..7697dfa 100644 --- a/packages/pylon/src/index.ts +++ b/packages/pylon/src/index.ts @@ -1,8 +1,7 @@ -import {YogaServerOptions} from 'graphql-yoga' -import {Context, Env} from './context.js' +import {Env} from './context.js' export {ServiceError} from './define-pylon.js' -export * from './auth/index.js' +export {useAuth, requireAuth, authMiddleware} from './plugins/use-auth/index.js' export { Context, Env, diff --git a/packages/pylon/src/plugins/use-auth/auth-require.ts b/packages/pylon/src/plugins/use-auth/auth-require.ts new file mode 100644 index 0000000..5b17c93 --- /dev/null +++ b/packages/pylon/src/plugins/use-auth/auth-require.ts @@ -0,0 +1,95 @@ +import {MiddlewareHandler} from 'hono' +import {env} from 'hono/adapter' +import {HTTPException} from 'hono/http-exception' +import {ContentfulStatusCode} from 'hono/utils/http-status' +import {ServiceError} from '../../define-pylon' +import {Env, getContext} from '../../context' +import {createDecorator} from '../../create-decorator' + +export type AuthRequireChecks = { + roles?: string[] +} + +export const authMiddleware = (checks: AuthRequireChecks = {}) => { + const middleware: MiddlewareHandler = async (ctx, next) => { + const AUTH_PROJECT_ID = env(ctx).AUTH_PROJECT_ID + + // Check if user is authenticated + const auth = ctx.get('auth') + + if (!auth) { + throw new HTTPException(401, { + message: 'Authentication required' + }) + } + + if (checks.roles && auth.user) { + const roles = auth.user.roles + + const hasRole = checks.roles.some(role => { + return ( + roles.includes(role) || roles.includes(`${AUTH_PROJECT_ID}:${role}`) + ) + }) + + if (!hasRole) { + const resError = new Response('Forbidden', { + status: 403, + statusText: 'Forbidden', + headers: { + 'Missing-Roles': checks.roles.join(','), + 'Obtained-Roles': roles.join(',') + } + }) + + throw new HTTPException(resError.status as ContentfulStatusCode, { + res: resError + }) + } + } + + return next() + } + + return middleware +} + +export function requireAuth(checks?: AuthRequireChecks) { + const checkAuth = async (c: any) => { + const ctx = await c + + try { + await authMiddleware(checks)(ctx, async () => {}) + } catch (e) { + if (e instanceof HTTPException) { + if (e.status === 401) { + throw new ServiceError(e.message, { + statusCode: 401, + code: 'AUTH_REQUIRED' + }) + } else if (e.status === 403) { + const res = e.getResponse() + + throw new ServiceError(res.statusText, { + statusCode: res.status, + code: 'AUTHORIZATION_REQUIRED', + details: { + missingRoles: res.headers.get('Missing-Roles')?.split(','), + obtainedRoles: res.headers.get('Obtained-Roles')?.split(',') + } + }) + } else { + throw e + } + } + + throw e + } + } + + return createDecorator(async () => { + const ctx = getContext() + + await checkAuth(ctx) + }) +} diff --git a/packages/pylon/src/plugins/use-auth/import-private-key.ts b/packages/pylon/src/plugins/use-auth/import-private-key.ts new file mode 100644 index 0000000..16045af --- /dev/null +++ b/packages/pylon/src/plugins/use-auth/import-private-key.ts @@ -0,0 +1,61 @@ +import * as crypto from 'crypto' + +/* +Convert a string into an ArrayBuffer +from https://developers.google.com/web/updates/2012/06/How-to-convert-ArrayBuffer-to-and-from-String +*/ +function str2ab(str) { + const buf = new ArrayBuffer(str.length) + const bufView = new Uint8Array(buf) + for (let i = 0, strLen = str.length; i < strLen; i++) { + bufView[i] = str.charCodeAt(i) + } + return buf +} + +const convertPKCS1ToPKCS8 = (pkcs1: string) => { + // with cryto module + + const key = crypto.createPrivateKey(pkcs1) + + return key.export({ + type: 'pkcs8', + format: 'pem' + }) +} + +/* +Import a PEM encoded RSA private key, to use for RSA-PSS signing. +Takes a string containing the PEM encoded key, and returns a Promise +that will resolve to a CryptoKey representing the private key. +*/ +function importPKCS8PrivateKey(pem) { + // fetch the part of the PEM string between header and footer + const pemHeader = '-----BEGIN PRIVATE KEY-----' + const pemFooter = '-----END PRIVATE KEY-----' + const pemContents = pem.substring( + pemHeader.length, + pem.length - pemFooter.length - 1 + ) + // base64 decode the string to get the binary data + const binaryDerString = atob(pemContents) + // convert from a binary string to an ArrayBuffer + const binaryDer = str2ab(binaryDerString) + + return crypto.subtle.importKey( + 'pkcs8', + binaryDer, + { + name: 'RSASSA-PKCS1-v1_5', + hash: 'SHA-256' + }, + true, + ['sign'] + ) +} + +export const importPrivateKey = async (pkcs1Pem: string) => { + const pkcs8Pem = convertPKCS1ToPKCS8(pkcs1Pem) + + return await importPKCS8PrivateKey(pkcs8Pem) +} diff --git a/packages/pylon/src/plugins/use-auth/index.ts b/packages/pylon/src/plugins/use-auth/index.ts new file mode 100644 index 0000000..dc49559 --- /dev/null +++ b/packages/pylon/src/plugins/use-auth/index.ts @@ -0,0 +1,3 @@ +export {useAuth} from './use-auth' +export {requireAuth, authMiddleware} from './auth-require' +export {AuthState} from './types' diff --git a/packages/pylon/src/plugins/use-auth/types.ts b/packages/pylon/src/plugins/use-auth/types.ts new file mode 100644 index 0000000..a67e391 --- /dev/null +++ b/packages/pylon/src/plugins/use-auth/types.ts @@ -0,0 +1,9 @@ +import * as openid from 'openid-client' + +export type AuthState = { + user?: openid.UserInfoResponse & { + roles: string[] + } + + openidConfig: openid.Configuration +} diff --git a/packages/pylon/src/plugins/use-auth/use-auth.ts b/packages/pylon/src/plugins/use-auth/use-auth.ts new file mode 100644 index 0000000..8ef5daa --- /dev/null +++ b/packages/pylon/src/plugins/use-auth/use-auth.ts @@ -0,0 +1,253 @@ +import {promises as fs} from 'fs' +import {deleteCookie, getCookie, setCookie} from 'hono/cookie' +import {HTTPException} from 'hono/http-exception' +import * as openid from 'openid-client' +import path from 'path' +import {getContext, type Plugin} from '../../index' +import {importPrivateKey} from './import-private-key' + +type AuthKey = { + type: 'application' + keyId: string + key: string + appId: string + clientId: string +} + +const loadAuthKey = async (keyPath: string): Promise => { + const authKeyFilePath = path.join(process.cwd(), keyPath) + + const env = getContext().env + + if (env.AUTH_KEY) { + try { + return JSON.parse(env.AUTH_KEY) + } catch (error) { + throw new Error( + 'Error while reading AUTH_KEY. Make sure it is valid JSON' + ) + } + } + + try { + const ketFileContent = await fs.readFile(authKeyFilePath, 'utf-8') + + try { + return JSON.parse(ketFileContent) + } catch (error) { + throw new Error( + 'Error while reading key file. Make sure it is valid JSON' + ) + } + } catch (error) { + throw new Error('Error while reading key file. Make sure it exists') + } +} + +let openidConfigCache: openid.Configuration | undefined + +const bootstrapAuth = async (issuer: string, keyPath: string) => { + if (!openidConfigCache) { + const authKey = await loadAuthKey(keyPath) + + openidConfigCache = await openid.discovery( + new URL(issuer), + authKey.clientId, + undefined, + openid.PrivateKeyJwt({ + key: await importPrivateKey(authKey.key), + kid: authKey.keyId + }) + ) + } + + return openidConfigCache +} + +class PylonAuthException extends HTTPException { + // Same constructor as HTTPException + constructor(...args: ConstructorParameters) { + // Prefix the message with "PylonAuthException: " + args[1] = { + ...args[1], + message: `PylonAuthException: ${args[1]?.message}` + } + + super(...args) + } +} + +export function useAuth(args: { + issuer: string + endpoint?: string + keyPath?: string +}): Plugin { + const {issuer, endpoint = '/auth', keyPath = 'key.json'} = args + + const loginPath = `${endpoint}/login` + const logoutPath = `${endpoint}/logout` + const callbackPath = `${endpoint}/callback` + + return { + middleware: async (ctx, next) => { + const openidConfig = await bootstrapAuth(issuer, keyPath) + + ctx.set('auth', {openidConfig}) + + // Introspect token + const authCookieToken = getCookie(ctx, 'pylon-auth') + const authHeader = ctx.req.header('Authorization') + const authQueryToken = ctx.req.query('token') + + if (authCookieToken || authHeader || authQueryToken) { + let token: string | undefined + + if (authHeader) { + const [type, value] = authHeader.split(' ') + if (type === 'Bearer') { + token = value + } + } else if (authQueryToken) { + token = authQueryToken + } else if (authCookieToken) { + token = authCookieToken + } + + if (!token) { + throw new PylonAuthException(401, { + message: 'Invalid token' + }) + } + + const introspection = await openid.tokenIntrospection( + openidConfig, + token, + { + scope: 'openid email profile' + } + ) + + if (!introspection.active) { + throw new PylonAuthException(401, { + message: 'Token is not active' + }) + } + + if (!introspection.sub) { + throw new PylonAuthException(401, { + message: 'Token is missing subject' + }) + } + + // Fetch user info + const userInfo = await openid.fetchUserInfo( + openidConfig, + token, + introspection.sub + ) + + const roles = Object.keys( + introspection['urn:zitadel:iam:org:projects:roles']?.valueOf() || {} + ) + + ctx.set('auth', { + user: { + ...userInfo, + roles + }, + openidConfig + }) + + return next() + } + }, + setup(app) { + app.get(loginPath, async ctx => { + const openidConfig = ctx.get('auth').openidConfig + + const codeVerifier = openid.randomPKCECodeVerifier() // PKCE code verifier + const codeChallenge = await openid.calculatePKCECodeChallenge( + codeVerifier + ) + + // Store the code verifier in a secure cookie (not accessible to JavaScript) + setCookie(ctx, 'pylon_code_verifier', codeVerifier, { + httpOnly: true, + maxAge: 300 // 5 minutes + }) + + let scope = + 'openid profile email urn:zitadel:iam:user:resourceowner urn:zitadel:iam:org:projects:roles' + + const parameters: Record = { + scope, + code_challenge: codeChallenge, + code_challenge_method: 'S256', + redirect_uri: new URL(ctx.req.url).origin + '/auth/callback', + state: openid.randomState() + } + + const authorizationUrl = openid.buildAuthorizationUrl( + openidConfig, + parameters + ) + + return ctx.redirect(authorizationUrl) + }) + + app.get(logoutPath, async ctx => { + // Remove auth cookie + deleteCookie(ctx, 'pylon-auth') + + return ctx.redirect('/') + }) + + app.get(callbackPath, async ctx => { + const openidConfig = ctx.get('auth').openidConfig + + const params = ctx.req.query() + const code = params.code + const state = params.state + + if (!code || !state) { + throw new PylonAuthException(400, { + message: 'Missing authorization code or state' + }) + } + + const codeVerifier = getCookie(ctx, 'pylon_code_verifier') + if (!codeVerifier) { + throw new PylonAuthException(400, { + message: 'Missing code verifier' + }) + } + + try { + const cbUrl = new URL(ctx.req.url) + // Exchange the authorization code for tokens + let tokenSet = await openid.authorizationCodeGrant( + openidConfig, + cbUrl, + { + pkceCodeVerifier: codeVerifier, + expectedState: state + }, + cbUrl.searchParams + ) + + // Store tokens in secure cookies + setCookie(ctx, `pylon-auth`, tokenSet.access_token, { + httpOnly: true, + maxAge: tokenSet.expires_in || 3600 // Default to 1 hour if not specified + }) + + return ctx.redirect('/') + } catch (error) { + console.error('Error during token exchange:', error) + + return ctx.text('Authentication failed!', 500) + } + }) + } + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9618d1d..ef1dfb4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -200,8 +200,8 @@ importers: specifier: ^9.0.2 version: 9.0.2 openid-client: - specifier: ^5.6.4 - version: 5.7.1 + specifier: ^6.1.7 + version: 6.2.0 toucan-js: specifier: ^4.1.0 version: 4.1.0 @@ -3575,8 +3575,8 @@ packages: resolution: {integrity: sha512-qjdpeo2yKlYTH7nFdK0vbZWuTCesk4o63v5iVOlhMQPfuIZQfW/HI35SjfhA+4qpg36rnFSvUK5b1m+ckIblQQ==} engines: {node: '>= 0.6.0'} - jose@4.15.9: - resolution: {integrity: sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==} + jose@5.10.0: + resolution: {integrity: sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==} js-git@0.7.8: resolution: {integrity: sha512-+E5ZH/HeRnoc/LW0AmAyhU+mNcWBzAKE+30+IDMLSLbbK+Tdt02AdkOKq9u15rlJsDEGFqtgckc8ZM59LhhiUA==} @@ -4023,21 +4023,16 @@ packages: nullthrows@1.1.1: resolution: {integrity: sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==} + oauth4webapi@3.2.0: + resolution: {integrity: sha512-2sYwQXuuzGKOHpnM7QL9BssDrly5gKCgJKTyrhmFIHzJRj0fFsr6GVJEdesmrX6NpMg2u63V4hJwRsZE6PUSSA==} + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} - object-hash@2.2.0: - resolution: {integrity: sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==} - engines: {node: '>= 6'} - ohash@1.1.4: resolution: {integrity: sha512-FlDryZAahJmEF3VR3w1KogSEdWX3WhA5GPakFx4J81kEAiHyLMpdLLElS8n8dfNadMgAne/MywcvmogzscVt4g==} - oidc-token-hash@5.0.3: - resolution: {integrity: sha512-IF4PcGgzAr6XXSff26Sk/+P4KZFJVuHAJZj3wgO3vX2bMdNVp/QXTP3P7CEm9V1IdG8lDLY3HhiqpsE/nOwpPw==} - engines: {node: ^10.13.0 || >=12.0.0} - once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} @@ -4052,8 +4047,8 @@ packages: resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} engines: {node: '>=12'} - openid-client@5.7.1: - resolution: {integrity: sha512-jDBPgSVfTnkIh71Hg9pRvtJc6wTwqjRkN88+gCFtYWrlP4Yx2Dsrow8uPi3qLr/aeymPF3o2+dS+wOpglK04ew==} + openid-client@6.2.0: + resolution: {integrity: sha512-pvLVkLcRWNU7YuKKTto376rgL//+rn3ca0XRqsrQVN30lVlpXBPHhSLcGoM/hPbux5p+Ha4tdoz96eEYpyguOQ==} os-tmpdir@1.0.2: resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} @@ -8673,7 +8668,7 @@ snapshots: java-properties@1.0.2: {} - jose@4.15.9: {} + jose@5.10.0: {} js-git@0.7.8: dependencies: @@ -9047,14 +9042,12 @@ snapshots: nullthrows@1.1.1: {} - object-assign@4.1.1: {} + oauth4webapi@3.2.0: {} - object-hash@2.2.0: {} + object-assign@4.1.1: {} ohash@1.1.4: {} - oidc-token-hash@5.0.3: {} - once@1.4.0: dependencies: wrappy: 1.0.2 @@ -9071,12 +9064,10 @@ snapshots: dependencies: mimic-fn: 4.0.0 - openid-client@5.7.1: + openid-client@6.2.0: dependencies: - jose: 4.15.9 - lru-cache: 6.0.0 - object-hash: 2.2.0 - oidc-token-hash: 5.0.3 + jose: 5.10.0 + oauth4webapi: 3.2.0 os-tmpdir@1.0.2: {} From a305c717389a8a51ee1a1a35dfdddb9f60e0ad57 Mon Sep 17 00:00:00 2001 From: Nico Schett Date: Tue, 18 Feb 2025 23:29:41 +0100 Subject: [PATCH 005/138] add usePages plugin for file-based routing support in Fullstack React --- .changeset/rotten-ravens-sin.md | 26 + packages/pylon/package.json | 44 +- packages/pylon/src/app/index.ts | 6 +- packages/pylon/src/get-env.ts | 6 +- packages/pylon/src/index.ts | 2 + packages/pylon/src/pages/index.ts | 2 + .../src/plugins/use-pages/build/app-utils.ts | 145 +++ .../src/plugins/use-pages/build/index.ts | 183 ++++ .../use-pages/build/plugins/image-plugin.ts | 85 ++ .../build/plugins/inject-app-hydration.ts | 71 ++ .../use-pages/build/plugins/postcss-plugin.ts | 28 + packages/pylon/src/plugins/use-pages/index.ts | 12 + .../plugins/use-pages/setup/app-loader.tsx | 33 + .../src/plugins/use-pages/setup/index.tsx | 397 +++++++ pnpm-lock.yaml | 970 +++++++++++++++--- 15 files changed, 1862 insertions(+), 148 deletions(-) create mode 100644 .changeset/rotten-ravens-sin.md create mode 100644 packages/pylon/src/pages/index.ts create mode 100644 packages/pylon/src/plugins/use-pages/build/app-utils.ts create mode 100644 packages/pylon/src/plugins/use-pages/build/index.ts create mode 100644 packages/pylon/src/plugins/use-pages/build/plugins/image-plugin.ts create mode 100644 packages/pylon/src/plugins/use-pages/build/plugins/inject-app-hydration.ts create mode 100644 packages/pylon/src/plugins/use-pages/build/plugins/postcss-plugin.ts create mode 100644 packages/pylon/src/plugins/use-pages/index.ts create mode 100644 packages/pylon/src/plugins/use-pages/setup/app-loader.tsx create mode 100644 packages/pylon/src/plugins/use-pages/setup/index.tsx diff --git a/.changeset/rotten-ravens-sin.md b/.changeset/rotten-ravens-sin.md new file mode 100644 index 0000000..4f5cec5 --- /dev/null +++ b/.changeset/rotten-ravens-sin.md @@ -0,0 +1,26 @@ +--- +'@getcronit/pylon': minor +--- + +Add `usePages` plugin to support file-based (Fullstack React) routing. https://github.com/getcronit/pylon/issues/69 + +```ts +import {app, usePages, PylonConfig} from '@getcronit/pylon' + +export const graphql = { + Query: { + hello: () => { + return 'Hello, world!' + }, + post: (slug: string) => { + return {title: `Post: ${slug}`, content: 'This is a blog post.'} + } + } +} + +export const config: PylonConfig = { + plugins: [usePages()] // Enables the Pages Router +} + +export default app +``` diff --git a/packages/pylon/package.json b/packages/pylon/package.json index 3d54ce2..0e8701b 100644 --- a/packages/pylon/package.json +++ b/packages/pylon/package.json @@ -4,8 +4,21 @@ "type": "module", "main": "dist/index.js", "types": "dist/index.d.ts", + "exports": { + ".": { + "import": "./dist/index.js", + "types": "./dist/index.d.ts" + }, + "./pages": { + "import": "./dist/pages/index.js", + "types": "./dist/pages/index.d.ts" + }, + "./tsconfig.pylon.json": "./tsconfig.pylon.json" + }, "scripts": { - "build": "rimraf ./dist && esbuild ./src/index.ts --bundle --platform=node --target=node18 --format=esm --outdir=./dist --sourcemap=linked --packages=external && pnpm run build:declarations", + "build": "rimraf ./dist && pnpm run build:server && pnpm run build:pages", + "build:server": "esbuild ./src/index.ts --bundle --platform=node --target=node18 --format=esm --outdir=./dist --sourcemap=linked --packages=external && pnpm run build:declarations", + "build:pages": "esbuild ./src/pages/index.ts --bundle --packages=external --platform=browser --target=esnext --format=esm --outdir=./dist/pages --sourcemap=linked", "build:declarations": "tsc --declaration --emitDeclarationOnly --outDir ./dist" }, "files": [ @@ -23,23 +36,50 @@ "dependencies": { "@envelop/core": "^5.0.3", "@getcronit/pylon-telemetry": "workspace:^", + "@gqty/react": "^3.1.0", "@hono/sentry": "^1.2.0", "@sentry/bun": "^8.17.0", "@sentry/node": "^8.54.0", "consola": "^3.2.3", + "gqty": "3.4.0-canary-20250207102900.644ad9fdeafa6318516627577b1d4d754d5c5a98", "graphql": "^16.9.0", "graphql-scalars": "^1.24.0", "graphql-yoga": "^5.6.2", "hono": "^4.0.8", "jsonwebtoken": "^9.0.2", "openid-client": "^6.1.7", + "react-router": "^7.1.5", + "sharp": "^0.33.5", + "tiny-glob": "^0.2.9", "toucan-js": "^4.1.0", - "winston": "^3.8.2" + "postcss-load-config": "^6.0.1", + "chokidar": "^4.0.3" }, "engines": { "node": ">=18.0.0" }, "devDependencies": { "@sentry/types": "^8.54.0" + }, + "peerDependencies": { + "@tailwindcss/postcss": "^4.0.4", + "autoprefixer": "^10.4.20", + "postcss": "^8.5.1", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "@tailwindcss/postcss": { + "optional": true + }, + "autoprefixer": { + "optional": true + } } } diff --git a/packages/pylon/src/app/index.ts b/packages/pylon/src/app/index.ts index af0449a..d507eca 100644 --- a/packages/pylon/src/app/index.ts +++ b/packages/pylon/src/app/index.ts @@ -1,5 +1,7 @@ -import {sentry} from '@hono/sentry' import {Hono, MiddlewareHandler} from 'hono' +import {logger} from 'hono/logger' +import {sentry} from '@hono/sentry' +import {except} from 'hono/combine' import {asyncContext, Env} from '../context' @@ -19,6 +21,8 @@ app.use('*', async (c, next) => { }) }) +app.use('*', except(['/__pylon/static/*'], logger())) + app.use((c, next) => { // @ts-ignore c.req.id = crypto.randomUUID() diff --git a/packages/pylon/src/get-env.ts b/packages/pylon/src/get-env.ts index c03b9df..15dcbe7 100644 --- a/packages/pylon/src/get-env.ts +++ b/packages/pylon/src/get-env.ts @@ -11,7 +11,11 @@ export function getEnv() { // Fall back to process.env or an empty object if no context is available // This is useful for testing // ref: https://hono.dev/docs/guides/testing#env - return context.env || process.env || {} + const ctx = context.env || process.env || {} + + ctx.NODE_ENV = ctx.NODE_ENV || process.env.NODE_ENV || 'development' + + return ctx } catch { return process.env } finally { diff --git a/packages/pylon/src/index.ts b/packages/pylon/src/index.ts index 7697dfa..34c02eb 100644 --- a/packages/pylon/src/index.ts +++ b/packages/pylon/src/index.ts @@ -18,6 +18,8 @@ export {getEnv} from './get-env.js' export {createDecorator} from './create-decorator.js' export {createPubSub as experimentalCreatePubSub} from 'graphql-yoga' +export {usePages} from './plugins/use-pages/index' + import type {Plugin as YogaPlugin} from 'graphql-yoga' import {MiddlewareHandler} from 'hono' import {BuildContext, BuildOptions} from 'esbuild' diff --git a/packages/pylon/src/pages/index.ts b/packages/pylon/src/pages/index.ts new file mode 100644 index 0000000..39bd119 --- /dev/null +++ b/packages/pylon/src/pages/index.ts @@ -0,0 +1,2 @@ +export * as __PYLON_ROUTER_INTERNALS_DO_NOT_USE from 'react-router' +export {type PageProps, type PageData} from '../plugins/use-pages/index' diff --git a/packages/pylon/src/plugins/use-pages/build/app-utils.ts b/packages/pylon/src/plugins/use-pages/build/app-utils.ts new file mode 100644 index 0000000..28ef10b --- /dev/null +++ b/packages/pylon/src/plugins/use-pages/build/app-utils.ts @@ -0,0 +1,145 @@ +import path from 'path' +import glob from 'tiny-glob' + +function fnv1aHash(str: string) { + let hash = 0x811c9dc5 // FNV offset basis + for (let i = 0; i < str.length; i++) { + hash ^= str.charCodeAt(i) + hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24) + } + return (hash >>> 0).toString(16) +} + +const APP_DIR = path.join(process.cwd(), 'pages') + +export type PageRoute = { + pagePath: string + slug: string + layouts: string[] +} + +// Helper function to get page routes with layouts +export async function getPageRoutes(dir = APP_DIR) { + const routes: PageRoute[] = [] + + // Glob pattern for `page.tsx` and `page.js` files + const pagePattern = path.join(dir, '**/page.{ts,tsx,js}') // Matches page.tsx, page.js, page.ts + + // Glob pattern for layout files + const layoutPattern = path.join(dir, '**/layout.tsx') // Matches layout.tsx files + + // Get all page files + const pageFiles = await glob(pagePattern) + + // Get all layout files + const layoutFiles = await glob(layoutPattern) + + for (const pagePath of pageFiles) { + const relativePagePath = path.relative(APP_DIR, pagePath) // Get the relative path from the app folder + let slug = + '/' + + relativePagePath + .replace(/page\.(ts|tsx|js)$/, '') + .replace(/\[([\w-]+)\]/g, ':$1') + + // Make sure there is no trailing slash + slug = slug.replace(/\/$/, '') + + // Find layouts relevant to this page + const layouts = layoutFiles.filter(layout => { + return pagePath.startsWith(layout.replace('layout.tsx', '')) + }) + + const layoutsWithoutRootLayout = layouts.slice(1) + + routes.push({ + pagePath: pagePath, + slug: slug || '/', + layouts: layoutsWithoutRootLayout + }) + } + + return routes +} + +export const generateAppFile = (pageRoutes: PageRoute[]): string => { + const makePageMap = (routes: PageRoute[]) => { + const pageMap: Record = {} + for (const route of routes) { + pageMap[route.pagePath] = `Page${fnv1aHash(route.pagePath)}` + } + return pageMap + } + + const makeLayoutMap = (routes: PageRoute[]) => { + const layoutMap: Record = {} + for (const route of routes) { + for (const layout of route.layouts) { + layoutMap[layout] = `Layout${fnv1aHash(layout)}` + } + } + return layoutMap + } + + const pageMap = makePageMap(pageRoutes) + const layoutMap = makeLayoutMap(pageRoutes) + + const importPages = Object.keys(pageMap) + .map((pagePath, index) => { + const importLocation = `../${pagePath}`.replace('.tsx', '.js') + const componentName = pageMap[pagePath] + + return `const ${componentName} = lazy(() => import('${importLocation}')) + ` + }) + .join('\n') + + const importLayouts = Object.keys(layoutMap) + .map((layoutPath, index) => { + const importLocation = `../${layoutPath}`.replace('.tsx', '.js') + const componentName = layoutMap[layoutPath] + + return `const ${componentName} = lazy(() => import('${importLocation}')) + ` + }) + .join('\n') + + // Dynamically build the App component with React Router Routes + const appComponent = `"use client"; + import {lazy, Suspense} from 'react' + import { __PYLON_ROUTER_INTERNALS_DO_NOT_USE } from '@getcronit/pylon/pages'; + const {Routes, Route} = __PYLON_ROUTER_INTERNALS_DO_NOT_USE + ${importPages} + const RootLayout = lazy(() => import('../pages/layout.js')) + ${importLayouts} + + const App: React.FC<{pageProps: any}> = ({pageProps}) => ( + + + + + + ${pageRoutes + .map((route, index) => { + return `...}> + ${route.layouts.reduceRight((child, layoutPath, layoutIndex) => { + const layoutName = layoutMap[layoutPath] + + return `<${layoutName}>${child}` + }, `<${pageMap[route.pagePath]} {...pageProps} />`)} + + } />` + }) + .join('\n')} + + + ); + + export default App; + ` + + return appComponent +} diff --git a/packages/pylon/src/plugins/use-pages/build/index.ts b/packages/pylon/src/plugins/use-pages/build/index.ts new file mode 100644 index 0000000..559a80c --- /dev/null +++ b/packages/pylon/src/plugins/use-pages/build/index.ts @@ -0,0 +1,183 @@ +import path from 'path' +import {Plugin} from '../../..' +import {generateAppFile, getPageRoutes} from './app-utils' +import chokidar, {FSWatcher} from 'chokidar' +import fs from 'fs/promises' +import esbuild from 'esbuild' +import {injectAppHydrationPlugin} from './plugins/inject-app-hydration' +import {imagePlugin} from './plugins/image-plugin' +import {postcssPlugin} from './plugins/postcss-plugin' + +const DIST_STATIC_DIR = path.join(process.cwd(), '.pylon/__pylon/static') +const DIST_PAGES_DIR = path.join(process.cwd(), '.pylon/__pylon/pages') + +async function updateFileIfChanged(path: string, newContent: string) { + try { + const currentContent = await fs.readFile(path, 'utf8') + if (currentContent === newContent) { + return false // No update needed + } + } catch (err: any) { + if (err.code !== 'ENOENT') throw err // Ignore file not found error + } + + await fs.writeFile(path, newContent, 'utf8') + return true // File created or updated +} + +export const build: Plugin['build'] = async () => { + const buildAppFile = async () => { + const pagesRoutes = await getPageRoutes() + const appContent = generateAppFile(pagesRoutes) + + const pagesFile = path.resolve(process.cwd(), '.pylon', 'pages.json') + await updateFileIfChanged(pagesFile, JSON.stringify(pagesRoutes, null, 2)) + + // Write if the file doesn't exist or the content is different + const appFilePath = path.resolve(process.cwd(), '.pylon', 'app.tsx') + + const state = await updateFileIfChanged(appFilePath, appContent) + + if (state) { + } + } + + const copyPublicDir = async () => { + // Copy the ./public directory content to the .pylon/__pylon/static directory + const publicDir = path.resolve(process.cwd(), 'public') + const pylonPublicDir = path.resolve( + process.cwd(), + '.pylon', + '__pylon', + 'public' + ) + + try { + await fs.access(publicDir) + + // Copy recursively the public directory to the static directory + await fs.mkdir(pylonPublicDir, {recursive: true}) + await fs.cp(publicDir, pylonPublicDir, {recursive: true}) + } catch (err: any) { + if (err.code !== 'ENOENT') throw err // Ignore file not found error + } + } + + const writeOnEndPlugin: esbuild.Plugin = { + name: 'write-on-end', + setup(build) { + build.onEnd(async result => { + await Promise.all( + result.outputFiles!.map(async file => { + await fs.mkdir(path.dirname(file.path), {recursive: true}) + await updateFileIfChanged(file.path, file.text) + }) + ) + }) + } + } + + const nodePaths = [ + path.join(process.cwd(), 'node_modules'), + path.join(process.cwd(), 'node_modules', '@getcronit/pylon/node_modules') + ] + + let pagesWatcher: FSWatcher | null = null + + const clientCtx = await esbuild.context({ + write: false, + metafile: true, + nodePaths, + absWorkingDir: process.cwd(), + plugins: [ + injectAppHydrationPlugin, + imagePlugin, + postcssPlugin, + writeOnEndPlugin + ], + publicPath: '/__pylon/static', + assetNames: 'assets/[name]-[hash]', + chunkNames: 'chunks/[name]-[hash]', + format: 'esm', + platform: 'browser', + entryPoints: ['.pylon/app.tsx'], + outdir: DIST_STATIC_DIR, + bundle: true, + splitting: true, + minify: true, + loader: { + // Map file extensions to the file loader + + '.svg': 'file', + '.woff': 'file', + '.woff2': 'file' + }, + define: { + 'process.env.NODE_ENV': '"production"' + }, + mainFields: ['browser', 'module', 'main'] + }) + + const serverCtx = await esbuild.context({ + write: false, + absWorkingDir: process.cwd(), + nodePaths, + plugins: [imagePlugin, postcssPlugin, writeOnEndPlugin], + publicPath: '/__pylon/static', + assetNames: 'assets/[name]-[hash]', + chunkNames: 'chunks/[name]-[hash]', + format: 'esm', + platform: 'node', + entryPoints: ['.pylon/app.tsx'], + packages: 'external', + outdir: DIST_PAGES_DIR, + bundle: true, + splitting: false, + minify: true, + loader: { + // Map file extensions to the file loader + + '.svg': 'file', + '.woff': 'file', + '.woff2': 'file' + } + }) + + return { + watch: async () => { + pagesWatcher = chokidar.watch('pages', {ignoreInitial: true}) + + pagesWatcher!.on('all', async (event, path) => { + if (['add', 'change', 'unlink'].includes(event)) { + await buildAppFile() + await copyPublicDir() + } + }) + + await Promise.all([clientCtx.watch(), serverCtx.watch()]) + }, + dispose: async () => { + if (pagesWatcher) { + pagesWatcher.close() + } + + Promise.all([clientCtx.dispose(), serverCtx.dispose()]) + }, + rebuild: async () => { + await buildAppFile() + + await copyPublicDir() + + await Promise.all([clientCtx.rebuild(), serverCtx.rebuild()]) + + return {} as any + }, + cancel: async () => { + if (pagesWatcher) { + await pagesWatcher.close() + } + + await Promise.all([clientCtx.cancel(), serverCtx.cancel()]) + } + } +} diff --git a/packages/pylon/src/plugins/use-pages/build/plugins/image-plugin.ts b/packages/pylon/src/plugins/use-pages/build/plugins/image-plugin.ts new file mode 100644 index 0000000..3c02485 --- /dev/null +++ b/packages/pylon/src/plugins/use-pages/build/plugins/image-plugin.ts @@ -0,0 +1,85 @@ +import {createHash} from 'crypto' +import {Plugin} from 'esbuild' +import path from 'path' +import sharp from 'sharp' +import fs from 'fs/promises' + +export const imagePlugin: Plugin = { + name: 'image-plugin', + setup(build) { + const outdir = build.initialOptions.outdir + const publicPath = build.initialOptions.publicPath + + if (!outdir || !publicPath) { + throw new Error('outdir and publicPath must be set in esbuild options') + } + + build.onResolve({filter: /\.(png|jpe?g)$/}, async args => { + const filePath = path.resolve(args.resolveDir, args.path) + + const fileName = path.basename(filePath) + const extname = path.extname(filePath) + const hash = createHash('md5') + .update(filePath + (await fs.readFile(filePath))) + .digest('hex') + .slice(0, 8) + const newFilename = `${fileName}-${hash}${extname}` + const newFilePath = path.join(outdir, 'media', newFilename) + + // Ensure the directory exists + await fs.mkdir(path.dirname(newFilePath), {recursive: true}) + + // Copy the file + await fs.copyFile(filePath, newFilePath) + + return { + path: newFilePath, + namespace: 'image' + } + }) + + build.onLoad({filter: /\.png$|\.jpg$/}, async args => { + // Load file and read the dimensions + const image = sharp(args.path) + const metadata = await image.metadata() + + // Build the URL with the publicPath and w/h search params + const url = `${publicPath}/media/${path.basename(args.path)}` + + const searchParams = new URLSearchParams({}) + + if (metadata.width) { + searchParams.set('w', metadata.width.toString()) + } + if (metadata.height) { + searchParams.set('h', metadata.height.toString()) + } + + const output = image + .resize({ + width: Math.min(metadata.width ?? 16, 16), + height: Math.min(metadata.height ?? 16, 16), + fit: 'inside' + }) + .toFormat('webp', { + quality: 20, + alphaQuality: 20, + smartSubsample: true + }) + + const {data, info} = await output.toBuffer({resolveWithObject: true}) + const dataURIBase64 = `data:image/${info.format};base64,${data.toString( + 'base64' + )}` + + if (dataURIBase64) { + searchParams.set('blurDataURL', dataURIBase64) + } + + return { + contents: `${url}?${searchParams.toString()}`, + loader: 'text' + } + }) + } +} diff --git a/packages/pylon/src/plugins/use-pages/build/plugins/inject-app-hydration.ts b/packages/pylon/src/plugins/use-pages/build/plugins/inject-app-hydration.ts new file mode 100644 index 0000000..49f6be6 --- /dev/null +++ b/packages/pylon/src/plugins/use-pages/build/plugins/inject-app-hydration.ts @@ -0,0 +1,71 @@ +import {Plugin} from 'esbuild' +import path from 'path' +import fs from 'fs/promises' + +export const injectAppHydrationPlugin: Plugin = { + name: 'inject-hydration', + setup(build) { + build.onLoad({filter: /.*/, namespace: 'file'}, async args => { + // check if the file is the app.tsx file + if (args.path === path.resolve(process.cwd(), '.pylon', 'app.tsx')) { + let contents = await fs.readFile(args.path, 'utf-8') + + const clientPath = path.resolve(process.cwd(), '.pylon/client') + + const pathToClient = path.relative(path.dirname(args.path), clientPath) + + contents += ` + import {hydrateRoot} from 'react-dom/client' + import * as client from './${pathToClient}' + import { __PYLON_ROUTER_INTERNALS_DO_NOT_USE } from '@getcronit/pylon/pages'; + const {BrowserRouter} = __PYLON_ROUTER_INTERNALS_DO_NOT_USE + import React, {useMemo} from 'react' + + const pylonData = window.__PYLON_DATA__ + + const AppLoader = (props: { + client: any + pylonData: { + pageProps: Omit + cacheSnapshot?: any + } + App: React.FC<{ + pageProps: PageProps + }> + Router: React.FC + routerProps: any + }) => { + props.client.useHydrateCache({cacheSnapshot: props.pylonData.cacheSnapshot}) + + const data = props.client.useQuery() + const page = useMemo(() => { + const page = ( + + ) + + return page + }, [props]) + + return {page} + } + + + hydrateRoot( + document, + + ) + ` + + return { + loader: 'tsx', + contents + } + } + }) + } +} diff --git a/packages/pylon/src/plugins/use-pages/build/plugins/postcss-plugin.ts b/packages/pylon/src/plugins/use-pages/build/plugins/postcss-plugin.ts new file mode 100644 index 0000000..3653e42 --- /dev/null +++ b/packages/pylon/src/plugins/use-pages/build/plugins/postcss-plugin.ts @@ -0,0 +1,28 @@ +import {Plugin} from 'esbuild' +import path from 'path' +import fs from 'fs/promises' +import loadConfig from 'postcss-load-config' +import postcss from 'postcss' + +export const postcssPlugin: Plugin = { + name: 'postcss-plugin', + setup(build) { + build.onLoad({filter: /.css$/, namespace: 'file'}, async args => { + const {plugins, options} = await loadConfig() + + const css = await fs.readFile(args.path, 'utf-8') + + const result = await postcss(plugins) + .process(css, { + ...options, + from: args.path + }) + .then(result => result) + + return { + contents: result.css, + loader: 'css' + } + }) + } +} diff --git a/packages/pylon/src/plugins/use-pages/index.ts b/packages/pylon/src/plugins/use-pages/index.ts new file mode 100644 index 0000000..91280a5 --- /dev/null +++ b/packages/pylon/src/plugins/use-pages/index.ts @@ -0,0 +1,12 @@ +import {Plugin} from '../..' +import {setup, PageData, PageProps} from './setup' +import {build} from './build' + +export {PageData, PageProps} + +export function usePages(): Plugin { + return { + setup, + build + } +} diff --git a/packages/pylon/src/plugins/use-pages/setup/app-loader.tsx b/packages/pylon/src/plugins/use-pages/setup/app-loader.tsx new file mode 100644 index 0000000..6964888 --- /dev/null +++ b/packages/pylon/src/plugins/use-pages/setup/app-loader.tsx @@ -0,0 +1,33 @@ +import React, {useMemo} from 'react' +import {PageProps} from '..' + +export const AppLoader = (props: { + client: any + pylonData: { + pageProps: Omit + cacheSnapshot?: any + } + App: React.FC<{ + pageProps: PageProps + }> + Router: React.FC + routerProps: any +}) => { + props.client.useHydrateCache({cacheSnapshot: props.pylonData.cacheSnapshot}) + + const data = props.client.useQuery() + const page = useMemo(() => { + const page = ( + + ) + + return page + }, [props]) + + return {page} +} diff --git a/packages/pylon/src/plugins/use-pages/setup/index.tsx b/packages/pylon/src/plugins/use-pages/setup/index.tsx new file mode 100644 index 0000000..380cb8f --- /dev/null +++ b/packages/pylon/src/plugins/use-pages/setup/index.tsx @@ -0,0 +1,397 @@ +import fs from 'fs' +import path from 'path' +import reactServer from 'react-dom/server' + +import { UseHydrateCacheOptions } from '@gqty/react' +import { Readable } from 'stream' +import { AppLoader } from './app-loader' +import { getEnv, type Plugin } from '../../../index' +import { cloneElement, createElement } from 'react' +import { trimTrailingSlash } from 'hono/trailing-slash' +import { StaticRouter } from 'react-router' + +export interface PageData { } + +export type PageProps = { + data: PageData + params: Record + searchParams: Record + path: string +} + +const disableCacheMiddleware: MiddlewareHandler = async (c, next) => { + const env = getEnv() + if (env.NODE_ENV === 'development') { + c.header( + 'Cache-Control', + 'no-store, no-cache, must-revalidate, proxy-revalidate' + ) + c.header('Pragma', 'no-cache') + c.header('Expires', '0') + c.header('Surrogate-Control', 'no-store') + } + + return next() +} + +export const setup: Plugin['setup'] = app => { + const pagesFilePath = path.resolve(process.cwd(), '.pylon', 'pages.json') + + let pageRoutes: PageRoute[] = [] + try { + pageRoutes = JSON.parse(fs.readFileSync(pagesFilePath, 'utf-8')) + } catch (error) { + console.error('Error reading pages.json', error) + } + + app.use(trimTrailingSlash()) + + let App: any = undefined + let client: any = undefined + + app.on( + 'GET', + pageRoutes.map(pageRoute => pageRoute.slug), + disableCacheMiddleware, + async c => { + if (!App) { + const module = await import( + `${process.cwd()}/.pylon/__pylon/pages/app.js` + ) + + App = module.default + } + + if (!client) { + client = await import(`${process.cwd()}/.pylon/client`) + } + + const pageProps = { + params: c.req.param(), + searchParams: c.req.query(), + path: c.req.path + } + + let cacheSnapshot: UseHydrateCacheOptions | undefined = undefined + + const prepared = await client.prepareReactRender( + + ) + + cacheSnapshot = prepared.cacheSnapshot + + const stream = await reactServer.renderToReadableStream( + , + { + bootstrapModules: ['/__pylon/static/app.js'], + bootstrapScriptContent: `window.__PYLON_DATA__ = ${JSON.stringify({ + pageProps: pageProps, + cacheSnapshot: cacheSnapshot + })}` + } + ) + + return c.body(stream) + } + ) + + const publicFilesPath = path.resolve(process.cwd(), '.pylon', '__pylon', 'public') + let publicFiles: string[] = [] + + try { + publicFiles = fs.readdirSync(publicFilesPath) + } catch (error) { + console.error('Error reading public files', error) + } + + + app.on('GET', + publicFiles.map(file => `/${file}`), disableCacheMiddleware, async c => { + const publicFilePath = path.resolve( + process.cwd(), + '.pylon', + '__pylon', + 'public', + c.req.path.replace('/', '') + ) + + + try { + await fs.promises.access(publicFilePath) + + if (publicFilePath.endsWith('.js')) { + c.res.headers.set('Content-Type', 'text/javascript') + } else if (publicFilePath.endsWith('.css')) { + c.res.headers.set('Content-Type', 'text/css') + } else if (publicFilePath.endsWith('.html')) { + c.res.headers.set('Content-Type', 'text/html') + } else if (publicFilePath.endsWith('.json')) { + c.res.headers.set('Content-Type', 'application/json') + } else if (publicFilePath.endsWith('.png')) { + c.res.headers.set('Content-Type', 'image/png') + } else if (publicFilePath.endsWith('.jpg') || publicFilePath.endsWith('.jpeg')) { + c.res.headers.set('Content-Type', 'image/jpeg') + } else if (publicFilePath.endsWith('.gif')) { + c.res.headers.set('Content-Type', 'image/gif') + } else if (publicFilePath.endsWith('.svg')) { + c.res.headers.set('Content-Type', 'image/svg+xml') + } else if (publicFilePath.endsWith('.ico')) { + c.res.headers.set('Content-Type', 'image/x-icon') + } + + const stream = fs.createReadStream(publicFilePath) + + const a = Readable.toWeb(stream) as ReadableStream + + return c.body(a) + + } catch { + return c.status(404) + } + }) + + + app.get('/__pylon/static/*', disableCacheMiddleware, async c => { + const filePath = path.resolve( + process.cwd(), + '.pylon', + '__pylon', + 'static', + c.req.path.replace('/__pylon/static/', '') + ) + + if (!fs.existsSync(filePath)) { + return c.status(404) + } + + if (filePath.endsWith('.js')) { + c.res.headers.set('Content-Type', 'text/javascript') + } else if (filePath.endsWith('.css')) { + c.res.headers.set('Content-Type', 'text/css') + } else if (filePath.endsWith('.html')) { + c.res.headers.set('Content-Type', 'text/html') + } else if (filePath.endsWith('.json')) { + c.res.headers.set('Content-Type', 'application/json') + } else if (filePath.endsWith('.png')) { + c.res.headers.set('Content-Type', 'image/png') + } else if (filePath.endsWith('.jpg') || filePath.endsWith('.jpeg')) { + c.res.headers.set('Content-Type', 'image/jpeg') + } else if (filePath.endsWith('.gif')) { + c.res.headers.set('Content-Type', 'image/gif') + } else if (filePath.endsWith('.svg')) { + c.res.headers.set('Content-Type', 'image/svg+xml') + } else if (filePath.endsWith('.ico')) { + c.res.headers.set('Content-Type', 'image/x-icon') + } + + const stream = fs.createReadStream(filePath) + + const a = Readable.toWeb(stream) as ReadableStream + + return c.body(a) + }) + + // Image optimization route + app.get('/__pylon/image', async c => { + try { + const { src, w, h, q = '75', format = 'webp' } = c.req.query() + + const queryStringHash = createHash('sha256') + .update(JSON.stringify(c.req.query())) + .digest('hex') + + if (!src) { + return c.json({ error: 'Missing parameters.' }, 400) + } + + let imagePath = path.join(process.cwd(), '.pylon', src) + + if (src.startsWith('http://') || src.startsWith('https://')) { + imagePath = await downloadImage(src) + } + + // Check if the image exists asynchronously + try { + await fs.promises.access(imagePath) + } catch { + return c.json({ error: 'Image not found' }, 404) + } + + // Get image metadata (width and height) to calculate aspect ratio + const metadata = await sharp(imagePath).metadata() + + // Validate if the metadata contains width and height + if (!metadata.width || !metadata.height) { + return c.json( + { + error: + 'Invalid image metadata. Width and height are required for resizing.' + }, + 400 + ) + } + + // Calculate missing dimension + const { width: finalWidth, height: finalHeight } = calculateDimensions( + metadata.width, + metadata.height, + w ? parseInt(w) : undefined, + h ? parseInt(h) : undefined + ) + + // Check cache first + const cachePath = path.join(IMAGE_CACHE_DIR, queryStringHash) + + let imageFormat = format.toLowerCase() + + if (!isSupportedFormat(imageFormat)) { + throw new Error('Unsupported image format') + } + + // Serve cached image if it exists + // try { + // await fs.promises.access(cachePath); + + // const stream = fs.createReadStream(cachePath) + + // c.res.headers.set('Content-Type', getContentType(imageFormat)); + + // return c.body(Readable.toWeb(stream) as ReadableStream) + // } catch { + // // Proceed to optimize and cache the image if it doesn't exist + // } + + const quality = parseInt(q) + + + // Optimize the image using sharp + const image = await sharp(imagePath) + .resize(finalWidth, finalHeight) + .toFormat(imageFormat, { + quality + }) + .toFile(cachePath) + + + c.res.headers.set('Content-Type', getContentType(image.format)) + + // Serve the optimized image + return c.body( + Readable.toWeb(fs.createReadStream(cachePath)) as ReadableStream + ) + } catch (error) { + console.error('Error processing the image:', error) + return c.json({ error: 'Error processing the image' }, 500) + } + }) +} + +import sharp, { FormatEnum } from 'sharp' +import { createHash } from 'crypto' + +// Cache directory +const IMAGE_CACHE_DIR = path.join(process.cwd(), '.cache/__pylon/images') + +// Ensure the cache directory exists +fs.promises.mkdir(IMAGE_CACHE_DIR, { recursive: true }) + +// Helper function to generate the cached image path +const getCachedImagePath = ( + src: string, + width: number, + height: number, + format: keyof FormatEnum +) => { + const fileName = `${path.basename( + src, + path.extname(src) + )}-${width}x${height}.${format}` + return path.join(IMAGE_CACHE_DIR, fileName) +} + +// Utility function to calculate missing dimension based on aspect ratio +const calculateDimensions = ( + originalWidth: number, + originalHeight: number, + width?: number, + height?: number +) => { + if (!width && !height) { + return { width: originalWidth, height: originalHeight } + } + if (width && !height) { + // Calculate height based on the aspect ratio + height = Math.round((width * originalHeight) / originalWidth) + } else if (height && !width) { + // Calculate width based on the aspect ratio + width = Math.round((height * originalWidth) / originalHeight) + } + return { width, height } +} + +function isSupportedFormat(format: string): format is keyof FormatEnum { + const supportedFormats = sharp.format + return Object.keys(supportedFormats).includes(format) +} + +// Helper function to get the correct Content-Type based on the format +const getContentType = (format: string) => { + switch (format.toLowerCase()) { + case 'webp': + return 'image/webp' + case 'jpeg': + case 'jpg': + return 'image/jpeg' + case 'png': + return 'image/png' + case 'gif': + return 'image/gif' + case 'svg': + return 'image/svg+xml' + default: + return 'application/octet-stream' // Fallback type if format is unknown + } +} + +import { tmpdir } from 'os' +import { promisify } from 'util' +import { pipeline } from 'stream/promises' +import { PageRoute } from '../build/app-utils' +import { MiddlewareHandler } from 'hono' + +const downloadImage = async (url: string): Promise => { + const response = await fetch(url) + if (!response.ok) + throw new Error(`Failed to download image: ${response.statusText}`) + + const ext = path.extname(new URL(url).pathname) || '.jpg' + const tempFilePath = path.join(tmpdir(), `image-${Date.now()}${ext}`) + + const fileStream = fs.createWriteStream(tempFilePath) + + await pipeline(response.body!, fileStream) + + return tempFilePath +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ef1dfb4..3a149b0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -65,11 +65,11 @@ importers: version: link:../../packages/pylon drizzle-orm: specifier: ^0.33.0 - version: 0.33.0(@cloudflare/workers-types@4.20250129.0)(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(bun-types@1.2.2) + version: 0.33.0(@cloudflare/workers-types@4.20250129.0)(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(bun-types@1.2.2)(react@19.0.0) devDependencies: '@cloudflare/vitest-pool-workers': specifier: ^0.4.5 - version: 0.4.31(@cloudflare/workers-types@4.20250129.0)(@vitest/runner@1.5.3)(@vitest/snapshot@1.5.3)(vitest@1.5.3(@types/node@22.13.1)) + version: 0.4.31(@cloudflare/workers-types@4.20250129.0)(@vitest/runner@1.5.3)(@vitest/snapshot@1.5.3)(vitest@1.5.3(@types/node@22.13.1)(lightningcss@1.29.1)) '@cloudflare/workers-types': specifier: ^4.20240903.0 version: 4.20250129.0 @@ -172,6 +172,9 @@ importers: '@getcronit/pylon-telemetry': specifier: workspace:^ version: link:../pylon-telemetry + '@gqty/react': + specifier: ^3.1.0 + version: 3.1.0(gqty@3.4.0-canary-20250207102900.644ad9fdeafa6318516627577b1d4d754d5c5a98(graphql@16.10.0))(graphql@16.10.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@hono/sentry': specifier: ^1.2.0 version: 1.2.0(hono@4.6.20) @@ -181,9 +184,21 @@ importers: '@sentry/node': specifier: ^8.54.0 version: 8.54.0 + '@tailwindcss/postcss': + specifier: ^4.0.4 + version: 4.0.7 + autoprefixer: + specifier: ^10.4.20 + version: 10.4.20(postcss@8.5.1) + chokidar: + specifier: ^4.0.3 + version: 4.0.3 consola: specifier: ^3.2.3 version: 3.4.0 + gqty: + specifier: 3.4.0-canary-20250207102900.644ad9fdeafa6318516627577b1d4d754d5c5a98 + version: 3.4.0-canary-20250207102900.644ad9fdeafa6318516627577b1d4d754d5c5a98(graphql@16.10.0) graphql: specifier: ^16.9.0 version: 16.10.0 @@ -202,12 +217,30 @@ importers: openid-client: specifier: ^6.1.7 version: 6.2.0 + postcss: + specifier: ^8.5.1 + version: 8.5.1 + postcss-load-config: + specifier: ^6.0.1 + version: 6.0.1(jiti@2.4.2)(postcss@8.5.1) + react: + specifier: ^19.0.0 + version: 19.0.0 + react-dom: + specifier: ^19.0.0 + version: 19.0.0(react@19.0.0) + react-router: + specifier: ^7.1.5 + version: 7.2.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + sharp: + specifier: ^0.33.5 + version: 0.33.5 + tiny-glob: + specifier: ^0.2.9 + version: 0.2.9 toucan-js: specifier: ^4.1.0 version: 4.1.0 - winston: - specifier: ^3.8.2 - version: 3.17.0 devDependencies: '@sentry/types': specifier: ^8.54.0 @@ -258,6 +291,10 @@ importers: packages: + '@alloc/quick-lru@5.2.0': + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} + '@ampproject/remapping@2.3.0': resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} @@ -666,10 +703,6 @@ packages: resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} engines: {node: '>=0.1.90'} - '@colors/colors@1.6.0': - resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==} - engines: {node: '>=0.1.90'} - '@commander-js/extra-typings@13.1.0': resolution: {integrity: sha512-q5P52BYb1hwVWE6dtID7VvuJWrlfbCv4klj7BjUUOqMz4jbSZD4C9fJ9lRjL2jnBGTg+gDDlaXN51rkWcLk4fg==} peerDependencies: @@ -679,12 +712,12 @@ packages: resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} - '@dabh/diagnostics@2.0.3': - resolution: {integrity: sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==} - '@drizzle-team/brocli@0.10.2': resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==} + '@emnapi/runtime@1.3.1': + resolution: {integrity: sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==} + '@envelop/core@5.0.3': resolution: {integrity: sha512-SE3JxL7odst8igN6x77QWyPpXKXz/Hs5o5Y27r+9Br6WHIhkW90lYYVITWIJQ/qYgn5PkpbaVgeFY9rgqQaZ/A==} engines: {node: '>=18.0.0'} @@ -1408,6 +1441,22 @@ packages: trading-signals: optional: true + '@gqty/react@3.1.0': + resolution: {integrity: sha512-MFGFmAvDi+N4KX2WUSSiFMGzv5fdmrWd0naVxRRDff/+CeNjXHCmwAKi5B2aezuSXrh+rENh/+6ZlZgB3ffOIg==} + engines: {node: ^12.20.0 || >=14.13.0} + peerDependencies: + gqty: ^3.3.0 + graphql: ^16.9.0 + graphql-sse: ^2.5.4 + graphql-ws: ^5.16.2 + react: ^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + graphql-sse: + optional: true + graphql-ws: + optional: true + '@graphql-codegen/core@4.0.2': resolution: {integrity: sha512-IZbpkhwVqgizcjNiaVzNAzm/xbWT6YnGgeOLwVjm4KbJn3V2jchVtuzHH09G5/WkkLSk2wgbXNdwjM41JxO6Eg==} peerDependencies: @@ -1524,6 +1573,111 @@ packages: peerDependencies: hono: '>=3.*' + '@img/sharp-darwin-arm64@0.33.5': + resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.33.5': + resolution: {integrity: sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.0.4': + resolution: {integrity: sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.0.4': + resolution: {integrity: sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.0.4': + resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linux-arm@1.0.5': + resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==} + cpu: [arm] + os: [linux] + + '@img/sharp-libvips-linux-s390x@1.0.4': + resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==} + cpu: [s390x] + os: [linux] + + '@img/sharp-libvips-linux-x64@1.0.4': + resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==} + cpu: [x64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-arm64@1.0.4': + resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-x64@1.0.4': + resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==} + cpu: [x64] + os: [linux] + + '@img/sharp-linux-arm64@0.33.5': + resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linux-arm@0.33.5': + resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + + '@img/sharp-linux-s390x@0.33.5': + resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + + '@img/sharp-linux-x64@0.33.5': + resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-linuxmusl-arm64@0.33.5': + resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linuxmusl-x64@0.33.5': + resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-wasm32@0.33.5': + resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-ia32@0.33.5': + resolution: {integrity: sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.33.5': + resolution: {integrity: sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + '@inquirer/checkbox@2.5.0': resolution: {integrity: sha512-sMgdETOfi2dUHT8r7TT1BTKOwNvdDGFDXYWtQ2J69SvlYNntk9I/gJe7r5yvMwwsuKnYbuRs3pNhx4tgNck5aA==} engines: {node: '>=18'} @@ -2035,6 +2189,19 @@ packages: '@prisma/instrumentation@5.22.0': resolution: {integrity: sha512-LxccF392NN37ISGxIurUljZSh1YWnphO34V5a0+T7FVQG2u9bhAXRTJpgmQ3483woVhkraQZFF7cbRrpbw/F4Q==} + '@react-hookz/deep-equal@1.0.4': + resolution: {integrity: sha512-N56fTrAPUDz/R423pag+n6TXWbvlBZDtTehaGFjK0InmN+V2OFWLE/WmORhmn6Ce7dlwH5+tQN1LJFw3ngTJVg==} + + '@react-hookz/web@23.1.0': + resolution: {integrity: sha512-fvbURdsa1ukttbLR1ASE/XmqXP09vZ1PiCYppYeR1sNMzCrdkG0iBnjxniFSVjJ8gIw2fRs6nqMTbeBz2uAkuA==} + peerDependencies: + js-cookie: ^3.0.5 + react: ^16.8 || ^17 || ^18 + react-dom: ^16.8 || ^17 || ^18 + peerDependenciesMeta: + js-cookie: + optional: true + '@repeaterjs/repeater@3.0.6': resolution: {integrity: sha512-Javneu5lsuhwNCryN+pXH93VPQ8g0dBX7wItHFgYiwQmzE1sVdg5tWHiOgHywzL2W21XQopa7IwIEnNbmeUJYA==} @@ -2228,12 +2395,91 @@ packages: resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} engines: {node: '>=18'} + '@tailwindcss/node@4.0.7': + resolution: {integrity: sha512-dkFXufkbRB2mu3FPsW5xLAUWJyexpJA+/VtQj18k3SUiJVLdpgzBd1v1gRRcIpEJj7K5KpxBKfOXlZxT3ZZRuA==} + + '@tailwindcss/oxide-android-arm64@4.0.7': + resolution: {integrity: sha512-5iQXXcAeOHBZy8ASfHFm1k0O/9wR2E3tKh6+P+ilZZbQiMgu+qrnfpBWYPc3FPuQdWiWb73069WT5D+CAfx/tg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.0.7': + resolution: {integrity: sha512-7yGZtEc5IgVYylqK/2B0yVqoofk4UAbkn1ygNpIJZyrOhbymsfr8uUFCueTu2fUxmAYIfMZ8waWo2dLg/NgLgg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.0.7': + resolution: {integrity: sha512-tPQDV20fBjb26yWbPqT1ZSoDChomMCiXTKn4jupMSoMCFyU7+OJvIY1ryjqBuY622dEBJ8LnCDDWsnj1lX9nNQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.0.7': + resolution: {integrity: sha512-sZqJpTyTZiknU9LLHuByg5GKTW+u3FqM7q7myequAXxKOpAFiOfXpY710FuMY+gjzSapyRbDXJlsTQtCyiTo5w==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.0.7': + resolution: {integrity: sha512-PBgvULgeSswjd8cbZ91gdIcIDMdc3TUHV5XemEpxlqt9M8KoydJzkuB/Dt910jYdofOIaTWRL6adG9nJICvU4A==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.0.7': + resolution: {integrity: sha512-By/a2yeh+e9b+C67F88ndSwVJl2A3tcUDb29FbedDi+DZ4Mr07Oqw9Y1DrDrtHIDhIZ3bmmiL1dkH2YxrtV+zw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-musl@4.0.7': + resolution: {integrity: sha512-WHYs3cpPEJb/ccyT20NOzopYQkl7JKncNBUbb77YFlwlXMVJLLV3nrXQKhr7DmZxz2ZXqjyUwsj2rdzd9stYdw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-gnu@4.0.7': + resolution: {integrity: sha512-7bP1UyuX9kFxbOwkeIJhBZNevKYPXB6xZI37v09fqi6rqRJR8elybwjMUHm54GVP+UTtJ14ueB1K54Dy1tIO6w==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-musl@4.0.7': + resolution: {integrity: sha512-gBQIV8nL/LuhARNGeroqzXymMzzW5wQzqlteVqOVoqwEfpHOP3GMird5pGFbnpY+NP0fOlsZGrxxOPQ4W/84bQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-win32-arm64-msvc@4.0.7': + resolution: {integrity: sha512-aH530NFfx0kpQpvYMfWoeG03zGnRCMVlQG8do/5XeahYydz+6SIBxA1tl/cyITSJyWZHyVt6GVNkXeAD30v0Xg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.0.7': + resolution: {integrity: sha512-8Cva6bbJN7ZJx320k7vxGGdU0ewmpfS5A4PudyzUuofdi8MgeINuiiWiPQ0VZCda/GX88K6qp+6UpDZNVr8HMQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.0.7': + resolution: {integrity: sha512-yr6w5YMgjy+B+zkJiJtIYGXW+HNYOPfRPtSs+aqLnKwdEzNrGv4ZuJh9hYJ3mcA+HMq/K1rtFV+KsEr65S558g==} + engines: {node: '>= 10'} + + '@tailwindcss/postcss@4.0.7': + resolution: {integrity: sha512-zXcKs1uGssVDlnsQ+iwrkul5GPKvsXPynGCuk/eXLx3DVhHlQKMpA6tXN2oO28x2ki1xRBTfadKiHy2taVvp7g==} + '@tootallnate/quickjs-emscripten@0.23.0': resolution: {integrity: sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==} '@types/connect@3.4.36': resolution: {integrity: sha512-P63Zd/JUGq+PdrM1lv0Wv5SBYeA2+CORvbrXbngriYY0jzLUWfQMQQxOhjONEz/wlHOAxOdY7CY65rgQdTjq2w==} + '@types/cookie@0.6.0': + resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} + '@types/estree@1.0.6': resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} @@ -2273,9 +2519,6 @@ packages: '@types/tedious@4.0.14': resolution: {integrity: sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw==} - '@types/triple-beam@1.3.5': - resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==} - '@types/wrap-ansi@3.0.0': resolution: {integrity: sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==} @@ -2431,6 +2674,13 @@ packages: resolution: {integrity: sha512-Hdw8qdNiqdJ8LqT0iK0sVzkFbzg6fhnQqqfWhBDxcHZvU75+B+ayzTy8x+k5Ix0Y92XOhOUlx74ps+bA6BeYMQ==} engines: {node: '>=8'} + autoprefixer@10.4.20: + resolution: {integrity: sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + babel-plugin-syntax-trailing-function-commas@7.0.0-beta.0: resolution: {integrity: sha512-Xj9XuRuz3nTSbaTXWv3itLOcxyF4oPD8douBBmj7U9BBC6nEBYfyOJYQMf/8PJAFotC62UY5dFfIGEPr7WswzQ==} @@ -2568,6 +2818,10 @@ packages: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + ci-info@3.9.0: resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} engines: {node: '>=8'} @@ -2623,11 +2877,9 @@ packages: color-string@1.9.1: resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} - color@3.2.1: - resolution: {integrity: sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==} - - colorspace@1.1.4: - resolution: {integrity: sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==} + color@4.2.3: + resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} + engines: {node: '>=12.5.0'} commander@12.1.0: resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} @@ -2692,6 +2944,10 @@ packages: resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} engines: {node: '>= 0.6'} + cookie@1.0.2: + resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==} + engines: {node: '>=18'} + core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} @@ -2810,6 +3066,15 @@ packages: resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} engines: {node: '>=8'} + detect-libc@1.0.3: + resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==} + engines: {node: '>=0.10'} + hasBin: true + + detect-libc@2.0.3: + resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} + engines: {node: '>=8'} + devalue@4.3.3: resolution: {integrity: sha512-UH8EL6H2ifcY8TbD2QsxwCC/pr5xSwPvv85LrLXVihmHVC3T3YqTCIwnR5ak0yO1KYqlxrPVOA/JVZJYPy2ATg==} @@ -2954,8 +3219,9 @@ packages: emojilib@2.4.0: resolution: {integrity: sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==} - enabled@2.0.0: - resolution: {integrity: sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==} + enhanced-resolve@5.18.1: + resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==} + engines: {node: '>=10.13.0'} enquirer@2.3.6: resolution: {integrity: sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==} @@ -3115,9 +3381,6 @@ packages: fclone@1.0.11: resolution: {integrity: sha512-GDqVQezKzRABdeqflsgMr7ktzgF9CyS+p2oe0jJqUY6izSSbhPIQJDpoU4PtGcD7VPM9xh/dVrTu6z1nwgmEGw==} - fecha@4.2.3: - resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==} - figures@2.0.0: resolution: {integrity: sha512-Oa2M9atig69ZkfwiApY8F2Yy+tzMbazyvqv21R0NsSC8floSOC09BbT1ITWAdoMGQvJ/aZnR1KMwdx9tvHnTNA==} engines: {node: '>=4'} @@ -3157,9 +3420,6 @@ packages: flatted@3.3.2: resolution: {integrity: sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==} - fn.name@1.1.0: - resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==} - follow-redirects@1.15.9: resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} engines: {node: '>=4.0'} @@ -3176,6 +3436,9 @@ packages: forwarded-parse@2.1.2: resolution: {integrity: sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==} + fraction.js@4.3.7: + resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} + frail-map@1.0.10: resolution: {integrity: sha512-aawqlQUlg9ye61T879jXUoii8lNNHVZJyRL6XBbcNZ4Yu2MZfcQj1Q6yKpC7cAn5xUPMcVSsQ+Yql/AFcrfp9w==} @@ -3284,6 +3547,9 @@ packages: resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} engines: {node: '>=4'} + globalyzer@0.1.0: + resolution: {integrity: sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==} + globby@11.1.0: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} engines: {node: '>=10'} @@ -3292,6 +3558,24 @@ packages: resolution: {integrity: sha512-s3Fq41ZVh7vbbe2PN3nrW7yC7U7MFVc5c98/iTl9c2GawNMKx/J648KQRW6WKkuU8GIbbh2IXfIRQjOZnXcTnw==} engines: {node: '>=18'} + globrex@0.1.2: + resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} + + gqty@3.4.0-canary-20250207102900.644ad9fdeafa6318516627577b1d4d754d5c5a98: + resolution: {integrity: sha512-s/50A1kLL4625nmQ6R8Esl6w6/1Oao0HSJu43wugY6XvFK9G6ag7HUrR5IEoTV7/Ow/Vi0lj3bEprcZT6n0QtA==} + engines: {node: ^12.20.0 || >=14.13.0} + peerDependencies: + graphql: ^16.9.0 + graphql-sse: ^2.5.4 + graphql-ws: ^5.16.2 + peerDependenciesMeta: + graphql: + optional: true + graphql-sse: + optional: true + graphql-ws: + optional: true + gqty@3.4.1: resolution: {integrity: sha512-rd/FdqykHaGBfbFnmXQwN02QFz7p/MygGh/QBdTeLm2fP4JzoEvpIQyL4zt7j8b1ZGWv2OMbNotI4udWBsKFvQ==} engines: {node: ^12.20.0 || >=14.13.0} @@ -3575,6 +3859,10 @@ packages: resolution: {integrity: sha512-qjdpeo2yKlYTH7nFdK0vbZWuTCesk4o63v5iVOlhMQPfuIZQfW/HI35SjfhA+4qpg36rnFSvUK5b1m+ckIblQQ==} engines: {node: '>= 0.6.0'} + jiti@2.4.2: + resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==} + hasBin: true + jose@5.10.0: resolution: {integrity: sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==} @@ -3648,13 +3936,78 @@ packages: jws@3.2.2: resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==} - kuler@2.0.0: - resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==} - lazy@1.0.11: resolution: {integrity: sha512-Y+CjUfLmIpoUCCRl0ub4smrYtGGr5AOa2AKOaWelGHOGz33X/Y/KizefGqbkwfz44+cnq/+9habclf8vOmu2LA==} engines: {node: '>=0.2.0'} + lightningcss-darwin-arm64@1.29.1: + resolution: {integrity: sha512-HtR5XJ5A0lvCqYAoSv2QdZZyoHNttBpa5EP9aNuzBQeKGfbyH5+UipLWvVzpP4Uml5ej4BYs5I9Lco9u1fECqw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.29.1: + resolution: {integrity: sha512-k33G9IzKUpHy/J/3+9MCO4e+PzaFblsgBjSGlpAaFikeBFm8B/CkO3cKU9oI4g+fjS2KlkLM/Bza9K/aw8wsNA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.29.1: + resolution: {integrity: sha512-0SUW22fv/8kln2LnIdOCmSuXnxgxVC276W5KLTwoehiO0hxkacBxjHOL5EtHD8BAXg2BvuhsJPmVMasvby3LiQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.29.1: + resolution: {integrity: sha512-sD32pFvlR0kDlqsOZmYqH/68SqUMPNj+0pucGxToXZi4XZgZmqeX/NkxNKCPsswAXU3UeYgDSpGhu05eAufjDg==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.29.1: + resolution: {integrity: sha512-0+vClRIZ6mmJl/dxGuRsE197o1HDEeeRk6nzycSy2GofC2JsY4ifCRnvUWf/CUBQmlrvMzt6SMQNMSEu22csWQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.29.1: + resolution: {integrity: sha512-UKMFrG4rL/uHNgelBsDwJcBqVpzNJbzsKkbI3Ja5fg00sgQnHw/VrzUTEc4jhZ+AN2BvQYz/tkHu4vt1kLuJyw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.29.1: + resolution: {integrity: sha512-u1S+xdODy/eEtjADqirA774y3jLcm8RPtYztwReEXoZKdzgsHYPl0s5V52Tst+GKzqjebkULT86XMSxejzfISw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.29.1: + resolution: {integrity: sha512-L0Tx0DtaNUTzXv0lbGCLB/c/qEADanHbu4QdcNOXLIe1i8i22rZRpbT3gpWYsCh9aSL9zFujY/WmEXIatWvXbw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.29.1: + resolution: {integrity: sha512-QoOVnkIEFfbW4xPi+dpdft/zAKmgLgsRHfJalEPYuJDOWf7cLQzYg0DEh8/sn737FaeMJxHZRc1oBreiwZCjog==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.29.1: + resolution: {integrity: sha512-NygcbThNBe4JElP+olyTI/doBNGJvLs3bFCRPdvuCcxZCcCZ71B858IHpdm7L1btZex0FvCmM17FK98Y9MRy1Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.29.1: + resolution: {integrity: sha512-FmGoeD4S05ewj+AkhTY+D+myDvXI6eL27FjHIjoyUkO/uw7WZD1fBVs0QxeYWa7E17CUHJaYX/RUGISCtcrG4Q==} + engines: {node: '>= 12.0.0'} + + lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} + engines: {node: '>=14'} + lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} @@ -3717,10 +4070,6 @@ packages: lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} - logform@2.7.0: - resolution: {integrity: sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==} - engines: {node: '>= 12.0.0'} - loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true @@ -3926,6 +4275,10 @@ packages: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} + normalize-range@0.1.2: + resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} + engines: {node: '>=0.10.0'} + normalize-url@8.0.1: resolution: {integrity: sha512-IO9QvjUMWxPQQhs60oOu10CRkWCiZzSUkzbXGGV9pviYl1fXYcvkzQ5jV9z8Y6un8ARoVRl4EtC6v6jNqbaJ/w==} engines: {node: '>=14.16'} @@ -4036,9 +4389,6 @@ packages: once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} - one-time@1.0.0: - resolution: {integrity: sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==} - onetime@5.1.2: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} engines: {node: '>=6'} @@ -4057,6 +4407,10 @@ packages: outdent@0.5.0: resolution: {integrity: sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==} + p-debounce@4.0.0: + resolution: {integrity: sha512-4Ispi9I9qYGO4lueiLDhe4q4iK5ERK8reLsuzH6BPaXn53EGaua8H66PXIFGrW897hwjXp+pVLrm/DLxN0RF0A==} + engines: {node: '>=12'} + p-defer@3.0.0: resolution: {integrity: sha512-ugZxsxmtTln604yeYd29EGrNhazN2lywetzpKhfmQjW/VJmhpDmWbiX+h0zL8V91R0UXkhb3KtPmyq9PZw3aYw==} engines: {node: '>=8'} @@ -4315,6 +4669,27 @@ packages: engines: {node: '>=12.0.0'} hasBin: true + postcss-load-config@6.0.1: + resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} + engines: {node: '>= 18'} + peerDependencies: + jiti: '>=1.21.0' + postcss: '>=8.0.9' + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + jiti: + optional: true + postcss: + optional: true + tsx: + optional: true + yaml: + optional: true + + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + postcss@8.5.1: resolution: {integrity: sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==} engines: {node: ^10 || ^12 || >=14} @@ -4380,9 +4755,33 @@ packages: resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} hasBin: true + react-dom@19.0.0: + resolution: {integrity: sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==} + peerDependencies: + react: ^19.0.0 + react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + react-router@7.2.0: + resolution: {integrity: sha512-fXyqzPgCPZbqhrk7k3hPcCpYIlQ2ugIXDboHUzhJISFVy2DEPsmHgN588MyGmkIOv3jDgNfUE3kJi83L28s/LQ==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + peerDependenciesMeta: + react-dom: + optional: true + + react-ssr-prepass@1.6.0: + resolution: {integrity: sha512-M10nxc95Sfm00fXm+tLkC1MWG5NLWEBgWoGrPSnAqEFM4BUaoy97JvVw+m3iL74ZHzj86M33rPiFi738hEFLWg==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + + react@19.0.0: + resolution: {integrity: sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==} + engines: {node: '>=0.10.0'} + read-package-up@11.0.0: resolution: {integrity: sha512-MbgfoNPANMdb4oRBNg5eqLbB2t2r+o5Ua1pNt8BqGp4I0FJZhuVSOj3PaBPni4azWuSzEdNn2evevzVmEk1ohQ==} engines: {node: '>=18'} @@ -4406,14 +4805,14 @@ packages: readable-stream@2.3.8: resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} - readable-stream@3.6.2: - resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} - engines: {node: '>= 6'} - readdirp@3.6.0: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + regenerator-runtime@0.14.1: resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} @@ -4497,16 +4896,15 @@ packages: safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} - safe-stable-stringify@2.5.0: - resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} - engines: {node: '>=10'} - safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} sax@1.4.1: resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==} + scheduler@0.25.0: + resolution: {integrity: sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==} + selfsigned@2.4.1: resolution: {integrity: sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==} engines: {node: '>=10'} @@ -4555,9 +4953,16 @@ packages: sentence-case@3.0.4: resolution: {integrity: sha512-8LS0JInaQMCRoQ7YUytAo/xUu5W2XnQxV2HI/6uM6U7CITS1RqPElr30V6uIqyMKM9lJGRVFy5/4CuzcixNYSg==} + set-cookie-parser@2.7.1: + resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==} + setimmediate@1.0.5: resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} + sharp@0.33.5: + resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -4664,9 +5069,6 @@ packages: sprintf-js@1.1.3: resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} - stack-trace@0.0.10: - resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==} - stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} @@ -4762,6 +5164,13 @@ packages: os: [darwin, linux, win32, freebsd, openbsd, netbsd, sunos, android] hasBin: true + tailwindcss@4.0.7: + resolution: {integrity: sha512-yH5bPPyapavo7L+547h3c4jcBXcrKwybQRjwdEIVAd9iXRvy/3T1CC6XSQEgZtRySjKfqvo3Cc0ZF1DTheuIdA==} + + tapable@2.2.1: + resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} + engines: {node: '>=6'} + temp-dir@2.0.0: resolution: {integrity: sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==} engines: {node: '>=8'} @@ -4782,9 +5191,6 @@ packages: resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} engines: {node: '>=8'} - text-hex@1.0.0: - resolution: {integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==} - thenify-all@1.6.0: resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} engines: {node: '>=0.8'} @@ -4799,6 +5205,9 @@ packages: resolution: {integrity: sha512-75voc/9G4rDIJleOo4jPvN4/YC4GRZrY8yy1uU4lwrB3XEQbWve8zXoO5No4eFrGcTAMYyoY67p8jRQdtA1HbA==} engines: {node: '>=12'} + tiny-glob@0.2.9: + resolution: {integrity: sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==} + tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -4831,10 +5240,6 @@ packages: resolution: {integrity: sha512-aXJDbk6SnumuaZSANd21XAo15ucCDE38H4fkqiGsc3MhCK+wOlZvLP9cB/TvpHT0mOyWgC4Z8EwRlzqYSUzdsA==} engines: {node: '>= 0.4'} - triple-beam@1.4.1: - resolution: {integrity: sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==} - engines: {node: '>= 14.0.0'} - tslib@1.9.3: resolution: {integrity: sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==} @@ -4844,6 +5249,9 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + turbo-stream@2.4.0: + resolution: {integrity: sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==} + tv4@1.3.0: resolution: {integrity: sha512-afizzfpJgvPr+eDkREK4MxJ/+r8nEEHcmitwgnPUqpaP+FpwQyadnxNoSACbgc/b1LsZYtODGoPiFxQrgJgjvw==} engines: {node: '>= 0.8.0'} @@ -4966,6 +5374,11 @@ packages: urlpattern-polyfill@10.0.0: resolution: {integrity: sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg==} + use-sync-external-store@1.4.0: + resolution: {integrity: sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -5057,14 +5470,6 @@ packages: engines: {node: '>=8'} hasBin: true - winston-transport@4.9.0: - resolution: {integrity: sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==} - engines: {node: '>= 12.0.0'} - - winston@3.17.0: - resolution: {integrity: sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==} - engines: {node: '>= 12.0.0'} - wordwrap@1.0.0: resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} @@ -5194,6 +5599,8 @@ packages: snapshots: + '@alloc/quick-lru@5.2.0': {} + '@ampproject/remapping@2.3.0': dependencies: '@jridgewell/gen-mapping': 0.3.8 @@ -5696,7 +6103,7 @@ snapshots: dependencies: mime: 3.0.0 - '@cloudflare/vitest-pool-workers@0.4.31(@cloudflare/workers-types@4.20250129.0)(@vitest/runner@1.5.3)(@vitest/snapshot@1.5.3)(vitest@1.5.3(@types/node@22.13.1))': + '@cloudflare/vitest-pool-workers@0.4.31(@cloudflare/workers-types@4.20250129.0)(@vitest/runner@1.5.3)(@vitest/snapshot@1.5.3)(vitest@1.5.3(@types/node@22.13.1)(lightningcss@1.29.1))': dependencies: '@vitest/runner': 1.5.3 '@vitest/snapshot': 1.5.3 @@ -5706,7 +6113,7 @@ snapshots: esbuild: 0.17.19 miniflare: 3.20240909.0 semver: 7.7.1 - vitest: 1.5.3(@types/node@22.13.1) + vitest: 1.5.3(@types/node@22.13.1)(lightningcss@1.29.1) wrangler: 3.77.0(@cloudflare/workers-types@4.20250129.0) zod: 3.24.1 transitivePeerDependencies: @@ -5755,8 +6162,6 @@ snapshots: '@colors/colors@1.5.0': optional: true - '@colors/colors@1.6.0': {} - '@commander-js/extra-typings@13.1.0(commander@13.1.0)': dependencies: commander: 13.1.0 @@ -5765,14 +6170,13 @@ snapshots: dependencies: '@jridgewell/trace-mapping': 0.3.9 - '@dabh/diagnostics@2.0.3': - dependencies: - colorspace: 1.1.4 - enabled: 2.0.0 - kuler: 2.0.0 - '@drizzle-team/brocli@0.10.2': {} + '@emnapi/runtime@1.3.1': + dependencies: + tslib: 2.8.1 + optional: true + '@envelop/core@5.0.3': dependencies: '@envelop/types': 5.0.0 @@ -6173,6 +6577,21 @@ snapshots: - supports-color - typescript + '@gqty/react@3.1.0(gqty@3.4.0-canary-20250207102900.644ad9fdeafa6318516627577b1d4d754d5c5a98(graphql@16.10.0))(graphql@16.10.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@react-hookz/web': 23.1.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + gqty: 3.4.0-canary-20250207102900.644ad9fdeafa6318516627577b1d4d754d5c5a98(graphql@16.10.0) + graphql: 16.10.0 + multidict: 1.0.9 + p-debounce: 4.0.0 + p-defer: 3.0.0 + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + react-ssr-prepass: 1.6.0(react@19.0.0) + use-sync-external-store: 1.4.0(react@19.0.0) + transitivePeerDependencies: + - js-cookie + '@graphql-codegen/core@4.0.2(graphql@16.10.0)': dependencies: '@graphql-codegen/plugin-helpers': 5.1.0(graphql@16.10.0) @@ -6341,6 +6760,81 @@ snapshots: hono: 4.6.20 toucan-js: 4.1.0 + '@img/sharp-darwin-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.0.4 + optional: true + + '@img/sharp-darwin-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.0.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.0.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.0.5': + optional: true + + '@img/sharp-libvips-linux-s390x@1.0.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.0.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.0.4': + optional: true + + '@img/sharp-linux-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.0.4 + optional: true + + '@img/sharp-linux-arm@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.0.5 + optional: true + + '@img/sharp-linux-s390x@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.0.4 + optional: true + + '@img/sharp-linux-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.0.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.0.4 + optional: true + + '@img/sharp-wasm32@0.33.5': + dependencies: + '@emnapi/runtime': 1.3.1 + optional: true + + '@img/sharp-win32-ia32@0.33.5': + optional: true + + '@img/sharp-win32-x64@0.33.5': + optional: true + '@inquirer/checkbox@2.5.0': dependencies: '@inquirer/core': 9.2.1 @@ -7023,6 +7517,14 @@ snapshots: transitivePeerDependencies: - supports-color + '@react-hookz/deep-equal@1.0.4': {} + + '@react-hookz/web@23.1.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@react-hookz/deep-equal': 1.0.4 + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + '@repeaterjs/repeater@3.0.6': {} '@rollup/rollup-android-arm-eabi@4.34.2': @@ -7254,12 +7756,76 @@ snapshots: '@sindresorhus/merge-streams@4.0.0': {} + '@tailwindcss/node@4.0.7': + dependencies: + enhanced-resolve: 5.18.1 + jiti: 2.4.2 + tailwindcss: 4.0.7 + + '@tailwindcss/oxide-android-arm64@4.0.7': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.0.7': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.0.7': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.0.7': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.0.7': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.0.7': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.0.7': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.0.7': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.0.7': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.0.7': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.0.7': + optional: true + + '@tailwindcss/oxide@4.0.7': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.0.7 + '@tailwindcss/oxide-darwin-arm64': 4.0.7 + '@tailwindcss/oxide-darwin-x64': 4.0.7 + '@tailwindcss/oxide-freebsd-x64': 4.0.7 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.0.7 + '@tailwindcss/oxide-linux-arm64-gnu': 4.0.7 + '@tailwindcss/oxide-linux-arm64-musl': 4.0.7 + '@tailwindcss/oxide-linux-x64-gnu': 4.0.7 + '@tailwindcss/oxide-linux-x64-musl': 4.0.7 + '@tailwindcss/oxide-win32-arm64-msvc': 4.0.7 + '@tailwindcss/oxide-win32-x64-msvc': 4.0.7 + + '@tailwindcss/postcss@4.0.7': + dependencies: + '@alloc/quick-lru': 5.2.0 + '@tailwindcss/node': 4.0.7 + '@tailwindcss/oxide': 4.0.7 + lightningcss: 1.29.1 + postcss: 8.5.1 + tailwindcss: 4.0.7 + '@tootallnate/quickjs-emscripten@0.23.0': {} '@types/connect@3.4.36': dependencies: '@types/node': 18.19.75 + '@types/cookie@0.6.0': {} + '@types/estree@1.0.6': {} '@types/mute-stream@0.0.4': @@ -7304,8 +7870,6 @@ snapshots: dependencies: '@types/node': 18.19.75 - '@types/triple-beam@1.3.5': {} - '@types/wrap-ansi@3.0.0': {} '@types/ws@8.5.14': @@ -7459,6 +8023,16 @@ snapshots: auto-bind@4.0.0: {} + autoprefixer@10.4.20(postcss@8.5.1): + dependencies: + browserslist: 4.24.4 + caniuse-lite: 1.0.30001697 + fraction.js: 4.3.7 + normalize-range: 0.1.2 + picocolors: 1.1.1 + postcss: 8.5.1 + postcss-value-parser: 4.2.0 + babel-plugin-syntax-trailing-function-commas@7.0.0-beta.0: {} babel-preset-fbjs@3.4.0(@babel/core@7.26.7): @@ -7655,6 +8229,10 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + ci-info@3.9.0: {} cjs-module-lexer@1.4.3: {} @@ -7715,16 +8293,11 @@ snapshots: color-name: 1.1.4 simple-swizzle: 0.2.2 - color@3.2.1: + color@4.2.3: dependencies: - color-convert: 1.9.3 + color-convert: 2.0.1 color-string: 1.9.1 - colorspace@1.1.4: - dependencies: - color: 3.2.1 - text-hex: 1.0.0 - commander@12.1.0: {} commander@13.1.0: {} @@ -7779,6 +8352,8 @@ snapshots: cookie@0.7.2: {} + cookie@1.0.2: {} + core-util-is@1.0.3: {} cosmiconfig@9.0.0(typescript@5.7.3): @@ -7879,6 +8454,10 @@ snapshots: detect-indent@6.1.0: {} + detect-libc@1.0.3: {} + + detect-libc@2.0.3: {} + devalue@4.3.3: {} diff-sequences@29.6.3: {} @@ -7909,12 +8488,13 @@ snapshots: transitivePeerDependencies: - supports-color - drizzle-orm@0.33.0(@cloudflare/workers-types@4.20250129.0)(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(bun-types@1.2.2): + drizzle-orm@0.33.0(@cloudflare/workers-types@4.20250129.0)(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(bun-types@1.2.2)(react@19.0.0): optionalDependencies: '@cloudflare/workers-types': 4.20250129.0 '@opentelemetry/api': 1.9.0 '@types/pg': 8.6.1 bun-types: 1.2.2 + react: 19.0.0 dset@3.1.4: {} @@ -7936,7 +8516,10 @@ snapshots: emojilib@2.4.0: {} - enabled@2.0.0: {} + enhanced-resolve@5.18.1: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.2.1 enquirer@2.3.6: dependencies: @@ -8226,8 +8809,6 @@ snapshots: fclone@1.0.11: {} - fecha@4.2.3: {} - figures@2.0.0: dependencies: escape-string-regexp: 1.0.5 @@ -8264,8 +8845,6 @@ snapshots: flatted@3.3.2: {} - fn.name@1.1.0: {} - follow-redirects@1.15.9(debug@4.3.7): optionalDependencies: debug: 4.3.7 @@ -8277,6 +8856,8 @@ snapshots: forwarded-parse@2.1.2: {} + fraction.js@4.3.7: {} + frail-map@1.0.10: {} from2@2.3.0: @@ -8392,6 +8973,8 @@ snapshots: globals@11.12.0: {} + globalyzer@0.1.0: {} + globby@11.1.0: dependencies: array-union: 2.1.0 @@ -8410,6 +8993,23 @@ snapshots: slash: 5.1.0 unicorn-magic: 0.1.0 + globrex@0.1.2: {} + + gqty@3.4.0-canary-20250207102900.644ad9fdeafa6318516627577b1d4d754d5c5a98(graphql@16.10.0): + dependencies: + debounce-microtasks: 0.1.8 + flatted: 3.3.2 + frail-map: 1.0.10 + just-extend: 6.2.0 + just-has: 2.3.0 + just-memoize: 2.2.0 + just-safe-get: 4.2.0 + just-safe-set: 4.2.1 + multidict: 1.0.9 + p-defer: 3.0.0 + optionalDependencies: + graphql: 16.10.0 + gqty@3.4.1(graphql@16.10.0): dependencies: debounce-microtasks: 0.1.8 @@ -8668,6 +9268,8 @@ snapshots: java-properties@1.0.2: {} + jiti@2.4.2: {} + jose@5.10.0: {} js-git@0.7.8: @@ -8747,10 +9349,55 @@ snapshots: jwa: 1.4.1 safe-buffer: 5.1.2 - kuler@2.0.0: {} - lazy@1.0.11: {} + lightningcss-darwin-arm64@1.29.1: + optional: true + + lightningcss-darwin-x64@1.29.1: + optional: true + + lightningcss-freebsd-x64@1.29.1: + optional: true + + lightningcss-linux-arm-gnueabihf@1.29.1: + optional: true + + lightningcss-linux-arm64-gnu@1.29.1: + optional: true + + lightningcss-linux-arm64-musl@1.29.1: + optional: true + + lightningcss-linux-x64-gnu@1.29.1: + optional: true + + lightningcss-linux-x64-musl@1.29.1: + optional: true + + lightningcss-win32-arm64-msvc@1.29.1: + optional: true + + lightningcss-win32-x64-msvc@1.29.1: + optional: true + + lightningcss@1.29.1: + dependencies: + detect-libc: 1.0.3 + optionalDependencies: + lightningcss-darwin-arm64: 1.29.1 + lightningcss-darwin-x64: 1.29.1 + lightningcss-freebsd-x64: 1.29.1 + lightningcss-linux-arm-gnueabihf: 1.29.1 + lightningcss-linux-arm64-gnu: 1.29.1 + lightningcss-linux-arm64-musl: 1.29.1 + lightningcss-linux-x64-gnu: 1.29.1 + lightningcss-linux-x64-musl: 1.29.1 + lightningcss-win32-arm64-msvc: 1.29.1 + lightningcss-win32-x64-msvc: 1.29.1 + + lilconfig@3.1.3: {} + lines-and-columns@1.2.4: {} load-json-file@4.0.0: @@ -8805,15 +9452,6 @@ snapshots: lodash@4.17.21: {} - logform@2.7.0: - dependencies: - '@colors/colors': 1.6.0 - '@types/triple-beam': 1.3.5 - fecha: 4.2.3 - ms: 2.1.3 - safe-stable-stringify: 2.5.0 - triple-beam: 1.4.1 - loose-envify@1.4.0: dependencies: js-tokens: 4.0.0 @@ -9018,6 +9656,8 @@ snapshots: normalize-path@3.0.0: {} + normalize-range@0.1.2: {} + normalize-url@8.0.1: {} npm-run-path@4.0.1: @@ -9052,10 +9692,6 @@ snapshots: dependencies: wrappy: 1.0.2 - one-time@1.0.0: - dependencies: - fn.name: 1.1.0 - onetime@5.1.2: dependencies: mimic-fn: 2.1.0 @@ -9073,6 +9709,8 @@ snapshots: outdent@0.5.0: {} + p-debounce@4.0.0: {} + p-defer@3.0.0: {} p-each-series@2.2.0: {} @@ -9360,6 +9998,15 @@ snapshots: - supports-color - utf-8-validate + postcss-load-config@6.0.1(jiti@2.4.2)(postcss@8.5.1): + dependencies: + lilconfig: 3.1.3 + optionalDependencies: + jiti: 2.4.2 + postcss: 8.5.1 + + postcss-value-parser@4.2.0: {} + postcss@8.5.1: dependencies: nanoid: 3.3.8 @@ -9428,8 +10075,29 @@ snapshots: minimist: 1.2.8 strip-json-comments: 2.0.1 + react-dom@19.0.0(react@19.0.0): + dependencies: + react: 19.0.0 + scheduler: 0.25.0 + react-is@18.3.1: {} + react-router@7.2.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + dependencies: + '@types/cookie': 0.6.0 + cookie: 1.0.2 + react: 19.0.0 + set-cookie-parser: 2.7.1 + turbo-stream: 2.4.0 + optionalDependencies: + react-dom: 19.0.0(react@19.0.0) + + react-ssr-prepass@1.6.0(react@19.0.0): + dependencies: + react: 19.0.0 + + react@19.0.0: {} + read-package-up@11.0.0: dependencies: find-up-simple: 1.0.0 @@ -9472,16 +10140,12 @@ snapshots: string_decoder: 1.1.1 util-deprecate: 1.0.2 - readable-stream@3.6.2: - dependencies: - inherits: 2.0.4 - string_decoder: 1.1.1 - util-deprecate: 1.0.2 - readdirp@3.6.0: dependencies: picomatch: 2.3.1 + readdirp@4.1.2: {} + regenerator-runtime@0.14.1: {} registry-auth-token@5.0.3: @@ -9588,12 +10252,12 @@ snapshots: safe-buffer@5.2.1: {} - safe-stable-stringify@2.5.0: {} - safer-buffer@2.1.2: {} sax@1.4.1: {} + scheduler@0.25.0: {} + selfsigned@2.4.1: dependencies: '@types/node-forge': 1.3.11 @@ -9679,8 +10343,36 @@ snapshots: tslib: 2.8.1 upper-case-first: 2.0.2 + set-cookie-parser@2.7.1: {} + setimmediate@1.0.5: {} + sharp@0.33.5: + dependencies: + color: 4.2.3 + detect-libc: 2.0.3 + semver: 7.7.1 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.33.5 + '@img/sharp-darwin-x64': 0.33.5 + '@img/sharp-libvips-darwin-arm64': 1.0.4 + '@img/sharp-libvips-darwin-x64': 1.0.4 + '@img/sharp-libvips-linux-arm': 1.0.5 + '@img/sharp-libvips-linux-arm64': 1.0.4 + '@img/sharp-libvips-linux-s390x': 1.0.4 + '@img/sharp-libvips-linux-x64': 1.0.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 + '@img/sharp-libvips-linuxmusl-x64': 1.0.4 + '@img/sharp-linux-arm': 0.33.5 + '@img/sharp-linux-arm64': 0.33.5 + '@img/sharp-linux-s390x': 0.33.5 + '@img/sharp-linux-x64': 0.33.5 + '@img/sharp-linuxmusl-arm64': 0.33.5 + '@img/sharp-linuxmusl-x64': 0.33.5 + '@img/sharp-wasm32': 0.33.5 + '@img/sharp-win32-ia32': 0.33.5 + '@img/sharp-win32-x64': 0.33.5 + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -9781,8 +10473,6 @@ snapshots: sprintf-js@1.1.3: {} - stack-trace@0.0.10: {} - stackback@0.0.2: {} stacktracey@2.1.8: @@ -9868,6 +10558,10 @@ snapshots: systeminformation@5.25.11: optional: true + tailwindcss@4.0.7: {} + + tapable@2.2.1: {} + temp-dir@2.0.0: {} temp-dir@3.0.0: {} @@ -9889,8 +10583,6 @@ snapshots: term-size@2.2.1: {} - text-hex@1.0.0: {} - thenify-all@1.6.0: dependencies: thenify: 3.3.1 @@ -9908,6 +10600,11 @@ snapshots: dependencies: convert-hrtime: 5.0.0 + tiny-glob@0.2.9: + dependencies: + globalyzer: 0.1.0 + globrex: 0.1.2 + tinybench@2.9.0: {} tinypool@0.8.4: {} @@ -9936,14 +10633,14 @@ snapshots: traverse@0.6.8: {} - triple-beam@1.4.1: {} - tslib@1.9.3: {} tslib@2.6.3: {} tslib@2.8.1: {} + turbo-stream@2.4.0: {} + tv4@1.3.0: {} tx2@1.0.5: @@ -10037,6 +10734,10 @@ snapshots: urlpattern-polyfill@10.0.0: {} + use-sync-external-store@1.4.0(react@19.0.0): + dependencies: + react: 19.0.0 + util-deprecate@1.0.2: {} validate-npm-package-license@3.0.4: @@ -10046,13 +10747,13 @@ snapshots: value-or-promise@1.0.12: {} - vite-node@1.5.3(@types/node@22.13.1): + vite-node@1.5.3(@types/node@22.13.1)(lightningcss@1.29.1): dependencies: cac: 6.7.14 debug: 4.4.0 pathe: 1.1.2 picocolors: 1.1.1 - vite: 5.4.14(@types/node@22.13.1) + vite: 5.4.14(@types/node@22.13.1)(lightningcss@1.29.1) transitivePeerDependencies: - '@types/node' - less @@ -10064,7 +10765,7 @@ snapshots: - supports-color - terser - vite@5.4.14(@types/node@22.13.1): + vite@5.4.14(@types/node@22.13.1)(lightningcss@1.29.1): dependencies: esbuild: 0.21.5 postcss: 8.5.1 @@ -10072,8 +10773,9 @@ snapshots: optionalDependencies: '@types/node': 22.13.1 fsevents: 2.3.3 + lightningcss: 1.29.1 - vitest@1.5.3(@types/node@22.13.1): + vitest@1.5.3(@types/node@22.13.1)(lightningcss@1.29.1): dependencies: '@vitest/expect': 1.5.3 '@vitest/runner': 1.5.3 @@ -10092,8 +10794,8 @@ snapshots: strip-literal: 2.1.1 tinybench: 2.9.0 tinypool: 0.8.4 - vite: 5.4.14(@types/node@22.13.1) - vite-node: 1.5.3(@types/node@22.13.1) + vite: 5.4.14(@types/node@22.13.1)(lightningcss@1.29.1) + vite-node: 1.5.3(@types/node@22.13.1)(lightningcss@1.29.1) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 22.13.1 @@ -10130,26 +10832,6 @@ snapshots: siginfo: 2.0.0 stackback: 0.0.2 - winston-transport@4.9.0: - dependencies: - logform: 2.7.0 - readable-stream: 3.6.2 - triple-beam: 1.4.1 - - winston@3.17.0: - dependencies: - '@colors/colors': 1.6.0 - '@dabh/diagnostics': 2.0.3 - async: 3.2.6 - is-stream: 2.0.1 - logform: 2.7.0 - one-time: 1.0.0 - readable-stream: 3.6.2 - safe-stable-stringify: 2.5.0 - stack-trace: 0.0.10 - triple-beam: 1.4.1 - winston-transport: 4.9.0 - wordwrap@1.0.0: {} workerd@1.20240909.0: From a4910f4edbcd5081fd019aa04f1ca0e5e88e3a38 Mon Sep 17 00:00:00 2001 From: Nico Schett Date: Wed, 19 Feb 2025 09:46:15 +0100 Subject: [PATCH 006/138] improve compatibility --- packages/pylon/package.json | 9 +- .../pylon/src/plugins/use-auth/use-auth.ts | 2 +- .../use-pages/build/plugins/image-plugin.ts | 3 +- .../src/plugins/use-pages/setup/index.tsx | 187 ++++++++++++------ 4 files changed, 135 insertions(+), 66 deletions(-) diff --git a/packages/pylon/package.json b/packages/pylon/package.json index 0e8701b..01b1545 100644 --- a/packages/pylon/package.json +++ b/packages/pylon/package.json @@ -40,6 +40,7 @@ "@hono/sentry": "^1.2.0", "@sentry/bun": "^8.17.0", "@sentry/node": "^8.54.0", + "chokidar": "^4.0.3", "consola": "^3.2.3", "gqty": "3.4.0-canary-20250207102900.644ad9fdeafa6318516627577b1d4d754d5c5a98", "graphql": "^16.9.0", @@ -48,18 +49,18 @@ "hono": "^4.0.8", "jsonwebtoken": "^9.0.2", "openid-client": "^6.1.7", + "postcss-load-config": "^6.0.1", "react-router": "^7.1.5", "sharp": "^0.33.5", "tiny-glob": "^0.2.9", - "toucan-js": "^4.1.0", - "postcss-load-config": "^6.0.1", - "chokidar": "^4.0.3" + "toucan-js": "^4.1.0" }, "engines": { "node": ">=18.0.0" }, "devDependencies": { - "@sentry/types": "^8.54.0" + "@sentry/types": "^8.54.0", + "@types/react-dom": "^19.0.4" }, "peerDependencies": { "@tailwindcss/postcss": "^4.0.4", diff --git a/packages/pylon/src/plugins/use-auth/use-auth.ts b/packages/pylon/src/plugins/use-auth/use-auth.ts index 8ef5daa..67ce7c4 100644 --- a/packages/pylon/src/plugins/use-auth/use-auth.ts +++ b/packages/pylon/src/plugins/use-auth/use-auth.ts @@ -55,7 +55,7 @@ const bootstrapAuth = async (issuer: string, keyPath: string) => { authKey.clientId, undefined, openid.PrivateKeyJwt({ - key: await importPrivateKey(authKey.key), + key: (await importPrivateKey(authKey.key)) as any, kid: authKey.keyId }) ) diff --git a/packages/pylon/src/plugins/use-pages/build/plugins/image-plugin.ts b/packages/pylon/src/plugins/use-pages/build/plugins/image-plugin.ts index 3c02485..3935dde 100644 --- a/packages/pylon/src/plugins/use-pages/build/plugins/image-plugin.ts +++ b/packages/pylon/src/plugins/use-pages/build/plugins/image-plugin.ts @@ -1,7 +1,6 @@ import {createHash} from 'crypto' import {Plugin} from 'esbuild' import path from 'path' -import sharp from 'sharp' import fs from 'fs/promises' export const imagePlugin: Plugin = { @@ -39,6 +38,8 @@ export const imagePlugin: Plugin = { }) build.onLoad({filter: /\.png$|\.jpg$/}, async args => { + const sharp = (await import('sharp')).default + // Load file and read the dimensions const image = sharp(args.path) const metadata = await image.metadata() diff --git a/packages/pylon/src/plugins/use-pages/setup/index.tsx b/packages/pylon/src/plugins/use-pages/setup/index.tsx index 380cb8f..a9ea4b7 100644 --- a/packages/pylon/src/plugins/use-pages/setup/index.tsx +++ b/packages/pylon/src/plugins/use-pages/setup/index.tsx @@ -3,10 +3,9 @@ import path from 'path' import reactServer from 'react-dom/server' import { UseHydrateCacheOptions } from '@gqty/react' -import { Readable } from 'stream' +import { PassThrough, Readable } from 'stream' import { AppLoader } from './app-loader' import { getEnv, type Plugin } from '../../../index' -import { cloneElement, createElement } from 'react' import { trimTrailingSlash } from 'hono/trailing-slash' import { StaticRouter } from 'react-router' @@ -63,7 +62,7 @@ export const setup: Plugin['setup'] = app => { } if (!client) { - client = await import(`${process.cwd()}/.pylon/client`) + client = await import(`${process.cwd()}/.pylon/client/index.js`) } const pageProps = { @@ -91,44 +90,88 @@ export const setup: Plugin['setup'] = app => { cacheSnapshot = prepared.cacheSnapshot - const stream = await reactServer.renderToReadableStream( - , - { - bootstrapModules: ['/__pylon/static/app.js'], - bootstrapScriptContent: `window.__PYLON_DATA__ = ${JSON.stringify({ - pageProps: pageProps, - cacheSnapshot: cacheSnapshot - })}` + if (reactServer.renderToReadableStream) { + const stream = await reactServer.renderToReadableStream( + , + { + bootstrapModules: ['/__pylon/static/app.js'], + bootstrapScriptContent: `window.__PYLON_DATA__ = ${JSON.stringify({ + pageProps: pageProps, + cacheSnapshot: cacheSnapshot + })}` + } + ) + + return c.body(stream) + } else if (reactServer.renderToPipeableStream) { + const pipableStream = reactServer.renderToPipeableStream( + , + { + bootstrapModules: ['/__pylon/static/app.js'], + bootstrapScriptContent: `window.__PYLON_DATA__ = ${JSON.stringify({ + pageProps: pageProps, + cacheSnapshot: cacheSnapshot + })}`, + onShellReady: () => { + c.header('Content-Type', 'text/html') + } + } + ) + + function pipeableToReadable(pipeable) { + const passThrough = new PassThrough() + pipeable.pipe(passThrough) + return Readable.toWeb(passThrough) } - ) - return c.body(stream) + const stream = pipeableToReadable(pipableStream) + + return c.body(stream as any) + } } ) - const publicFilesPath = path.resolve(process.cwd(), '.pylon', '__pylon', 'public') + const publicFilesPath = path.resolve( + process.cwd(), + '.pylon', + '__pylon', + 'public' + ) let publicFiles: string[] = [] try { publicFiles = fs.readdirSync(publicFilesPath) } catch (error) { - console.error('Error reading public files', error) + // Ignore error } - - app.on('GET', - publicFiles.map(file => `/${file}`), disableCacheMiddleware, async c => { + app.on( + 'GET', + publicFiles.map(file => `/${file}`), + disableCacheMiddleware, + async c => { const publicFilePath = path.resolve( process.cwd(), '.pylon', @@ -137,7 +180,6 @@ export const setup: Plugin['setup'] = app => { c.req.path.replace('/', '') ) - try { await fs.promises.access(publicFilePath) @@ -151,7 +193,10 @@ export const setup: Plugin['setup'] = app => { c.res.headers.set('Content-Type', 'application/json') } else if (publicFilePath.endsWith('.png')) { c.res.headers.set('Content-Type', 'image/png') - } else if (publicFilePath.endsWith('.jpg') || publicFilePath.endsWith('.jpeg')) { + } else if ( + publicFilePath.endsWith('.jpg') || + publicFilePath.endsWith('.jpeg') + ) { c.res.headers.set('Content-Type', 'image/jpeg') } else if (publicFilePath.endsWith('.gif')) { c.res.headers.set('Content-Type', 'image/gif') @@ -163,15 +208,14 @@ export const setup: Plugin['setup'] = app => { const stream = fs.createReadStream(publicFilePath) - const a = Readable.toWeb(stream) as ReadableStream + const a = Readable.toWeb(stream) as unknown as ReadableStream return c.body(a) - } catch { return c.status(404) } - }) - + } + ) app.get('/__pylon/static/*', disableCacheMiddleware, async c => { const filePath = path.resolve( @@ -208,7 +252,7 @@ export const setup: Plugin['setup'] = app => { const stream = fs.createReadStream(filePath) - const a = Readable.toWeb(stream) as ReadableStream + const a = Readable.toWeb(stream) as unknown as ReadableStream return c.body(a) }) @@ -216,6 +260,8 @@ export const setup: Plugin['setup'] = app => { // Image optimization route app.get('/__pylon/image', async c => { try { + const sharp = (await import('sharp')).default + const { src, w, h, q = '75', format = 'webp' } = c.req.query() const queryStringHash = createHash('sha256') @@ -266,6 +312,11 @@ export const setup: Plugin['setup'] = app => { let imageFormat = format.toLowerCase() + function isSupportedFormat(format: string): format is keyof FormatEnum { + const supportedFormats = sharp.format + return Object.keys(supportedFormats).includes(format) + } + if (!isSupportedFormat(imageFormat)) { throw new Error('Unsupported image format') } @@ -285,22 +336,37 @@ export const setup: Plugin['setup'] = app => { const quality = parseInt(q) - - // Optimize the image using sharp - const image = await sharp(imagePath) - .resize(finalWidth, finalHeight) - .toFormat(imageFormat, { - quality - }) - .toFile(cachePath) - - - c.res.headers.set('Content-Type', getContentType(image.format)) - - // Serve the optimized image - return c.body( - Readable.toWeb(fs.createReadStream(cachePath)) as ReadableStream - ) + if (IS_IMAGE_CACHE_POSSIBLE) { + // Optimize the image using sharp + const image = await sharp(imagePath) + .resize(finalWidth, finalHeight) + .toFormat(imageFormat, { + quality + }) + .toFile(cachePath) + + c.res.headers.set('Content-Type', getContentType(image.format)) + + // Serve the optimized image + return c.body( + Readable.toWeb( + fs.createReadStream(cachePath) + ) as unknown as ReadableStream + ) + } else { + // Optimize the image using sharp + const image = await sharp(imagePath) + .resize(finalWidth, finalHeight) + .toFormat(imageFormat, { + quality + }) + .toBuffer({ resolveWithObject: true }) + + c.res.headers.set('Content-Type', getContentType(image.info.format)) + + // Serve the optimized image + return c.body(image.data as any) + } } catch (error) { console.error('Error processing the image:', error) return c.json({ error: 'Error processing the image' }, 500) @@ -308,14 +374,20 @@ export const setup: Plugin['setup'] = app => { }) } -import sharp, { FormatEnum } from 'sharp' +import type { FormatEnum } from 'sharp' import { createHash } from 'crypto' // Cache directory const IMAGE_CACHE_DIR = path.join(process.cwd(), '.cache/__pylon/images') -// Ensure the cache directory exists -fs.promises.mkdir(IMAGE_CACHE_DIR, { recursive: true }) +let IS_IMAGE_CACHE_POSSIBLE = true + +// Ensure the cache directory exists (if creating files is allowed) +try { + await fs.promises.mkdir(IMAGE_CACHE_DIR, { recursive: true }) +} catch (error) { + IS_IMAGE_CACHE_POSSIBLE = false +} // Helper function to generate the cached image path const getCachedImagePath = ( @@ -351,11 +423,6 @@ const calculateDimensions = ( return { width, height } } -function isSupportedFormat(format: string): format is keyof FormatEnum { - const supportedFormats = sharp.format - return Object.keys(supportedFormats).includes(format) -} - // Helper function to get the correct Content-Type based on the format const getContentType = (format: string) => { switch (format.toLowerCase()) { From 8ccdce2391a6c1364cfcb6d55c55be2575bb1b0f Mon Sep 17 00:00:00 2001 From: Nico Schett Date: Wed, 19 Feb 2025 09:46:44 +0100 Subject: [PATCH 007/138] add config extraction functionality to bundler --- .../pylon-dev/src/builder/bundler/bundler.ts | 5 +- .../src/builder/bundler/extract-config.ts | 72 +++++++++++++++++++ 2 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 packages/pylon-dev/src/builder/bundler/extract-config.ts diff --git a/packages/pylon-dev/src/builder/bundler/bundler.ts b/packages/pylon-dev/src/builder/bundler/bundler.ts index 608f103..33bef67 100644 --- a/packages/pylon-dev/src/builder/bundler/bundler.ts +++ b/packages/pylon-dev/src/builder/bundler/bundler.ts @@ -11,6 +11,7 @@ import { } from './plugins/inject-code-plugin' import {NotifyPluginOptions, notifyPlugin} from './plugins/notify-plugin' import {updateFileIfChanged} from '../update-file-if-changed' +import {extractConfig} from './extract-config' export interface BundlerBuildOptions { getBuildDefs: InjectCodePluginOptions['getBuildDefs'] @@ -27,7 +28,9 @@ export class Bundler { } private async initBuildPlugins(args: {onBuild: () => void}) { - const configPath = path.join(process.cwd(), this.outputDir, 'index.js') + const configPath = path.join(process.cwd(), this.outputDir, 'config.js') + + await extractConfig(this.sfiFilePath, configPath) let config: PylonConfig | undefined try { diff --git a/packages/pylon-dev/src/builder/bundler/extract-config.ts b/packages/pylon-dev/src/builder/bundler/extract-config.ts new file mode 100644 index 0000000..0a78616 --- /dev/null +++ b/packages/pylon-dev/src/builder/bundler/extract-config.ts @@ -0,0 +1,72 @@ +import esbuild from 'esbuild' +import {readFileSync} from 'fs' +import {dirname, resolve} from 'path' +import ts from 'typescript' + +/** + * Extracts the `config` export from a TypeScript file and writes it to `.pylon/config.js` + * @param inputFile The path to the source file (e.g., `server.ts`) + * @param outputFile The path to save the extracted config (default: `.pylon/config.js`) + */ +export async function extractConfig( + inputFile: string, + outputFile: string = '.pylon/config.js' +) { + const filePath = resolve(inputFile) + const source = readFileSync(filePath, 'utf8') + const sourceFile = ts.createSourceFile( + filePath, + source, + ts.ScriptTarget.ESNext, + true + ) + + let configCode = '' + const importStatements: string[] = [] + + // Iterate over the AST nodes + ts.forEachChild(sourceFile, node => { + // Collect import statements + if (ts.isImportDeclaration(node)) { + const importText = node.getFullText(sourceFile).trim() + importStatements.push(importText) + } + + // Find `export const config = {...}` + if ( + ts.isVariableStatement(node) && + node.declarationList.declarations.length > 0 + ) { + const declaration = node.declarationList.declarations[0] + if ( + ts.isIdentifier(declaration.name) && + declaration.name.text === 'config' && + declaration.initializer + ) { + configCode = `export const config = ${declaration.initializer.getText( + sourceFile + )}` + } + } + }) + + if (!configCode) { + return + } + + // Write extracted config to file + const finalConfig = [...importStatements, configCode].join('\n\n') + + await esbuild.build({ + stdin: { + contents: finalConfig, + resolveDir: dirname(filePath), + sourcefile: filePath, + loader: 'ts' + }, + bundle: true, + format: 'esm', + outfile: outputFile, + packages: 'external' + }) +} From 1a39dd6a7110c77434a0323164d72f6a843f5e21 Mon Sep 17 00:00:00 2001 From: Nico Schett Date: Wed, 19 Feb 2025 09:46:54 +0100 Subject: [PATCH 008/138] add esbuild integration for client bundling in buildClient function --- packages/pylon-dev/src/builder/build-client.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/pylon-dev/src/builder/build-client.ts b/packages/pylon-dev/src/builder/build-client.ts index e3e644d..ff6d3a7 100644 --- a/packages/pylon-dev/src/builder/build-client.ts +++ b/packages/pylon-dev/src/builder/build-client.ts @@ -1,6 +1,7 @@ import path from 'path' import fs from 'fs/promises' import {generateClient} from '@gqty/cli' +import esbuild from 'esbuild' import {buildSchema} from 'graphql' import {updateFileIfChanged} from './update-file-if-changed' @@ -54,6 +55,15 @@ export const buildClient = async ({schemaChanged}: BuildClientOptions) => { Object: 'Record' } }) + + await esbuild.build({ + entryPoints: [PYLON_CLIENT_PATH], + bundle: true, + outfile: path.join(process.cwd(), '.pylon/client/index.js'), + packages: 'external', + format: 'esm', + platform: 'node' + }) } const customClientIndex = `/** From 18659331a92062b5060e493118e9d1ea7a63b244 Mon Sep 17 00:00:00 2001 From: Nico Schett Date: Wed, 19 Feb 2025 11:50:37 +0100 Subject: [PATCH 009/138] create-pylon: enhance logging, simplify flags, and improve error handling --- .changeset/slimy-garlics-battle.md | 13 + packages/create-pylon/package.json | 5 +- .../src/create-directory/files.ts | 604 ++++++++++++++++++ .../src/create-directory/index.ts | 592 +++++++++++++++++ packages/create-pylon/src/detect-pm.ts | 145 ----- packages/create-pylon/src/index.ts | 566 ++++------------ .../create-pylon/src/install-pkg/detect.ts | 52 ++ .../create-pylon/src/install-pkg/index.ts | 2 + .../create-pylon/src/install-pkg/install.ts | 69 ++ .../templates/bun/default/Dockerfile | 47 -- .../templates/bun/default/package.json | 23 - .../templates/bun/default/src/index.ts | 12 - .../templates/bun/default/tsconfig.json | 8 - .../templates/cf-workers/default/package.json | 25 - .../templates/cf-workers/default/src/index.ts | 12 - .../cf-workers/default/tsconfig.json | 4 - .../default/worker-configuration.d.ts | 3 - .../cf-workers/default/wrangler.toml | 108 ---- .../deno/default/.vscode/extensions.json | 5 - .../deno/default/.vscode/settings.json | 6 - .../templates/deno/default/Dockerfile | 12 - .../templates/deno/default/deno.json | 15 - .../templates/deno/default/src/index.ts | 17 - .../templates/node/default/Dockerfile | 45 -- .../templates/node/default/package.json | 23 - .../templates/node/default/src/index.ts | 15 - .../templates/shared/.dockerignore | 15 - .../shared/.github/workflows/publish.yaml | 43 -- .../templates/shared/.gitignore.example | 175 ----- .../create-pylon/templates/shared/pylon.d.ts | 7 - .../templates/shared/tsconfig.json | 4 - pnpm-lock.yaml | 403 ++++++------ 32 files changed, 1654 insertions(+), 1421 deletions(-) create mode 100644 .changeset/slimy-garlics-battle.md create mode 100644 packages/create-pylon/src/create-directory/files.ts create mode 100644 packages/create-pylon/src/create-directory/index.ts delete mode 100644 packages/create-pylon/src/detect-pm.ts create mode 100644 packages/create-pylon/src/install-pkg/detect.ts create mode 100644 packages/create-pylon/src/install-pkg/index.ts create mode 100644 packages/create-pylon/src/install-pkg/install.ts delete mode 100644 packages/create-pylon/templates/bun/default/Dockerfile delete mode 100644 packages/create-pylon/templates/bun/default/package.json delete mode 100644 packages/create-pylon/templates/bun/default/src/index.ts delete mode 100644 packages/create-pylon/templates/bun/default/tsconfig.json delete mode 100644 packages/create-pylon/templates/cf-workers/default/package.json delete mode 100644 packages/create-pylon/templates/cf-workers/default/src/index.ts delete mode 100644 packages/create-pylon/templates/cf-workers/default/tsconfig.json delete mode 100644 packages/create-pylon/templates/cf-workers/default/worker-configuration.d.ts delete mode 100644 packages/create-pylon/templates/cf-workers/default/wrangler.toml delete mode 100644 packages/create-pylon/templates/deno/default/.vscode/extensions.json delete mode 100644 packages/create-pylon/templates/deno/default/.vscode/settings.json delete mode 100644 packages/create-pylon/templates/deno/default/Dockerfile delete mode 100644 packages/create-pylon/templates/deno/default/deno.json delete mode 100644 packages/create-pylon/templates/deno/default/src/index.ts delete mode 100644 packages/create-pylon/templates/node/default/Dockerfile delete mode 100644 packages/create-pylon/templates/node/default/package.json delete mode 100644 packages/create-pylon/templates/node/default/src/index.ts delete mode 100644 packages/create-pylon/templates/shared/.dockerignore delete mode 100644 packages/create-pylon/templates/shared/.github/workflows/publish.yaml delete mode 100644 packages/create-pylon/templates/shared/.gitignore.example delete mode 100644 packages/create-pylon/templates/shared/pylon.d.ts delete mode 100644 packages/create-pylon/templates/shared/tsconfig.json diff --git a/.changeset/slimy-garlics-battle.md b/.changeset/slimy-garlics-battle.md new file mode 100644 index 0000000..6064894 --- /dev/null +++ b/.changeset/slimy-garlics-battle.md @@ -0,0 +1,13 @@ +--- +'create-pylon': patch +--- + +- Use `consola` for clearer interactive prompts and logs. +- Remove `--client`, `--client-path`, and `--client-port` flags in favor of [GQty CLI](https://gqty.dev/api-reference/cli#basic-usage) +- Improved package manager detection and dependency installation. https://github.com/getcronit/pylon/issues/73 +- Removed `--template` flag in favor of `--features` flag. Each runtime can now support multiple features which pre-configure the project for different use-cases. + Currently supported features: + - `pages`: React SSR Pages with file-based routing + - `auth`: OIDC Authentication (Primarily for ZITADEL but can be used with any OIDC provider) +- The success message now only shows the `deploy` script if it is available. +- Improved error handling and messaging. diff --git a/packages/create-pylon/package.json b/packages/create-pylon/package.json index 93e2279..73145f2 100644 --- a/packages/create-pylon/package.json +++ b/packages/create-pylon/package.json @@ -20,12 +20,11 @@ }, "homepage": "https://pylon.cronit.io", "dependencies": { - "@getcronit/pylon-telemetry": "workspace:^", - "@inquirer/prompts": "^5.4.0", "chalk": "^5.3.0", "commander": "^12.1.0", "consola": "^3.2.3", - "hono": "^4" + "package-manager-detector": "^0.2.9", + "tinyexec": "^0.3.2" }, "engines": { "node": ">=18.0.0" diff --git a/packages/create-pylon/src/create-directory/files.ts b/packages/create-pylon/src/create-directory/files.ts new file mode 100644 index 0000000..a0b5ba0 --- /dev/null +++ b/packages/create-pylon/src/create-directory/files.ts @@ -0,0 +1,604 @@ +import {Runtime} from '.' + +const pylonVersion = '^2.0.0' +const pylonDevVersion = '^1.0.0' + +export const files: { + [key in Runtime | 'ALL']: { + path: string + content: string + specificRuntimes?: Runtime[] + }[] +} = { + ALL: [ + { + path: '.gitignore', + content: `# Logs + +logs +_.log +npm-debug.log_ +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) + +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# Runtime data + +pids +_.pid +_.seed +\*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover + +lib-cov + +# Coverage directory used by tools like istanbul + +coverage +\*.lcov + +# nyc test coverage + +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) + +.grunt + +# Bower dependency directory (https://bower.io/) + +bower_components + +# node-waf configuration + +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) + +build/Release + +# Dependency directories + +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) + +web_modules/ + +# TypeScript cache + +\*.tsbuildinfo + +# Optional npm cache directory + +.npm + +# Optional eslint cache + +.eslintcache + +# Optional stylelint cache + +.stylelintcache + +# Microbundle cache + +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history + +.node_repl_history + +# Output of 'npm pack' + +\*.tgz + +# Yarn Integrity file + +.yarn-integrity + +# dotenv environment variable files + +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) + +.cache +.parcel-cache + +# Next.js build output + +.next +out + +# Nuxt.js build / generate output + +.nuxt +dist + +# Gatsby files + +.cache/ + +# Comment in the public line in if your project uses Gatsby and not Next.js + +# https://nextjs.org/blog/next-9-1#public-directory-support + +# public + +# vuepress build output + +.vuepress/dist + +# vuepress v2.x temp and cache directory + +.temp +.cache + +# Docusaurus cache and generated files + +.docusaurus + +# Serverless directories + +.serverless/ + +# FuseBox cache + +.fusebox/ + +# DynamoDB Local files + +.dynamodb/ + +# TernJS port file + +.tern-port + +# Stores VSCode versions used for testing VSCode extensions + +.vscode-test + +# yarn v2 + +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.\* + +# wrangler project + +.dev.vars +.wrangler/ + +# Pylon project +.pylon +` + }, + { + path: '.dockerignore', + content: `node_modules +Dockerfile* +docker-compose* +.dockerignore +.git +.gitignore +README.md +LICENSE +.vscode +Makefile +helm-charts +.env +.editorconfig +.idea +coverage* +`, + specificRuntimes: ['node', 'bun'] + }, + { + path: '.github/workflows/publish.yml', + content: `name: publish + +on: [push] +env: + IMAGE_NAME: __PYLON_NAME__ + +jobs: + # Push image to GitHub Packages. + # See also https://docs.docker.com/docker-hub/builds/ + publish-container: + runs-on: ubuntu-latest + permissions: + packages: write + contents: read + + steps: + - uses: actions/checkout@v4 + + - name: Build image + run: docker build . --file Dockerfile --tag $IMAGE_NAME + + - name: Log into registry + run: echo "\${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u \${{ github.actor }} --password-stdin + + - name: Push image + run: | + IMAGE_ID=ghcr.io/\${{ github.repository_owner }}/$IMAGE_NAME + + # Change all uppercase to lowercase + IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]') + # Strip git ref prefix from version + VERSION=$(echo "\${{ github.ref }}" | sed -e 's,.*/\\(.*\\),\\1,') + # Strip "v" prefix from tag name + [[ "\${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//') + # Use Docker \`latest\` tag convention + [ "$VERSION" == "main" ] && VERSION=latest + echo IMAGE_ID=$IMAGE_ID + echo VERSION=$VERSION + docker tag $IMAGE_NAME $IMAGE_ID:$VERSION + docker push $IMAGE_ID:$VERSION + +# SPDX-License-Identifier: (EUPL-1.2) +# Copyright © 2024 cronit KG`, + specificRuntimes: ['node', 'bun'] + } + ], + bun: [ + { + path: 'package.json', + content: `{ + "name": "__PYLON_NAME__", + "private": true, + "version": "0.0.1", + "type": "module", + "description": "Generated with \`npm create pylon\`", + "scripts": { + "dev": "pylon dev -c \\"bun run .pylon/index.js\\"", + "build": "pylon build" + }, + "dependencies": { + "@getcronit/pylon": "${pylonVersion}", + }, + "devDependencies": { + "@getcronit/pylon-dev": "${pylonDevVersion}", + "@types/bun": "^1.0.0" + }, + "repository": { + "type": "git", + "url": "https://github.com/getcronit/pylon.git" + }, + "homepage": "https://pylon.cronit.io", + "packageManager": "bun" +} +` + }, + { + path: 'Dockerfile', + content: `# use the official Bun image +# see all versions at https://hub.docker.com/r/oven/bun/tags +FROM oven/bun:1 as base + +LABEL description="Offical docker image for Pylon services (Bun)" +LABEL org.opencontainers.image.source="https://github.com/getcronit/pylon" +LABEL maintainer="office@cronit.io" + +WORKDIR /usr/src/pylon + + +# install dependencies into temp directory +# this will cache them and speed up future builds +FROM base AS install +RUN mkdir -p /temp/dev +COPY package.json bun.lockb /temp/dev/ +RUN cd /temp/dev && bun install --frozen-lockfile + +# install with --production (exclude devDependencies) +RUN mkdir -p /temp/prod +COPY package.json bun.lockb /temp/prod/ +RUN cd /temp/prod && bun install --frozen-lockfile --production + +# copy node_modules from temp directory +# then copy all (non-ignored) project files into the image +FROM install AS prerelease +COPY --from=install /temp/dev/node_modules node_modules +COPY . . + +# [optional] tests & build +ENV NODE_ENV=production + +# Create .pylon folder (mkdir) +RUN mkdir -p .pylon +# RUN bun test +RUN bun run pylon build + +# copy production dependencies and source code into final image +FROM base AS release +COPY --from=install /temp/prod/node_modules node_modules +COPY --from=prerelease /usr/src/pylon/.pylon .pylon +COPY --from=prerelease /usr/src/pylon/package.json . + +# run the app +USER bun +EXPOSE 3000/tcp +ENTRYPOINT [ "bun", "run", "/usr/src/pylon/.pylon/index.js" ] +` + } + ], + node: [ + { + path: 'package.json', + content: `{ + "name": "__PYLON_NAME__", + "private": true, + "version": "0.0.1", + "type": "module", + "description": "Generated with \`npm create pylon\`", + "scripts": { + "dev": "pylon dev -c \\"node --enable-source-maps .pylon/index.js\\"", + "build": "pylon build" + }, + "dependencies": { + "@getcronit/pylon": "${pylonVersion}", + "@hono/node-server": "^1.12.2" + }, + "devDependencies": { + "@getcronit/pylon-dev": "${pylonDevVersion}" + }, + "repository": { + "type": "git", + "url": "https://github.com/getcronit/pylon.git" + }, + "homepage": "https://pylon.cronit.io" +} +` + }, + { + path: 'Dockerfile', + content: `# Use the official Node.js 20 image as the base +FROM node:20-alpine as base + +LABEL description="Offical docker image for Pylon services (Node.js)" +LABEL org.opencontainers.image.source="https://github.com/getcronit/pylon" +LABEL maintainer="office@cronit.io" + +WORKDIR /usr/src/pylon + +# install dependencies into a temp directory +# this will cache them and speed up future builds +FROM base AS install +RUN mkdir -p /temp/dev +COPY package.json package-lock.json /temp/dev/ +RUN cd /temp/dev && npm ci + +# install with --production (exclude devDependencies) +RUN mkdir -p /temp/prod +COPY package.json package-lock.json /temp/prod/ +RUN cd /temp/prod && npm ci --only=production + +# copy node_modules from temp directory +# then copy all (non-ignored) project files into the image +FROM install AS prerelease +COPY --from=install /temp/dev/node_modules node_modules +COPY . . + +# [optional] tests & build +ENV NODE_ENV=production + +# Create .pylon folder (mkdir) +RUN mkdir -p .pylon +# RUN npm test +RUN npm run pylon build + +# copy production dependencies and source code into final image +FROM base AS release +COPY --from=install /temp/prod/node_modules node_modules +COPY --from=prerelease /usr/src/pylon/.pylon .pylon +COPY --from=prerelease /usr/src/pylon/package.json . + +# run the app +USER node +EXPOSE 3000/tcp +ENTRYPOINT [ "node", "/usr/src/pylon/.pylon/index.js" ] +` + } + ], + 'cf-workers': [ + { + path: 'package.json', + content: `{ + "name": "__PYLON_NAME__", + "type": "module", + "description": "Generated with \`npm create pylon\`", + "version": "0.0.1", + "private": true, + "scripts": { + "deploy": "pylon build && wrangler deploy", + "dev": "pylon dev -c \\"wrangler dev\\"", + "cf-typegen": "wrangler types" + }, + "dependencies": { + "@getcronit/pylon": "${pylonVersion}", + }, + "devDependencies": { + "@getcronit/pylon-dev": "${pylonDevVersion}", + "@cloudflare/vitest-pool-workers": "^0.4.5", + "@cloudflare/workers-types": "^4.20240903.0", + "typescript": "^5.5.2", + "wrangler": "^3.60.3" + }, + "repository": { + "type": "git", + "url": "https://github.com/getcronit/pylon.git" + }, + "homepage": "https://pylon.cronit.io" +} +` + }, + { + path: 'wrangler.toml', + content: `#:schema node_modules/wrangler/config-schema.json +name = "__PYLON_NAME__" +main = ".pylon/index.js" +compatibility_date = "2024-09-03" +compatibility_flags = ["nodejs_compat_v2"] + +# Automatically place your workloads in an optimal location to minimize latency. +# If you are running back-end logic in a Worker, running it closer to your back-end infrastructure +# rather than the end user may result in better performance. +# Docs: https://developers.cloudflare.com/workers/configuration/smart-placement/#smart-placement +# [placement] +# mode = "smart" + +# Variable bindings. These are arbitrary, plaintext strings (similar to environment variables) +# Docs: +# - https://developers.cloudflare.com/workers/wrangler/configuration/#environment-variables +# Note: Use secrets to store sensitive data. +# - https://developers.cloudflare.com/workers/configuration/secrets/ +# [vars] +# MY_VARIABLE = "production_value" + +# Bind the Workers AI model catalog. Run machine learning models, powered by serverless GPUs, on Cloudflare’s global network +# Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#workers-ai +# [ai] +# binding = "AI" + +# Bind an Analytics Engine dataset. Use Analytics Engine to write analytics within your Pages Function. +# Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#analytics-engine-datasets +# [[analytics_engine_datasets]] +# binding = "MY_DATASET" + +# Bind a headless browser instance running on Cloudflare's global network. +# Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#browser-rendering +# [browser] +# binding = "MY_BROWSER" + +# Bind a D1 database. D1 is Cloudflare’s native serverless SQL database. +# Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#d1-databases +# [[d1_databases]] +# binding = "MY_DB" +# database_name = "my-database" +# database_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + +# Bind a dispatch namespace. Use Workers for Platforms to deploy serverless functions programmatically on behalf of your customers. +# Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#dispatch-namespace-bindings-workers-for-platforms +# [[dispatch_namespaces]] +# binding = "MY_DISPATCHER" +# namespace = "my-namespace" + +# Bind a Durable Object. Durable objects are a scale-to-zero compute primitive based on the actor model. +# Durable Objects can live for as long as needed. Use these when you need a long-running "server", such as in realtime apps. +# Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#durable-objects +# [[durable_objects.bindings]] +# name = "MY_DURABLE_OBJECT" +# class_name = "MyDurableObject" + +# Durable Object migrations. +# Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#migrations +# [[migrations]] +# tag = "v1" +# new_classes = ["MyDurableObject"] + +# Bind a Hyperdrive configuration. Use to accelerate access to your existing databases from Cloudflare Workers. +# Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#hyperdrive +# [[hyperdrive]] +# binding = "MY_HYPERDRIVE" +# id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + +# Bind a KV Namespace. Use KV as persistent storage for small key-value pairs. +# Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#kv-namespaces +# [[kv_namespaces]] +# binding = "MY_KV_NAMESPACE" +# id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + +# Bind an mTLS certificate. Use to present a client certificate when communicating with another service. +# Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#mtls-certificates +# [[mtls_certificates]] +# binding = "MY_CERTIFICATE" +# certificate_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + +# Bind a Queue producer. Use this binding to schedule an arbitrary task that may be processed later by a Queue consumer. +# Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#queues +# [[queues.producers]] +# binding = "MY_QUEUE" +# queue = "my-queue" + +# Bind a Queue consumer. Queue Consumers can retrieve tasks scheduled by Producers to act on them. +# Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#queues +# [[queues.consumers]] +# queue = "my-queue" + +# Bind an R2 Bucket. Use R2 to store arbitrarily large blobs of data, such as files. +# Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#r2-buckets +# [[r2_buckets]] +# binding = "MY_BUCKET" +# bucket_name = "my-bucket" + +# Bind another Worker service. Use this binding to call another Worker without network overhead. +# Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#service-bindings +# [[services]] +# binding = "MY_SERVICE" +# service = "my-service" + +# Bind a Vectorize index. Use to store and query vector embeddings for semantic search, classification and other vector search use-cases. +# Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#vectorize-indexes +# [[vectorize]] +# binding = "MY_INDEX" +# index_name = "my-index" +` + } + ], + deno: [ + { + path: '.vscode/settings.json', + content: `{ + "deno.enablePaths": [ + "./" + ], + "editor.inlayHints.enabled": "off" +}` + }, + { + path: '.vscode/extensions.json', + content: `{ + "recommendations": [ + "denoland.vscode-deno" + ] +}` + }, + { + path: 'deno.json', + content: `{ + "imports": { + "@getcronit/pylon-dev": "npm:@getcronit/pylon-dev@${pylonDevVersion}", + "@getcronit/pylon": "npm:@getcronit/pylon@${pylonVersion}" + }, + "tasks": { + "dev": "pylon dev -c \\"deno run -A .pylon/index.js --config tsconfig.json\\"", + "build": "pylon build" + }, + "compilerOptions": { + "jsx": "precompile", + "jsxImportSource": "hono/jsx" + }, + "nodeModulesDir": "auto", + "packageManager": "deno" +} +` + } + ] +} diff --git a/packages/create-pylon/src/create-directory/index.ts b/packages/create-pylon/src/create-directory/index.ts new file mode 100644 index 0000000..c2ee111 --- /dev/null +++ b/packages/create-pylon/src/create-directory/index.ts @@ -0,0 +1,592 @@ +import fs from 'fs/promises' +import path from 'path' +import {files} from './files' + +export const runtimes = [ + { + key: 'bun', + name: 'Bun.js', + website: 'https://bunjs.dev', + supportedFeatures: ['auth', 'pages'] + }, + { + key: 'node', + name: 'Node.js', + website: 'https://nodejs.org', + supportedFeatures: ['auth', 'pages'] + }, + { + key: 'cf-workers', + name: 'Cloudflare Workers', + website: 'https://workers.cloudflare.com', + supportedFeatures: ['auth'] + }, + { + key: 'deno', + name: 'Deno', + website: 'https://deno.land' + } +] + +export const features = [ + { + key: 'auth', + name: 'Authentication', + website: 'https://pylon.cronit.io/docs/authentication' + }, + { + key: 'pages', + name: 'Pages', + website: 'https://pylon.cronit.io/docs/pages' + } +] + +export type Runtime = (typeof runtimes)[number]['key'] +export type Feature = (typeof features)[number]['key'] + +interface CreateDirectoryOptions { + variables: Record + destination: string + runtime: Runtime + features: Feature[] +} + +const makeIndexFile = (runtime: Runtime, features: Feature[]) => { + const pylonImports: string[] = ['app', 'PylonConfig'] + const pylonConfigPlugins: string[] = [] + + if (features.includes('auth')) { + pylonImports.push('useAuth') + pylonConfigPlugins.push( + "useAuth({issuer: 'https://test-0o6zvq.zitadel.cloud'})" + ) + } + + if (features.includes('pages')) { + pylonImports.push('usePages') + pylonConfigPlugins.push('usePages()') + } + + let content: string = '' + + // Add imports + content += `import {${pylonImports.join(', ')}} from '@getcronit/pylon'\n\n` + + if (runtime === 'node') { + content += `import {serve} from '@hono/node-server'\n` + } + + content += '\n\n' + + // Add graphql + content += `export const graphql = { + Query: { + hello: () => { + return 'Hello, world!' + } + }, + Mutation: {} +}` + + content += '\n\n' + + if (runtime === 'bun' || runtime === 'cf-workers') { + content += `export default app` + } else if (runtime === 'node') { + content += `serve(app, info => { + console.log(\`Server running at \${info.port}\`) +})` + } else if (runtime === 'deno') { + content += `Deno.serve({port: 3000}, app.fetch) +` + } + + content += '\n\n' + + content += `export const config: PylonConfig = { + plugins: [${pylonConfigPlugins.join(', ')}] +}` + + return content +} + +const makePylonDefinition = async (runtime: Runtime, features: Feature[]) => { + let data = `import '@getcronit/pylon' + +declare module '@getcronit/pylon' { + interface Bindings {} + + interface Variables {} +} + + +` + + if (features.includes('pages')) { + data += `import {useQuery} from './.pylon/client' + +declare module '@getcronit/pylon/pages' { + interface PageData extends ReturnType {} +}` + } + + return data +} + +const makeTsConfig = async (runtime: Runtime, features: Feature[]) => { + const data: any = { + extends: '@getcronit/pylon/tsconfig.pylon.json', + include: ['pylon.d.ts', 'src/**/*.ts'] + } + + if (runtime === 'cf-workers') { + data.include.push('worker-configuration.d.ts') + } + + if (features.includes('pages')) { + data.compilerOptions = { + baseUrl: '.', + paths: { + '@/*': ['./*'] + }, + jsx: 'react-jsx' // support JSX + } + + data.include.push('pages', 'components', '.pylon') + } + + return JSON.stringify(data, null, 2) +} + +const injectPagesFeatureFiles = async ( + files: { + path: string + content: string + }[] +) => { + const pagesFiles = [ + { + path: 'pages/layout.tsx', + content: `import '../globals.css' + +export default function RootLayout({children}: {children: React.ReactNode}) { + return ( + + {children} + + ) +} +` + }, + { + path: 'pages/page.tsx', + content: `import { Button } from '@/components/ui/button' +import { PageProps } from '@getcronit/pylon/pages' + +const Page: React.FC = props => { + return ( +
+ {props.data.hello} + +
+ ) +} + +export default Page +` + }, + { + path: 'globals.css', + content: `@import 'tailwindcss'; + +@plugin 'tailwindcss-animate'; + +@custom-variant dark (&:is(.dark *)); + +@theme { + --color-background: hsl(var(--background)); + --color-foreground: hsl(var(--foreground)); + + --color-card: hsl(var(--card)); + --color-card-foreground: hsl(var(--card-foreground)); + + --color-popover: hsl(var(--popover)); + --color-popover-foreground: hsl(var(--popover-foreground)); + + --color-primary: hsl(var(--primary)); + --color-primary-foreground: hsl(var(--primary-foreground)); + + --color-secondary: hsl(var(--secondary)); + --color-secondary-foreground: hsl(var(--secondary-foreground)); + + --color-muted: hsl(var(--muted)); + --color-muted-foreground: hsl(var(--muted-foreground)); + + --color-accent: hsl(var(--accent)); + --color-accent-foreground: hsl(var(--accent-foreground)); + + --color-destructive: hsl(var(--destructive)); + --color-destructive-foreground: hsl(var(--destructive-foreground)); + + --color-border: hsl(var(--border)); + --color-input: hsl(var(--input)); + --color-ring: hsl(var(--ring)); + + --color-chart-1: hsl(var(--chart-1)); + --color-chart-2: hsl(var(--chart-2)); + --color-chart-3: hsl(var(--chart-3)); + --color-chart-4: hsl(var(--chart-4)); + --color-chart-5: hsl(var(--chart-5)); + + --color-sidebar: hsl(var(--sidebar-background)); + --color-sidebar-foreground: hsl(var(--sidebar-foreground)); + --color-sidebar-primary: hsl(var(--sidebar-primary)); + --color-sidebar-primary-foreground: hsl(var(--sidebar-primary-foreground)); + --color-sidebar-accent: hsl(var(--sidebar-accent)); + --color-sidebar-accent-foreground: hsl(var(--sidebar-accent-foreground)); + --color-sidebar-border: hsl(var(--sidebar-border)); + --color-sidebar-ring: hsl(var(--sidebar-ring)); + + --radius-lg: var(--radius); + --radius-md: calc(var(--radius) - 2px); + --radius-sm: calc(var(--radius) - 4px); + + --animate-accordion-down: accordion-down 0.2s ease-out; + --animate-accordion-up: accordion-up 0.2s ease-out; + + @keyframes accordion-down { + from { + height: 0; + } + to { + height: var(--radix-accordion-content-height); + } + } + @keyframes accordion-up { + from { + height: var(--radix-accordion-content-height); + } + to { + height: 0; + } + } +} + +/* + The default border color has changed to \`currentColor\` in Tailwind CSS v4, + so we've added these compatibility styles to make sure everything still + looks the same as it did with Tailwind CSS v3. + + If we ever want to remove these styles, we need to add an explicit border + color utility to any element that depends on these defaults. +*/ +@layer base { + *, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + border-color: var(--color-gray-200, currentColor); + } +} + +@layer utilities { + body { + font-family: Arial, Helvetica, sans-serif; + } +} + +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 0 0% 3.9%; + --card: 0 0% 100%; + --card-foreground: 0 0% 3.9%; + --popover: 0 0% 100%; + --popover-foreground: 0 0% 3.9%; + --primary: 0 0% 9%; + --primary-foreground: 0 0% 98%; + --secondary: 0 0% 96.1%; + --secondary-foreground: 0 0% 9%; + --muted: 0 0% 96.1%; + --muted-foreground: 0 0% 45.1%; + --accent: 0 0% 96.1%; + --accent-foreground: 0 0% 9%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 0 0% 98%; + --border: 0 0% 89.8%; + --input: 0 0% 89.8%; + --ring: 0 0% 3.9%; + --chart-1: 12 76% 61%; + --chart-2: 173 58% 39%; + --chart-3: 197 37% 24%; + --chart-4: 43 74% 66%; + --chart-5: 27 87% 67%; + --radius: 0.5rem; + --sidebar-background: 0 0% 98%; + --sidebar-foreground: 240 5.3% 26.1%; + --sidebar-primary: 240 5.9% 10%; + --sidebar-primary-foreground: 0 0% 98%; + --sidebar-accent: 240 4.8% 95.9%; + --sidebar-accent-foreground: 240 5.9% 10%; + --sidebar-border: 220 13% 91%; + --sidebar-ring: 217.2 91.2% 59.8%; + } + .dark { + --background: 0 0% 3.9%; + --foreground: 0 0% 98%; + --card: 0 0% 3.9%; + --card-foreground: 0 0% 98%; + --popover: 0 0% 3.9%; + --popover-foreground: 0 0% 98%; + --primary: 0 0% 98%; + --primary-foreground: 0 0% 9%; + --secondary: 0 0% 14.9%; + --secondary-foreground: 0 0% 98%; + --muted: 0 0% 14.9%; + --muted-foreground: 0 0% 63.9%; + --accent: 0 0% 14.9%; + --accent-foreground: 0 0% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 0 0% 98%; + --border: 0 0% 14.9%; + --input: 0 0% 14.9%; + --ring: 0 0% 83.1%; + --chart-1: 220 70% 50%; + --chart-2: 160 60% 45%; + --chart-3: 30 80% 55%; + --chart-4: 280 65% 60%; + --chart-5: 340 75% 55%; + --sidebar-background: 240 5.9% 10%; + --sidebar-foreground: 240 4.8% 95.9%; + --sidebar-primary: 224.3 76.3% 48%; + --sidebar-primary-foreground: 0 0% 100%; + --sidebar-accent: 240 3.7% 15.9%; + --sidebar-accent-foreground: 240 4.8% 95.9%; + --sidebar-border: 240 3.7% 15.9%; + --sidebar-ring: 217.2 91.2% 59.8%; + } +} + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } +} + +/* + ---break--- +*/ + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } +} +` + }, + { + path: 'postcss.config.js', + content: `import tailwindPostCss from '@tailwindcss/postcss' + +export default { + plugins: [tailwindPostCss] +} +` + }, + { + path: 'components.json', + content: `{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "tailwind.config.js", + "css": "globals.css", + "baseColor": "zinc", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +}` + }, + { + path: 'lib/utils.ts', + content: `import {clsx, type ClassValue} from 'clsx' +import {twMerge} from 'tailwind-merge' + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +}` + }, + { + path: 'components/ui/button.tsx', + content: `import * as React from 'react' +import {Slot} from '@radix-ui/react-slot' +import {cva, type VariantProps} from 'class-variance-authority' + +import {cn} from '@/lib/utils' + +const buttonVariants = cva( + "inline-flexxx items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-[color,box-shadow] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 ring-ring/10 dark:ring-ring/20 dark:outline-ring/40 outline-ring/50 focus-visible:ring-4 focus-visible:outline-1 aria-invalid:focus-visible:ring-0", + { + variants: { + variant: { + default: + 'bg-primary text-primary-foreground shadow-sm hover:bg-primary/90', + destructive: + 'bg-destructive text-destructive-foreground shadow-xs hover:bg-destructive/90', + outline: + 'border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground', + secondary: + 'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80', + ghost: 'hover:bg-accent hover:text-accent-foreground', + link: 'text-primary underline-offset-4 hover:underline' + }, + size: { + default: 'h-9 px-4 py-2 has-[>svg]:px-3', + sm: 'h-8 rounded-md px-3 has-[>svg]:px-2.5', + lg: 'h-10 rounded-md px-6 has-[>svg]:px-4', + icon: 'size-9' + } + }, + defaultVariants: { + variant: 'default', + size: 'default' + } + } +) + +function Button({ + className, + variant, + size, + asChild = false, + ...props +}: React.ComponentProps<'button'> & + VariantProps & { + asChild?: boolean + }) { + const Comp = asChild ? Slot : 'button' + + return ( + + ) +} + +export {Button, buttonVariants} +` + } + ] + + files.push(...pagesFiles) + + // Overwrite the package.json file and add the necessary dependencies + + const packageJsonFile = files.find(file => file.path === 'package.json') + + if (packageJsonFile) { + const packageJson = JSON.parse(packageJsonFile.content) + + packageJson.dependencies = { + ...packageJson.dependencies, + '@gqty/react': '^3.1.0', + gqty: '^3.4.0', + '@radix-ui/react-slot': '^1.1.2', + 'class-variance-authority': '^0.7.1', + clsx: '^2.1.1', + 'lucide-react': '^0.474.0', + react: '^19.0.0', + 'react-dom': '^19.0.0', + 'tailwind-merge': '^3.0.1', + tailwindcss: '^4.0.4', + 'tailwindcss-animate': '^1.0.7' + } + + packageJson.devDependencies = { + ...packageJson.devDependencies, + '@tailwindcss/postcss': '^4.0.6', + '@types/react': '^19.0.8' + } + + packageJsonFile.content = JSON.stringify(packageJson, null, 2) + } + + return files +} + +const injectVariablesInContent = ( + content: string, + variables: Record +) => { + let result = content + + Object.entries(variables).forEach(([key, value]) => { + result = result.replaceAll(key, value) + }) + + return result +} + +export const createDirectory = async (options: CreateDirectoryOptions) => { + const {destination, runtime, features} = options + + let runtimeFiles = files.ALL.concat(files[runtime] || []).filter(file => { + if (!file.specificRuntimes) { + return true + } + + return file.specificRuntimes.includes(runtime) + }) + + const indexFile = makeIndexFile(runtime, features) + const tsConfig = await makeTsConfig(runtime, features) + const pylonDefinition = await makePylonDefinition(runtime, features) + + runtimeFiles.push( + { + path: 'tsconfig.json', + content: tsConfig + }, + { + path: 'pylon.d.ts', + content: pylonDefinition + }, + { + path: 'src/index.ts', + content: indexFile + } + ) + + if (features.includes('pages')) { + runtimeFiles = await injectPagesFeatureFiles(runtimeFiles) + } + + for (const file of runtimeFiles) { + const filePath = path.join(destination, file.path) + + await fs.mkdir(path.dirname(filePath), {recursive: true}) + await fs.writeFile( + filePath, + injectVariablesInContent(file.content, options.variables) + ) + } +} diff --git a/packages/create-pylon/src/detect-pm.ts b/packages/create-pylon/src/detect-pm.ts deleted file mode 100644 index 1c1c86a..0000000 --- a/packages/create-pylon/src/detect-pm.ts +++ /dev/null @@ -1,145 +0,0 @@ -import * as fs from 'node:fs' -import * as path from 'node:path' -import process from 'node:process' -import {execSync} from 'node:child_process' -import consola from 'consola' - -// Helper function to check if a command exists -function isCommandAvailable(command: string): boolean { - try { - execSync(`${command} --version`, {stdio: 'ignore'}) - return true - } catch (e) { - console.error(e) - return false - } -} - -// Detect Bun -function isBun(): boolean { - // @ts-ignore: Bun may not be defined - return typeof Bun !== 'undefined' && isCommandAvailable('bun') -} - -// Detect npm -function isNpm(): boolean { - return process.env.npm_execpath?.includes('npm') ?? false -} - -// Detect Yarn -function isYarn(): boolean { - return process.env.npm_execpath?.includes('yarn') ?? false -} - -// Detect Deno -function isDeno(): boolean { - // @ts-ignore: Deno may not be defined - return typeof Deno !== 'undefined' && isCommandAvailable('deno') -} - -// Detect pnpm -function isPnpm(): boolean { - return process.env.npm_execpath?.includes('pnpm') ?? false -} - -// Detect based on lock files -function detectByLockFiles(cwd: string): PackageManager | null { - if (fs.existsSync(path.join(cwd, 'bun.lockb'))) { - return 'bun' - } - if (fs.existsSync(path.join(cwd, 'package-lock.json'))) { - return 'npm' - } - if (fs.existsSync(path.join(cwd, 'yarn.lock'))) { - return 'yarn' - } - if ( - fs.existsSync(path.join(cwd, 'deno.json')) || - fs.existsSync(path.join(cwd, 'deno.lock')) - ) { - return 'deno' - } - if (fs.existsSync(path.join(cwd, 'pnpm-lock.yaml'))) { - return 'pnpm' - } - return null -} - -export type PackageManager = - | 'bun' - | 'npm' - | 'yarn' - | 'pnpm' - | 'deno' - | 'unknown' - -// Main detection function -export function detectPackageManager({ - preferredPm, - cwd = process.cwd() -}: { - preferredPm?: PackageManager - cwd?: string -}): PackageManager { - // Check the preferred package manager first - if (preferredPm && isCommandAvailable(preferredPm)) { - return preferredPm - } - - // Proceed with detection logic - if (isBun()) { - return 'bun' - } - if (isNpm()) { - return 'npm' - } - if (isPnpm()) { - return 'pnpm' - } - if (isDeno()) { - return 'deno' - } - if (isYarn()) { - return 'yarn' - } - - // Fallback to lock file detection - const lockFileDetection = detectByLockFiles(cwd) - if (lockFileDetection) { - consola.info(`Detected package manager by lock file: ${lockFileDetection}`) - if (isCommandAvailable(lockFileDetection)) { - return lockFileDetection - } else { - consola.warn( - `Lock file detected, but ${lockFileDetection} is not installed.` - ) - } - } - - return 'unknown' -} - -type PackageManagerScript = - | 'bun' - | 'npm run' - | 'yarn' - | 'pnpm run' - | 'deno task' - -// Run script detection -export function getRunScript(pm: PackageManager): PackageManagerScript { - switch (pm) { - case 'bun': - return 'bun' - case 'npm': - return 'npm run' - case 'yarn': - return 'yarn' - case 'pnpm': - return 'pnpm run' - case 'deno': - return 'deno task' - default: - throw new Error('Unknown package manager') - } -} diff --git a/packages/create-pylon/src/index.ts b/packages/create-pylon/src/index.ts index 71fdbca..9d589b0 100644 --- a/packages/create-pylon/src/index.ts +++ b/packages/create-pylon/src/index.ts @@ -1,16 +1,21 @@ #!/usr/bin/env node +import chalk from 'chalk' import {Option, program, type Command} from 'commander' import consola from 'consola' -import {input, select, confirm} from '@inquirer/prompts' -import path from 'path' -import chalk from 'chalk' import * as fs from 'fs' +import path from 'path' -import * as telemetry from '@getcronit/pylon-telemetry' - -import {fileURLToPath} from 'url' import {dirname} from 'path' +import {fileURLToPath} from 'url' + +import {createDirectory, features, runtimes} from './create-directory' +import { + detectPackageManager, + getRunScript, + installPackage, + PackageManager +} from './install-pkg' const __filename = fileURLToPath(import.meta.url) const __dirname = dirname(__filename) @@ -21,255 +26,6 @@ const version = (() => { ).version as string })() -function mkdirp(dir: string) { - try { - fs.mkdirSync(dir, {recursive: true}) - } catch (e) { - if (e instanceof Error) { - if ('code' in e && e.code === 'EEXIST') return - } - throw e - } -} - -const runtimes: { - key: string - name: string - website: string - templates?: string[] -}[] = [ - { - key: 'bun', - name: 'Bun.js', - website: 'https://bunjs.dev', - templates: ['default'] - }, - { - key: 'node', - name: 'Node.js', - website: 'https://nodejs.org', - templates: ['default'] - }, - { - key: 'cf-workers', - name: 'Cloudflare Workers', - website: 'https://workers.cloudflare.com', - templates: ['default'] - }, - { - key: 'deno', - name: 'Deno', - website: 'https://deno.land', - templates: ['default'] - } -] - -const templates: { - key: string - name: string - description: string -}[] = [ - { - key: 'default', - name: 'Default', - description: 'Default template' - }, - { - key: 'database', - name: 'Database (Prisma)', - description: 'Template with Prisma ORM' - } -] - -const injectVariablesInContent = ( - content: string, - variables: Record -) => { - let result = content - - Object.entries(variables).forEach(([key, value]) => { - result = result.replaceAll(key, value) - }) - - return result -} -const readdirFilesSyncRecursive = (dir: string): string[] => { - const run = (dir: string): string[] => { - const result: string[] = [] - - const files = fs.readdirSync(dir) - - files.forEach(file => { - const filePath = path.join(dir, file) - - if (fs.statSync(filePath).isDirectory()) { - result.push(...run(filePath)) - } - - // Only add files - if (fs.statSync(filePath).isFile()) { - result.push(filePath) - } - }) - - return result - } - - return run(dir).map(file => { - return file.replace(dir, '.') - }) -} - -const createTemplate = async (options: { - name: string - runtime: string - template: string - target: string -}) => { - const {runtime, template, target} = options - - const runtimeName = runtimes.find(({key}) => key === runtime)?.name - const templateName = templates.find(({key}) => key === template)?.name - - if (!runtimeName) { - throw new Error(`Invalid runtime: ${runtime}`) - } - - if (!templateName) { - throw new Error(`Invalid template: ${template}`) - } - - // The templates are stored in the `templates` directory - const sharedTemplateDir = path.join(__dirname, '..', 'templates', 'shared') - - if (!fs.existsSync(sharedTemplateDir)) { - throw new Error(`Shared templates not found: ${sharedTemplateDir}`) - } - - const templateDir = path.join(__dirname, '..', 'templates', runtime, template) - - if (!fs.existsSync(templateDir)) { - throw new Error(`Template not found: ${templateDir}`) - } - - // The target directory is already created - const targetDirectoryPath = path.join(process.cwd(), target) - - consola.start(`Creating pylon in ${targetDirectoryPath}`) - - const inject = (content: string) => { - return injectVariablesInContent(content, { - __PYLON_NAME__: options.name - }) - } - - // Copy the shared template files - readdirFilesSyncRecursive(sharedTemplateDir).forEach(file => { - const source = path.join(sharedTemplateDir, file) - let target = path.join(targetDirectoryPath, file) - - // Create folder recursively and copy file - - const targetDir = path.dirname(target) - - // Skip the .github/workflows directory for cf-workers runtime - if ( - runtime === 'cf-workers' && - source.includes('.github/workflows/publish.yaml') - ) { - return - } - - if (!fs.existsSync(targetDir)) { - fs.mkdirSync(targetDir, {recursive: true}) - } - - // If the target ends with `.example`, remove the suffix. - // This is useful for `.gitignore.example` files because they are not published in - // the `create-pylon` package when named `.gitignore`. - if (target.endsWith('.example')) { - target = target.replace('.example', '') - } - - const injectedContent = inject(fs.readFileSync(source, 'utf-8')) - - fs.writeFileSync(target, injectedContent) - }) - - // Copy the runtime specific template files - readdirFilesSyncRecursive(templateDir).forEach(file => { - const source = path.join(templateDir, file) - let target = path.join(targetDirectoryPath, file) - - // Create folder recursively and copy file - const targetDir = path.dirname(target) - - if (!fs.existsSync(targetDir)) { - fs.mkdirSync(targetDir, {recursive: true}) - } - - // If the target ends with `.example`, remove the suffix. - // This is useful for `.gitignore.example` files because they are not published in - // the `create-pylon` package when named `.gitignore`. - if (target.endsWith('.example')) { - target = target.replace('.example', '') - } - - const injectedContent = inject(fs.readFileSync(source, 'utf-8')) - - fs.writeFileSync(target, injectedContent) - }) - - consola.success(`Pylon created`) -} - -import {spawnSync} from 'child_process' -import {detectPackageManager, getRunScript, PackageManager} from './detect-pm' - -const installDependencies = async (args: { - target: string - packageManager: PackageManager -}) => { - const target = path.resolve(args.target) - const packageManager = args.packageManager - - let command = '' - - switch (packageManager) { - case 'yarn': - command = 'yarn' - break - case 'npm': - command = 'npm install' - break - case 'pnpm': - command = 'pnpm install' - break - case 'bun': - command = 'bun install' - break - case 'deno': - command = 'deno install' - break - default: - throw new Error(`Invalid package manager: ${packageManager}`) - } - - consola.start(`Installing dependencies using ${packageManager}`) - - const proc = spawnSync(command, { - cwd: target, - shell: true, - stdio: 'inherit' - }) - - if (proc.status !== 0) { - throw new Error(`Failed to install dependencies`) - } - - consola.success(`Dependencies installed`) -} - program .name('create-pylon') .version(version) @@ -280,38 +36,21 @@ program runtimes.map(({key}) => key) ) ) - .addOption(new Option('-t, --template