diff --git a/package.json b/package.json index 0fb6371..2fd976b 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,11 @@ { "name": "@caido-community/dev", - "version": "0.0.2", + "version": "0.0.3", "description": "Development tools for building Caido plugins", "type": "module", + "exports": { + ".": "./dist/index.js" + }, "bin": { "caido-dev": "./dist/cli.js" }, @@ -32,19 +35,21 @@ "express": "5.0.0", "jiti": "2.4.2", "jszip": "3.10.1", - "tsup": "8.3.5", - "vite": "6.0.6", - "vitest": "2.1.8", "ws": "8.18.0", "zod": "3.24.1" }, + "peerDependencies": { + "vite": "^6.0.0", + "tsup": "^8.0.0" + }, "devDependencies": { "@types/express": "5.0.0", "@types/node": "22.10.2", "@types/ws": "8.5.13", - "prettier": "3.4.2", "tsup": "8.3.5", "tsx": "4.19.2", - "typescript": "5.7.2" + "typescript": "5.7.2", + "vitest": "2.1.8", + "vite": "6.0.6" } } diff --git a/playgrounds/build-backend/__tests__/build-backend.spec.ts b/playgrounds/build-backend/__tests__/build-backend.spec.ts index 5941315..84d5c36 100644 --- a/playgrounds/build-backend/__tests__/build-backend.spec.ts +++ b/playgrounds/build-backend/__tests__/build-backend.spec.ts @@ -11,13 +11,13 @@ describe('build-backend', () => { expect(manifestJsonContent).toEqual(JSON.stringify({ "id": "build-backend", - "name": "build-backend", + "name": "Backend", "version": "1.0.0", - "description": "", + "description": "Backend plugin", "author": { - "name": "Caido Labs Inc.", - "email": "hello@caido.com", - "url": "https://caido.com" + "name": "John Doe", + "email": "john.doe@example.com", + "url": "https://example.com" }, "plugins": [ { diff --git a/playgrounds/build-backend/caido.config.ts b/playgrounds/build-backend/caido.config.ts index f8811ce..279bfdf 100644 --- a/playgrounds/build-backend/caido.config.ts +++ b/playgrounds/build-backend/caido.config.ts @@ -1,7 +1,17 @@ export default { + id: "build-backend", + name: "Backend", + description: "Backend plugin", + version: "1.0.0", + author: { + name: "John Doe", + email: "john.doe@example.com", + url: "https://example.com", + }, plugins: [ { kind: "backend", + id: "backend", root: "./packages/backend", } ], diff --git a/playgrounds/build-frontend/__tests__/build-frontend.spec.ts b/playgrounds/build-frontend/__tests__/build-frontend.spec.ts index 74a1f5c..ec72b8c 100644 --- a/playgrounds/build-frontend/__tests__/build-frontend.spec.ts +++ b/playgrounds/build-frontend/__tests__/build-frontend.spec.ts @@ -11,21 +11,21 @@ describe('build-frontend', () => { expect(manifestJsonContent).toEqual(JSON.stringify({ "id": "build-frontend", - "name": "build-frontend", + "name": "Frontend", "version": "1.0.0", - "description": "", + "description": "Frontend plugin", "author": { - "name": "Caido Labs Inc.", - "email": "hello@caido.com", - "url": "https://caido.com" + "name": "John Doe", + "email": "john.doe@example.com", + "url": "https://example.com" }, "plugins": [ { "id": "frontend", "kind": "frontend", "name": "frontend", - "js": "frontend/index.js", - "css": "frontend/index.css", + "entrypoint": "frontend/index.js", + "style": "frontend/index.css", "backend": null } ] diff --git a/playgrounds/build-frontend/caido.config.ts b/playgrounds/build-frontend/caido.config.ts index 3a29676..d10adbf 100644 --- a/playgrounds/build-frontend/caido.config.ts +++ b/playgrounds/build-frontend/caido.config.ts @@ -1,7 +1,17 @@ export default { + id: "build-frontend", + name: "Frontend", + description: "Frontend plugin", + version: "1.0.0", + author: { + name: "John Doe", + email: "john.doe@example.com", + url: "https://example.com", + }, plugins: [ { kind: "frontend", + id: "frontend", root: "./packages/frontend", } ], diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c5d635e..acd70ce 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,15 +29,6 @@ importers: jszip: specifier: 3.10.1 version: 3.10.1 - tsup: - specifier: 8.3.5 - version: 8.3.5(jiti@2.4.2)(postcss@8.4.49)(tsx@4.19.2)(typescript@5.7.2) - vite: - specifier: 6.0.6 - version: 6.0.6(@types/node@22.10.2)(jiti@2.4.2)(tsx@4.19.2) - vitest: - specifier: 2.1.8 - version: 2.1.8(@types/node@22.10.2) ws: specifier: 8.18.0 version: 8.18.0 @@ -54,15 +45,21 @@ importers: '@types/ws': specifier: 8.5.13 version: 8.5.13 - prettier: - specifier: 3.4.2 - version: 3.4.2 + tsup: + specifier: 8.3.5 + version: 8.3.5(jiti@2.4.2)(postcss@8.4.49)(tsx@4.19.2)(typescript@5.7.2) tsx: specifier: 4.19.2 version: 4.19.2 typescript: specifier: 5.7.2 version: 5.7.2 + vite: + specifier: 6.0.6 + version: 6.0.6(@types/node@22.10.2)(jiti@2.4.2)(tsx@4.19.2) + vitest: + specifier: 2.1.8 + version: 2.1.8(@types/node@22.10.2) packages: @@ -1220,11 +1217,6 @@ packages: resolution: {integrity: sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==} engines: {node: ^10 || ^12 || >=14} - prettier@3.4.2: - resolution: {integrity: sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==} - engines: {node: '>=14'} - hasBin: true - process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} @@ -2543,8 +2535,6 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 - prettier@3.4.2: {} - process-nextick-args@2.0.1: {} proxy-addr@2.0.7: diff --git a/src/build/backend.ts b/src/build/backend.ts index 0e37827..b1fb099 100644 --- a/src/build/backend.ts +++ b/src/build/backend.ts @@ -1,7 +1,6 @@ import path from 'path'; import { defineConfig, build, Options } from 'tsup'; import type { BackendPluginConfig, BackendBuildOutput } from '../types'; -import { getPluginPackageJson } from '../package'; import { logInfo } from '../utils'; import { builtinModules } from 'module'; @@ -28,8 +27,6 @@ function createTsupConfig(cwd: string, plugin: BackendPluginConfig) { clean: true, sourcemap: false, external: [/caido:.+/, ...builtinModules], - - }) as Options; } @@ -45,15 +42,12 @@ export async function buildBackendPlugin(cwd: string, pluginConfig: BackendPlugi logInfo(`Building backend plugin: ${pluginRoot}`); const tsupConfig = createTsupConfig(cwd, pluginConfig); await build(tsupConfig); - - const packageJson = getPluginPackageJson(pluginRoot); - - logInfo(`Frontend backend built successfully`); + logInfo(`Backend built successfully`); return { kind: 'backend', - id: packageJson.name, - name: pluginConfig.name ?? packageJson.name, + id: pluginConfig.id, + name: pluginConfig.name ?? "backend", fileName: path.join(pluginRoot, 'dist', 'index.js'), } } \ No newline at end of file diff --git a/src/build/frontend.ts b/src/build/frontend.ts index 6f3345d..9e0e507 100644 --- a/src/build/frontend.ts +++ b/src/build/frontend.ts @@ -1,8 +1,7 @@ import path from 'path'; -import { defineConfig, build } from 'vite'; +import { defineConfig, build, mergeConfig } from 'vite'; import { existsSync } from 'fs'; import type { FrontendBuildOutput, FrontendPluginConfig } from '../types'; -import { getPluginPackageJson } from '../package'; import { logInfo } from '../utils'; /** @@ -14,7 +13,7 @@ import { logInfo } from '../utils'; function createViteConfig(cwd: string, plugin: FrontendPluginConfig) { // Set the entry point const root = path.resolve(cwd, plugin.root); - return defineConfig({ + const baseConfig = defineConfig({ root, build: { outDir: 'dist', @@ -25,11 +24,13 @@ function createViteConfig(cwd: string, plugin: FrontendPluginConfig) { fileName: () => 'index.js', cssFileName: 'index' }, - rollupOptions: { - external: ['@caido/frontend-sdk'] - } + }, + define: { + 'process.env.NODE_ENV': '"production"' } }) + + return mergeConfig(baseConfig, plugin.vite ?? {}); } /** @@ -46,14 +47,12 @@ export async function buildFrontendPlugin(cwd: string, pluginConfig: FrontendPlu await build(viteConfig); const hasCss = existsSync(`${pluginRoot}/dist/index.css`); - const packageJson = getPluginPackageJson(pluginRoot); - logInfo(`Frontend plugin built successfully`); return { kind: 'frontend', - id: packageJson.name, - name: pluginConfig.name ?? packageJson.name, + id: pluginConfig.id, + name: pluginConfig.name ?? "frontend", fileName: path.join(pluginRoot, 'dist', 'index.js'), cssFileName: hasCss ? path.join(pluginRoot, 'dist', 'index.css') : undefined, backendId: pluginConfig.backend?.id diff --git a/src/bundle/frontend.ts b/src/bundle/frontend.ts index b3856fe..b1d2a39 100644 --- a/src/bundle/frontend.ts +++ b/src/bundle/frontend.ts @@ -30,8 +30,8 @@ export function bundleFrontendPlugin(pluginPackageDir: string, buildOutput: Fron id: buildOutput.id, kind: 'frontend', name: buildOutput.name ?? buildOutput.id, - js: jsRelativePath, - css: cssRelativePath, + entrypoint: jsRelativePath, + style: cssRelativePath, backend: buildOutput.backendId ? { id: buildOutput.backendId, } : null diff --git a/src/bundle/index.ts b/src/bundle/index.ts index 2bff26a..e607783 100644 --- a/src/bundle/index.ts +++ b/src/bundle/index.ts @@ -2,8 +2,7 @@ import path from 'path'; import fs from 'fs/promises'; import { validateManifest } from '@caido/plugin-manifest'; import { createManifest } from '../manifest'; -import { RootPackageJson } from '../types'; -import type { BuildOutput } from '../types'; +import type { BuildOutput, CaidoConfig } from '../types'; import JSZip from 'jszip'; import { addDirectoryToZip, logInfo, logSuccess } from '../utils'; import { bundleFrontendPlugin } from './frontend'; @@ -34,17 +33,17 @@ async function createDistDirectories(cwd: string) { */ export async function bundlePackage(options: { cwd: string, - packageJson: RootPackageJson, - buildOutputs: BuildOutput[] + buildOutputs: BuildOutput[], + config: CaidoConfig }): Promise { logInfo('Bundling plugin package'); - const { cwd, packageJson, buildOutputs } = options; + const { cwd, buildOutputs, config } = options; // Create dist directories const { distDir, pluginPackageDir } = await createDistDirectories(cwd); // Create manifest - const manifest = createManifest({ packageJson }); + const manifest = createManifest({ config }); // Copy build outputs to dist directory for (const buildOutput of buildOutputs) { diff --git a/src/cli.ts b/src/cli.ts index 7584e5e..da4f8b0 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -17,6 +17,7 @@ function runner(fn: (...args: any[]) => Promise) { } catch (error) { const buildError = error instanceof Error ? error : new Error('Unknown error occurred'); console.error(chalk.red(`\n${buildError.message}`)); + console.error(chalk.red(`${buildError.stack}`)); process.exit(1); } } diff --git a/src/commands/build.ts b/src/commands/build.ts index fe272a9..aa73348 100644 --- a/src/commands/build.ts +++ b/src/commands/build.ts @@ -1,6 +1,5 @@ import { loadConfig } from '../config'; import { bundlePackage } from '../bundle'; -import { getRootPackageJson } from '../package'; import { buildFrontendPlugin, buildBackendPlugin } from '../build'; import type { BuildOutput } from '../types'; import { logInfo, logSuccess } from '../utils'; @@ -29,11 +28,10 @@ export async function build(options: { } // Bundle the plugin - const packageJson = getRootPackageJson(cwd); await bundlePackage({ cwd, - packageJson, - buildOutputs + buildOutputs, + config }); logSuccess('Plugin package built successfully'); } \ No newline at end of file diff --git a/src/commands/watch.ts b/src/commands/watch.ts index 54566ec..0f6d7b2 100644 --- a/src/commands/watch.ts +++ b/src/commands/watch.ts @@ -6,7 +6,8 @@ import { build } from './build'; import path from 'path'; import fs from 'fs/promises'; import { loadConfig } from '../config'; -import { logInfo } from '../utils'; +import { logError, logInfo } from '../utils'; +import { ConnectedMessage, ErrorMessage, RebuildMessage } from '../types'; export async function watch(options: { path?: string; @@ -14,6 +15,12 @@ export async function watch(options: { }) { const { path: cwd = process.cwd(), config: configPath } = options; + + const config = await loadConfig(cwd, configPath); + const { port = 3000 } = config.watch ?? {}; + + const downloadUrl = `http://localhost:${port}/plugin_package.zip`; + const app = express(); const server = createServer(app); const wss = new WebSocketServer({ server }); @@ -24,12 +31,32 @@ export async function watch(options: { wss.on('connection', (ws: WebSocket) => { clients.add(ws); ws.on('close', () => clients.delete(ws)); + + const message: ConnectedMessage = { + kind: 'connected', + downloadUrl, + }; + + ws.send(JSON.stringify(message)); }); // Function to notify all clients of a rebuild - const notifyRebuild = (success: boolean, error?: string) => { - const message = JSON.stringify({ type: 'rebuild', success, error }); - clients.forEach(client => client.send(message)); + const notifyRebuild = () => { + const message: RebuildMessage = { + kind: 'rebuild', + downloadUrl, + }; + clients.forEach(client => client.send(JSON.stringify(message))); + }; + + // Function to notify all clients of an error + const notifyError = (error: string) => { + const message: ErrorMessage = { + kind: 'error', + error, + }; + + clients.forEach(client => client.send(JSON.stringify(message))); }; // Serve the plugin package @@ -46,13 +73,18 @@ export async function watch(options: { // Initial build try { await build(options); - notifyRebuild(true); + notifyRebuild(); } catch (error) { - notifyRebuild(false, error instanceof Error ? error.message : 'Unknown error'); + if (error instanceof Error) { + logError(error.message); + notifyError(error.message); + } else { + logError('Unknown error'); + notifyError('Unknown error'); + } } // Watch for changes - const config = await loadConfig(cwd, configPath); const watchPatterns = config.plugins.map(plugin => path.join(cwd, plugin.root, '**/*')); const watchFiles = await Promise.all( watchPatterns.map(async pattern => { @@ -77,14 +109,13 @@ export async function watch(options: { logInfo(`File ${filePath} has been ${event}`); try { await build(options); - notifyRebuild(true); + notifyRebuild(); } catch (error) { - notifyRebuild(false, error instanceof Error ? error.message : 'Unknown error'); + notifyError(error instanceof Error ? error.message : 'Unknown error'); } }); // Start the server - const port = 3000; server.listen(port, () => { logInfo(`Development server running at http://localhost:${port}`); logInfo(`WebSocket server running at ws://localhost:${port}`); diff --git a/src/config.ts b/src/config.ts index a7fbf39..0d9f6a1 100644 --- a/src/config.ts +++ b/src/config.ts @@ -21,3 +21,8 @@ export async function loadConfig(cwd: string, configPath?: string): Promise { + return config; +}; diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..07ae4bb --- /dev/null +++ b/src/index.ts @@ -0,0 +1 @@ +export { defineConfig } from './config'; \ No newline at end of file diff --git a/src/manifest.ts b/src/manifest.ts index 92a840e..8a5dab6 100644 --- a/src/manifest.ts +++ b/src/manifest.ts @@ -1,19 +1,19 @@ import type { Manifest, ManifestPlugin } from '@caido/plugin-manifest'; -import { RootPackageJson } from './types'; +import { CaidoConfig } from './types'; export function createManifest(options: { - packageJson: RootPackageJson + config: CaidoConfig }): Manifest { - const { packageJson } = options; + const { config } = options; return { - id: packageJson.name, - name: packageJson.name, - version: packageJson.version, - description: packageJson.description, + id: config.id, + name: config.name, + version: config.version, + description: config.description, author: { - name: packageJson.author.name, - email: packageJson.author?.email, - url: packageJson.author?.url, + name: config.author.name, + email: config.author?.email, + url: config.author?.url, }, plugins: [] }; diff --git a/src/package.ts b/src/package.ts deleted file mode 100644 index a5f8379..0000000 --- a/src/package.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { readFileSync } from 'fs'; -import { join } from 'path'; -import { existsSync } from 'fs'; -import { type RootPackageJson, RootPackageJsonSchema, PluginPackageJsonSchema, PluginPackageJson } from './types'; - - -/** - * Loads the package.json file from the current working directory - * @returns The parsed and validated package.json contents or null if not found - */ -export function getRootPackageJson(cwd: string): RootPackageJson { - const packagePath = join(cwd, 'package.json'); - - if (existsSync(packagePath)) { - try { - const contents = readFileSync(packagePath, 'utf-8'); - const parsed = JSON.parse(contents); - return RootPackageJsonSchema.parse(parsed); - } catch (error) { - if (error instanceof Error) { - throw new Error(`Missing fields in package.json: ${error.message}`); - } - throw new Error(`Could not parse ${packagePath}`); - } - } else { - throw new Error(`Could not find package.json in ${process.cwd()}`); - } -} - -export function getPluginPackageJson(path: string): PluginPackageJson { - const packagePath = join(path, 'package.json'); - const contents = readFileSync(packagePath, 'utf-8'); - return PluginPackageJsonSchema.parse(JSON.parse(contents)); -} \ No newline at end of file diff --git a/src/types.ts b/src/types.ts index d5b35b9..d148de0 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,3 +1,4 @@ +import { UserConfig as ViteConfig } from "vite"; import { z } from "zod"; export type FrontendBuildOutput = { @@ -22,16 +23,21 @@ export const backendReferenceConfigSchema = z.strictObject({ id: z.string(), }); +const viteSchema: z.ZodType = z.record(z.string(), z.unknown()); + export const frontendPluginConfigSchema = z.strictObject({ kind: z.literal('frontend'), + id: z.string(), name: z.string().optional(), root: z.string(), backend: backendReferenceConfigSchema.nullable().optional(), + vite: viteSchema.optional(), }); export const backendPluginConfigSchema = z.strictObject({ kind: z.literal('backend'), - name: z.string().nullable().optional(), + id: z.string(), + name: z.string().optional(), root: z.string(), }); @@ -43,12 +49,20 @@ export const workflowPluginConfigSchema = z.strictObject({ definition: z.string(), }); -export const devConfigSchema = z.strictObject({ +export const watchConfigSchema = z.strictObject({ port: z.number().optional(), - host: z.string().optional(), }); export const caidoConfigSchema = z.strictObject({ + id: z.string(), + name: z.string(), + description: z.string(), + version: z.string().regex(/^\d+\.\d+\.\d+$/), + author: z.object({ + name: z.string(), + email: z.string().email().optional(), + url: z.string().url().optional(), + }), plugins: z.array( z.discriminatedUnion('kind', [ frontendPluginConfigSchema, @@ -56,7 +70,7 @@ export const caidoConfigSchema = z.strictObject({ workflowPluginConfigSchema, ]) ), - dev: devConfigSchema.optional(), + watch: watchConfigSchema.optional(), }); // Type inference @@ -64,23 +78,21 @@ export type BackendReferenceConfig = z.infer; export type BackendPluginConfig = z.infer; export type WorkflowPluginConfig = z.infer; -export type DevConfig = z.infer; +export type WatchConfig = z.infer; export type CaidoConfig = z.infer; -export const RootPackageJsonSchema = z.object({ - name: z.string(), - version: z.string(), - description: z.string(), - author: z.object({ - name: z.string(), - email: z.string().email(), - url: z.string().url(), - }), -}); -export const PluginPackageJsonSchema = z.object({ - name: z.string(), -}); +export type ConnectedMessage = { + kind: 'connected'; + downloadUrl: string; +} + +export type RebuildMessage = { + kind: 'rebuild'; + downloadUrl: string; +} -export type RootPackageJson = z.infer; -export type PluginPackageJson = z.infer; \ No newline at end of file +export type ErrorMessage = { + kind: 'error'; + error: string; +} \ No newline at end of file diff --git a/tsup.config.ts b/tsup.config.ts index 133a41d..9bd614a 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -1,7 +1,7 @@ import { defineConfig } from 'tsup' export default defineConfig({ - entry: ['src/cli.ts'], + entry: ['src/cli.ts', 'src/index.ts'], format: ['esm'], target: 'es2020', dts: true,