diff --git a/.gitignore b/.gitignore index a81172a..2399d1d 100644 --- a/.gitignore +++ b/.gitignore @@ -132,6 +132,4 @@ archives .http .vercel .netlify -examples/with-vercel/api -examples/with-vercel/vercel.json .DS_Store \ No newline at end of file diff --git a/examples/with-entries/icon.png b/examples/with-entries/icon.png new file mode 100644 index 0000000..a6b9fd3 Binary files /dev/null and b/examples/with-entries/icon.png differ diff --git a/examples/with-entries/package.json b/examples/with-entries/package.json new file mode 100644 index 0000000..09e5ecc --- /dev/null +++ b/examples/with-entries/package.json @@ -0,0 +1,21 @@ +{ + "name": "ingest-with-entries", + "version": "1.0.0", + "description": "A simple boilerplate for using Ingest with plugins.", + "private": true, + "plugins": [ + "./src/plugin" + ], + "scripts": { + "build": "tsc", + "dev": "ts-node src/scripts/serve.ts" + }, + "dependencies": { + "@stackpress/ingest": "0.3.6" + }, + "devDependencies": { + "@types/node": "22.9.3", + "ts-node": "10.9.2", + "typescript": "5.7.2" + } +} \ No newline at end of file diff --git a/examples/with-entries/src/config.ts b/examples/with-entries/src/config.ts new file mode 100644 index 0000000..80b5c0e --- /dev/null +++ b/examples/with-entries/src/config.ts @@ -0,0 +1,24 @@ +import type { CookieOptions } from '@stackpress/ingest'; + +export const environment = process.env.SERVER_ENV || 'development'; +export const config: Config = { + server: { + cwd: process.cwd(), + mode: environment, + bodySize: 0 + }, + cookie: { path: '/' }, + body: { size: 0 } +}; + +export type Config = { + server: { + cwd: string, + mode: string, + bodySize: number + }, + cookie: CookieOptions, + body: { + size: number + } +}; \ No newline at end of file diff --git a/examples/with-entries/src/error.ts b/examples/with-entries/src/error.ts new file mode 100644 index 0000000..20ad1f7 --- /dev/null +++ b/examples/with-entries/src/error.ts @@ -0,0 +1,3 @@ +export default function ShouldNotWork(message: string) { + throw new Error(message); +}; \ No newline at end of file diff --git a/examples/with-entries/src/events/error.ts b/examples/with-entries/src/events/error.ts new file mode 100644 index 0000000..fa4f9d3 --- /dev/null +++ b/examples/with-entries/src/events/error.ts @@ -0,0 +1,14 @@ +import { ServerRequest, Response } from '@stackpress/ingest'; + +export default function Error(req: ServerRequest, res: Response) { + const html = [ `

${res.error}

` ]; + const stack = res.stack?.map((log, i) => { + const { line, char } = log; + const method = log.method.replace(//g, ">"); + const file = log.file.replace(//g, ">"); + return `#${i + 1} ${method} - ${file}:${line}:${char}`; + }) || []; + html.push(`
${stack.join('

')}
`); + + res.setHTML(html.join('
')); +} \ No newline at end of file diff --git a/examples/with-entries/src/plugin.ts b/examples/with-entries/src/plugin.ts new file mode 100644 index 0000000..edaf4d2 --- /dev/null +++ b/examples/with-entries/src/plugin.ts @@ -0,0 +1,34 @@ +import type { HTTPServer } from '@stackpress/ingest'; +import type { Config } from './config'; + +import path from 'path'; +import { config } from './config'; + +export default function plugin(server: HTTPServer) { + server.config.set(config); + + server.get('/', path.join(__dirname, 'routes/home')); + server.get('/login', path.join(__dirname, 'routes/login')); + + server.get('/user', path.join(__dirname, 'routes/user/search')); + server.post('/user', path.join(__dirname, 'routes/user/create')); + server.get('/user/:id', path.join(__dirname, 'routes/user/detail')); + server.put('/user/:id', path.join(__dirname, 'routes/user/update')); + server.delete('/user/:id', path.join(__dirname, 'routes/user/remove')); + + server.get('/redirect', path.join(__dirname, 'routes/redirect')); + server.get('/icon.png', path.join(__dirname, 'routes/icon')); + server.get('/stream', path.join(__dirname, 'routes/stream')); + server.get('/__sse__', path.join(__dirname, 'routes/sse')); + + server.get('/error', path.join(__dirname, 'routes/error')); + server.get('/catch', path.join(__dirname, 'routes/catch')); + server.get('/**', path.join(__dirname, 'routes/404')); + + server.on('error', path.join(__dirname, 'events/error')); + + server.register('project', { welcome: 'Hello, World!!' }); + server.on('request', (req, res) => { + console.log('Request:', req.url); + }); +} \ No newline at end of file diff --git a/examples/with-entries/src/routes/404.ts b/examples/with-entries/src/routes/404.ts new file mode 100644 index 0000000..b7dbfc0 --- /dev/null +++ b/examples/with-entries/src/routes/404.ts @@ -0,0 +1,8 @@ +import { ServerRequest, Response } from '@stackpress/ingest'; + +export default function NotFound(req: ServerRequest, res: Response) { + if (!res.code && !res.status && !res.sent) { + //send the response + res.setHTML('Not Found'); + } +}; \ No newline at end of file diff --git a/examples/with-plugins/src/routes/error.ts b/examples/with-entries/src/routes/catch.ts similarity index 59% rename from examples/with-plugins/src/routes/error.ts rename to examples/with-entries/src/routes/catch.ts index c84fafd..2f64a34 100644 --- a/examples/with-plugins/src/routes/error.ts +++ b/examples/with-entries/src/routes/catch.ts @@ -1,6 +1,6 @@ -import { Context, Response, Exception } from '@stackpress/ingest'; +import { ServerRequest, Response, Exception } from '@stackpress/ingest'; -export default function ErrorResponse(req: Context, res: Response) { +export default function ErrorResponse(req: ServerRequest, res: Response) { try { throw Exception.for('Not implemented'); } catch (e) { diff --git a/examples/with-entries/src/routes/error.ts b/examples/with-entries/src/routes/error.ts new file mode 100644 index 0000000..ba12634 --- /dev/null +++ b/examples/with-entries/src/routes/error.ts @@ -0,0 +1,6 @@ +import { ServerRequest, Response } from '@stackpress/ingest'; +import Error from '../error'; + +export default function ErrorResponse(req: ServerRequest, res: Response) { + Error('Not implemented'); +}; \ No newline at end of file diff --git a/examples/with-entries/src/routes/home.ts b/examples/with-entries/src/routes/home.ts new file mode 100644 index 0000000..6a819b7 --- /dev/null +++ b/examples/with-entries/src/routes/home.ts @@ -0,0 +1,6 @@ +import { ServerRequest, Response } from '@stackpress/ingest'; + +export default async function HomePage(req: ServerRequest, res: Response) { + const project = req.context.plugin<{ welcome: string }>('project'); + res.setHTML(project.welcome); +}; \ No newline at end of file diff --git a/examples/with-netlify/src/routes/icon.ts b/examples/with-entries/src/routes/icon.ts similarity index 64% rename from examples/with-netlify/src/routes/icon.ts rename to examples/with-entries/src/routes/icon.ts index ae6b9fd..7c8ac5b 100644 --- a/examples/with-netlify/src/routes/icon.ts +++ b/examples/with-entries/src/routes/icon.ts @@ -1,8 +1,8 @@ import fs from 'fs'; import path from 'path'; -import { Context, Response } from '@stackpress/ingest'; +import { ServerRequest, Response } from '@stackpress/ingest'; -export default async function Icon(req: Context, res: Response) { +export default async function Icon(req: ServerRequest, res: Response) { if (res.code || res.status || res.body) return; const file = path.resolve(process.cwd(), 'icon.png'); if (fs.existsSync(file)) { diff --git a/examples/with-vercel/src/routes/login.ts b/examples/with-entries/src/routes/login.ts similarity index 80% rename from examples/with-vercel/src/routes/login.ts rename to examples/with-entries/src/routes/login.ts index e01af9b..9fa846c 100644 --- a/examples/with-vercel/src/routes/login.ts +++ b/examples/with-entries/src/routes/login.ts @@ -1,4 +1,4 @@ -import { Context, Response } from '@stackpress/ingest'; +import { ServerRequest, Response } from '@stackpress/ingest'; const template = ` @@ -19,7 +19,7 @@ const template = ` `; -export default function Login(req: Context, res: Response) { +export default function Login(req: ServerRequest, res: Response) { //send the response res.setHTML(template.trim()); }; \ No newline at end of file diff --git a/examples/with-entries/src/routes/redirect.ts b/examples/with-entries/src/routes/redirect.ts new file mode 100644 index 0000000..89a7e56 --- /dev/null +++ b/examples/with-entries/src/routes/redirect.ts @@ -0,0 +1,5 @@ +import { ServerRequest, Response } from '@stackpress/ingest'; + +export default function Redirect(req: ServerRequest, res: Response) { + res.redirect('/user'); +}; \ No newline at end of file diff --git a/examples/with-vercel/src/routes/sse.ts b/examples/with-entries/src/routes/sse.ts similarity index 80% rename from examples/with-vercel/src/routes/sse.ts rename to examples/with-entries/src/routes/sse.ts index 26cc2d7..207147c 100644 --- a/examples/with-vercel/src/routes/sse.ts +++ b/examples/with-entries/src/routes/sse.ts @@ -1,6 +1,6 @@ -import { Context, Response } from '@stackpress/ingest'; +import { ServerRequest, Response } from '@stackpress/ingest'; -export default async function SSE(req: Context, res: Response) { +export default async function SSE(req: ServerRequest, res: Response) { res.headers .set('Cache-Control', 'no-cache') .set('Content-Encoding', 'none') diff --git a/examples/with-plugins/src/routes/stream.ts b/examples/with-entries/src/routes/stream.ts similarity index 78% rename from examples/with-plugins/src/routes/stream.ts rename to examples/with-entries/src/routes/stream.ts index 2ea616c..e1f2d13 100644 --- a/examples/with-plugins/src/routes/stream.ts +++ b/examples/with-entries/src/routes/stream.ts @@ -1,4 +1,4 @@ -import { Context, Response } from '@stackpress/ingest'; +import { ServerRequest, Response } from '@stackpress/ingest'; const template = ` @@ -21,7 +21,7 @@ const template = ` `; -export default function Stream(req: Context, res: Response) { +export default function Stream(req: ServerRequest, res: Response) { //send the response res.setHTML(template.trim()); }; \ No newline at end of file diff --git a/examples/with-fetch/src/routes/user/create.ts b/examples/with-entries/src/routes/user/create.ts similarity index 61% rename from examples/with-fetch/src/routes/user/create.ts rename to examples/with-entries/src/routes/user/create.ts index 97379b3..257dd6c 100644 --- a/examples/with-fetch/src/routes/user/create.ts +++ b/examples/with-entries/src/routes/user/create.ts @@ -1,8 +1,8 @@ -import { Context, Response } from '@stackpress/ingest'; +import { ServerRequest, Response } from '@stackpress/ingest'; let id = 0; -export default function UserCreate(req: Context, res: Response) { +export default function UserCreate(req: ServerRequest, res: Response) { //get form body const form = req.data(); //maybe insert into database? diff --git a/examples/with-plugins/src/routes/user/detail.ts b/examples/with-entries/src/routes/user/detail.ts similarity index 70% rename from examples/with-plugins/src/routes/user/detail.ts rename to examples/with-entries/src/routes/user/detail.ts index e3109ee..115fb35 100644 --- a/examples/with-plugins/src/routes/user/detail.ts +++ b/examples/with-entries/src/routes/user/detail.ts @@ -1,6 +1,6 @@ -import { Context, Response } from '@stackpress/ingest'; +import { ServerRequest, Response } from '@stackpress/ingest'; -export default function UserDetail(req: Context, res: Response) { +export default function UserDetail(req: ServerRequest, res: Response) { //get params const id = parseInt(req.data('id') || ''); if (!id) { diff --git a/examples/with-plugins/src/routes/user/remove.ts b/examples/with-entries/src/routes/user/remove.ts similarity index 70% rename from examples/with-plugins/src/routes/user/remove.ts rename to examples/with-entries/src/routes/user/remove.ts index 05ff009..f44b6fb 100644 --- a/examples/with-plugins/src/routes/user/remove.ts +++ b/examples/with-entries/src/routes/user/remove.ts @@ -1,6 +1,6 @@ -import { Context, Response } from '@stackpress/ingest'; +import { ServerRequest, Response } from '@stackpress/ingest'; -export default function UserRemove(req: Context, res: Response) { +export default function UserRemove(req: ServerRequest, res: Response) { //get params const id = parseInt(req.data('id') || ''); if (!id) { diff --git a/examples/with-netlify/src/routes/user/search.ts b/examples/with-entries/src/routes/user/search.ts similarity index 75% rename from examples/with-netlify/src/routes/user/search.ts rename to examples/with-entries/src/routes/user/search.ts index 69838a7..0630f2d 100644 --- a/examples/with-netlify/src/routes/user/search.ts +++ b/examples/with-entries/src/routes/user/search.ts @@ -1,6 +1,6 @@ -import { Context, Response } from '@stackpress/ingest'; +import { ServerRequest, Response } from '@stackpress/ingest'; -export default function UserSearch(req: Context, res: Response) { +export default function UserSearch(req: ServerRequest, res: Response) { //get filters //const filters = req.query.get>('filter'); //maybe get from database? diff --git a/examples/with-fetch/src/routes/user/update.ts b/examples/with-entries/src/routes/user/update.ts similarity index 70% rename from examples/with-fetch/src/routes/user/update.ts rename to examples/with-entries/src/routes/user/update.ts index d61e997..78da158 100644 --- a/examples/with-fetch/src/routes/user/update.ts +++ b/examples/with-entries/src/routes/user/update.ts @@ -1,6 +1,6 @@ -import { Context, Response } from '@stackpress/ingest'; +import { ServerRequest, Response } from '@stackpress/ingest'; -export default function UserUpdate(req: Context, res: Response) { +export default function UserUpdate(req: ServerRequest, res: Response) { //get params const id = parseInt(req.data('id') || ''); if (!id) { diff --git a/examples/with-entries/src/scripts/serve.ts b/examples/with-entries/src/scripts/serve.ts new file mode 100644 index 0000000..0f29281 --- /dev/null +++ b/examples/with-entries/src/scripts/serve.ts @@ -0,0 +1,13 @@ +import server from '../server'; + +async function main() { + //load the plugins + await server.bootstrap(); + //start the server + server.create().listen(3000, () => { + console.log('Server is running on port 3000'); + console.log('------------------------------'); + }); +}; + +main().catch(console.error); \ No newline at end of file diff --git a/examples/with-entries/src/server.ts b/examples/with-entries/src/server.ts new file mode 100644 index 0000000..93a7b78 --- /dev/null +++ b/examples/with-entries/src/server.ts @@ -0,0 +1,10 @@ +//types +import type { Config } from './config'; +//ingest +import { server } from '@stackpress/ingest/http'; +//local +import { environment } from './config'; + +export default server({ + cache: environment === 'production' +}); \ No newline at end of file diff --git a/examples/with-netlify/tsconfig.json b/examples/with-entries/tsconfig.json similarity index 100% rename from examples/with-netlify/tsconfig.json rename to examples/with-entries/tsconfig.json diff --git a/examples/with-fetch/package.json b/examples/with-fetch/package.json index 0281020..a713747 100644 --- a/examples/with-fetch/package.json +++ b/examples/with-fetch/package.json @@ -1,16 +1,14 @@ { - "name": "ingest-with-vercel", + "name": "ingest-with-fetch", "version": "1.0.0", - "description": "A simple boilerplate for using Ingest with Vercel.", + "description": "A simple boilerplate for using Ingest with fetch API.", "private": true, "scripts": { - "generate": "yarn build:tsc && yarn build:files", - "build:tsc": "tsc", - "build:files": "ts-node src/scripts/build.ts", - "dev": "ts-node src/scripts/server.ts" + "build": "tsc", + "dev": "ts-node src/server.ts" }, "dependencies": { - "@stackpress/ingest-vercel": "0.2.14" + "@stackpress/ingest": "0.3.6" }, "devDependencies": { "@types/node": "22.9.3", diff --git a/examples/with-fetch/plugin.d.ts b/examples/with-fetch/plugin.d.ts deleted file mode 100644 index 1f1ce10..0000000 --- a/examples/with-fetch/plugin.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -import plugin from './dist/plugin'; -export default plugin; \ No newline at end of file diff --git a/examples/with-fetch/plugin.js b/examples/with-fetch/plugin.js deleted file mode 100644 index 098f7a9..0000000 --- a/examples/with-fetch/plugin.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('./dist/plugin'); \ No newline at end of file diff --git a/examples/with-fetch/src/build.ts b/examples/with-fetch/src/build.ts deleted file mode 100644 index de27385..0000000 --- a/examples/with-fetch/src/build.ts +++ /dev/null @@ -1,4 +0,0 @@ -import server from './routes'; -server.build().then(({ build }) => { - console.log('done.', build); -}); \ No newline at end of file diff --git a/examples/with-fetch/src/routes.ts b/examples/with-fetch/src/routes.ts deleted file mode 100644 index 4482138..0000000 --- a/examples/with-fetch/src/routes.ts +++ /dev/null @@ -1,22 +0,0 @@ -import path from 'path'; -import { server } from '@stackpress/ingest/build'; - -const build = server(); - -build.get('/user', path.resolve(__dirname, 'routes/user/search')); -build.post('/user', path.resolve(__dirname, 'routes/user/create')); -build.get('/user/:id', path.resolve(__dirname, 'routes/user/detail')); -build.put('/user/:id', path.resolve(__dirname, 'routes/user/update')); -build.delete('/user/:id', path.resolve(__dirname, 'routes/user/remove')); - -build.get('/redirect', path.resolve(__dirname, 'routes/redirect')); -build.get('/error', path.resolve(__dirname, 'routes/error')); -build.get('/login', path.resolve(__dirname, 'routes/login')); -build.get('/icon.png', path.resolve(__dirname, 'routes/icon')); -build.get('/stream', path.resolve(__dirname, 'routes/stream')); -build.get('/__sse__', path.resolve(__dirname, 'routes/sse')); -build.get('/', path.resolve(__dirname, 'routes/home')); - -build.get('/**', path.resolve(__dirname, 'routes/404')); - -export default build; \ No newline at end of file diff --git a/examples/with-fetch/src/routes/404.ts b/examples/with-fetch/src/routes/404.ts deleted file mode 100644 index f00d5d3..0000000 --- a/examples/with-fetch/src/routes/404.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Context, Response } from '@stackpress/ingest'; - -export default function NotFound(req: Context, res: Response) { - if (!res.code && !res.status && !res.sent) { - //send the response - res.setHTML('Not Found'); - } -}; \ No newline at end of file diff --git a/examples/with-fetch/src/routes/error.ts b/examples/with-fetch/src/routes/error.ts deleted file mode 100644 index c84fafd..0000000 --- a/examples/with-fetch/src/routes/error.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Context, Response, Exception } from '@stackpress/ingest'; - -export default function ErrorResponse(req: Context, res: Response) { - try { - throw Exception.for('Not implemented'); - } catch (e) { - const error = e as Exception; - res.setError({ - code: error.code, - error: error.message, - stack: error.trace() - }); - } -}; \ No newline at end of file diff --git a/examples/with-fetch/src/routes/home.ts b/examples/with-fetch/src/routes/home.ts deleted file mode 100644 index 3f232a6..0000000 --- a/examples/with-fetch/src/routes/home.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Context, Response } from '@stackpress/ingest'; - -import bootstrap from '../client'; - -export default async function HomePage(req: Context, res: Response) { - const client = await bootstrap(req, res); - const project = client.plugin<{ welcome: string }>('project'); - res.setHTML(project.welcome); -}; \ No newline at end of file diff --git a/examples/with-fetch/src/routes/hooks.ts b/examples/with-fetch/src/routes/hooks.ts new file mode 100644 index 0000000..103be8b --- /dev/null +++ b/examples/with-fetch/src/routes/hooks.ts @@ -0,0 +1,52 @@ +import { Exception } from '@stackpress/ingest'; +import { router } from '@stackpress/ingest/fetch'; + +const route = router(); + +/** + * Error handlers + */ +route.get('/catch', function ErrorResponse(req, res) { + try { + throw Exception.for('Not implemented'); + } catch (e) { + const error = e as Exception; + res.setError({ + code: error.code, + error: error.message, + stack: error.trace() + }); + } +}); + +/** + * Error handlers + */ +route.get('/error', function ErrorResponse(req, res) { + throw Exception.for('Not implemented'); +}); + +/** + * 404 handler + */ +route.get('/**', function NotFound(req, res) { + if (!res.code && !res.status && !res.sent) { + //send the response + res.setHTML('Not Found'); + } +}); + +route.on('error', function Error(req, res) { + const html = [ `

${res.error}

` ]; + const stack = res.stack?.map((log, i) => { + const { line, char } = log; + const method = log.method.replace(//g, ">"); + const file = log.file.replace(//g, ">"); + return `#${i + 1} ${method} - ${file}:${line}:${char}`; + }) || []; + html.push(`
${stack.join('

')}
`); + + res.setHTML(html.join('
')); +}); + +export default route; \ No newline at end of file diff --git a/examples/with-fetch/src/routes/icon.ts b/examples/with-fetch/src/routes/icon.ts deleted file mode 100644 index ae6b9fd..0000000 --- a/examples/with-fetch/src/routes/icon.ts +++ /dev/null @@ -1,11 +0,0 @@ -import fs from 'fs'; -import path from 'path'; -import { Context, Response } from '@stackpress/ingest'; - -export default async function Icon(req: Context, res: Response) { - if (res.code || res.status || res.body) return; - const file = path.resolve(process.cwd(), 'icon.png'); - if (fs.existsSync(file)) { - res.setBody('image/png', fs.createReadStream(file)); - } -}; \ No newline at end of file diff --git a/examples/with-netlify/src/routes/login.ts b/examples/with-fetch/src/routes/pages.ts similarity index 65% rename from examples/with-netlify/src/routes/login.ts rename to examples/with-fetch/src/routes/pages.ts index e01af9b..7b47c37 100644 --- a/examples/with-netlify/src/routes/login.ts +++ b/examples/with-fetch/src/routes/pages.ts @@ -1,4 +1,4 @@ -import { Context, Response } from '@stackpress/ingest'; +import { router } from '@stackpress/ingest/fetch'; const template = ` @@ -19,7 +19,21 @@ const template = ` `; -export default function Login(req: Context, res: Response) { +const route = router(); + +/** + * Home page + */ +route.get('/', function HomePage(req, res) { + res.setHTML('Hello, World'); +}); + +/** + * Login page + */ +route.get('/login', function Login(req, res) { //send the response res.setHTML(template.trim()); -}; \ No newline at end of file +}); + +export default route; \ No newline at end of file diff --git a/examples/with-fetch/src/routes/redirect.ts b/examples/with-fetch/src/routes/redirect.ts deleted file mode 100644 index 0e6c164..0000000 --- a/examples/with-fetch/src/routes/redirect.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { Context, Response } from '@stackpress/ingest'; - -export default function Redirect(req: Context, res: Response) { - res.redirect('/user'); -}; \ No newline at end of file diff --git a/examples/with-fetch/src/routes/sse.ts b/examples/with-fetch/src/routes/sse.ts deleted file mode 100644 index 26cc2d7..0000000 --- a/examples/with-fetch/src/routes/sse.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Context, Response } from '@stackpress/ingest'; - -export default async function SSE(req: Context, res: Response) { - res.headers - .set('Cache-Control', 'no-cache') - .set('Content-Encoding', 'none') - .set('Connection', 'keep-alive') - .set('Access-Control-Allow-Origin', '*'); - - let timerId: any; - const msg = new TextEncoder().encode("data: hello\r\n\r\n"); - res.setBody('text/event-stream', new ReadableStream({ - start(controller) { - timerId = setInterval(() => { - controller.enqueue(msg); - }, 2500); - }, - cancel() { - if (typeof timerId === 'number') { - clearInterval(timerId); - } - }, - })); -}; \ No newline at end of file diff --git a/examples/with-fetch/src/routes/stream.ts b/examples/with-fetch/src/routes/stream.ts deleted file mode 100644 index 2ea616c..0000000 --- a/examples/with-fetch/src/routes/stream.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Context, Response } from '@stackpress/ingest'; - -const template = ` - - - - SSE - - -
    - - - -`; - -export default function Stream(req: Context, res: Response) { - //send the response - res.setHTML(template.trim()); -}; \ No newline at end of file diff --git a/examples/with-fetch/src/routes/tests.ts b/examples/with-fetch/src/routes/tests.ts new file mode 100644 index 0000000..ccc846e --- /dev/null +++ b/examples/with-fetch/src/routes/tests.ts @@ -0,0 +1,81 @@ +import fs from 'fs'; +import path from 'path'; + +import { router } from '@stackpress/ingest/fetch'; + +const template = ` + + + + SSE + + +
      + + + +`; + +const route = router(); + +/** + * Redirect test + */ +route.get('/redirect', function Redirect(req, res) { + res.redirect('/user'); +}); + +/** + * Static file test + */ +route.get('/icon.png', function Icon(req, res) { + if (res.code || res.status || res.body) return; + const file = path.resolve(process.cwd(), 'icon.png'); + if (fs.existsSync(file)) { + res.setBody('image/png', fs.createReadStream(file)); + } +}); + +/** + * Stream template for SSE test + */ +route.get('/stream', function Stream(req, res) { + //send the response + res.setHTML(template.trim()); +}); + +/** + * SSE test + */ +route.get('/__sse__', function SSE(req, res) { + res.headers + .set('Cache-Control', 'no-cache') + .set('Content-Encoding', 'none') + .set('Connection', 'keep-alive') + .set('Access-Control-Allow-Origin', '*'); + + let timerId: any; + const msg = new TextEncoder().encode("data: hello\r\n\r\n"); + res.setBody('text/event-stream', new ReadableStream({ + start(controller) { + timerId = setInterval(() => { + controller.enqueue(msg); + }, 2500); + }, + cancel() { + if (typeof timerId === 'number') { + clearInterval(timerId); + } + }, + })); +}); + +export default route; \ No newline at end of file diff --git a/examples/with-fetch/src/routes/user.ts b/examples/with-fetch/src/routes/user.ts new file mode 100644 index 0000000..149fbd6 --- /dev/null +++ b/examples/with-fetch/src/routes/user.ts @@ -0,0 +1,102 @@ +import { router } from '@stackpress/ingest/fetch'; + +const route = router(); + +let id = 0; + +/** + * Example user API search + */ +route.get('/user', function UserSearch(req, res) { + //get filters + //const filters = req.query.get>('filter'); + //maybe get from database? + const results = [ + { + id: 1, + name: 'John Doe', + age: 21, + created: new Date().toISOString() + }, + { + id: 2, + name: 'Jane Doe', + age: 30, + created: new Date().toISOString() + } + ]; + //send the response + res.setRows(results, 100); +}); + +/** + * Example user API create (POST) + * Need to use Postman to see this... + */ +route.post('/user', function UserCreate(req, res) { + //get form body + const form = req.data(); + //maybe insert into database? + const results = { ...form, id: ++id, created: new Date().toISOString() }; + //send the response + res.setResults(results); +}); + +/** + * Example user API detail + */ +route.get('/user/:id', function UserDetail(req, res) { + //get params + const id = parseInt(req.data('id') || ''); + if (!id) { + res.setError('ID is required'); + return; + } + //maybe get from database? + const results = { + id: id, + name: 'John Doe', + age: 21, + created: new Date().toISOString() + }; + //send the response + res.setResults(results); +}); +route.put('/user/:id', function UserUpdate(req, res) { + //get params + const id = parseInt(req.data('id') || ''); + if (!id) { + res.setError('ID is required'); + return; + } + //get form body + const form = req.post(); + //maybe insert into database? + const results = { ...form, id, created: new Date().toISOString() }; + //send the response + res.setResults(results); +}); + +/** + * Example user API delete (DELETE) + * Need to use Postman to see this... + */ +route.delete('/user/:id', function UserRemove(req, res) { + //get params + const id = parseInt(req.data('id') || ''); + if (!id) { + res.setError('ID is required'); + return; + } + //maybe get from database? + const results = { + id: 1, + name: 'John Doe', + age: 21, + created: new Date().toISOString() + }; + //send the response + res.setResults(results); +}); + +export default route; \ No newline at end of file diff --git a/examples/with-fetch/src/routes/user/detail.ts b/examples/with-fetch/src/routes/user/detail.ts deleted file mode 100644 index e3109ee..0000000 --- a/examples/with-fetch/src/routes/user/detail.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Context, Response } from '@stackpress/ingest'; - -export default function UserDetail(req: Context, res: Response) { - //get params - const id = parseInt(req.data('id') || ''); - if (!id) { - return res.setError('ID is required'); - } - //maybe get from database? - const results = { - id: id, - name: 'John Doe', - age: 21, - created: new Date().toISOString() - }; - //send the response - res.setResults(results); -}; \ No newline at end of file diff --git a/examples/with-fetch/src/routes/user/remove.ts b/examples/with-fetch/src/routes/user/remove.ts deleted file mode 100644 index 05ff009..0000000 --- a/examples/with-fetch/src/routes/user/remove.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Context, Response } from '@stackpress/ingest'; - -export default function UserRemove(req: Context, res: Response) { - //get params - const id = parseInt(req.data('id') || ''); - if (!id) { - return res.setError('ID is required'); - } - //maybe get from database? - const results = { - id: 1, - name: 'John Doe', - age: 21, - created: new Date().toISOString() - }; - //send the response - res.setResults(results); -}; \ No newline at end of file diff --git a/examples/with-fetch/src/routes/user/search.ts b/examples/with-fetch/src/routes/user/search.ts deleted file mode 100644 index 69838a7..0000000 --- a/examples/with-fetch/src/routes/user/search.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Context, Response } from '@stackpress/ingest'; - -export default function UserSearch(req: Context, res: Response) { - //get filters - //const filters = req.query.get>('filter'); - //maybe get from database? - const results = [ - { - id: 1, - name: 'John Doe', - age: 21, - created: new Date().toISOString() - }, - { - id: 2, - name: 'Jane Doe', - age: 30, - created: new Date().toISOString() - } - ]; - //send the response - res.setRows(results, 100); -}; \ No newline at end of file diff --git a/examples/with-fetch/src/server.ts b/examples/with-fetch/src/server.ts index abb0a78..522a1ce 100644 --- a/examples/with-fetch/src/server.ts +++ b/examples/with-fetch/src/server.ts @@ -1,6 +1,17 @@ -import server from './routes'; -server.create().listen(3000, () => { +//ingest +import { server } from '@stackpress/ingest/fetch'; +//routes +import hooks from './routes/hooks'; +import pages from './routes/pages'; +import tests from './routes/tests'; +import user from './routes/user'; + +//make a new app +const app = server(); +//use the routes +app.use(pages).use(tests).use(user).use(hooks); +//start the server +app.create().listen(3000, () => { console.log('Server is running on port 3000'); console.log('------------------------------'); - console.log(server.router.listeners); }); \ No newline at end of file diff --git a/examples/with-http/icon.png b/examples/with-http/icon.png new file mode 100644 index 0000000..a6b9fd3 Binary files /dev/null and b/examples/with-http/icon.png differ diff --git a/examples/with-http/package.json b/examples/with-http/package.json new file mode 100644 index 0000000..b13fbbf --- /dev/null +++ b/examples/with-http/package.json @@ -0,0 +1,18 @@ +{ + "name": "ingest-with-http", + "version": "1.0.0", + "description": "A simple boilerplate for using Ingest with HTTP.", + "private": true, + "scripts": { + "build": "tsc", + "dev": "ts-node src/server.ts" + }, + "dependencies": { + "@stackpress/ingest": "0.3.6" + }, + "devDependencies": { + "@types/node": "22.9.3", + "ts-node": "10.9.2", + "typescript": "5.7.2" + } +} \ No newline at end of file diff --git a/examples/with-http/src/routes/hooks.ts b/examples/with-http/src/routes/hooks.ts new file mode 100644 index 0000000..0b51da9 --- /dev/null +++ b/examples/with-http/src/routes/hooks.ts @@ -0,0 +1,32 @@ +import { Exception } from '@stackpress/ingest'; +import { router } from '@stackpress/ingest/http'; + +const route = router(); + +/** + * Error handlers + */ +route.get('/error', function ErrorResponse(req, res) { + try { + throw Exception.for('Not implemented'); + } catch (e) { + const error = e as Exception; + res.setError({ + code: error.code, + error: error.message, + stack: error.trace() + }); + } +}); + +/** + * 404 handler + */ +route.get('/**', function NotFound(req, res) { + if (!res.code && !res.status && !res.sent) { + //send the response + res.setHTML('Not Found'); + } +}); + +export default route; \ No newline at end of file diff --git a/examples/with-fetch/src/routes/login.ts b/examples/with-http/src/routes/pages.ts similarity index 65% rename from examples/with-fetch/src/routes/login.ts rename to examples/with-http/src/routes/pages.ts index e01af9b..72450f1 100644 --- a/examples/with-fetch/src/routes/login.ts +++ b/examples/with-http/src/routes/pages.ts @@ -1,4 +1,4 @@ -import { Context, Response } from '@stackpress/ingest'; +import { router } from '@stackpress/ingest/http'; const template = ` @@ -19,7 +19,21 @@ const template = ` `; -export default function Login(req: Context, res: Response) { +const route = router(); + +/** + * Home page + */ +route.get('/', function HomePage(req, res) { + res.setHTML('Hello, World'); +}); + +/** + * Login page + */ +route.get('/login', function Login(req, res) { //send the response res.setHTML(template.trim()); -}; \ No newline at end of file +}); + +export default route; \ No newline at end of file diff --git a/examples/with-http/src/routes/tests.ts b/examples/with-http/src/routes/tests.ts new file mode 100644 index 0000000..55e93a0 --- /dev/null +++ b/examples/with-http/src/routes/tests.ts @@ -0,0 +1,81 @@ +import fs from 'fs'; +import path from 'path'; + +import { router } from '@stackpress/ingest/http'; + +const template = ` + + + + SSE + + +
        + + + +`; + +const route = router(); + +/** + * Redirect test + */ +route.get('/redirect', function Redirect(req, res) { + res.redirect('/user'); +}); + +/** + * Static file test + */ +route.get('/icon.png', function Icon(req, res) { + if (res.code || res.status || res.body) return; + const file = path.resolve(process.cwd(), 'icon.png'); + if (fs.existsSync(file)) { + res.setBody('image/png', fs.createReadStream(file)); + } +}); + +/** + * Stream template for SSE test + */ +route.get('/stream', function Stream(req, res) { + //send the response + res.setHTML(template.trim()); +}); + +/** + * SSE test + */ +route.get('/__sse__', function SSE(req, res) { + res.headers + .set('Cache-Control', 'no-cache') + .set('Content-Encoding', 'none') + .set('Connection', 'keep-alive') + .set('Access-Control-Allow-Origin', '*'); + + let timerId: any; + const msg = new TextEncoder().encode("data: hello\r\n\r\n"); + res.setBody('text/event-stream', new ReadableStream({ + start(controller) { + timerId = setInterval(() => { + controller.enqueue(msg); + }, 2500); + }, + cancel() { + if (typeof timerId === 'number') { + clearInterval(timerId); + } + }, + })); +}); + +export default route; \ No newline at end of file diff --git a/examples/with-http/src/routes/user.ts b/examples/with-http/src/routes/user.ts new file mode 100644 index 0000000..5370008 --- /dev/null +++ b/examples/with-http/src/routes/user.ts @@ -0,0 +1,102 @@ +import { router } from '@stackpress/ingest/http'; + +const route = router(); + +let id = 0; + +/** + * Example user API search + */ +route.get('/user', function UserSearch(req, res) { + //get filters + //const filters = req.query.get>('filter'); + //maybe get from database? + const results = [ + { + id: 1, + name: 'John Doe', + age: 21, + created: new Date().toISOString() + }, + { + id: 2, + name: 'Jane Doe', + age: 30, + created: new Date().toISOString() + } + ]; + //send the response + res.setRows(results, 100); +}); + +/** + * Example user API create (POST) + * Need to use Postman to see this... + */ +route.post('/user', function UserCreate(req, res) { + //get form body + const form = req.data(); + //maybe insert into database? + const results = { ...form, id: ++id, created: new Date().toISOString() }; + //send the response + res.setResults(results); +}); + +/** + * Example user API detail + */ +route.get('/user/:id', function UserDetail(req, res) { + //get params + const id = parseInt(req.data('id') || ''); + if (!id) { + res.setError('ID is required'); + return; + } + //maybe get from database? + const results = { + id: id, + name: 'John Doe', + age: 21, + created: new Date().toISOString() + }; + //send the response + res.setResults(results); +}); +route.put('/user/:id', function UserUpdate(req, res) { + //get params + const id = parseInt(req.data('id') || ''); + if (!id) { + res.setError('ID is required'); + return; + } + //get form body + const form = req.post(); + //maybe insert into database? + const results = { ...form, id, created: new Date().toISOString() }; + //send the response + res.setResults(results); +}); + +/** + * Example user API delete (DELETE) + * Need to use Postman to see this... + */ +route.delete('/user/:id', function UserRemove(req, res) { + //get params + const id = parseInt(req.data('id') || ''); + if (!id) { + res.setError('ID is required'); + return; + } + //maybe get from database? + const results = { + id: 1, + name: 'John Doe', + age: 21, + created: new Date().toISOString() + }; + //send the response + res.setResults(results); +}); + +export default route; \ No newline at end of file diff --git a/examples/with-http/src/server.ts b/examples/with-http/src/server.ts new file mode 100644 index 0000000..c6e61ce --- /dev/null +++ b/examples/with-http/src/server.ts @@ -0,0 +1,17 @@ +//ingest +import { server } from '@stackpress/ingest/http'; +//routes +import hooks from './routes/hooks'; +import pages from './routes/pages'; +import tests from './routes/tests'; +import user from './routes/user'; + +//make a new app +const app = server(); +//use the routes +app.use(pages).use(tests).use(user).use(hooks); +//start the server +app.create().listen(3000, () => { + console.log('Server is running on port 3000'); + console.log('------------------------------'); +}); \ No newline at end of file diff --git a/packages/ingest-vercel/tsconfig.json b/examples/with-http/tsconfig.json similarity index 88% rename from packages/ingest-vercel/tsconfig.json rename to examples/with-http/tsconfig.json index 29034a6..17a566b 100644 --- a/packages/ingest-vercel/tsconfig.json +++ b/examples/with-http/tsconfig.json @@ -15,5 +15,5 @@ "skipLibCheck": true }, "include": [ "src/**/*.ts" ], - "exclude": [ "dist", "docs", "node_modules", "test"] + "exclude": [ "dist", "node_modules" ] } diff --git a/examples/with-netlify/package.json b/examples/with-netlify/package.json deleted file mode 100644 index 07deff3..0000000 --- a/examples/with-netlify/package.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "ingest-with-netlify", - "version": "1.0.0", - "description": "A simple boilerplate for using Ingest with Netlify.", - "private": true, - "scripts": { - "generate": "yarn build:tsc && yarn build:files", - "build:tsc": "tsc", - "build:files": "ts-node src/build.ts", - "dev": "ts-node src/server.ts" - }, - "dependencies": { - "@stackpress/ingest-netlify": "0.2.14" - }, - "devDependencies": { - "@types/node": "22.9.3", - "ts-node": "10.9.2", - "typescript": "5.7.2" - } -} \ No newline at end of file diff --git a/examples/with-netlify/src/build.ts b/examples/with-netlify/src/build.ts deleted file mode 100644 index de27385..0000000 --- a/examples/with-netlify/src/build.ts +++ /dev/null @@ -1,4 +0,0 @@ -import server from './routes'; -server.build().then(({ build }) => { - console.log('done.', build); -}); \ No newline at end of file diff --git a/examples/with-netlify/src/routes.ts b/examples/with-netlify/src/routes.ts deleted file mode 100644 index dba82e1..0000000 --- a/examples/with-netlify/src/routes.ts +++ /dev/null @@ -1,20 +0,0 @@ -import path from 'path'; -import netlify from '@stackpress/ingest-netlify'; - -const server = netlify(); - -server.get('/user', path.resolve(__dirname, 'routes/user/search')); -server.post('/user', path.resolve(__dirname, 'routes/user/create')); -server.get('/user/:id', path.resolve(__dirname, 'routes/user/detail')); -server.put('/user/:id', path.resolve(__dirname, 'routes/user/update')); -server.delete('/user/:id', path.resolve(__dirname, 'routes/user/remove')); - -server.get('/redirect', path.resolve(__dirname, 'routes/redirect')); -server.get('/error', path.resolve(__dirname, 'routes/error')); -server.get('/login', path.resolve(__dirname, 'routes/login')); -server.get('/icon.png', path.resolve(__dirname, 'routes/icon')); -server.get('/', path.resolve(__dirname, 'routes/home')); - -server.get('/**', path.resolve(__dirname, 'routes/404')); - -export default server; \ No newline at end of file diff --git a/examples/with-netlify/src/routes/404.ts b/examples/with-netlify/src/routes/404.ts deleted file mode 100644 index f00d5d3..0000000 --- a/examples/with-netlify/src/routes/404.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Context, Response } from '@stackpress/ingest'; - -export default function NotFound(req: Context, res: Response) { - if (!res.code && !res.status && !res.sent) { - //send the response - res.setHTML('Not Found'); - } -}; \ No newline at end of file diff --git a/examples/with-netlify/src/routes/error.ts b/examples/with-netlify/src/routes/error.ts deleted file mode 100644 index c84fafd..0000000 --- a/examples/with-netlify/src/routes/error.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Context, Response, Exception } from '@stackpress/ingest'; - -export default function ErrorResponse(req: Context, res: Response) { - try { - throw Exception.for('Not implemented'); - } catch (e) { - const error = e as Exception; - res.setError({ - code: error.code, - error: error.message, - stack: error.trace() - }); - } -}; \ No newline at end of file diff --git a/examples/with-netlify/src/routes/home.ts b/examples/with-netlify/src/routes/home.ts deleted file mode 100644 index c51184b..0000000 --- a/examples/with-netlify/src/routes/home.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { Context, Response } from '@stackpress/ingest'; - -export default async function HomePage(req: Context, res: Response) { - res.setHTML('hello'); -}; \ No newline at end of file diff --git a/examples/with-netlify/src/routes/redirect.ts b/examples/with-netlify/src/routes/redirect.ts deleted file mode 100644 index 0e6c164..0000000 --- a/examples/with-netlify/src/routes/redirect.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { Context, Response } from '@stackpress/ingest'; - -export default function Redirect(req: Context, res: Response) { - res.redirect('/user'); -}; \ No newline at end of file diff --git a/examples/with-netlify/src/routes/user/create.ts b/examples/with-netlify/src/routes/user/create.ts deleted file mode 100644 index b5f2cb0..0000000 --- a/examples/with-netlify/src/routes/user/create.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Context, Response } from '@stackpress/ingest'; - -let id = 0; - -export default function UserCreate(req: Context, res: Response) { - //get form body - const form = req.data.get(); - //maybe insert into database? - const results = { ...form, id: ++id, created: new Date().toISOString() }; - //send the response - res.setResults(results); -}; \ No newline at end of file diff --git a/examples/with-netlify/src/routes/user/detail.ts b/examples/with-netlify/src/routes/user/detail.ts deleted file mode 100644 index 03c6262..0000000 --- a/examples/with-netlify/src/routes/user/detail.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Context, Response } from '@stackpress/ingest'; - -export default function UserDetail(req: Context, res: Response) { - //get params - const id = parseInt(req.data.get('id') || ''); - if (!id) { - return res.setError('ID is required'); - } - //maybe get from database? - const results = { - id: id, - name: 'John Doe', - age: 21, - created: new Date().toISOString() - }; - //send the response - res.setResults(results); -}; \ No newline at end of file diff --git a/examples/with-netlify/src/routes/user/remove.ts b/examples/with-netlify/src/routes/user/remove.ts deleted file mode 100644 index 19cb630..0000000 --- a/examples/with-netlify/src/routes/user/remove.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Context, Response } from '@stackpress/ingest'; - -export default function UserRemove(req: Context, res: Response) { - //get params - const id = parseInt(req.data.get('id') || ''); - if (!id) { - return res.setError('ID is required'); - } - //maybe get from database? - const results = { - id: 1, - name: 'John Doe', - age: 21, - created: new Date().toISOString() - }; - //send the response - res.setResults(results); -}; \ No newline at end of file diff --git a/examples/with-netlify/src/routes/user/update.ts b/examples/with-netlify/src/routes/user/update.ts deleted file mode 100644 index 112ea8d..0000000 --- a/examples/with-netlify/src/routes/user/update.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Context, Response } from '@stackpress/ingest'; - -export default function UserUpdate(req: Context, res: Response) { - //get params - const id = parseInt(req.data.get('id') || ''); - if (!id) { - return res.setError('ID is required'); - } - //get form body - const form = req.post.get(); - //maybe insert into database? - const results = { ...form, id, created: new Date().toISOString() }; - //send the response - res.setResults(results); -}; \ No newline at end of file diff --git a/examples/with-netlify/src/server.ts b/examples/with-netlify/src/server.ts deleted file mode 100644 index abb0a78..0000000 --- a/examples/with-netlify/src/server.ts +++ /dev/null @@ -1,6 +0,0 @@ -import server from './routes'; -server.create().listen(3000, () => { - console.log('Server is running on port 3000'); - console.log('------------------------------'); - console.log(server.router.listeners); -}); \ No newline at end of file diff --git a/examples/with-plugins/package.json b/examples/with-plugins/package.json index aa829ca..3cf8b96 100644 --- a/examples/with-plugins/package.json +++ b/examples/with-plugins/package.json @@ -3,12 +3,15 @@ "version": "1.0.0", "description": "A simple boilerplate for using Ingest with plugins.", "private": true, + "plugins": [ + "./src/plugin" + ], "scripts": { "build": "tsc", "dev": "ts-node src/scripts/serve.ts" }, "dependencies": { - "@stackpress/ingest-vercel": "0.2.14" + "@stackpress/ingest": "0.3.6" }, "devDependencies": { "@types/node": "22.9.3", diff --git a/examples/with-plugins/plugins.json b/examples/with-plugins/plugins.json deleted file mode 100644 index f1d1741..0000000 --- a/examples/with-plugins/plugins.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "build": [ - "./dist/plugin/build" - ], - "client": [ - "./dist/plugin/client" - ] -} \ No newline at end of file diff --git a/examples/with-plugins/src/error.ts b/examples/with-plugins/src/error.ts new file mode 100644 index 0000000..20ad1f7 --- /dev/null +++ b/examples/with-plugins/src/error.ts @@ -0,0 +1,3 @@ +export default function ShouldNotWork(message: string) { + throw new Error(message); +}; \ No newline at end of file diff --git a/examples/with-plugins/src/plugin.ts b/examples/with-plugins/src/plugin.ts new file mode 100644 index 0000000..6417437 --- /dev/null +++ b/examples/with-plugins/src/plugin.ts @@ -0,0 +1,18 @@ +import type { HTTPServer } from '@stackpress/ingest'; +import type { Config } from './config'; + +import { config } from './config'; + +import hooks from './routes/hooks'; +import pages from './routes/pages'; +import tests from './routes/tests'; +import user from './routes/user'; + +export default function plugin(server: HTTPServer) { + server.config.set(config); + server.use(pages).use(tests).use(user).use(hooks); + server.register('project', { welcome: 'Hello, World!!' }); + server.on('request', (req, res) => { + console.log('Request:', req.url); + }); +} \ No newline at end of file diff --git a/examples/with-plugins/src/plugin/build.ts b/examples/with-plugins/src/plugin/build.ts deleted file mode 100644 index b38a35b..0000000 --- a/examples/with-plugins/src/plugin/build.ts +++ /dev/null @@ -1,37 +0,0 @@ -import type { Server } from '@stackpress/ingest/build'; -import type { Config } from '../config'; - -import path from 'path'; -import { config } from '../config'; - -export default function plugin(server: Server) { - server.config.set(config); - server.on('route', _ => { - const { cwd, mode } = server.config.data.server; - const routes = mode === 'development' - ? path.join(cwd, 'src/routes') - : path.join(cwd, 'dist/routes'); - - server.get('/user', path.resolve(routes, 'user/search')); - server.post('/user', path.resolve(routes, 'user/create')); - server.get('/user/:id', path.resolve(routes, 'user/detail')); - server.put('/user/:id', path.resolve(routes, 'user/update')); - server.delete('/user/:id', path.resolve(routes, 'user/remove')); - - server.get('/redirect', path.resolve(routes, 'redirect')); - server.get('/error', path.resolve(routes, 'error')); - server.get('/login', path.resolve(routes, 'login')); - server.get('/icon.png', path.resolve(routes, 'icon')); - server.get('/stream', path.resolve(routes, 'stream')); - server.get('/__sse__', path.resolve(routes, 'sse')); - server.get('/', path.resolve(routes, 'home')); - - server.router.emitter.on('error', async (req, res) => { - const module = path.resolve(routes, 'home'); - const entry = await import(module); - const action = entry.default; - delete require.cache[require.resolve(module)]; - return await action(req, res); - }); - }); -} \ No newline at end of file diff --git a/examples/with-plugins/src/plugin/client.ts b/examples/with-plugins/src/plugin/client.ts deleted file mode 100644 index 8a36811..0000000 --- a/examples/with-plugins/src/plugin/client.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { Factory } from '@stackpress/ingest'; -import type { Config } from '../config'; - -import { config } from '../config'; - -export default function plugin(client: Factory) { - client.config.set(config); - client.on('request', _ => { - client.register('project', { welcome: 'Hello, World!!' }); - }); -} \ No newline at end of file diff --git a/examples/with-plugins/src/routes/404.ts b/examples/with-plugins/src/routes/404.ts deleted file mode 100644 index f00d5d3..0000000 --- a/examples/with-plugins/src/routes/404.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Context, Response } from '@stackpress/ingest'; - -export default function NotFound(req: Context, res: Response) { - if (!res.code && !res.status && !res.sent) { - //send the response - res.setHTML('Not Found'); - } -}; \ No newline at end of file diff --git a/examples/with-plugins/src/routes/home.ts b/examples/with-plugins/src/routes/home.ts deleted file mode 100644 index 00dcc00..0000000 --- a/examples/with-plugins/src/routes/home.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { route } from '@stackpress/ingest'; - -export default route(async function HomePage(req, res) { - const project = req.context.plugin<{ welcome: string }>('project'); - res.setHTML(project.welcome); -}); \ No newline at end of file diff --git a/examples/with-plugins/src/routes/hooks.ts b/examples/with-plugins/src/routes/hooks.ts new file mode 100644 index 0000000..99b8b12 --- /dev/null +++ b/examples/with-plugins/src/routes/hooks.ts @@ -0,0 +1,54 @@ +import type { Config } from '../config'; +import { Exception } from '@stackpress/ingest'; +import { router } from '@stackpress/ingest/http'; +import Error from '../error'; + +const route = router(); + +/** + * Error handlers + */ +route.get('/catch', function ErrorResponse(req, res) { + try { + throw Exception.for('Not implemented'); + } catch (e) { + const error = e as Exception; + res.setError({ + code: error.code, + error: error.message, + stack: error.trace() + }); + } +}); + +/** + * Error handlers + */ +route.get('/error', function ErrorResponse(req, res) { + Error('Not implemented'); +}); + +/** + * 404 handler + */ +route.get('/**', function NotFound(req, res) { + if (!res.code && !res.status && !res.sent) { + //send the response + res.setHTML('Not Found'); + } +}); + +route.on('error', function Error(req, res) { + const html = [ `

        ${res.error}

        ` ]; + const stack = res.stack?.map((log, i) => { + const { line, char } = log; + const method = log.method.replace(//g, ">"); + const file = log.file.replace(//g, ">"); + return `#${i + 1} ${method} - ${file}:${line}:${char}`; + }) || []; + html.push(`
        ${stack.join('

        ')}
        `); + + res.setHTML(html.join('
        ')); +}); + +export default route; \ No newline at end of file diff --git a/examples/with-plugins/src/routes/icon.ts b/examples/with-plugins/src/routes/icon.ts deleted file mode 100644 index ae6b9fd..0000000 --- a/examples/with-plugins/src/routes/icon.ts +++ /dev/null @@ -1,11 +0,0 @@ -import fs from 'fs'; -import path from 'path'; -import { Context, Response } from '@stackpress/ingest'; - -export default async function Icon(req: Context, res: Response) { - if (res.code || res.status || res.body) return; - const file = path.resolve(process.cwd(), 'icon.png'); - if (fs.existsSync(file)) { - res.setBody('image/png', fs.createReadStream(file)); - } -}; \ No newline at end of file diff --git a/examples/with-plugins/src/routes/pages.ts b/examples/with-plugins/src/routes/pages.ts new file mode 100644 index 0000000..31697d7 --- /dev/null +++ b/examples/with-plugins/src/routes/pages.ts @@ -0,0 +1,41 @@ +import type { Config } from '../config'; +import { router } from '@stackpress/ingest/http'; + +const template = ` + + + + Login + + +

        Login

        +
        + + + + + +
        + + +`; + +const route = router(); + +/** + * Home page + */ +route.get('/', function HomePage(req, res) { + const project = req.context.plugin<{ welcome: string }>('project'); + res.setHTML(project.welcome); +}); + +/** + * Login page + */ +route.get('/login', function Login(req, res) { + //send the response + res.setHTML(template.trim()); +}); + +export default route; \ No newline at end of file diff --git a/examples/with-plugins/src/routes/redirect.ts b/examples/with-plugins/src/routes/redirect.ts deleted file mode 100644 index 0e6c164..0000000 --- a/examples/with-plugins/src/routes/redirect.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { Context, Response } from '@stackpress/ingest'; - -export default function Redirect(req: Context, res: Response) { - res.redirect('/user'); -}; \ No newline at end of file diff --git a/examples/with-plugins/src/routes/sse.ts b/examples/with-plugins/src/routes/sse.ts deleted file mode 100644 index 26cc2d7..0000000 --- a/examples/with-plugins/src/routes/sse.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Context, Response } from '@stackpress/ingest'; - -export default async function SSE(req: Context, res: Response) { - res.headers - .set('Cache-Control', 'no-cache') - .set('Content-Encoding', 'none') - .set('Connection', 'keep-alive') - .set('Access-Control-Allow-Origin', '*'); - - let timerId: any; - const msg = new TextEncoder().encode("data: hello\r\n\r\n"); - res.setBody('text/event-stream', new ReadableStream({ - start(controller) { - timerId = setInterval(() => { - controller.enqueue(msg); - }, 2500); - }, - cancel() { - if (typeof timerId === 'number') { - clearInterval(timerId); - } - }, - })); -}; \ No newline at end of file diff --git a/examples/with-plugins/src/routes/tests.ts b/examples/with-plugins/src/routes/tests.ts new file mode 100644 index 0000000..1a310a5 --- /dev/null +++ b/examples/with-plugins/src/routes/tests.ts @@ -0,0 +1,83 @@ +import type { Config } from '../config'; + +import fs from 'fs'; +import path from 'path'; + +import { router } from '@stackpress/ingest/http'; + +const template = ` + + + + SSE + + +
          + + + +`; + +const route = router(); + +/** + * Redirect test + */ +route.get('/redirect', function Redirect(req, res) { + res.redirect('/user'); +}); + +/** + * Static file test + */ +route.get('/icon.png', function Icon(req, res) { + if (res.code || res.status || res.body) return; + const file = path.resolve(process.cwd(), 'icon.png'); + if (fs.existsSync(file)) { + res.setBody('image/png', fs.createReadStream(file)); + } +}); + +/** + * Stream template for SSE test + */ +route.get('/stream', function Stream(req, res) { + //send the response + res.setHTML(template.trim()); +}); + +/** + * SSE test + */ +route.get('/__sse__', function SSE(req, res) { + res.headers + .set('Cache-Control', 'no-cache') + .set('Content-Encoding', 'none') + .set('Connection', 'keep-alive') + .set('Access-Control-Allow-Origin', '*'); + + let timerId: any; + const msg = new TextEncoder().encode("data: hello\r\n\r\n"); + res.setBody('text/event-stream', new ReadableStream({ + start(controller) { + timerId = setInterval(() => { + controller.enqueue(msg); + }, 2500); + }, + cancel() { + if (typeof timerId === 'number') { + clearInterval(timerId); + } + }, + })); +}); + +export default route; \ No newline at end of file diff --git a/examples/with-plugins/src/routes/user.ts b/examples/with-plugins/src/routes/user.ts new file mode 100644 index 0000000..3ee1b8f --- /dev/null +++ b/examples/with-plugins/src/routes/user.ts @@ -0,0 +1,103 @@ +import type { Config } from '../config'; +import { router } from '@stackpress/ingest/http'; + +const route = router(); + +let id = 0; + +/** + * Example user API search + */ +route.get('/user', function UserSearch(req, res) { + //get filters + //const filters = req.query.get>('filter'); + //maybe get from database? + const results = [ + { + id: 1, + name: 'John Doe', + age: 21, + created: new Date().toISOString() + }, + { + id: 2, + name: 'Jane Doe', + age: 30, + created: new Date().toISOString() + } + ]; + //send the response + res.setRows(results, 100); +}); + +/** + * Example user API create (POST) + * Need to use Postman to see this... + */ +route.post('/user', function UserCreate(req, res) { + //get form body + const form = req.data(); + //maybe insert into database? + const results = { ...form, id: ++id, created: new Date().toISOString() }; + //send the response + res.setResults(results); +}); + +/** + * Example user API detail + */ +route.get('/user/:id', function UserDetail(req, res) { + //get params + const id = parseInt(req.data('id') || ''); + if (!id) { + res.setError('ID is required'); + return; + } + //maybe get from database? + const results = { + id: id, + name: 'John Doe', + age: 21, + created: new Date().toISOString() + }; + //send the response + res.setResults(results); +}); +route.put('/user/:id', function UserUpdate(req, res) { + //get params + const id = parseInt(req.data('id') || ''); + if (!id) { + res.setError('ID is required'); + return; + } + //get form body + const form = req.post(); + //maybe insert into database? + const results = { ...form, id, created: new Date().toISOString() }; + //send the response + res.setResults(results); +}); + +/** + * Example user API delete (DELETE) + * Need to use Postman to see this... + */ +route.delete('/user/:id', function UserRemove(req, res) { + //get params + const id = parseInt(req.data('id') || ''); + if (!id) { + res.setError('ID is required'); + return; + } + //maybe get from database? + const results = { + id: 1, + name: 'John Doe', + age: 21, + created: new Date().toISOString() + }; + //send the response + res.setResults(results); +}); + +export default route; \ No newline at end of file diff --git a/examples/with-plugins/src/routes/user/create.ts b/examples/with-plugins/src/routes/user/create.ts deleted file mode 100644 index 97379b3..0000000 --- a/examples/with-plugins/src/routes/user/create.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Context, Response } from '@stackpress/ingest'; - -let id = 0; - -export default function UserCreate(req: Context, res: Response) { - //get form body - const form = req.data(); - //maybe insert into database? - const results = { ...form, id: ++id, created: new Date().toISOString() }; - //send the response - res.setResults(results); -}; \ No newline at end of file diff --git a/examples/with-plugins/src/routes/user/search.ts b/examples/with-plugins/src/routes/user/search.ts deleted file mode 100644 index 69838a7..0000000 --- a/examples/with-plugins/src/routes/user/search.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Context, Response } from '@stackpress/ingest'; - -export default function UserSearch(req: Context, res: Response) { - //get filters - //const filters = req.query.get>('filter'); - //maybe get from database? - const results = [ - { - id: 1, - name: 'John Doe', - age: 21, - created: new Date().toISOString() - }, - { - id: 2, - name: 'Jane Doe', - age: 30, - created: new Date().toISOString() - } - ]; - //send the response - res.setRows(results, 100); -}; \ No newline at end of file diff --git a/examples/with-plugins/src/routes/user/update.ts b/examples/with-plugins/src/routes/user/update.ts deleted file mode 100644 index d61e997..0000000 --- a/examples/with-plugins/src/routes/user/update.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Context, Response } from '@stackpress/ingest'; - -export default function UserUpdate(req: Context, res: Response) { - //get params - const id = parseInt(req.data('id') || ''); - if (!id) { - return res.setError('ID is required'); - } - //get form body - const form = req.post(); - //maybe insert into database? - const results = { ...form, id, created: new Date().toISOString() }; - //send the response - res.setResults(results); -}; \ No newline at end of file diff --git a/examples/with-plugins/src/scripts/build.ts b/examples/with-plugins/src/scripts/build.ts deleted file mode 100644 index 595592f..0000000 --- a/examples/with-plugins/src/scripts/build.ts +++ /dev/null @@ -1,15 +0,0 @@ -import server from '../server'; - -async function main() { - //load the plugins - await server.bootstrap(); - //load the plugin routes - await server.emit('route', server.request(), server.response()); - //build() is only available in vercel, netlify, - //and other serverless environments - // server.build().then(({ build }) => { - // console.log('done.', build); - // }); -} - -main().catch(console.error); \ No newline at end of file diff --git a/examples/with-plugins/src/scripts/serve.ts b/examples/with-plugins/src/scripts/serve.ts index 2159186..0f29281 100644 --- a/examples/with-plugins/src/scripts/serve.ts +++ b/examples/with-plugins/src/scripts/serve.ts @@ -3,14 +3,11 @@ import server from '../server'; async function main() { //load the plugins await server.bootstrap(); - //load the plugin routes - await server.emit('route', server.request(), server.response()); //start the server server.create().listen(3000, () => { console.log('Server is running on port 3000'); console.log('------------------------------'); - console.log(server.router.listeners); }); -} +}; main().catch(console.error); \ No newline at end of file diff --git a/examples/with-plugins/src/server.ts b/examples/with-plugins/src/server.ts index 5855a21..93a7b78 100644 --- a/examples/with-plugins/src/server.ts +++ b/examples/with-plugins/src/server.ts @@ -1,17 +1,10 @@ //types import type { Config } from './config'; -//modules -import path from 'path'; //ingest -import { server } from '@stackpress/ingest/build'; +import { server } from '@stackpress/ingest/http'; //local -import { config } from './config'; - -export type { Config }; -export { config }; +import { environment } from './config'; export default server({ - size: 0, - cookie: { path: '/' }, - tsconfig: path.resolve(__dirname, '../tsconfig.json') + cache: environment === 'production' }); \ No newline at end of file diff --git a/examples/with-vercel/api/handle.ts b/examples/with-vercel/api/handle.ts new file mode 100644 index 0000000..e865c2b --- /dev/null +++ b/examples/with-vercel/api/handle.ts @@ -0,0 +1,11 @@ +import handle from '../src/scripts/handle'; + +export const CONNECT = handle; +export const DELETE = handle; +export const GET = handle; +export const HEAD = handle; +export const OPTIONS = handle; +export const PATCH = handle; +export const POST = handle; +export const PUT = handle; +export const TRACE = handle; \ No newline at end of file diff --git a/examples/with-vercel/package.json b/examples/with-vercel/package.json index 0281020..812b1cf 100644 --- a/examples/with-vercel/package.json +++ b/examples/with-vercel/package.json @@ -3,14 +3,17 @@ "version": "1.0.0", "description": "A simple boilerplate for using Ingest with Vercel.", "private": true, + "plugins": [ + "./dist/plugin" + ], "scripts": { "generate": "yarn build:tsc && yarn build:files", "build:tsc": "tsc", "build:files": "ts-node src/scripts/build.ts", - "dev": "ts-node src/scripts/server.ts" + "dev": "ts-node src/scripts/serve.ts" }, "dependencies": { - "@stackpress/ingest-vercel": "0.2.14" + "@stackpress/ingest": "0.3.6" }, "devDependencies": { "@types/node": "22.9.3", diff --git a/examples/with-vercel/plugin.d.ts b/examples/with-vercel/plugin.d.ts deleted file mode 100644 index 1f1ce10..0000000 --- a/examples/with-vercel/plugin.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -import plugin from './dist/plugin'; -export default plugin; \ No newline at end of file diff --git a/examples/with-vercel/plugin.js b/examples/with-vercel/plugin.js deleted file mode 100644 index 098f7a9..0000000 --- a/examples/with-vercel/plugin.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('./dist/plugin'); \ No newline at end of file diff --git a/examples/with-vercel/src/build.ts b/examples/with-vercel/src/build.ts deleted file mode 100644 index de27385..0000000 --- a/examples/with-vercel/src/build.ts +++ /dev/null @@ -1,4 +0,0 @@ -import server from './routes'; -server.build().then(({ build }) => { - console.log('done.', build); -}); \ No newline at end of file diff --git a/examples/with-vercel/src/config.ts b/examples/with-vercel/src/config.ts new file mode 100644 index 0000000..cea6181 --- /dev/null +++ b/examples/with-vercel/src/config.ts @@ -0,0 +1,10 @@ +export const environment = process.env.SERVER_ENV || 'development'; +export const config = { + server: { + cwd: process.cwd(), + mode: environment, + bodySize: 0 + }, + cookie: { path: '/' }, + body: { size: 0 } +}; \ No newline at end of file diff --git a/examples/with-vercel/src/plugin.ts b/examples/with-vercel/src/plugin.ts new file mode 100644 index 0000000..2813ddb --- /dev/null +++ b/examples/with-vercel/src/plugin.ts @@ -0,0 +1,17 @@ +import type { Server } from '@stackpress/ingest'; + +import { config } from './config'; + +import hooks from './routes/hooks'; +import pages from './routes/pages'; +import tests from './routes/tests'; +import user from './routes/user'; + +export default function plugin(server: Server) { + server.config.set(config); + server.use(pages).use(tests).use(user).use(hooks); + server.register('project', { welcome: 'Hello, World!!' }); + server.on('request', (req, res) => { + console.log('Request:', req.url); + }); +}; \ No newline at end of file diff --git a/examples/with-vercel/src/routes.ts b/examples/with-vercel/src/routes.ts deleted file mode 100644 index 90cfa3c..0000000 --- a/examples/with-vercel/src/routes.ts +++ /dev/null @@ -1,22 +0,0 @@ -import path from 'path'; -import vercel from '@stackpress/ingest-vercel'; - -const server = vercel(); - -server.get('/user', path.resolve(__dirname, 'routes/user/search')); -server.post('/user', path.resolve(__dirname, 'routes/user/create')); -server.get('/user/:id', path.resolve(__dirname, 'routes/user/detail')); -server.put('/user/:id', path.resolve(__dirname, 'routes/user/update')); -server.delete('/user/:id', path.resolve(__dirname, 'routes/user/remove')); - -server.get('/redirect', path.resolve(__dirname, 'routes/redirect')); -server.get('/error', path.resolve(__dirname, 'routes/error')); -server.get('/login', path.resolve(__dirname, 'routes/login')); -server.get('/icon.png', path.resolve(__dirname, 'routes/icon')); -server.get('/stream', path.resolve(__dirname, 'routes/stream')); -server.get('/__sse__', path.resolve(__dirname, 'routes/sse')); -server.get('/', path.resolve(__dirname, 'routes/home')); - -server.get('/**', path.resolve(__dirname, 'routes/404')); - -export default server; \ No newline at end of file diff --git a/examples/with-vercel/src/routes/404.ts b/examples/with-vercel/src/routes/404.ts deleted file mode 100644 index f00d5d3..0000000 --- a/examples/with-vercel/src/routes/404.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Context, Response } from '@stackpress/ingest'; - -export default function NotFound(req: Context, res: Response) { - if (!res.code && !res.status && !res.sent) { - //send the response - res.setHTML('Not Found'); - } -}; \ No newline at end of file diff --git a/examples/with-vercel/src/routes/error.ts b/examples/with-vercel/src/routes/error.ts deleted file mode 100644 index c84fafd..0000000 --- a/examples/with-vercel/src/routes/error.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Context, Response, Exception } from '@stackpress/ingest'; - -export default function ErrorResponse(req: Context, res: Response) { - try { - throw Exception.for('Not implemented'); - } catch (e) { - const error = e as Exception; - res.setError({ - code: error.code, - error: error.message, - stack: error.trace() - }); - } -}; \ No newline at end of file diff --git a/examples/with-vercel/src/routes/home.ts b/examples/with-vercel/src/routes/home.ts deleted file mode 100644 index 3f232a6..0000000 --- a/examples/with-vercel/src/routes/home.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Context, Response } from '@stackpress/ingest'; - -import bootstrap from '../client'; - -export default async function HomePage(req: Context, res: Response) { - const client = await bootstrap(req, res); - const project = client.plugin<{ welcome: string }>('project'); - res.setHTML(project.welcome); -}; \ No newline at end of file diff --git a/examples/with-vercel/src/routes/hooks.ts b/examples/with-vercel/src/routes/hooks.ts new file mode 100644 index 0000000..3777291 --- /dev/null +++ b/examples/with-vercel/src/routes/hooks.ts @@ -0,0 +1,32 @@ +import { Exception } from '@stackpress/ingest'; +import { ServerRouter } from '@stackpress/ingest'; + +const router = new ServerRouter(); + +/** + * Error handlers + */ +router.get('/error', function ErrorResponse(req, res) { + try { + throw Exception.for('Not implemented'); + } catch (e) { + const error = e as Exception; + res.setError({ + code: error.code, + error: error.message, + stack: error.trace() + }); + } +}); + +/** + * 404 handler + */ +router.get('/**', function NotFound(req, res) { + if (!res.code && !res.status && !res.sent) { + //send the response + res.setHTML('Not Found'); + } +}); + +export default router; \ No newline at end of file diff --git a/examples/with-vercel/src/routes/icon.ts b/examples/with-vercel/src/routes/icon.ts deleted file mode 100644 index ae6b9fd..0000000 --- a/examples/with-vercel/src/routes/icon.ts +++ /dev/null @@ -1,11 +0,0 @@ -import fs from 'fs'; -import path from 'path'; -import { Context, Response } from '@stackpress/ingest'; - -export default async function Icon(req: Context, res: Response) { - if (res.code || res.status || res.body) return; - const file = path.resolve(process.cwd(), 'icon.png'); - if (fs.existsSync(file)) { - res.setBody('image/png', fs.createReadStream(file)); - } -}; \ No newline at end of file diff --git a/examples/with-plugins/src/routes/login.ts b/examples/with-vercel/src/routes/pages.ts similarity index 58% rename from examples/with-plugins/src/routes/login.ts rename to examples/with-vercel/src/routes/pages.ts index e01af9b..5393860 100644 --- a/examples/with-plugins/src/routes/login.ts +++ b/examples/with-vercel/src/routes/pages.ts @@ -1,4 +1,4 @@ -import { Context, Response } from '@stackpress/ingest'; +import { ServerRouter } from '@stackpress/ingest'; const template = ` @@ -19,7 +19,23 @@ const template = ` `; -export default function Login(req: Context, res: Response) { + +const router = new ServerRouter(); + +/** + * Home page + */ +router.get('/', function HomePage(req, res) { + const project = req.context.plugin<{ welcome: string }>('project'); + res.setHTML(project.welcome); +}); + +/** + * Login page + */ +router.get('/login', function Login(req, res) { //send the response res.setHTML(template.trim()); -}; \ No newline at end of file +}); + +export default router; \ No newline at end of file diff --git a/examples/with-vercel/src/routes/redirect.ts b/examples/with-vercel/src/routes/redirect.ts deleted file mode 100644 index 0e6c164..0000000 --- a/examples/with-vercel/src/routes/redirect.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { Context, Response } from '@stackpress/ingest'; - -export default function Redirect(req: Context, res: Response) { - res.redirect('/user'); -}; \ No newline at end of file diff --git a/examples/with-vercel/src/routes/stream.ts b/examples/with-vercel/src/routes/stream.ts deleted file mode 100644 index 2ea616c..0000000 --- a/examples/with-vercel/src/routes/stream.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Context, Response } from '@stackpress/ingest'; - -const template = ` - - - - SSE - - -
            - - - -`; - -export default function Stream(req: Context, res: Response) { - //send the response - res.setHTML(template.trim()); -}; \ No newline at end of file diff --git a/examples/with-vercel/src/routes/tests.ts b/examples/with-vercel/src/routes/tests.ts new file mode 100644 index 0000000..6a69a0d --- /dev/null +++ b/examples/with-vercel/src/routes/tests.ts @@ -0,0 +1,80 @@ +import fs from 'fs'; +import path from 'path'; +import { ServerRouter } from '@stackpress/ingest'; + +const template = ` + + + + SSE + + +
              + + + +`; + +const router = new ServerRouter(); + +/** + * Redirect test + */ +router.get('/redirect', function Redirect(req, res) { + res.redirect('/user'); +}); + +/** + * Static file test + */ +router.get('/icon.png', function Icon(req, res) { + if (res.code || res.status || res.body) return; + const file = path.resolve(process.cwd(), 'icon.png'); + if (fs.existsSync(file)) { + res.setBody('image/png', fs.createReadStream(file)); + } +}); + +/** + * Stream template for SSE test + */ +router.get('/stream', function Stream(req, res) { + //send the response + res.setHTML(template.trim()); +}); + +/** + * SSE test + */ +router.get('/__sse__', function SSE(req, res) { + res.headers + .set('Cache-Control', 'no-cache') + .set('Content-Encoding', 'none') + .set('Connection', 'keep-alive') + .set('Access-Control-Allow-Origin', '*'); + + let timerId: any; + const msg = new TextEncoder().encode("data: hello\r\n\r\n"); + res.setBody('text/event-stream', new ReadableStream({ + start(controller) { + timerId = setInterval(() => { + controller.enqueue(msg); + }, 2500); + }, + cancel() { + if (typeof timerId === 'number') { + clearInterval(timerId); + } + }, + })); +}); + +export default router; \ No newline at end of file diff --git a/examples/with-vercel/src/routes/user.ts b/examples/with-vercel/src/routes/user.ts new file mode 100644 index 0000000..a6d95ac --- /dev/null +++ b/examples/with-vercel/src/routes/user.ts @@ -0,0 +1,107 @@ +import { ServerRouter } from '@stackpress/ingest'; + +const router = new ServerRouter(); + +let id = 0; + +/** + * Example user API search + */ +router.get('/user', function UserSearch(req, res) { + //get filters + //const filters = req.query.get>('filter'); + //maybe get from database? + const results = [ + { + id: 1, + name: 'John Doe', + age: 21, + created: new Date().toISOString() + }, + { + id: 2, + name: 'Jane Doe', + age: 30, + created: new Date().toISOString() + } + ]; + //send the response + res.setRows(results, 100); +}); + +/** + * Example user API create (POST) + * Need to use Postman to see this... + */ +router.post('/user', function UserCreate(req, res) { + //get form body + const form = req.data(); + //maybe insert into database? + const results = { ...form, id: ++id, created: new Date().toISOString() }; + //send the response + res.setResults(results); +}); + +/** + * Example user API detail + */ +router.get('/user/:id', function UserDetail(req, res) { + //get params + const id = parseInt(req.data('id') || ''); + if (!id) { + res.setError('ID is required'); + return; + } + //maybe get from database? + const results = { + id: id, + name: 'John Doe', + age: 21, + created: new Date().toISOString() + }; + //send the response + res.setResults(results); +}); + +/** + * Example user API update (POST) + * Need to use Postman to see this... + */ +router.put('/user/:id', function UserUpdate(req, res) { + //get params + const id = parseInt(req.data('id') || ''); + if (!id) { + res.setError('ID is required'); + return; + } + //get form body + const form = req.post(); + //maybe insert into database? + const results = { ...form, id, created: new Date().toISOString() }; + //send the response + res.setResults(results); +}); + +/** + * Example user API delete (DELETE) + * Need to use Postman to see this... + */ +router.delete('/user/:id', function UserRemove(req, res) { + //get params + const id = parseInt(req.data('id') || ''); + if (!id) { + res.setError('ID is required'); + return; + } + //maybe get from database? + const results = { + id: 1, + name: 'John Doe', + age: 21, + created: new Date().toISOString() + }; + //send the response + res.setResults(results); +}); + +export default router; \ No newline at end of file diff --git a/examples/with-vercel/src/routes/user/create.ts b/examples/with-vercel/src/routes/user/create.ts deleted file mode 100644 index 97379b3..0000000 --- a/examples/with-vercel/src/routes/user/create.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Context, Response } from '@stackpress/ingest'; - -let id = 0; - -export default function UserCreate(req: Context, res: Response) { - //get form body - const form = req.data(); - //maybe insert into database? - const results = { ...form, id: ++id, created: new Date().toISOString() }; - //send the response - res.setResults(results); -}; \ No newline at end of file diff --git a/examples/with-vercel/src/routes/user/detail.ts b/examples/with-vercel/src/routes/user/detail.ts deleted file mode 100644 index e3109ee..0000000 --- a/examples/with-vercel/src/routes/user/detail.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Context, Response } from '@stackpress/ingest'; - -export default function UserDetail(req: Context, res: Response) { - //get params - const id = parseInt(req.data('id') || ''); - if (!id) { - return res.setError('ID is required'); - } - //maybe get from database? - const results = { - id: id, - name: 'John Doe', - age: 21, - created: new Date().toISOString() - }; - //send the response - res.setResults(results); -}; \ No newline at end of file diff --git a/examples/with-vercel/src/routes/user/remove.ts b/examples/with-vercel/src/routes/user/remove.ts deleted file mode 100644 index 05ff009..0000000 --- a/examples/with-vercel/src/routes/user/remove.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Context, Response } from '@stackpress/ingest'; - -export default function UserRemove(req: Context, res: Response) { - //get params - const id = parseInt(req.data('id') || ''); - if (!id) { - return res.setError('ID is required'); - } - //maybe get from database? - const results = { - id: 1, - name: 'John Doe', - age: 21, - created: new Date().toISOString() - }; - //send the response - res.setResults(results); -}; \ No newline at end of file diff --git a/examples/with-vercel/src/routes/user/search.ts b/examples/with-vercel/src/routes/user/search.ts deleted file mode 100644 index 69838a7..0000000 --- a/examples/with-vercel/src/routes/user/search.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Context, Response } from '@stackpress/ingest'; - -export default function UserSearch(req: Context, res: Response) { - //get filters - //const filters = req.query.get>('filter'); - //maybe get from database? - const results = [ - { - id: 1, - name: 'John Doe', - age: 21, - created: new Date().toISOString() - }, - { - id: 2, - name: 'Jane Doe', - age: 30, - created: new Date().toISOString() - } - ]; - //send the response - res.setRows(results, 100); -}; \ No newline at end of file diff --git a/examples/with-vercel/src/routes/user/update.ts b/examples/with-vercel/src/routes/user/update.ts deleted file mode 100644 index d61e997..0000000 --- a/examples/with-vercel/src/routes/user/update.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Context, Response } from '@stackpress/ingest'; - -export default function UserUpdate(req: Context, res: Response) { - //get params - const id = parseInt(req.data('id') || ''); - if (!id) { - return res.setError('ID is required'); - } - //get form body - const form = req.post(); - //maybe insert into database? - const results = { ...form, id, created: new Date().toISOString() }; - //send the response - res.setResults(results); -}; \ No newline at end of file diff --git a/examples/with-vercel/src/scripts/handle.ts b/examples/with-vercel/src/scripts/handle.ts new file mode 100644 index 0000000..70889fb --- /dev/null +++ b/examples/with-vercel/src/scripts/handle.ts @@ -0,0 +1,9 @@ +import { server } from '@stackpress/ingest/fetch'; +export default async function handle(request: Request) { + //we need to create a new server instance + const app = server(); + //in order to re bootstrap the server + await app.bootstrap(); + //now we can handle the request + return app.handle(request, undefined); +} \ No newline at end of file diff --git a/examples/with-vercel/src/scripts/serve.ts b/examples/with-vercel/src/scripts/serve.ts new file mode 100644 index 0000000..0f29281 --- /dev/null +++ b/examples/with-vercel/src/scripts/serve.ts @@ -0,0 +1,13 @@ +import server from '../server'; + +async function main() { + //load the plugins + await server.bootstrap(); + //start the server + server.create().listen(3000, () => { + console.log('Server is running on port 3000'); + console.log('------------------------------'); + }); +}; + +main().catch(console.error); \ No newline at end of file diff --git a/examples/with-vercel/src/server.ts b/examples/with-vercel/src/server.ts index abb0a78..5f731df 100644 --- a/examples/with-vercel/src/server.ts +++ b/examples/with-vercel/src/server.ts @@ -1,6 +1,6 @@ -import server from './routes'; -server.create().listen(3000, () => { - console.log('Server is running on port 3000'); - console.log('------------------------------'); - console.log(server.router.listeners); -}); \ No newline at end of file +//ingest +import { server } from '@stackpress/ingest/fetch'; + +const app = server(); + +export default app; \ No newline at end of file diff --git a/examples/with-vercel/vercel.json b/examples/with-vercel/vercel.json new file mode 100644 index 0000000..9673d66 --- /dev/null +++ b/examples/with-vercel/vercel.json @@ -0,0 +1,6 @@ +{ + "version": 2, + "rewrites": [ + { "source": "/:path*", "destination": "/api/handle" } + ] +} \ No newline at end of file diff --git a/package.json b/package.json index 6aa6d4b..d6b4bd9 100644 --- a/package.json +++ b/package.json @@ -5,15 +5,13 @@ "packages/*" ], "scripts": { - "build": "yarn run build:ingest && yarn run build:vercel && yarn run build:netlify", - "build:ingest": "yarn --cwd packages/ingest build", - "build:vercel": "yarn --cwd packages/ingest-vercel build", - "build:netlify": "yarn --cwd packages/ingest-netlify build", - "vercel:build": "yarn --cwd examples/with-vercel generate", - "vercel:build:ts": "yarn --cwd examples/with-vercel build:tsc", - "vercel:dev": "yarn --cwd examples/with-vercel dev", - "netlify:build": "yarn --cwd examples/with-netlify generate", - "netlify:dev": "yarn --cwd examples/with-netlify dev", + "build": "yarn --cwd packages/ingest build", + "entries:build": "yarn --cwd examples/with-entries build", + "entries:dev": "yarn --cwd examples/with-entries dev", + "fetch:build": "yarn --cwd examples/with-fetch build", + "fetch:dev": "yarn --cwd examples/with-fetch dev", + "http:build": "yarn --cwd examples/with-http build", + "http:dev": "yarn --cwd examples/with-http dev", "plugins:build": "yarn --cwd examples/with-plugins build", "plugins:dev": "yarn --cwd examples/with-plugins dev", "test": "yarn --cwd packages/ingest test" diff --git a/packages/ingest-netlify/LICENSE b/packages/ingest-netlify/LICENSE deleted file mode 100644 index a14eed5..0000000 --- a/packages/ingest-netlify/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright 2024 STACKPRESS.IO - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/packages/ingest-netlify/README.md b/packages/ingest-netlify/README.md deleted file mode 100644 index 39400e5..0000000 --- a/packages/ingest-netlify/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# á—Š Ingest - Netlify - -An unopinionated, event driven, pluggable, Netlify framework. diff --git a/packages/ingest-netlify/index.d.ts b/packages/ingest-netlify/index.d.ts deleted file mode 100644 index 8270aa3..0000000 --- a/packages/ingest-netlify/index.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -import vercel from './dist'; -export * from './dist'; -export default vercel; \ No newline at end of file diff --git a/packages/ingest-netlify/index.js b/packages/ingest-netlify/index.js deleted file mode 100644 index 66dfe34..0000000 --- a/packages/ingest-netlify/index.js +++ /dev/null @@ -1,3 +0,0 @@ -const vercel = require('./dist'); -module.exports = vercel.default -Object.assign(module.exports, vercel); \ No newline at end of file diff --git a/packages/ingest-netlify/package.json b/packages/ingest-netlify/package.json deleted file mode 100644 index 8fc6a8c..0000000 --- a/packages/ingest-netlify/package.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name": "@stackpress/ingest-netlify", - "version": "0.2.14", - "license": "Apache-2.0", - "description": "Using Ingest to generate Netlify functions.", - "author": "Chris ", - "homepage": "https://github.com/stackpress/ingest/tree/main/packages/types", - "bugs": "https://github.com/stackpress/ingest/issues", - "repository": "stackpress/ingest", - "main": "index.js", - "types": "index.d.ts", - "files": [ - "index.js", - "index.d.ts", - "dist", - "LICENSE", - "README.md" - ], - "scripts": { - "build": "tsc" - }, - "dependencies": { - "@stackpress/ingest": "0.2.14" - }, - "devDependencies": { - "@types/node": "22.9.3", - "ts-node": "10.9.2", - "typescript": "5.7.2" - } -} \ No newline at end of file diff --git a/packages/ingest-netlify/src/Builder.ts b/packages/ingest-netlify/src/Builder.ts deleted file mode 100644 index 0bb84e7..0000000 --- a/packages/ingest-netlify/src/Builder.ts +++ /dev/null @@ -1,114 +0,0 @@ -import type { - ManifestOptions, - CookieOptions, - ServerOptions, - TranspileInfo, - Transpiler, - UnknownNest -} from '@stackpress/ingest/dist/buildtime/types'; - -import path from 'path'; -import Server from '@stackpress/ingest/dist/buildtime/Server'; -import { - IndentationText, - createSourceFile, - VariableDeclarationKind -} from '@stackpress/ingest/dist/buildtime/helpers'; - -export default class NetlifyBuilder< - C extends UnknownNest = UnknownNest -> extends Server { - /** - * Loads the plugins and returns the factory - */ - public static async bootstrap< - C extends UnknownNest = UnknownNest - >(options: ServerOptions = {}) { - const factory = new NetlifyBuilder(options); - return await factory.bootstrap(); - } - - /** - * Creates an entry file - */ - public transpile(info: TranspileInfo) { - const tsconfig = this.config('server', 'tsconfig') - || path.join(this.loader.cwd, 'tsconfig.json'); - //create a new source file - const { source } = createSourceFile('entry.ts', { - tsConfigFilePath: tsconfig, - skipAddingFilesFromTsConfig: true, - compilerOptions: { - // Generates corresponding '.d.ts' file. - declaration: true, - // Generates a sourcemap for each corresponding '.d.ts' file. - declarationMap: true, - // Generates corresponding '.map' file. - sourceMap: true - }, - manipulationSettings: { - indentationText: IndentationText.TwoSpaces - } - }); - //get cookie options - const cookie = JSON.stringify( - this.config('cookie') || { path: '/' } - ); - //import type { RouteAction } from '@stackpress/ingest/dist/runtime/fetch/types' - source.addImportDeclaration({ - isTypeOnly: true, - moduleSpecifier: '@stackpress/ingest/dist/runtime/fetch/types', - namedImports: [ 'RouteAction' ] - }); - //import Route from '@stackpress/ingest/dist/runtime/fetch/Route'; - source.addImportDeclaration({ - moduleSpecifier: '@stackpress/ingest/dist/runtime/fetch/Route', - defaultImport: 'Route' - }); - //import task1 from [entry] - info.actions.forEach((entry, i) => { - source.addImportDeclaration({ - moduleSpecifier: entry, - defaultImport: `task_${i}` - }); - }); - //export const config = { path: '/user/:id' }; - source.addVariableStatement({ - isExported: true, - declarationKind: VariableDeclarationKind.Const, - declarations: [{ - name: 'config', - initializer: `{ path: '${info.route}' }` - }] - }); - source.addFunction({ - isDefaultExport: true, - name: info.method, - parameters: [{ name: 'request', type: 'Request' }], - statements: (` - if (request.method.toUpperCase() !== '${info.method}') return; - const route = new Route({ cookie: ${cookie} }); - const actions = new Set(); - ${info.actions.map( - (_, i) => `actions.add(task_${i});` - ).join('\n')} - return route.handle('${info.route}', actions, request); - `).trim() - }); - return source; - } - - /** - * Builds the final entry files - */ - public async build(options: ManifestOptions = {}) { - const transpiler: Transpiler = entries => { - return this.transpile(entries); - } - const manifest = this.router.manifest({ - buildDir: './.netlify/functions', - ...options - }); - return await manifest.build(transpiler); - } -} \ No newline at end of file diff --git a/packages/ingest-netlify/src/index.ts b/packages/ingest-netlify/src/index.ts deleted file mode 100644 index 20c3b92..0000000 --- a/packages/ingest-netlify/src/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -//stackpress -import type { - ServerOptions, - UnknownNest -} from '@stackpress/ingest/dist/buildtime/types'; -//netlify -import NetlifyBuilder from './Builder'; - -export * from '@stackpress/ingest/dist/buildtime'; - -export { NetlifyBuilder }; - -export function bootstrap< - C extends UnknownNest = UnknownNest ->(options: ServerOptions = {}) { - return NetlifyBuilder.bootstrap(options); -} - -export default function netlify< - C extends UnknownNest = UnknownNest ->(options: ServerOptions = {}) { - return new NetlifyBuilder(options); -} \ No newline at end of file diff --git a/packages/ingest-netlify/tsconfig.json b/packages/ingest-netlify/tsconfig.json deleted file mode 100644 index 29034a6..0000000 --- a/packages/ingest-netlify/tsconfig.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "compilerOptions": { - "declaration": true, - "esModuleInterop": true, - "lib": [ "es2021", "es7", "es6", "dom" ], - "module": "commonjs", - "noUnusedLocals": true, - "outDir": "./dist/", - "preserveConstEnums": true, - "resolveJsonModule": true, - "removeComments": true, - "sourceMap": false, - "strict": true, - "target": "es6", - "skipLibCheck": true - }, - "include": [ "src/**/*.ts" ], - "exclude": [ "dist", "docs", "node_modules", "test"] -} diff --git a/packages/ingest-vercel/LICENSE b/packages/ingest-vercel/LICENSE deleted file mode 100644 index a14eed5..0000000 --- a/packages/ingest-vercel/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright 2024 STACKPRESS.IO - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/packages/ingest-vercel/README.md b/packages/ingest-vercel/README.md deleted file mode 100644 index 5cfbe03..0000000 --- a/packages/ingest-vercel/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# á—Š Ingest - Vercel - -An unopinionated, event driven, pluggable, Vercel framework. diff --git a/packages/ingest-vercel/index.d.ts b/packages/ingest-vercel/index.d.ts deleted file mode 100644 index 8270aa3..0000000 --- a/packages/ingest-vercel/index.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -import vercel from './dist'; -export * from './dist'; -export default vercel; \ No newline at end of file diff --git a/packages/ingest-vercel/index.js b/packages/ingest-vercel/index.js deleted file mode 100644 index 66dfe34..0000000 --- a/packages/ingest-vercel/index.js +++ /dev/null @@ -1,3 +0,0 @@ -const vercel = require('./dist'); -module.exports = vercel.default -Object.assign(module.exports, vercel); \ No newline at end of file diff --git a/packages/ingest-vercel/package.json b/packages/ingest-vercel/package.json deleted file mode 100644 index 4dfd46c..0000000 --- a/packages/ingest-vercel/package.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name": "@stackpress/ingest-vercel", - "version": "0.2.14", - "license": "Apache-2.0", - "description": "Using Ingest to generate Vercel functions.", - "author": "Chris ", - "homepage": "https://github.com/stackpress/ingest/tree/main/packages/types", - "bugs": "https://github.com/stackpress/ingest/issues", - "repository": "stackpress/ingest", - "main": "index.js", - "types": "index.d.ts", - "files": [ - "index.js", - "index.d.ts", - "dist", - "LICENSE", - "README.md" - ], - "scripts": { - "build": "tsc" - }, - "dependencies": { - "@stackpress/ingest": "0.2.14" - }, - "devDependencies": { - "@types/node": "22.9.3", - "ts-node": "10.9.2", - "typescript": "5.7.2" - } -} \ No newline at end of file diff --git a/packages/ingest-vercel/src/Builder.ts b/packages/ingest-vercel/src/Builder.ts deleted file mode 100644 index aa74503..0000000 --- a/packages/ingest-vercel/src/Builder.ts +++ /dev/null @@ -1,128 +0,0 @@ -import type { - ManifestOptions, - CookieOptions, - ServerOptions, - TranspileInfo, - Transpiler, - UnknownNest -} from '@stackpress/ingest/dist/buildtime/types'; - -import path from 'path'; -import Server from '@stackpress/ingest/dist/buildtime/Server'; -import { - IndentationText, - createSourceFile -} from '@stackpress/ingest/dist/buildtime/helpers'; - -export default class VercelBuilder< - C extends UnknownNest = UnknownNest -> extends Server { - /** - * Loads the plugins and returns the factory - */ - public static async bootstrap< - C extends UnknownNest = UnknownNest - >(options: ServerOptions = {}) { - const factory = new VercelBuilder(options); - return await factory.bootstrap(); - } - - /** - * Creates an entry file - */ - public transpile(info: TranspileInfo) { - const tsconfig = this.config('server', 'tsconfig') - || path.join(this.loader.cwd, 'tsconfig.json'); - //create a new source file - const { source } = createSourceFile('entry.ts', { - tsConfigFilePath: tsconfig, - skipAddingFilesFromTsConfig: true, - compilerOptions: { - // Generates corresponding '.d.ts' file. - declaration: true, - // Generates a sourcemap for each corresponding '.d.ts' file. - declarationMap: true, - // Generates corresponding '.map' file. - sourceMap: true - }, - manipulationSettings: { - indentationText: IndentationText.TwoSpaces - } - }); - //get cookie options - const cookie = JSON.stringify( - this.config('cookie') || { path: '/' } - ); - //import type { RouteAction } from '@stackpress/ingest/dist/runtime/fetch/types' - source.addImportDeclaration({ - isTypeOnly: true, - moduleSpecifier: '@stackpress/ingest/dist/runtime/fetch/types', - namedImports: [ 'RouteAction' ] - }); - //import Route from '@stackpress/ingest/dist/runtime/fetch/Route'; - source.addImportDeclaration({ - moduleSpecifier: '@stackpress/ingest/dist/runtime/fetch/Route', - defaultImport: 'Route' - }); - //import task1 from [entry] - info.actions.forEach((entry, i) => { - source.addImportDeclaration({ - moduleSpecifier: entry, - defaultImport: `task_${i}` - }); - }); - //this is the interface required by vercel functions... - // /resize/100/50 would be rewritten to /api/sharp?width=100&height=50 - source.addFunction({ - isDefaultExport: info.method === 'ALL', - isExported: info.method !== 'ALL', - //isAsync: true, - name: info.method, - parameters: [{ name: 'request', type: 'Request' }], - statements: (` - const route = new Route({ cookie: ${cookie} }); - const actions = new Set(); - ${info.actions.map( - (_, i) => `actions.add(task_${i});` - ).join('\n')} - return route.handle('${info.route}', actions, request); - `).trim() - }); - return source; - } - - /** - * Builds the final entry files - */ - public async build(options: ManifestOptions = {}) { - const transpiler: Transpiler = entries => { - return this.transpile(entries); - } - const manifest = this.router.manifest({ - buildDir: './api', - ...options - }); - const results = await manifest.build(transpiler); - //write the manifest to disk - const json = { - version: 2, - rewrites: Array - .from(results.build) - .filter(result => result.type === 'endpoint') - .map(result => ({ - source: result.route - //replace the stars - //* -> ([^/]+) - .replaceAll('*', '([^/]+)') - //** -> ([^/]+)([^/]+) -> (.*) - .replaceAll('([^/]+)([^/]+)', '(.*)'), - destination: `/api/${result.id}` - })) - }; - this.loader.fs.writeFileSync( - path.join(this.loader.cwd, 'vercel.json'), - JSON.stringify(json, null, 2) - ); - return results; - } -} \ No newline at end of file diff --git a/packages/ingest-vercel/src/index.ts b/packages/ingest-vercel/src/index.ts deleted file mode 100644 index 9e33333..0000000 --- a/packages/ingest-vercel/src/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -//stackpress -import type { - ServerOptions, - UnknownNest -} from '@stackpress/ingest/dist/buildtime/types'; -//vercel -import VercelBuilder from './Builder'; - -export * from '@stackpress/ingest/dist/buildtime'; - -export { VercelBuilder }; - -export function bootstrap< - C extends UnknownNest = UnknownNest ->(options: ServerOptions = {}) { - return VercelBuilder.bootstrap(options); -} - -export default function vercel< - C extends UnknownNest = UnknownNest ->(options: ServerOptions = {}) { - return new VercelBuilder(options); -} \ No newline at end of file diff --git a/packages/ingest/build.d.ts b/packages/ingest/build.d.ts deleted file mode 100644 index 9a43fcf..0000000 --- a/packages/ingest/build.d.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './dist/buildtime'; \ No newline at end of file diff --git a/packages/ingest/build.js b/packages/ingest/build.js deleted file mode 100644 index 963574f..0000000 --- a/packages/ingest/build.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('./dist/buildtime') \ No newline at end of file diff --git a/packages/ingest/fetch.d.ts b/packages/ingest/fetch.d.ts index 040117e..2bc262a 100644 --- a/packages/ingest/fetch.d.ts +++ b/packages/ingest/fetch.d.ts @@ -1 +1 @@ -export * from './dist/runtime/fetch'; \ No newline at end of file +export * from './dist/fetch'; \ No newline at end of file diff --git a/packages/ingest/fetch.js b/packages/ingest/fetch.js index 6789df5..b34e378 100644 --- a/packages/ingest/fetch.js +++ b/packages/ingest/fetch.js @@ -1 +1 @@ -module.exports = require('./dist/runtime/fetch') \ No newline at end of file +module.exports = require('./dist/fetch') \ No newline at end of file diff --git a/packages/ingest/http.d.ts b/packages/ingest/http.d.ts index 6e62d86..a67efc6 100644 --- a/packages/ingest/http.d.ts +++ b/packages/ingest/http.d.ts @@ -1 +1 @@ -export * from './dist/runtime/http'; \ No newline at end of file +export * from './dist/http'; \ No newline at end of file diff --git a/packages/ingest/http.js b/packages/ingest/http.js index 7f4822d..885afff 100644 --- a/packages/ingest/http.js +++ b/packages/ingest/http.js @@ -1 +1 @@ -module.exports = require('./dist/runtime/http') \ No newline at end of file +module.exports = require('./dist/http') \ No newline at end of file diff --git a/packages/ingest/package.json b/packages/ingest/package.json index 839115d..a1542a3 100644 --- a/packages/ingest/package.json +++ b/packages/ingest/package.json @@ -1,6 +1,6 @@ { "name": "@stackpress/ingest", - "version": "0.2.14", + "version": "0.3.6", "license": "Apache-2.0", "description": "An event driven serverless framework", "author": "Chris ", @@ -10,8 +10,6 @@ "main": "index.js", "types": "index.d.ts", "files": [ - "build.js", - "build.d.ts", "fetch.js", "fetch.d.ts", "http.js", @@ -27,10 +25,9 @@ "test": "nyc ts-mocha tests/*.test.ts" }, "dependencies": { - "@stackpress/types": "0.2.11", - "cookie": "1.0.2", - "esbuild": "0.24.0", - "ts-morph": "24.0.0" + "@stackpress/types": "0.3.6", + "@whatwg-node/server": "0.6.7", + "cookie": "1.0.2" }, "devDependencies": { "@types/chai": "4.3.20", diff --git a/packages/ingest/src/Context.ts b/packages/ingest/src/Context.ts deleted file mode 100644 index ad333c2..0000000 --- a/packages/ingest/src/Context.ts +++ /dev/null @@ -1,133 +0,0 @@ -import type { CallableMap, CallableSet } from '@stackpress/types/dist/types'; -import type { ContextInitializer } from './types'; -import type Request from './Request'; - -import { map, set } from '@stackpress/types/dist/helpers'; -import { isHash } from './helpers'; - -export default class RequestContext { - //request - public readonly request: Request; - //context args - public readonly args: CallableSet; - //context params - public readonly params: CallableMap; - - /** - * Returns the body - */ - public get body() { - return this.request.body; - } - - /** - * Returns the context - */ - public get context() { - return this.request.context; - } - - /** - * Returns the request data - */ - public get data() { - return this.request.data; - } - - /** - * Returns the request headers - */ - public get headers() { - return this.request.headers; - } - - /** - * Returns whether if the body was loaded - */ - public get loaded() { - return this.request.loaded; - } - - /** - * Returns the request method - */ - public get method() { - return this.request.method; - } - - /** - * Returns the request body mimetype - */ - public get mimetype() { - return this.request.mimetype; - } - - /** - * Returns the request post - */ - public get post() { - return this.request.post; - } - - /** - * Returns the request query - */ - public get query() { - return this.request.query; - } - - /** - * Returns the resource - */ - public get resource() { - return this.request.resource; - } - - /** - * Returns the request session - */ - public get session() { - return this.request.session; - } - - /** - * Returns the type of body - * string|Buffer|Uint8Array|Record|Array - */ - public get type() { - return this.request.type; - } - - /** - * Returns the request url - */ - public get url() { - return this.request.url; - } - - /** - * Sets the request and the context initializer - */ - constructor(request: Request, init: ContextInitializer = {}) { - this.request = request; - this.args = set( - init.args instanceof Set - ? Array.from(init.args.values()) - : Array.isArray(init.args) - ? init.args - : undefined - ); - this.params = map( - init.params instanceof Map - ? Array.from(init.params.entries()) - : isHash(init.params) - ? Object.entries(init.params as Record) - : undefined - ); - - this.params.forEach((value, key) => { - //only add if it doesn't exist - !this.data.has(key) && this.data.set(key, value); - }); - } -} \ No newline at end of file diff --git a/packages/ingest/src/Factory.ts b/packages/ingest/src/Factory.ts deleted file mode 100644 index e6275e8..0000000 --- a/packages/ingest/src/Factory.ts +++ /dev/null @@ -1,117 +0,0 @@ -import type { - Task, - CallableMap, - CallableNest, - UnknownNest -} from '@stackpress/types'; -import type { - RequestInitializer, - ResponseInitializer, - PluginLoaderOptions, - FactoryEvents -} from './types'; - -import EventEmitter from '@stackpress/types/dist/EventEmitter'; -import { nest } from '@stackpress/types/dist/Nest'; -import { map } from '@stackpress/types/dist/helpers'; - -import Request from './Request'; -import Response from './Response'; -import { PluginLoader } from './Loader'; - -export default class Factory { - /** - * Loads the plugins and returns the factory - */ - public static async bootstrap( - options: PluginLoaderOptions = {} - ) { - const factory = new Factory(options); - return await factory.bootstrap(); - } - - public readonly config: CallableNest; - //event emitter - public readonly emitter: EventEmitter; - //plugin loader - public readonly loader: PluginLoader; - //list of plugin configurations - public readonly plugins: CallableMap; - - /** - * Sets up the plugin loader - */ - public constructor(options: PluginLoaderOptions = {}) { - this.config = nest(); - this.plugins = map(); - this.emitter = new EventEmitter(); - this.loader = new PluginLoader(options); - } - - /** - * Loads the plugins and allows them to - * self bootstrap and configure themselves - */ - public async bootstrap() { - await this.loader.bootstrap(async (name, plugin) => { - if (typeof plugin === 'function') { - const config = await plugin(this); - if (config && typeof config === 'object') { - this.register(name, config); - } - } else if (plugin && typeof plugin === 'object') { - this.register(name, plugin); - } - }); - return this; - } - - /** - * Emits an event - */ - public async emit(event: string, request: Request, response: Response) { - return await this.emitter.emit(event, request, response); - } - - /** - * Adds an event listener - */ - public on( - event: string | RegExp, - action: Task<[Request, Response]>, - priority?: number - ) { - this.emitter.on(event, action, priority); - return this; - } - - /** - * Gets the plugin by name - */ - public plugin | undefined>(name: string) { - return this.plugins.get(name) as T; - } - - /** - * Registers a plugin - */ - public register(name: string, config: Record) { - this.plugins.set(name, config); - return this; - } - - /** - * Creates a new request - */ - public request(init: RequestInitializer> = {}) { - init.context = this; - return new Request>(init); - } - - /** - * Creates a new response - */ - public response(init?: ResponseInitializer) { - return new Response(init); - } -}; \ No newline at end of file diff --git a/packages/ingest/src/Loader.ts b/packages/ingest/src/Loader.ts index 43df7ba..f7b2a26 100644 --- a/packages/ingest/src/Loader.ts +++ b/packages/ingest/src/Loader.ts @@ -1,24 +1,37 @@ +//modules +import path from 'node:path'; +//stackpress +import NodeFS from '@stackpress/types/dist/system/NodeFS'; +import FileLoader from '@stackpress/types/dist/system/FileLoader'; +//local import type { ConfigLoaderOptions, PluginLoaderOptions } from './types'; - -import path from 'path'; -import NodeFS from '@stackpress/types/dist/filesystem/NodeFS'; -import FileLoader from '@stackpress/types/dist/filesystem/FileLoader'; import Exception from './Exception'; export class ConfigLoader extends FileLoader { + //list of filenames and extensions to look for protected _filenames: string[]; + //whether to use require.cache + protected _cache: boolean; + /** * Setups up the current working directory */ public constructor(options: ConfigLoaderOptions = {}) { super(options.fs || new NodeFS(), options.cwd || process.cwd()); - this._filenames = options.filenames || [ - '/plugins.js', - '/plugins.json', - '/package.json', - '.js', - '.json' - ]; + const { + cache = true, + filenames = [ + '/plugins.js', + '/plugins.json', + '/package.json', + '/plugins.ts', + '.js', + '.json', + '.ts' + ] + } = options; + this._cache = cache; + this._filenames = filenames } /** @@ -39,7 +52,14 @@ export class ConfigLoader extends FileLoader { return defaults; } //require the plugin - let imported = await import(file); + const basepath = this.basepath(file); + let imported = await import(basepath); + //if dont cache + if (!this._cache) { + //delete it from the require cache + //so it can be processed again + delete require.cache[require.resolve(basepath)]; + } //if using import if (imported.default) { imported = imported.default; @@ -69,7 +89,14 @@ export class ConfigLoader extends FileLoader { return defaults; } //require the plugin - let imported = require(file); + const basepath = this.basepath(file); + let imported = require(basepath); + //if dont cache + if (!this._cache) { + //delete it from the require cache + //so it can be processed again + delete require.cache[require.resolve(basepath)]; + } //if using import if (imported.default) { imported = imported.default; @@ -88,6 +115,18 @@ export class ConfigLoader extends FileLoader { //get the absolute path return super.resolve(pathname, this.cwd, this._filenames); } + + /** + * Removes the extension (.js or .ts) from the pathname + */ + public basepath(pathname: string) { + //if .js or .ts + if (pathname.endsWith('.js') || pathname.endsWith('.ts')) { + //remove the extname + return pathname.substring(0, pathname.length - 3); + } + return pathname; + } } export class PluginLoader extends ConfigLoader { @@ -107,7 +146,17 @@ export class PluginLoader extends ConfigLoader { public get plugins(): string[] { if (!this._plugins) { const file = this.resolve(); - let plugins = file ? require(file): []; + let plugins: any = []; + if (file) { + const basepath = this.basepath(file); + plugins = require(basepath); + //if dont cache + if (!this._cache) { + //delete it from the require cache + //so it can be processed again + delete require.cache[require.resolve(basepath)]; + } + } //if import if (plugins.default) { plugins = plugins.default; @@ -135,7 +184,7 @@ export class PluginLoader extends ConfigLoader { const { plugins, modules = this.modules(), - key = 'plugin' + key = 'plugins' } = options; this._key = key; diff --git a/packages/ingest/src/Request.ts b/packages/ingest/src/Request.ts index 55971fc..acc7117 100644 --- a/packages/ingest/src/Request.ts +++ b/packages/ingest/src/Request.ts @@ -1,28 +1,29 @@ +//modules +import * as cookie from 'cookie'; +//stackpress import type { CallableMap, CallableNest } from '@stackpress/types/dist/types'; import type { Method } from '@stackpress/types/dist/types'; +import map from '@stackpress/types/dist/data/map'; +import { nest } from '@stackpress/types/dist/data/Nest'; +//local import type { - IM, Body, CallableSession, - RequestLoader, - RequestInitializer, - FetchRequest + RequestLoader, + RequestInitializer } from './types'; - -import * as cookie from 'cookie'; -import { nest } from '@stackpress/types/dist/Nest'; -import { map } from '@stackpress/types/dist/helpers'; - -import { - isHash, - objectFromQuery, - eventParams, - routeParams -} from './helpers'; -import Context from './Context'; import { session } from './Session'; +import { isHash, objectFromQuery } from './helpers'; -export default class Request { +/** + * Generic request wrapper that works with + * IncomingMessage and WHATWG (Fetch) Request + * + * - native body reader using loader() + * - access to original request resource + * - attach a context (like a server/app class) + */ +export default class Request { //data controller public readonly data: CallableNest; //head controller @@ -40,15 +41,15 @@ export default class Request { //payload body protected _body: Body|null; //the server or route - protected _context?: C; + protected _context?: X; //body mimetype protected _mimetype: string; //whether if the body was loaded protected _loaded = false; //body loader - protected _loader?: RequestLoader; + protected _loader?: RequestLoader; //original request resource - protected _resource?: IM|FetchRequest; + protected _resource?: R; /** * Returns the body @@ -61,7 +62,7 @@ export default class Request { * Returns the context */ public get context() { - return this._context as C; + return this._context as X; } /** @@ -82,7 +83,7 @@ export default class Request { * Returns the original resource */ public get resource() { - return this._resource; + return this._resource as R; } /** @@ -109,14 +110,14 @@ export default class Request { /** * Sets Loader */ - public set loader(loader: RequestLoader) { + public set loader(loader: RequestLoader) { this._loader = loader; } /** * Sets request defaults */ - public constructor(init: RequestInitializer = {}) { + public constructor(init: Partial> = {}) { this.data = nest(); this.url = init.url instanceof URL ? init.url : typeof init.url === 'string' ? new URL(init.url) @@ -179,25 +180,6 @@ export default class Request { } } - /** - * Returns a new request context with pattern - * ie. req.fromPattern(/foo/) - * ie. req.fromPattern('/foo/') - */ - public fromPattern(pattern: string|RegExp) { - const args = eventParams(pattern.toString(), this.url.pathname); - return new Context(this, { args }); - } - - /** - * Returns a new request context with route - * ie. req.fromRoute('/foo/:bar') - */ - public fromRoute(route: string) { - const { args, params } = routeParams(route, this.url.pathname); - return new Context(this, { args, params }); - } - /** * Loads the body */ diff --git a/packages/ingest/src/Response.ts b/packages/ingest/src/Response.ts index dfb75a2..a646e3a 100644 --- a/packages/ingest/src/Response.ts +++ b/packages/ingest/src/Response.ts @@ -1,28 +1,34 @@ +//stackpress import type { - Status, Trace, NestedObject, CallableMap, - CallableNest + CallableNest, + ResponseStatus } from '@stackpress/types/dist/types'; +import map from '@stackpress/types/dist/data/map'; +import { nest } from '@stackpress/types/dist/data/Nest'; +import { getStatus } from '@stackpress/types/dist/Status'; +//local import type { - SR, Body, - FetchResponse, CallableSession, ResponseDispatcher, ResponseInitializer, ResponseErrorOptions } from './types'; - -import { nest } from '@stackpress/types/dist/Nest'; -import { map } from '@stackpress/types/dist/helpers'; -import { status } from '@stackpress/types/dist/StatusCode'; - import { session } from './Session'; import { isHash } from './helpers'; -export default class Response { +/** + * Generic response wrapper that works with + * ServerResponse and WHATWG (Fetch) Response + * + * - map to native resource using dispatcher() + * - access to original response resource + * - preconfigured response methods + */ +export default class Response { //head controller public readonly headers: CallableMap; //session controller @@ -34,13 +40,13 @@ export default class Response { //response status code protected _code = 0; //response dispatcher - protected _dispatcher?: ResponseDispatcher; + protected _dispatcher?: ResponseDispatcher; //body error message protected _error?: string; //body mimetype protected _mimetype?: string; //original request resource - protected _resource?: SR|FetchResponse; + protected _resource?: S; //whether if the response was sent protected _sent = false; //stack trace @@ -110,7 +116,7 @@ export default class Response { * Returns the original resource */ public get resource() { - return this._resource; + return this._resource as S; } /** @@ -146,13 +152,13 @@ export default class Response { */ public set code(code: number) { this._code = code; - this._status = status(code)?.status || ''; + this._status = getStatus(code)?.status || ''; } /** * Sets Dispatcher */ - public set dispatcher(dispatcher: ResponseDispatcher) { + public set dispatcher(dispatcher: ResponseDispatcher) { this._dispatcher = dispatcher; } @@ -166,7 +172,7 @@ export default class Response { /** * Sets the resource */ - public set resource(resource: SR|FetchResponse|undefined) { + public set resource(resource: S) { this._resource = resource; } @@ -180,7 +186,7 @@ export default class Response { /** * Sets a stack trace */ - public set status(status: Status) { + public set status(status: ResponseStatus) { this._code = status.code; this._status = status.status; } @@ -202,7 +208,7 @@ export default class Response { /** * Sets the initial values of the payload */ - constructor(init: ResponseInitializer = {}) { + constructor(init: Partial> = {}) { this._mimetype = init.mimetype; this._body = init.body || null; this._resource = init.resource; @@ -222,16 +228,14 @@ export default class Response { */ public async dispatch() { //if it's already sent, return - if (this._sent) { - return this; + if (!this._sent) { + const resource = typeof this._dispatcher === 'function' + ? await this._dispatcher(this) + : this.resource; + this._sent = true; + return resource; } - //if there is a loader is a function, use that - if (typeof this._dispatcher === 'function') { - await this._dispatcher(this); - } - //flag as sent - this.stop(); - return this; + return this.resource; } /** @@ -327,7 +331,7 @@ export default class Response { */ public setStatus(code: number, message?: string) { this._code = code; - this._status = message || status(code)?.status || ''; + this._status = message || getStatus(code)?.status || ''; return this; } diff --git a/packages/ingest/src/Route.ts b/packages/ingest/src/Route.ts new file mode 100644 index 0000000..f050703 --- /dev/null +++ b/packages/ingest/src/Route.ts @@ -0,0 +1,186 @@ +//stackpress +import type { UnknownNest } from '@stackpress/types/dist/types'; +import Status from '@stackpress/types/dist/Status'; +//local +import type Server from './Server'; +import type Request from './Request'; +import type Response from './Response'; +import Exception from './Exception'; + +/** + * Plugable route handler + * + * - before (request) hook + * - after (response) hook + * - properly formats the response + */ +export default class Route< + //configuration map + C extends UnknownNest = UnknownNest, + //request resource + R = unknown, + //response resource + S = unknown +> { + /** + * Hooks in plugins to the request lifecycle + */ + public static async emit< + C extends UnknownNest = UnknownNest, + R = unknown, + S = unknown + >( + event: string, + request: Request>, + response: Response + ) { + const route = new Route(event, request, response); + return route.emit(); + } + + public readonly event: string; + public readonly request: Request>; + public readonly response: Response; + + /** + * Gets everything needed from route.handle() + */ + constructor( + event: string, + request: Request>, + response: Response + ) { + this.event = event; + this.request = request; + this.response = response; + } + + /** + * Hooks in plugins to the request lifecycle + */ + public async emit() { + //try to trigger request pre-processors + if (!await this.prepare()) { + //if the request exits, then stop + return false; + } + // from here we can assume that it is okay to + // continue with processing the routes + if (!await this.process()) { + //if the request exits, then stop + return false; + } + //last call before dispatch + if (!await this.shutdown()) { + //if the dispatch exits, then stop + return false; + } + return true; + } + + /** + * Runs the 'request' event and interprets + */ + public async prepare() { + //default status + let status = Status.OK; + try { //to allow plugins to handle the request + status = await this.request.context.emit( + 'request', + this.request, + this.response + ); + } catch(error) { + //allow plugins to handle the error + status = await this._catch(error as Error); + } + //if the status was incomplete (309) + return status.code !== Status.ABORT.code; + } + + /** + * Handles a payload using events + */ + public async process() { + //default status + let status = Status.OK; + try { //to emit the route + await this.request.context.emit( + this.event, + this.request, + this.response + ); + } catch(error) { + //allow plugins to handle the error + status = await this._catch(error as Error); + } + //if the status was incomplete (309) + if (status.code === Status.ABORT.code) { + //the callback that set that should have already processed + //the request and is signaling to no longer continue + return false; + } + //if no body and status code + //NOTE: it's okay if there is no body as + // long as there is a status code + //ex. like in the case of a redirect + if (!this.response.body && !this.response.code) { + //make a not found exception + const exception = Exception + .for(Status.NOT_FOUND.status) + .withCode(Status.NOT_FOUND.code) + .toResponse(); + //set the exception as the error + this.response.setError(exception); + //allow plugins to handle the not found + status = await this.request.context.emit( + 'error', + this.request, + this.response + ); + } + //if no status was set + if (!this.response.code || !this.response.status) { + //make it okay + this.response.status = Status.OK; + } + //if the status was incomplete (309) + return status.code !== Status.ABORT.code; + } + + /** + * Runs the 'response' event and interprets + */ + public async shutdown() { + //default status + let status = Status.OK; + try { //to allow plugins to handle the response + status = await this.request.context.emit( + 'response', + this.request, + this.response + ); + } catch(error) { + //allow plugins to handle the error + status = await this._catch(error as Error); + } + //if the status was incomplete (309) + return status.code !== Status.ABORT.code; + } + + /** + * Default error flow + */ + protected async _catch(error: Error) { + //upgrade the error to an exception + const exception = Exception.upgrade(error as Error).toResponse(); + //set the exception as the error + this.response.setError(exception); + //allow plugins to handle the error + return await this.request.context.emit( + 'error', + this.request, + this.response + ); + } +} \ No newline at end of file diff --git a/packages/ingest/src/Router.ts b/packages/ingest/src/Router.ts new file mode 100644 index 0000000..596e8b5 --- /dev/null +++ b/packages/ingest/src/Router.ts @@ -0,0 +1,307 @@ +//stackpress +import type { Method } from '@stackpress/types/dist/types'; +import EventRouter from '@stackpress/types/dist/event/EventRouter'; +//local +import type { + EntryTask, + RouterEntry, + RouterEmitter, + RouterQueueArgs, + UnknownNest +} from './types'; +import type Server from './Server'; +import Request from './Request'; +import Response from './Response'; +import { routeParams } from './helpers'; + +/** + * Generic router class + * + * - all major http methods + * - generic request and response wrappers + * - adds route params to request data + */ +export default class Router< + //request resource + R = unknown, + //response resource + S = unknown, + //context (usually the server) + X = unknown +> + extends EventRouter, Response> +{ + //whether to use require cache + public readonly cache: boolean; + //A route map to task queues + public readonly entries = new Map>(); + + /** + * Determine whether to use require cache + */ + constructor(cache = true) { + super(); + this.cache = cache; + } + + /** + * Route for any method + */ + public all(path: string, action: RouterEntry, priority?: number) { + return this.route('[A-Z]+', path, action, priority); + } + + /** + * Route for CONNECT method + */ + public connect(path: string, action: RouterEntry, priority?: number) { + return this.route('CONNECT', path, action, priority); + } + + /** + * Route for DELETE method + */ + public delete(path: string, action: RouterEntry, priority?: number) { + return this.route('DELETE', path, action, priority); + } + + /** + * Route for GET method + */ + public get(path: string, action: RouterEntry, priority?: number) { + return this.route('GET', path, action, priority); + } + + /** + * Route for HEAD method + */ + public head(path: string, action: RouterEntry, priority?: number) { + return this.route('HEAD', path, action, priority); + } + + /** + * Route for OPTIONS method + */ + public options(path: string, action: RouterEntry, priority?: number) { + return this.route('OPTIONS', path, action, priority); + } + + /** + * Route for PATCH method + */ + public patch(path: string, action: RouterEntry, priority?: number) { + return this.route('PATCH', path, action, priority); + } + + /** + * Route for POST method + */ + public post(path: string, action: RouterEntry, priority?: number) { + return this.route('POST', path, action, priority); + } + + /** + * Route for PUT method + */ + public put(path: string, action: RouterEntry, priority?: number) { + return this.route('PUT', path, action, priority); + } + + /** + * Adds a callback to the given event listener + */ + public on( + event: string|RegExp, + action: RouterEntry, + priority = 0 + ) { + if (typeof action !== 'string') { + super.on(event, action, priority); + return this; + } + //cast entry file + const entry = action as string; + //create a key for the entry + const key = event.toString(); + //if the listener group does not exist, create it + if (!this.entries.has(key)) { + this.entries.set(key, new Set()); + } + //add the listener to the group + this.entries.get(key)?.add({ entry, priority }); + //scope the emitter + const emitter = this; + //now listen for the event + super.on(event, async function EntryFile(req, res) { + //import the action + const imports = await import(entry); + //get the default export + const action = imports.default; + //if dont cache + if (!emitter.cache) { + //delete it from the require cache + //so it can be processed again + delete require.cache[require.resolve(entry)]; + } + //run the action + //NOTE: it's probably better + // to not strongly type this... + return await action(req, res); + }, priority); + return this; + } + + /** + * Returns a route + */ + public route( + method: Method|'[A-Z]+', + path: string, + action: RouterEntry, + priority?: number + ) { + //convert path to a regex pattern + const pattern = path + //replace the :variable-_name01 + .replace(/(\:[a-zA-Z0-9\-_]+)/g, '*') + //replace the stars + //* -> ([^/]+) + .replaceAll('*', '([^/]+)') + //** -> ([^/]+)([^/]+) -> (.*) + .replaceAll('([^/]+)([^/]+)', '(.*)'); + //now form the event pattern + const event = new RegExp(`^${method}\\s${pattern}/*$`, 'ig'); + this.routes.set(event.toString(), { + method: method === '[A-Z]+' ? 'ALL' : method, + path: path + }); + //add to tasks + return this.on(event, action, priority); + } + + /** + * Returns a task queue for given the event + */ + public tasks(event: string) { + const matches = this.match(event); + const queue = this.makeQueue>(); + for (const [ event, match ] of matches) { + const tasks = this._listeners[event]; + //if no direct observers + if (typeof tasks === 'undefined') { + continue; + } + //check to see if this is a route + const route = this.routes.get(event); + //then loop the observers + tasks.forEach(task => { + queue.add(async (req, res) => { + //set the current + this._event = { + ...match, + ...task, + args: [ req, res ], + action: task.item + }; + //ADDING THIS CONDITIONAL + //if the route is found + if (route) { + //extract the params from the route + const context = routeParams(route.path, req.url.pathname); + //set the event keys + this._event.data.params = context.params; + //add the params to the request data + req.data.set(context.params); + //are there any args? + if (context.args.length) { + //update the event parameters + this._event.data.args = context.args; + //also add the args to the request data + req.data.set(context.args); + } + } + //before hook + if (typeof this._before === 'function' + && await this._before(this._event) === false + ) { + return false; + } + //if the method returns false + if (await task.item(req, res) === false) { + return false; + } + //after hook + if (typeof this._after === 'function' + && await this._after(this._event) === false + ) { + return false; + } + }, task.priority); + }); + } + + return queue; + } + + /** + * Allows events from other emitters to apply here + */ + public use(emitter: RouterEmitter) { + //check if the emitter is a router + const entryRouter = emitter instanceof Router; + const eventRouter = emitter instanceof EventRouter; + //first concat their regexp with this one + emitter.regexp.forEach(pattern => this.regexp.add(pattern)); + //next this listen to what they were listening to + //event listeners = event -> Set + //loop through the listeners of the emitter + for (const event in emitter.listeners) { + //get the observers + const tasks = emitter.listeners[event]; + //if no direct observers (shouldn't happen) + if (typeof tasks === 'undefined') { + //skip + continue; + } + //if the emitter is a router + if (eventRouter) { + //get the route from the source emitter + const route = emitter.routes.get(event); + //set the route + if (typeof route !== 'undefined') { + this.routes.set(event, route); + } + if (entryRouter) { + //get the entries from the source emitter + const entries = emitter.entries.get(event); + //if there are entries + if (typeof entries !== 'undefined') { + //if the entries do not exist, create them + if (!this.entries.has(event)) { + this.entries.set(event, new Set()); + } + //add the entries + for (const entry of entries) { + this.entries.get(event)?.add(entry); + } + } + } + } + //then loop the tasks + for (const { item, priority } of tasks) { + //listen to each task one by one + this.on(event, item, priority); + } + } + return this; + } +} + +export class ServerRouter< + //context (usually the server) + C extends UnknownNest = UnknownNest, + //request resource + R = unknown, + //response resource + S = unknown +> extends Router> {} \ No newline at end of file diff --git a/packages/ingest/src/Server.ts b/packages/ingest/src/Server.ts new file mode 100644 index 0000000..a206706 --- /dev/null +++ b/packages/ingest/src/Server.ts @@ -0,0 +1,169 @@ +//modules +import { createServer } from 'node:http'; +//stackpress +import type { + CallableMap, + CallableNest, + UnknownNest +} from '@stackpress/types'; +import map from '@stackpress/types/dist/data/map'; +import { nest } from '@stackpress/types/dist/data/Nest'; +//local +import type { + RequestInitializer, + ResponseInitializer, + ServerGateway, + ServerHandler, + ServerOptions, + NodeServerOptions +} from './types'; +import Router from './Router'; +import Request from './Request'; +import Response from './Response'; +import { PluginLoader } from './Loader'; + +/** + * Generic server class + * + * - extends router + * - extends event emitter + * - has an arbitrary config map + * - has a plugin manager + * - generic request and response wrappers + * - plug in http or fetch server with handler() + */ +export default class Server< + //configuration map + C extends UnknownNest = UnknownNest, + //request resource + R = unknown, + //response resource + S = unknown +> + extends Router> +{ + //arbitrary config map + public readonly config: CallableNest; + //plugin loader + public readonly loader: PluginLoader; + //list of plugin configurations + public readonly plugins: CallableMap; + //gateway used for development or stand alone + protected _gateway: ServerGateway; + //handler used for API entry + protected _handler: ServerHandler; + + /** + * Sets the request handler + */ + public set gateway(callback: ServerGateway) { + this._gateway = callback; + } + + /** + * Sets the request handler + */ + public set handler(callback: ServerHandler) { + this._handler = callback; + } + + /** + * Sets up the plugin loader + */ + public constructor(options: ServerOptions = {}) { + super(options.cache); + this.config = nest(); + this.plugins = map(); + this.loader = new PluginLoader(options); + this._gateway = (options.gateway || gateway)(this); + this._handler = options.handler || handler; + } + + /** + * Loads the plugins and allows them to + * self bootstrap and configure themselves + */ + public async bootstrap() { + await this.loader.bootstrap(async (name, plugin) => { + if (typeof plugin === 'function') { + const config = await plugin(this); + if (config && typeof config === 'object') { + this.register(name, config); + } + } else if (plugin && typeof plugin === 'object') { + this.register(name, plugin); + } + }); + return this; + } + + /** + * Creates a new server + */ + public create(options: NodeServerOptions = {}) { + return this._gateway(options); + } + + /** + * Handles a request + */ + public async handle(request: R, response: S) { + //handle the request + return await this._handler(this, request, response); + } + + /** + * Gets the plugin by name + */ + public plugin | undefined>(name: string) { + return this.plugins.get(name) as T; + } + + /** + * Registers a plugin + */ + public register(name: string, config: Record) { + this.plugins.set(name, config); + return this; + } + + /** + * Creates a new request + */ + public request(init: Partial>> = {}) { + init.context = this; + return new Request>(init); + } + + /** + * Creates a new response + */ + public response(init: Partial> = {}) { + return new Response(init); + } +}; + +/** + * Default server gateway + */ +export function gateway< + C extends UnknownNest = UnknownNest, + R = unknown, + S = unknown +>(server: Server) { + return (options: NodeServerOptions) => createServer( + options, + (im, sr) => server.handle(im as R, sr as S) + ); +}; + +/** + * Default server request handler + */ +export async function handler< + C extends UnknownNest = UnknownNest, + R = unknown, + S = unknown +>(ctx: Server, req: R, res: S) { + return res; +}; \ No newline at end of file diff --git a/packages/ingest/src/Session.ts b/packages/ingest/src/Session.ts index c1ae23e..1d43f5a 100644 --- a/packages/ingest/src/Session.ts +++ b/packages/ingest/src/Session.ts @@ -1,7 +1,8 @@ +//stackprss +import ReadonlyMap from '@stackpress/types/dist/data/ReadonlyMap'; +//local import type { Revision, CallableSession } from './types'; -import ReadonlyMap from '@stackpress/types/dist/readonly/Map'; - /** * Readonly session controller */ diff --git a/packages/ingest/src/buildtime/Emitter.ts b/packages/ingest/src/buildtime/Emitter.ts deleted file mode 100644 index 759d1ae..0000000 --- a/packages/ingest/src/buildtime/Emitter.ts +++ /dev/null @@ -1,54 +0,0 @@ -//stackpress -import EventEmitter from '@stackpress/types/dist/EventEmitter'; -//common -import type Request from '../Request'; -import type Response from '../Response'; -//local -import type { BuildMap, BuildTask } from './types'; - -/** - * A rendition of an event emitter that uses - * entry files instead of action callbacks. - */ -export default class Emitter { - public readonly emitter = new EventEmitter(); - //A route map to task queues - public readonly listeners = new Map>(); - - /** - * Calls all the callbacks of the given event passing the given arguments - */ - public emit(event: string, req: Request, res: Response) { - return this.emitter.emit(event, req, res); - } - - /** - * Adds a callback to the given event listener - */ - public on(event: string|RegExp, entry: string, priority = 0) { - //convert the event to a string - const pattern = event.toString(); - //if the listener group does not exist, create it - if (!this.listeners.has(pattern)) { - this.listeners.set(pattern, new Set()); - } - //add the listener to the group - this.listeners.get(pattern)?.add({ entry, priority }); - - //----------------------------------------------// - // NOTE: The following event only triggers when - // manually emitting the event. Server doesn't - // use this... - //----------------------------------------------// - - //add the event to the emitter - this.emitter.on(event, async (req, res) => { - const imports = await import(entry); - const action = imports.default; - //delete it from the require cache so it can be processed again - delete require.cache[require.resolve(entry)]; - return await action(req, res); - }, priority); - return this; - } -}; \ No newline at end of file diff --git a/packages/ingest/src/buildtime/Manifest.ts b/packages/ingest/src/buildtime/Manifest.ts deleted file mode 100644 index fafb0fe..0000000 --- a/packages/ingest/src/buildtime/Manifest.ts +++ /dev/null @@ -1,120 +0,0 @@ -//modules -import path from 'path'; -import esbuild from 'esbuild'; -//stackpress -import ItemQueue from '@stackpress/types/dist/ItemQueue'; -import FileLoader from '@stackpress/types/dist/filesystem/FileLoader'; -import NodeFS from '@stackpress/types/dist/filesystem/NodeFS'; -//local -import type { - SourceFile, - BuildInfo, - BuildData, - ManifestOptions, - ESBuildOptions, - Transpiler -} from './types'; -import type Router from './Router'; -import { esIngestPlugin } from './plugins'; -import { serialize } from './helpers'; - -export default class Manifest extends Set { - public readonly emitter: Router; - //loader - public readonly loader: FileLoader; - //build options - public readonly options: ESBuildOptions; - //build directory - public readonly buildDir: string; - //manifest path - public readonly path: string; - - /** - * Presets and distributes all the options - */ - public constructor(emitter: Router, options: ManifestOptions = {}) { - super(); - this.emitter = emitter; - const { - fs = new NodeFS(), - cwd = process.cwd(), - buildDir = './.build', - manifestName = 'manifest.json', - ...build - } = options; - - this.loader = new FileLoader(fs, cwd); - - this.options = { - bundle: true, - minify: false, - format: 'cjs', - platform: 'node', - preserveSymlinks: true, - write: true, - ...build - }; - this.buildDir = this.loader.absolute(buildDir); - this.path = path.resolve(this.buildDir, manifestName); - } - - /** - * Builds the final entry files - */ - public async build(transpile: Transpiler) { - const vfs = new Map(); - const build = new Set(); - for (const { tasks, ...info } of this) { - //create a new queue. We will use for just sorting purposes... - const entries = new ItemQueue(); - //add each route to the emitter - //(this will sort the entry files by priority) - tasks.forEach( - task => entries.add(task.entry, task.priority) - ); - //extract the actions from the emitter queue - const actions = Array.from(entries.queue).map(task => task.item); - //make an id from the sorted combination of entries - const id = serialize(actions.join(',')); - //determine the source and destination paths - const source = path.join(this.buildDir, `${id}.ts`); - const destination = path.join(this.buildDir, `${id}.js`); - //add the transpiled source code to the virtual file system - vfs.set(source, transpile({...info, actions })); - //then pre-add the bundled entry file to the build results - build.add({ ...info, id, entry: destination }); - } - //build out all the files we collected to disk - const results = await esbuild.build({ - ...this.options, - outdir: this.buildDir, - entryPoints: Array.from(vfs.keys()), - plugins: [ esIngestPlugin(vfs, this.loader) ] - }); - //be friendly - return { build, results, vfs }; - } - - /** - * Returns the manifest as a native array - */ - public toArray() { - return Array.from(this).map(build => ({ - ...build, - tasks: Array.from(build.tasks) - })); - } - - /** - * Serializes the manifest to a json - */ - public toString(minify = false) { - const serial = this.toArray().map(build => ({ - ...build, - pattern: build.pattern?.toString() - })); - return minify - ? JSON.stringify(serial) - : JSON.stringify(serial, null, 2); - } -} \ No newline at end of file diff --git a/packages/ingest/src/buildtime/Router.ts b/packages/ingest/src/buildtime/Router.ts deleted file mode 100644 index ded25fb..0000000 --- a/packages/ingest/src/buildtime/Router.ts +++ /dev/null @@ -1,215 +0,0 @@ -//stackpress -import type { Method, Route } from '@stackpress/types/dist/types'; -import ItemQueue from '@stackpress/types/dist/ItemQueue'; -//local -import type { ManifestOptions } from './types'; -import Emitter from './Emitter'; -import Manifest from './Manifest'; - -/** - * Event driven routing system. Allows the ability to listen to - * events made known by another piece of functionality. Events are - * items that transpire based on an action. With events you can add - * extra functionality right after the event has triggered. - */ -export default class Router extends Emitter { - //map of event names to routes - //^${method}\\s${pattern}/*$ -> { method, path } - public readonly routes = new Map; - - /** - * Route for any method - */ - public all(path: string, entry: string, priority?: number) { - return this.route('[A-Z]+', path, entry, priority); - } - - /** - * Route for CONNECT method - */ - public connect(path: string, entry: string, priority?: number) { - return this.route('CONNECT', path, entry, priority); - } - - /** - * Route for DELETE method - */ - public delete(path: string, entry: string, priority?: number) { - return this.route('DELETE', path, entry, priority); - } - - /** - * Returns a sorted list of entries given the route - */ - public entries(method: string, path: string) { - const entries = new Map>(); - //form the triggered event name - const event = method + ' ' + path; - //get the actions that match the triggered event name - //{ event, pattern, parameters } - const matches = this.emitter.match(event); - //loop through the matches - for (const match of matches.values()) { - //get listeners for the event - const listeners = this.listeners.get(match.pattern); - //skip if no listeners - if (!listeners) continue; - const route = this.routes.get(match.pattern); - //skip if not a route or methods don't match - if (!route || route.method !== method) continue; - //make a queue - //create a new queue. We will use for just sorting purposes... - const sorter = new ItemQueue(); - //loop the listeners - listeners.forEach( - //add entry to the queue (auto sort) - listener => sorter.add(listener.entry, listener.priority) - ); - //add to entries - const set = new Set(sorter.queue.map(({ item }) => item)); - entries.set(route.path, set); - } - return entries; - } - - /** - * Route for GET method - */ - public get(path: string, entry: string, priority?: number) { - return this.route('GET', path, entry, priority); - } - - /** - * Route for HEAD method - */ - public head(path: string, entry: string, priority?: number) { - return this.route('HEAD', path, entry, priority); - } - - /** - * Generates a manifest of all the - * entry points and its meta data - */ - public manifest(options: ManifestOptions = {}) { - const manifest = new Manifest(this, options); - //NOTE: groupings are by exact event name/pattern match - //it doesn't take into consideration an event trigger - //can match multiple patterns. For example the following - //wont be grouped together... - //ie. GET /user/:id and GET /user/search - this.listeners.forEach((tasks, event) => { - //{ method, route } - const uri = this.routes.get(event); - const type = uri ? 'endpoint' : 'function'; - const route = uri ? uri.path : event; - const pattern = this.emitter.regexp.has(event) ? new RegExp( - // pattern, - event.substring( - event.indexOf('/') + 1, - event.lastIndexOf('/') - 1 - ), - // flag - event.substring( - event.lastIndexOf('/') + 1 - ) - ): undefined; - const method = uri ? uri.method : 'ALL'; - manifest.add({ type, event, route, pattern, method, tasks }); - }); - return manifest; - } - - /** - * Route for OPTIONS method - */ - public options(path: string, entry: string, priority?: number) { - return this.route('OPTIONS', path, entry, priority); - } - - /** - * Route for PATCH method - */ - public patch(path: string, entry: string, priority?: number) { - return this.route('PATCH', path, entry, priority); - } - - /** - * Route for POST method - */ - public post(path: string, entry: string, priority?: number) { - return this.route('POST', path, entry, priority); - } - - /** - * Route for PUT method - */ - public put(path: string, entry: string, priority?: number) { - return this.route('PUT', path, entry, priority); - } - - /** - * Returns a route - */ - public route( - method: Method|'[A-Z]+', - path: string, - entry: string, - priority = 0 - ) { - //convert path to a regex pattern - const pattern = path - //replace the :variable-_name01 - .replace(/(\:[a-zA-Z0-9\-_]+)/g, '*') - //replace the stars - //* -> ([^/]+) - .replaceAll('*', '([^/]+)') - //** -> ([^/]+)([^/]+) -> (.*) - .replaceAll('([^/]+)([^/]+)', '(.*)'); - //now form the event pattern - const event = new RegExp(`^${method}\\s${pattern}/*$`, 'ig'); - this.routes.set(event.toString(), { - method: method === '[A-Z]+' ? 'ALL' : method, - path: path - }); - - //----------------------------------------------// - // NOTE: The following bypasses the emitter's - // `on` method in order to pass the context - // (instead of the request) to the action - //----------------------------------------------// - - //if the listener group does not exist, create it - if (!this.listeners.has(event.toString())) { - this.listeners.set(event.toString(), new Set()); - } - //add the listener to the group - this.listeners.get(event.toString())?.add({ entry, priority }); - - //----------------------------------------------// - // NOTE: The following event only triggers when - // manually emitting the event. Server doesn't - // use this... - //----------------------------------------------// - - //add the event to the emitter - this.emitter.on(event, async (req, res) => { - const imports = await import(entry); - const action = imports.default; - //delete it from the require cache so it can be processed again - delete require.cache[require.resolve(entry)]; - //get context - const context = req.fromRoute(path); - //now call the action - return await action(context, res); - }, priority); - - return this; - } - - /** - * Route for TRACE method - */ - public trace(path: string, action: string, priority?: number) { - return this.route('TRACE', path, action, priority); - } -}; \ No newline at end of file diff --git a/packages/ingest/src/buildtime/Server.ts b/packages/ingest/src/buildtime/Server.ts deleted file mode 100644 index 5ce8192..0000000 --- a/packages/ingest/src/buildtime/Server.ts +++ /dev/null @@ -1,270 +0,0 @@ -//modules -import type { ServerOptions as HTTPOptions } from 'http'; -import path from 'path'; -import http from 'http'; -import * as cookie from 'cookie'; -//stackpress -import type { Method } from '@stackpress/types/dist/types'; -import StatusCode from '@stackpress/types/dist/StatusCode'; -//common -import type { - IM, - SR, - CookieOptions, - IMRequestInitializer, - SRResponseInitializer -} from '../types'; -import Factory from '../Factory'; -import Request from '../Request'; -import Response from '../Response'; -import { objectFromQuery } from '../helpers'; -//runtime -import type { RouteAction } from '../runtime/http/types'; -import Route, { loader, dispatcher } from '../runtime/http/Route'; -//local -import type { - UnknownNest, - ServerOptions -} from './types'; -import Router from './Router'; -import { imToURL } from './helpers'; - -export { loader, dispatcher }; - -export default class Server - extends Factory -{ - /** - * Loads the plugins and returns the factory - */ - public static async bootstrap( - options: ServerOptions = {} - ) { - const factory = new Server(options); - return await factory.bootstrap(); - } - - //router to handle the requests - public readonly router: Router; - //body size - protected _size: number; - //cookie options - protected _cookie: CookieOptions; - //tsconfig path - protected _tsconfig: string; - - /** - * Sets up the emitter - */ - public constructor(options: ServerOptions = {}) { - //extract the router from the options - const { - size = 0, - cookie = { path: '/' }, - router = new Router(), - tsconfig, - ...config - } = options; - //factory constructor - super({ key: 'build', ...config }); - //save router - this.router = router; - this._size = size; - this._cookie = cookie; - this._tsconfig = tsconfig || path.resolve(this.loader.cwd, 'tsconfig.json'); - } - - /** - * Shortcut to all router - */ - public all(path: string, entry: string, priority?: number) { - this.router.all(path, entry, priority); - return this; - } - - /** - * Shortcut to connect router - */ - public connect(path: string, entry: string, priority?: number) { - this.router.connect(path, entry, priority); - return this; - } - - /** - * Creates an HTTP server with the given options - */ - public create(options: HTTPOptions = {}) { - return http.createServer(options, (im, sr) => this.handle(im, sr)); - } - - /** - * Shortcut to delete router - */ - public delete(path: string, entry: string, priority?: number) { - this.router.delete(path, entry, priority); - return this; - } - - /** - * Shortcut to get router - */ - public get(path: string, entry: string, priority?: number) { - this.router.get(path, entry, priority); - return this; - } - - /** - * Handles fetch requests - */ - public async handle(im: IM, sr: SR) { - //get the sorted entries given the route - const entries = this.router.entries( - im.method || 'GET', - imToURL(im).pathname - ); - //if no entries, return a server style 404 - if (!entries.size) { - sr.statusCode = StatusCode.NOT_FOUND.code; - sr.statusMessage = StatusCode.NOT_FOUND.status; - return sr.end(); - } - //loop through the entries. - //`path` is the route. - //`actions` are the entry file paths - for (const [ path, actions ] of entries.entries() ) { - //make a new task set - const tasks = new Set( - //for each action, create a new task callback - Array.from(actions).map(entry => async (req, res) => { - //import the action - const imports = await import(entry); - //get the action callback - const action = imports.default; - //delete it from the require cache so it can be processed again - delete require.cache[require.resolve(entry)]; - //now call the action - return await action(req, res); - }) - ); - //create a new route (with its own client plugins and bootstrap) - const route = new Route({ size: this._size, cookie: this._cookie }); - //handle the route - route.handle(path, tasks, im, sr); - } - return sr; - } - - /** - * Shortcut to head router - */ - public head(path: string, entry: string, priority?: number) { - this.router.head(path, entry, priority); - return this; - } - - /** - * Shortcut to options router - */ - public options(path: string, entry: string, priority?: number) { - this.router.options(path, entry, priority); - return this; - } - - /** - * Shortcut to patch router - */ - public patch(path: string, entry: string, priority?: number) { - this.router.patch(path, entry, priority); - return this; - } - - /** - * Shortcut to post router - */ - public post(path: string, entry: string, priority?: number) { - this.router.post(path, entry, priority); - return this; - } - - /** - * Shortcut to put router - */ - public put(path: string, entry: string, priority?: number) { - this.router.put(path, entry, priority); - return this; - } - - /** - * Sets up the request - */ - public request(init?: IMRequestInitializer>) { - if (!init) { - return new Request>({ context: this }); - } - const im = init.resource; - //set context - init.context = this; - //set method - init.method = init.method - || (im.method?.toUpperCase() || 'GET') as Method; - //set the type - init.mimetype = init.mimetype - || im.headers['content-type'] - || 'text/plain'; - //set the headers - init.headers = init.headers || Object.fromEntries( - Object.entries(im.headers).filter( - ([key, value]) => typeof value !== 'undefined' - ) - ) as Record; - //set session - init.session = init.session || cookie.parse( - im.headers.cookie as string || '' - ) as Record; - //set url - const url = imToURL(im); - init.url = init.url || imToURL(im); - //set query - init.query = init.query - || objectFromQuery(url.searchParams.toString()); - //make request - const req = new Request>(init); - const size = this.config('server', 'bodySize') || this._size; - req.loader = loader(im, size); - return req; - } - - /** - * Sets up the response - */ - public response(init?: SRResponseInitializer) { - if (!init) { - return new Response(); - } - const sr = init.resource; - const res = new Response(init); - const cookie = this.config('cookie') || this._cookie; - res.dispatcher = dispatcher(sr, cookie); - return res; - } - - /** - * Shortcut to trace router - */ - public trace(path: string, entry: string, priority?: number) { - this.router.trace(path, entry, priority); - return this; - } -}; - -export function bootstrap( - options: ServerOptions = {} -) { - return Server.bootstrap(options); -}; - -export function server( - options: ServerOptions = {} -) { - return new Server(options); -}; \ No newline at end of file diff --git a/packages/ingest/src/buildtime/helpers.ts b/packages/ingest/src/buildtime/helpers.ts deleted file mode 100644 index 6b60679..0000000 --- a/packages/ingest/src/buildtime/helpers.ts +++ /dev/null @@ -1,92 +0,0 @@ -//modules -import type { SourceFile, ProjectOptions } from 'ts-morph'; -import crypto from 'crypto'; -import { - Project, - IndentationText, - VariableDeclarationKind -} from 'ts-morph'; -//common -import type { IM } from '../types'; -import { objectFromQuery, withUnknownHost } from '../helpers'; - - -export { Project, IndentationText, VariableDeclarationKind }; - -/** - * Parsed query object - */ -export function imToURL(resource: IM) { - const { url, headers } = resource; - //determine protocol (by default https) - let protocol = 'https'; - //if there is an x-forwarded-proto header - const proto = headers['x-forwarded-proto']; - if (proto?.length) { - //then let's use that instead - if (Array.isArray(proto)) { - protocol = proto[0]; - } else { - protocol = proto; - } - protocol = protocol.trim(); - // Note: X-Forwarded-Proto is normally only ever a - // single value, but this is to be safe. - if (protocol.indexOf(',') !== -1) { - protocol = protocol.substring(0, protocol.indexOf(',')).trim(); - } - } - //form the URL - const uri = `${protocol}://${headers.host}${url || '/'}`; - //try to create a URL object - try { - return new URL(uri); - } catch(e) {} - //we need to return a URL object - return new URL(withUnknownHost(url || '/')); -}; - -/** - * Parsed URL query object - */ -export function imQueryToObject(resource: IM) { - return objectFromQuery(imToURL(resource).searchParams.toString()); -}; - -/** - * Converts source file to javascript - */ -export function toJS(source: SourceFile) { - return source - .getEmitOutput() - .getOutputFiles() - .filter(file => file.getFilePath().endsWith('.js')) - .map(file => file.getText()) - .join('\n'); -} - -/** - * Converts source file to typescript - */ -export function toTS(source: SourceFile) { - return source.getFullText(); -}; - -/** - * API to create a ts-morph source file - */ -export function createSourceFile(filePath: string, config: ProjectOptions) { - const project = new Project(config); - const source = project.createSourceFile(filePath); - return { project, source }; -}; - -/** - * Creates a serialized hash of a string - */ -export function serialize(string: string) { - return crypto - .createHash('shake256', { outputLength: 10 }) - .update(string) - .digest('hex'); -} \ No newline at end of file diff --git a/packages/ingest/src/buildtime/index.ts b/packages/ingest/src/buildtime/index.ts deleted file mode 100644 index bb13ed4..0000000 --- a/packages/ingest/src/buildtime/index.ts +++ /dev/null @@ -1,45 +0,0 @@ -//modules -import * as cookie from 'cookie'; -import esbuild from 'esbuild'; -//common -import Context from '../Context'; -import Exception from '../Exception'; -import Request from '../Request'; -import Response from '../Response'; -import { ReadSession, WriteSession } from '../Session'; -//local -import Emitter from './Emitter'; -import Manifest from './Manifest'; -import Router from './Router'; -import Server, { - loader, - dispatcher, - bootstrap, - server -} from './Server'; - -export type * from '../types'; -export type * from './types'; - -export * from '../helpers'; -export * from './helpers'; -export * from './plugins'; - -export { - cookie, - esbuild, - Context, - Exception, - Request, - Response, - ReadSession, - WriteSession, - Emitter, - Manifest, - Router, - Server, - loader, - dispatcher, - bootstrap, - server -}; \ No newline at end of file diff --git a/packages/ingest/src/buildtime/plugins.ts b/packages/ingest/src/buildtime/plugins.ts deleted file mode 100644 index a729f0b..0000000 --- a/packages/ingest/src/buildtime/plugins.ts +++ /dev/null @@ -1,48 +0,0 @@ -//modules -import type { PluginBuild } from 'esbuild'; -import path from 'path'; -//stackpress -import type FileLoader from '@stackpress/types/dist/filesystem/FileLoader'; -//local -import type { SourceFile } from './types'; -import { toTS } from './helpers'; - -export function esIngestPlugin( - vfs: Map, - loader: FileLoader, - extnames = [ '.js', '.json', '.ts' ] -) { - return { - name: 'ingest-plugin', - setup: (build: PluginBuild) => { - //should resolve everything... - build.onResolve({ filter: /.*/ }, args => { - //resolve virtual files... - if (vfs.has(args.path)) { - return { - path: args.path, - namespace: 'ingest-plugin' - }; - } - - const pwd = args.importer - ? path.dirname(args.importer) - : loader.cwd; - const resolved = loader.resolve(args.path, pwd, extnames); - - if (resolved) { - return { path: resolved }; - } - return undefined; - }); - - build.onLoad( - { filter: /.*/, namespace: 'ingest-plugin' }, - args => { - const source = vfs.get(args.path) as SourceFile; - return { contents: toTS(source), loader: 'ts' } - } - ); - } - }; -} \ No newline at end of file diff --git a/packages/ingest/src/buildtime/types.ts b/packages/ingest/src/buildtime/types.ts deleted file mode 100644 index 19e0dad..0000000 --- a/packages/ingest/src/buildtime/types.ts +++ /dev/null @@ -1,102 +0,0 @@ -//modules -import type { SourceFile, ProjectOptions } from 'ts-morph'; -import type { PluginBuild, BuildResult as ESBuildResult } from 'esbuild'; -//stackpress -import type { Method, UnknownNest } from '@stackpress/types/dist/types'; -import type FileSystem from '@stackpress/types/dist/filesystem/FileSystem'; -//common -import type { CookieOptions, PluginLoaderOptions } from '../types'; -import type Request from '../Request'; -import type Response from '../Response'; -//local -import type Router from './Router'; - -export type { SourceFile, ProjectOptions, CookieOptions, UnknownNest }; - -//--------------------------------------------------------------------// -// Build Types - -export type BuildMap = Record; -export type BuildTask = { entry: string, priority: number }; - -export type BuildType = 'function' | 'endpoint'; - -//this is the data struct generated in router -export type BuildInfo = { - type: BuildType, - method: Method, - event: string, - route: string, - pattern?: RegExp, - tasks: Set -}; - -// this is the data struct generated in manifest -export type BuildData = { - id: string; - type: BuildType; - method: Method; - event: string; - route: string; - pattern?: RegExp; - entry: string -}; - -export type BuildResult = { - build: Set, - results: ESBuildResult<{ - outdir: string; - entryPoints: string[]; - plugins: { - name: string; - setup: (build: PluginBuild) => void; - }[]; - minify?: boolean; - bundle?: boolean; - platform?: "node" | "browser"; - globalName?: string; - format?: "iife" | "esm" | "cjs"; - preserveSymlinks?: boolean; - write?: boolean; - }>, - vfs: Map -}; - -export type TranspileInfo = { - type: BuildType, - method: Method, - event: string, - route: string, - pattern?: RegExp, - actions: string[] -}; - -export type Transpiler = (info: TranspileInfo) => SourceFile; - -export type ESBuildOptions = { - minify?: boolean, - bundle?: boolean, - platform?: 'node'|'browser', - globalName?: string, - format?: 'iife'|'esm'|'cjs', - preserveSymlinks?: boolean, - write?: boolean, - plugins?: { - name: string, - setup: Function - }[] -}; - -export type ManifestOptions = ESBuildOptions & { - fs?: FileSystem, - cwd?: string, - buildDir?: string, - manifestName?: string -}; - -export type ServerOptions = PluginLoaderOptions & { - router?: Router, - cookie?: CookieOptions, - size?: number, - tsconfig?: string -} \ No newline at end of file diff --git a/packages/ingest/src/fetch/Adapter.ts b/packages/ingest/src/fetch/Adapter.ts new file mode 100644 index 0000000..f5d8b01 --- /dev/null +++ b/packages/ingest/src/fetch/Adapter.ts @@ -0,0 +1,233 @@ +//modules +import { Readable } from 'node:stream'; +import * as cookie from 'cookie'; +//stackpress +import type { Method, UnknownNest } from '@stackpress/types/dist/types'; +//common +import type { + Body, + FetchServer, + NodeRequest, + NodeResponse, + LoaderResults, + CookieOptions +} from '../types'; +import Route from '../Route'; +import Request from '../Request'; +import Response from '../Response'; +import { + isHash, + objectFromQuery, + formDataToObject +} from '../helpers'; +//local +import { + NativeResponse, + fetchToURL, + readableToReadableStream +} from './helpers'; + +export default class Adapter { + /** + * Server request handler + */ + public static async plug( + context: FetchServer, + request: NodeRequest + ) { + const server = new Adapter(context, request); + return server.plug(); + }; + + //the parent server context + protected _context: FetchServer; + //the native request + protected _request: NodeRequest; + + /** + * Sets up the server + */ + constructor(context: FetchServer, request: NodeRequest) { + this._context = context; + this._request = request; + } + + /** + * Handles the request + */ + public async plug() { + //initialize the request + const req = this.request(); + const res = this.response(); + //determine event name + const event = `${req.method} ${req.url.pathname}`; + //load the body + await req.load(); + //hook the plugins + await Route.emit( + event, req, res + ); + //if the response was not sent by now, + if (!res.sent) { + //send the response + return res.dispatch(); + } + return res.resource; + } + + /** + * Sets up the request + */ + public request() { + //set context + const context = this._context; + //set resource + const resource = this._request; + //set method + const method = (this._request.method?.toUpperCase() || 'GET') as Method; + //set the type + const mimetype = this._request.headers.get('content-type') || 'text/plain'; + //set the headers + const headers: Record = {}; + this._request.headers.forEach((value, key) => { + if (typeof value !== 'undefined') { + headers[key] = value; + } + }); + //set session + const session = cookie.parse( + this._request.headers.get('cookie') as string || '' + ) as Record; + //set url + const url = fetchToURL(this._request); + //set query + const query = objectFromQuery(url.searchParams.toString()); + //setup the payload + const request = new Request>({ + context, + resource, + headers, + method, + mimetype, + query, + session, + url + }); + request.loader = loader(this._request); + return request; + } + + /** + * Sets up the response + */ + public response() { + const response = new Response(); + response.dispatcher = dispatcher( + this._context.config('cookie') || { path: '/' } + ); + return response; + } +}; + +/** + * Request body loader + */ +export function loader( + resource: NodeRequest +) { + return async (req: Request>) => { + //if the body is cached + if (req.body !== null) { + return undefined; + } + //TODO: limit the size of the body + const body = await resource.text(); + const post = formDataToObject(req.type, body) + + return { body, post } as LoaderResults; + } +}; + +/** + * Maps out an Ingest Response to a Fetch Response + */ +export function dispatcher(options: CookieOptions = { path: '/' }) { + return async (res: Response) => { + //fetch type responses dont start with a resource + //so if it magically has a resource, then it must + //have been set in a route. So we can just return it. + if (res.resource instanceof NativeResponse) { + return res.resource; + } + let mimetype = res.mimetype; + let body: Body|null = null; + //if body is a valid response + if (typeof res.body === 'string' + || Buffer.isBuffer(res.body) + || res.body instanceof Uint8Array + || res.body instanceof ReadableStream + ) { + body = res.body; + //if it's a node stream + } else if (res.body instanceof Readable) { + body = readableToReadableStream(res.body); + //if body is an object or array + } else if (isHash(res.body) || Array.isArray(res.body)) { + res.mimetype = 'application/json'; + body = JSON.stringify({ + code: res.code, + status: res.status, + results: res.body, + error: res.error, + errors: res.errors.size > 0 ? res.errors.get() : undefined, + total: res.total > 0 ? res.total : undefined + }); + } else if (res.code && res.status) { + res.mimetype = 'application/json'; + body = JSON.stringify({ + code: res.code, + status: res.status, + error: res.error, + errors: res.errors.size > 0 ? res.errors.get() : undefined, + stack: res.stack ? res.stack : undefined + }); + } + //create response + const resource = new NativeResponse(body, { + status: res.code, + statusText: res.status + }); + //write cookies + for (const [name, entry] of res.session.revisions.entries()) { + if (entry.action === 'remove') { + resource.headers.set( + 'Set-Cookie', + cookie.serialize(name, '', { ...options, expires: new Date(0) }) + ); + } else if (entry.action === 'set' + && typeof entry.value !== 'undefined' + ) { + const { value } = entry; + const values = Array.isArray(value) ? value : [ value ]; + for (const value of values) { + resource.headers.set( + 'Set-Cookie', + cookie.serialize(name, value, options) + ); + } + } + } + //write headers + for (const [ name, value ] of res.headers.entries()) { + const values = Array.isArray(value) ? value : [ value ]; + for (const value of values) { + resource.headers.set(name, value); + } + } + //set content type + if (mimetype) { + resource.headers.set('Content-Type', mimetype); + } + return resource; + }; +}; \ No newline at end of file diff --git a/packages/ingest/src/fetch/helpers.ts b/packages/ingest/src/fetch/helpers.ts new file mode 100644 index 0000000..4f30a89 --- /dev/null +++ b/packages/ingest/src/fetch/helpers.ts @@ -0,0 +1,34 @@ +//modules +import { Readable } from 'node:stream'; +//local +import type { NodeRequest } from '../types'; +import { objectFromQuery } from '../helpers'; + +export const NativeRequest = global.Request; +export const NativeResponse = global.Response; + +/** + * Parsed query object + */ +export function fetchToURL(resource: NodeRequest) { + return new URL(resource.url); +}; + +/** + * Parsed URL query object + */ +export function fetchQueryToObject(resource: NodeRequest) { + return objectFromQuery(fetchToURL(resource).searchParams.toString()); +}; + +/** + * Converts the NodeJS Readable to a WebAPI ReadableStream + */ +export function readableToReadableStream(stream: Readable) { + return new ReadableStream({ + start(controller) { + stream.on('data', chunk => controller.enqueue(chunk)); + stream.on('end', () => controller.close()); + } + }); +}; \ No newline at end of file diff --git a/packages/ingest/src/fetch/index.ts b/packages/ingest/src/fetch/index.ts new file mode 100644 index 0000000..67fcbc2 --- /dev/null +++ b/packages/ingest/src/fetch/index.ts @@ -0,0 +1,81 @@ +//modules +import { createServer } from 'node:http'; +import { createServerAdapter } from '@whatwg-node/server'; +//stackpress +import type { UnknownNest } from '@stackpress/types/dist/types'; +//common +import type { + FetchServer, + NodeRequest, + NodeResponse, + NodeOptResponse, + ServerOptions, + NodeServerOptions +} from '../types'; +import Router from '../Router'; +import Server from '../Server'; +//local +import Adapter, { loader, dispatcher } from './Adapter'; +import { + NativeRequest, + NativeResponse, + fetchQueryToObject, + fetchToURL, + readableToReadableStream +} from './helpers'; + +export { + Adapter, + loader, + dispatcher, + NativeRequest, + NativeResponse, + fetchQueryToObject, + fetchToURL, + readableToReadableStream +}; + +/** + * Default server gateway + */ +export function gateway( + server: FetchServer +) { + return (options: NodeServerOptions) => { + const adapter = createServerAdapter((request: NodeRequest) => { + return server.handle(request, undefined) as Promise; + }); + return createServer(options, adapter); + }; +}; + +/** + * Server request handler + */ +export async function handler( + context: FetchServer, + request: NodeRequest, + response: NodeOptResponse +) { + return await Adapter.plug(context, request); +}; + +/** + * Default server factory + */ +export function server( + options: ServerOptions = {} +) { + options.gateway = options.gateway || gateway; + options.handler = options.handler || handler; + return new Server( + options + ); +}; + +/** + * Default router factory + */ +export function router() { + return new Router>(); +} \ No newline at end of file diff --git a/packages/ingest/src/helpers.ts b/packages/ingest/src/helpers.ts index ebddaeb..8f8ab2a 100644 --- a/packages/ingest/src/helpers.ts +++ b/packages/ingest/src/helpers.ts @@ -1,9 +1,5 @@ -//modules -import { Readable } from 'stream'; //stackpress -import Nest from '@stackpress/types/dist/Nest'; -//local -import { Req, Res } from './types'; +import Nest from '@stackpress/types/dist/data/Nest'; /** * Returns true if the value is a native JS object @@ -181,59 +177,3 @@ export function withUnknownHost(url: string) { return `http://unknownhost${url}`; }; -/** - * Converts the WebAPI ReadableStream to NodeJS Readable - */ -export function readableStreamToReadable(stream: ReadableStream) { - const reader = stream.getReader(); - return new Readable({ - async read(size) { - const { done, value } = await reader.read(); - if (done) { - this.push(null); - return; - } - this.push(value); - } - }); -} - -/** - * Converts the NodeJS Readable to a WebAPI ReadableStream - */ -export function readableToReadableStream(stream: Readable) { - return new ReadableStream({ - start(controller) { - stream.on('data', chunk => controller.enqueue(chunk)); - stream.on('end', () => controller.close()); - } - }); -} - -/** - * Basic task wrapper - */ -export function route( - action: (req: Req, res: Res) => Promise, - pluggable = true -) { - //if not pluggable - if (!pluggable) return action; - //add pluggable steps - return async (req: Req, res: Res) => { - //bootstrap the context - const context = await req.context.bootstrap(); - //call pre-event request - const pre = await context.emit('request', req.request, res); - //stop if aborted - if (pre.code === 309) return false; - //call the action - const cur = await action(req, res); - //stop if aborted - if (cur === false) return false; - //call post-event response - const pos = await context.emit('response', req.request, res); - //stop if aborted - if (pos.code === 309) return false; - }; -}; \ No newline at end of file diff --git a/packages/ingest/src/http/Adapter.ts b/packages/ingest/src/http/Adapter.ts new file mode 100644 index 0000000..75ace3d --- /dev/null +++ b/packages/ingest/src/http/Adapter.ts @@ -0,0 +1,241 @@ +//modules +import { Readable } from 'node:stream'; +import * as cookie from 'cookie'; +//stackpress +import type { Method, UnknownNest } from '@stackpress/types/dist/types'; +//common +import type { + IM, + SR, + HTTPServer, + LoaderResults, + CookieOptions +} from '../types'; +import Route from '../Route'; +import Request from '../Request'; +import Response from '../Response'; +import Exception from '../Exception'; +import { + isHash, + objectFromQuery, + formDataToObject +} from '../helpers'; +//local +import { imToURL, readableStreamToReadable } from './helpers'; + +export default class Adapter { + /** + * Server request handler + */ + public static async plug( + context: HTTPServer, + request: IM, + response: SR + ) { + const server = new Adapter(context, request, response); + return server.plug(); + }; + + //the parent server context + protected _context: HTTPServer; + //the native request + protected _request: IM; + //the native response + protected _response: SR; + + /** + * Sets up the server + */ + constructor(context: HTTPServer, request: IM, response: SR) { + this._context = context; + this._request = request; + this._response = response; + } + + /** + * Handles the request + */ + public async plug() { + //initialize the request + const req = this.request(); + const res = this.response(); + //determine event name + const event = `${req.method} ${req.url.pathname}`; + //load the body + await req.load(); + //hook the plugins + await Route.emit(event, req, res); + //if the response was not sent by now, + if (!res.sent) { + //send the response + return res.dispatch(); + } + return res.resource; + } + + /** + * Sets up the request + */ + public request() { + //set context + const context = this._context; + //set resource + const resource = this._request; + //set method + const method = (this._request.method?.toUpperCase() || 'GET') as Method; + //set the type + const mimetype = this._request.headers['content-type'] || 'text/plain'; + //set the headers + const headers = Object.fromEntries( + Object.entries(this._request.headers).filter( + ([key, value]) => typeof value !== 'undefined' + ) + ) as Record; + //set session + const session = cookie.parse( + this._request.headers.cookie as string || '' + ) as Record; + //set url + const url = imToURL(this._request); + //set query + const query = objectFromQuery(url.searchParams.toString()); + //setup the payload + const request = new Request>({ + context, + resource, + headers, + method, + mimetype, + query, + session, + url + }); + request.loader = loader(this._request); + return request; + } + + /** + * Sets up the response + */ + public response() { + const response = new Response({ resource: this._response }); + response.dispatcher = dispatcher( + this._context.config('cookie') || { path: '/' } + ); + return response; + } +} + + + +/** + * Request body loader + */ +export function loader( + resource: IM, + size = 0 +) { + return (req: Request>) => { + return new Promise(resolve => { + //if the body is cached + if (req.body !== null) { + resolve(undefined); + } + + //we can only request the body once + //so we need to cache the results + let body = ''; + resource.on('data', chunk => { + body += chunk; + Exception.require( + !size || body.length <= size, + `Request exceeds ${size}` + ); + }); + resource.on('end', () => { + resolve({ body, post: formDataToObject(req.mimetype, body) }); + }); + }); + } +}; + +/** + * Response dispatcher + */ +export function dispatcher(options: CookieOptions = { path: '/' }) { + return async (res: Response) => { + const resource = res.resource; + //set code and status + resource.statusCode = res.code; + resource.statusMessage = res.status; + //write cookies + for (const [name, entry] of res.session.revisions.entries()) { + if (entry.action === 'remove') { + resource.setHeader( + 'Set-Cookie', + cookie.serialize(name, '', { ...options, expires: new Date(0) }) + ); + } else if (entry.action === 'set' + && typeof entry.value !== 'undefined' + ) { + const { value } = entry; + const values = Array.isArray(value) ? value : [ value ]; + for (const value of values) { + resource.setHeader( + 'Set-Cookie', + cookie.serialize(name, value, options) + ); + } + } + } + //write headers + for (const [ name, value ] of res.headers.entries()) { + resource.setHeader(name, value); + } + //set content type + if (res.mimetype) { + resource.setHeader('Content-Type', res.mimetype); + } + //if body is a valid response + if (typeof res.body === 'string' + || Buffer.isBuffer(res.body) + || res.body instanceof Uint8Array + ) { + resource.end(res.body); + //if it's a node stream + } else if (res.body instanceof Readable) { + res.body.pipe(resource); + //if it's a web stream + } else if (res.body instanceof ReadableStream) { + //convert to node stream + readableStreamToReadable(res.body).pipe(resource); + //if body is an object or array + } else if (isHash(res.body) || Array.isArray(res.body)) { + resource.setHeader('Content-Type', 'application/json'); + resource.end(JSON.stringify({ + code: res.code, + status: res.status, + results: res.body, + error: res.error, + errors: res.errors.size > 0 ? res.errors.get() : undefined, + total: res.total > 0 ? res.total : undefined, + stack: res.stack ? res.stack : undefined + })); + } else if (res.code && res.status) { + resource.setHeader('Content-Type', 'application/json'); + resource.end(JSON.stringify({ + code: res.code, + status: res.status, + error: res.error, + errors: res.errors.size > 0 ? res.errors.get() : undefined, + stack: res.stack ? res.stack : undefined + })); + } + //type Body = string | Buffer | Uint8Array + // | Record | unknown[] + + //we cased for all possible types so it's + //better to not infer the response body + return resource; + } +}; \ No newline at end of file diff --git a/packages/ingest/src/runtime/http/helpers.ts b/packages/ingest/src/http/helpers.ts similarity index 67% rename from packages/ingest/src/runtime/http/helpers.ts rename to packages/ingest/src/http/helpers.ts index b848d71..dab8956 100644 --- a/packages/ingest/src/runtime/http/helpers.ts +++ b/packages/ingest/src/http/helpers.ts @@ -1,7 +1,8 @@ +//modules +import { Readable } from 'node:stream'; //common -import { objectFromQuery, withUnknownHost } from '../../helpers'; -//local -import type { IM } from '../../types'; +import type { IM } from '../types'; +import { objectFromQuery, withUnknownHost } from '../helpers'; /** * Parsed query object @@ -41,4 +42,21 @@ export function imToURL(resource: IM) { */ export function imQueryToObject(resource: IM) { return objectFromQuery(imToURL(resource).searchParams.toString()); +}; + +/** + * Converts the WebAPI ReadableStream to NodeJS Readable + */ +export function readableStreamToReadable(stream: ReadableStream) { + const reader = stream.getReader(); + return new Readable({ + async read(size) { + const { done, value } = await reader.read(); + if (done) { + this.push(null); + return; + } + this.push(value); + } + }); }; \ No newline at end of file diff --git a/packages/ingest/src/http/index.ts b/packages/ingest/src/http/index.ts new file mode 100644 index 0000000..79a80e5 --- /dev/null +++ b/packages/ingest/src/http/index.ts @@ -0,0 +1,71 @@ +//modules +import { createServer } from 'node:http'; +//stackpress +import type { UnknownNest } from '@stackpress/types/dist/types'; +//common +import type { + IM, + SR, + HTTPServer, + ServerOptions, + NodeServerOptions +} from '../types'; +import Router from '../Router'; +import Server from '../Server'; +//local +import Adapter, { loader, dispatcher } from './Adapter'; +import { + imQueryToObject, + imToURL, + readableStreamToReadable +} from './helpers'; + +export { + Adapter, + loader, + dispatcher, + imQueryToObject, + imToURL, + readableStreamToReadable +}; + +/** + * Default server gateway + */ +export function gateway( + server: HTTPServer +) { + return (options: NodeServerOptions) => createServer( + options, + (im, sr) => server.handle(im, sr) + ); +}; + +/** + * Server request handler + */ +export async function handler( + context: HTTPServer, + request: IM, + response: SR +) { + return await Adapter.plug(context, request, response); +}; + +/** + * Default server factory + */ +export function server( + options: ServerOptions = {} +) { + options.gateway = options.gateway || gateway; + options.handler = options.handler || handler; + return new Server(options); +}; + +/** + * Default router factory + */ +export function router() { + return new Router>(); +} \ No newline at end of file diff --git a/packages/ingest/src/index.ts b/packages/ingest/src/index.ts index 68ff4fa..c9cdac7 100644 --- a/packages/ingest/src/index.ts +++ b/packages/ingest/src/index.ts @@ -1,12 +1,12 @@ //modules import * as cookie from 'cookie'; //local -import Context from './Context'; import Exception from './Exception'; -import Factory from './Factory'; import { ConfigLoader, PluginLoader } from './Loader'; import Request from './Request'; import Response from './Response'; +import Router, { ServerRouter } from './Router'; +import Server, { handler, gateway } from './Server'; import { ReadSession, WriteSession, session } from './Session'; export type * from './types'; @@ -15,13 +15,16 @@ export * from './helpers'; export { cookie, session, - Context, + handler, + gateway, Exception, - Factory, ConfigLoader, PluginLoader, Request, Response, + Router, + ServerRouter, + Server, ReadSession, WriteSession }; \ No newline at end of file diff --git a/packages/ingest/src/runtime/fetch/Plugin.ts b/packages/ingest/src/runtime/fetch/Plugin.ts deleted file mode 100644 index 8f48189..0000000 --- a/packages/ingest/src/runtime/fetch/Plugin.ts +++ /dev/null @@ -1,197 +0,0 @@ -//stackpress -import type { UnknownNest } from '@stackpress/types/dist/types'; -import StatusCode from '@stackpress/types/dist/StatusCode'; -//common -import type Response from '../../Response'; -import Exception from '../../Exception'; -//local -import type { RouteContext, RouteRequest } from './types'; -import type Route from './Route'; -import type Queue from './Queue'; - -export default class Plugin { - /** - * Hooks in plugins to the request lifecycle - */ - public static async hook( - route: Route, - queue: Queue, - context: RouteContext, - response: Response - ) { - const plugin = new Plugin(route, queue, context, response); - return plugin.hook(); - } - - //queue of route tasks entry files - public readonly queue: Queue; - //route request context - public readonly context: RouteContext; - //route request - public readonly request: RouteRequest; - //route response - public readonly response: Response; - //route instance - public readonly route: Route; - - /** - * Gets everything needed from route.handle() - */ - constructor( - route: Route, - queue: Queue, - context: RouteContext, - response: Response - ) { - this.route = route; - this.queue = queue; - this.context = context; - this.request = context.request; - this.response = response; - } - - /** - * Hooks in plugins to the request lifecycle - */ - public async hook() { - //try to trigger request pre-processors - if (!await this.prepare()) { - //if the request exits, then stop - return false; - } - // from here we can assume that it is okay to - // continue with processing the routes - if (!await this.process()) { - //if the request exits, then stop - return false; - } - //last call before dispatch - if (!await this.shutdown()) { - //if the dispatch exits, then stop - return false; - } - return true; - } - - /** - * Runs the 'request' event and interprets - */ - public async prepare() { - //default status - let status = StatusCode.OK; - try { //to allow plugins to handle the request - status = await this.route.emit( - 'request', - this.request, - this.response - ); - } catch(error) { - //if there is an error - //upgrade the error to an exception - const exception = Exception - .upgrade(error as Error) - .toResponse() - //set the exception as the error - this.response.setError(exception); - //allow plugins to handle the error - status = await this.route.emit( - 'error', - this.request, - this.response - ); - } - //if the status was incomplete (309) - return status.code !== StatusCode.ABORT.code; - } - - /** - * Handles a payload using events - */ - public async process() { - //default status - let status = StatusCode.OK; - try { //to run the task queue - status = await this.queue.run( - this.context, - this.response - ); - } catch(error) { - //if there is an error - //upgrade the error to an exception - const exception = Exception - .upgrade(error as Error) - .toResponse(); - //set the exception as the error - this.response.setError(exception); - //allow plugins to handle the error - status = await this.route.emit( - 'error', - this.request, - this.response - ); - } - //if the status was incomplete (309) - if (status.code === StatusCode.ABORT.code) { - //the callback that set that should have already processed - //the request and is signaling to no longer continue - return false; - } - //if no body and status code - //NOTE: it's okay if there is no body as - // long as there is a status code - //ex. like in the case of a redirect - if (!this.response.body && !this.response.code) { - //make a not found exception - const exception = Exception - .for(StatusCode.NOT_FOUND.status) - .withCode(StatusCode.NOT_FOUND.code) - .toResponse(); - //set the exception as the error - this.response.setError(exception); - //allow plugins to handle the not found - status = await this.route.emit( - 'error', - this.request, - this.response - ); - } - //if no status was set - if (!this.response.code || !this.response.status) { - //make it okay - this.response.status = StatusCode.OK; - } - //if the status was incomplete (309) - return status.code !== StatusCode.ABORT.code; - } - - /** - * Runs the 'response' event and interprets - */ - public async shutdown() { - //default status - let status = StatusCode.OK; - try { //to allow plugins to handle the response - status = await this.route.emit( - 'response', - this.request, - this.response - ); - } catch(error) { - //if there is an error - //upgrade the error to an exception - const exception = Exception - .upgrade(error as Error) - .toResponse(); - //set the exception as the error - this.response.setError(exception); - //allow plugins to handle the error - status = await this.route.emit( - 'error', - this.request, - this.response - ); - } - //if the status was incomplete (309) - return status.code !== StatusCode.ABORT.code; - } -} \ No newline at end of file diff --git a/packages/ingest/src/runtime/fetch/Queue.ts b/packages/ingest/src/runtime/fetch/Queue.ts deleted file mode 100644 index df1277a..0000000 --- a/packages/ingest/src/runtime/fetch/Queue.ts +++ /dev/null @@ -1,11 +0,0 @@ -//stackpress -import type { UnknownNest } from '@stackpress/types/dist/types'; -import TaskQueue from '@stackpress/types/dist/TaskQueue'; -//common -import type Response from '../../Response'; -//local -import type { RouteContext } from './types'; - -export default class Queue - extends TaskQueue<[ RouteContext, Response ]> -{} \ No newline at end of file diff --git a/packages/ingest/src/runtime/fetch/Route.ts b/packages/ingest/src/runtime/fetch/Route.ts deleted file mode 100644 index 66dd4b5..0000000 --- a/packages/ingest/src/runtime/fetch/Route.ts +++ /dev/null @@ -1,269 +0,0 @@ -//modules -import { Readable } from 'stream'; -import * as cookie from 'cookie'; -//stackpress -import type { Method, UnknownNest } from '@stackpress/types/dist/types'; -//common -import type { - Body, - RouteOptions, - CookieOptions, - LoaderResponse, - FetchRequest, - ResponseInitializer, - FetchRequestInitializer -} from '../../types'; -import Factory from '../../Factory'; -import Request from '../../Request'; -import Response from '../../Response'; -import { - isHash, - formDataToObject, - objectFromQuery, - readableToReadableStream -} from '../../helpers'; -//local -import type { RouteAction } from './types'; -import Queue from './Queue'; -import Plugin from './Plugin'; -import { NativeResponse } from './helpers'; - -export default class Route - extends Factory -{ - /** - * Loads the plugins and returns the factory - */ - public static async bootstrap( - options: RouteOptions = {} - ) { - const factory = new Route(options); - return await factory.bootstrap(); - } - - //body size - protected _size: number; - //cookie options - protected _cookie: CookieOptions; - - /** - * Sets up the route - */ - public constructor(options: RouteOptions = {}) { - const { - size = 0, - cookie = { path: '/' }, - ...config - } = options; - super({ key: 'client', ...config }); - this._size = size; - this._cookie = cookie; - } - - /** - * Handles entry file requests - * - * NOTE: groupings are by exact event name/pattern match - * it doesn't take into consideration an event trigger - * can match multiple patterns. For example the following - * wont be grouped together. - * - * ie. GET /user/:id and GET /user/search - */ - public async handle( - route: string, - actions: Set, - request: FetchRequest - ) { - //initialize the request - const req = this.request({ resource: request }); - const res = this.response(); - const queue = this.queue(actions); - const ctx = req.fromRoute(route); - //load the body - await req.load(); - //bootstrap the plugins - await this.bootstrap(); - //hook the plugins - await Plugin.hook(this, queue, ctx, res); - //We would normally dispatch, but we can only create the - //fetch response when all the data is ready... - // if (!res.sent) { - // //send the response - // res.dispatch(); - // } - //just map the ingets response to a fetch response - const cookie = this.config('cookie') || this._cookie; - return response(res, cookie); - } - - /** - * Creates an emitter and populates it with actions - */ - public queue(actions: Set) { - const queue = new Queue(); - actions.forEach(action => queue.add(action)); - return queue; - } - - /** - * Sets up the request - */ - public request(init?: FetchRequestInitializer>) { - if (!init) { - return new Request>({ context: this }); - } - const request = init.resource; - //set context - init.context = this; - //set method - init.method = (request.method?.toUpperCase() || 'GET') as Method; - //set the type - init.mimetype = request.headers.get('content-type') || 'text/plain'; - //set the headers - const headers: Record = {}; - request.headers.forEach((value, key) => { - if (typeof value !== 'undefined') { - headers[key] = value; - } - }); - init.headers = init.headers || headers; - //set session - init.session = init.session || cookie.parse( - request.headers.get('cookie') as string || '' - ) as Record; - //set url - const url = new URL(request.url); - init.url = init.url || url; - //set query - init.query = init.query - || objectFromQuery(url.searchParams.toString()); - //setup the payload - const req = new Request>(init); - req.loader = loader(request); - return req; - } - - /** - * Sets up the response - */ - public response(init?: ResponseInitializer) { - return new Response(init); - } -} - -/** - * Request body loader - */ -export function loader(resource: FetchRequest, size = 0) { - return (req: Request) => { - return new Promise(async resolve => { - //if the body is cached - if (req.body !== null) { - resolve(undefined); - } - //TODO: limit the size of the body - const body = await resource.text(); - const post = formDataToObject(req.type, body) - - resolve({ body, post }); - }); - } -}; - -/** - * Maps out an Ingest Response to a Fetch Response - */ -export async function response( - res: Response, - options: CookieOptions = { path: '/' } -) { - //fetch type responses dont start with a resource - //so if it magically has a resource, then it must - //have been set in a route. So we can just return it. - if (res.resource instanceof NativeResponse) { - return res.resource; - } - let mimetype = res.mimetype; - let body: Body|null = null; - //if body is a valid response - if (typeof res.body === 'string' - || Buffer.isBuffer(res.body) - || res.body instanceof Uint8Array - || res.body instanceof ReadableStream - ) { - body = res.body; - //if it's a node stream - } else if (res.body instanceof Readable) { - body = readableToReadableStream(res.body); - //if body is an object or array - } else if (isHash(res.body) || Array.isArray(res.body)) { - res.mimetype = 'application/json'; - body = JSON.stringify({ - code: res.code, - status: res.status, - results: res.body, - error: res.error, - errors: res.errors.size > 0 ? res.errors.get() : undefined, - total: res.total > 0 ? res.total : undefined - }); - } else if (res.code && res.status) { - res.mimetype = 'application/json'; - body = JSON.stringify({ - code: res.code, - status: res.status, - error: res.error, - errors: res.errors.size > 0 ? res.errors.get() : undefined, - stack: res.stack ? res.stack : undefined - }); - } - //create response - const response = new NativeResponse(body, { - status: res.code, - statusText: res.status - }); - //write cookies - for (const [name, entry] of res.session.revisions.entries()) { - if (entry.action === 'remove') { - response.headers.set( - 'Set-Cookie', - cookie.serialize(name, '', { ...options, expires: new Date(0) }) - ); - } else if (entry.action === 'set' - && typeof entry.value !== 'undefined' - ) { - const { value } = entry; - const values = Array.isArray(value) ? value : [ value ]; - for (const value of values) { - response.headers.set( - 'Set-Cookie', - cookie.serialize(name, value, options) - ); - } - } - } - //write headers - for (const [ name, value ] of res.headers.entries()) { - const values = Array.isArray(value) ? value : [ value ]; - for (const value of values) { - response.headers.set(name, value); - } - } - //set content type - if (mimetype) { - response.headers.set('Content-Type', mimetype); - } - return response; -}; - -export function bootstrap( - options: RouteOptions = {} -) { - return Route.bootstrap(options); -}; - -export function route( - options: RouteOptions = {} -) { - return new Route(options); -}; \ No newline at end of file diff --git a/packages/ingest/src/runtime/fetch/helpers.ts b/packages/ingest/src/runtime/fetch/helpers.ts deleted file mode 100644 index 54c4ed9..0000000 --- a/packages/ingest/src/runtime/fetch/helpers.ts +++ /dev/null @@ -1,20 +0,0 @@ -//common -import type { FetchRequest } from '../../types'; -import { objectFromQuery } from '../../helpers'; - -export const NativeRequest = global.Request; -export const NativeResponse = global.Response; - -/** - * Parsed query object - */ -export function fetchToURL(resource: FetchRequest) { - return new URL(resource.url); -}; - -/** - * Parsed URL query object - */ -export function fetchQueryToObject(resource: FetchRequest) { - return objectFromQuery(fetchToURL(resource).searchParams.toString()); -}; \ No newline at end of file diff --git a/packages/ingest/src/runtime/fetch/index.ts b/packages/ingest/src/runtime/fetch/index.ts deleted file mode 100644 index 4c8a09d..0000000 --- a/packages/ingest/src/runtime/fetch/index.ts +++ /dev/null @@ -1,39 +0,0 @@ -//modules -import * as cookie from 'cookie'; -//common -import Context from '../../Context'; -import Exception from '../../Exception'; -import Request from '../../Request'; -import Response from '../../Response'; -import { ReadSession, WriteSession } from '../../Session'; -export type * from '../../types'; -export * from '../../helpers'; -//local -import Route, { - loader, - response, - route, - bootstrap -} from './Route'; -import Queue from './Queue'; -import Plugin from './Plugin'; - -export type * from './types'; -export * from './helpers'; - -export { - cookie, - Context, - Exception, - Request, - Response, - ReadSession, - WriteSession, - Queue, - Plugin, - Route, - loader, - response, - route, - bootstrap -}; \ No newline at end of file diff --git a/packages/ingest/src/runtime/fetch/types.ts b/packages/ingest/src/runtime/fetch/types.ts deleted file mode 100644 index e1b0a5a..0000000 --- a/packages/ingest/src/runtime/fetch/types.ts +++ /dev/null @@ -1,16 +0,0 @@ -//stackpress -import type { UnknownNest } from '@stackpress/types/dist/types'; -//common -import type Context from '../../Context'; -import type Request from '../../Request'; -import type Response from '../../Response'; -//local -import type Route from './Route'; - -export type RouteAction = ( - req: Context>, - res: Response -) => void | boolean | Promise; - -export type RouteRequest = Request>; -export type RouteContext = Context>; \ No newline at end of file diff --git a/packages/ingest/src/runtime/http/Plugin.ts b/packages/ingest/src/runtime/http/Plugin.ts deleted file mode 100644 index 9c67bfa..0000000 --- a/packages/ingest/src/runtime/http/Plugin.ts +++ /dev/null @@ -1,197 +0,0 @@ -//stackpress -import type { UnknownNest } from '@stackpress/types/dist/types'; -import StatusCode from '@stackpress/types/dist/StatusCode'; -//common -import type Response from '../../Response'; -import Exception from '../../Exception'; -//local -import type { RouteContext, RouteRequest } from './types'; -import type Route from './Route'; -import type Queue from './Queue'; - -export default class Plugin { - /** - * Hooks in plugins to the request lifecycle - */ - public static async hook( - route: Route, - queue: Queue, - context: RouteContext, - response: Response - ) { - const plugin = new Plugin(route, queue, context, response); - return plugin.hook(); - } - - //queue of route tasks entry files - public readonly queue: Queue; - //route request context - public readonly context: RouteContext; - //route request - public readonly request: RouteRequest; - //route response - public readonly response: Response; - //route instance - public readonly route: Route; - - /** - * Gets everything needed from route.handle() - */ - constructor( - route: Route, - queue: Queue, - context: RouteContext, - response: Response - ) { - this.route = route; - this.queue = queue; - this.context = context; - this.request = context.request; - this.response = response; - } - - /** - * Hooks in plugins to the request lifecycle - */ - public async hook() { - //try to trigger request pre-processors - if (!await this.prepare()) { - //if the request exits, then stop - return false; - } - // from here we can assume that it is okay to - // continue with processing the routes - if (!await this.process()) { - //if the request exits, then stop - return false; - } - //last call before dispatch - if (!await this.shutdown()) { - //if the dispatch exits, then stop - return false; - } - return true; - } - - /** - * Runs the 'request' event and interprets - */ - public async prepare() { - //default status - let status = StatusCode.OK; - try { //to allow plugins to handle the request - status = await this.route.emit( - 'request', - this.request, - this.response - ); - } catch(error) { - //if there is an error - //upgrade the error to an exception - const exception = Exception - .upgrade(error as Error) - .toResponse() - //set the exception as the error - this.response.setError(exception); - //allow plugins to handle the error - status = await this.route.emit( - 'error', - this.request, - this.response - ); - } - //if the status was incomplete (309) - return status.code !== StatusCode.ABORT.code; - } - - /** - * Handles a payload using events - */ - public async process() { - //default status - let status = StatusCode.OK; - try { //to run the task queue - status = await this.queue.run( - this.context, - this.response - ); - } catch(error) { - //if there is an error - //upgrade the error to an exception - const exception = Exception - .upgrade(error as Error) - .toResponse(); - //set the exception as the error - this.response.setError(exception); - //allow plugins to handle the error - status = await this.route.emit( - 'error', - this.request, - this.response - ); - } - //if the status was incomplete (309) - if (status.code === StatusCode.ABORT.code) { - //the callback that set that should have already processed - //the request and is signaling to no longer continue - return false; - } - //if no body and status code - //NOTE: it's okay if there is no body as - // long as there is a status code - //ex. like in the case of a redirect - if (!this.response.body && !this.response.code) { - //make a not found exception - const exception = Exception - .for(StatusCode.NOT_FOUND.status) - .withCode(StatusCode.NOT_FOUND.code) - .toResponse(); - //set the exception as the error - this.response.setError(exception); - //allow plugins to handle the not found - status = await this.route.emit( - 'error', - this.request, - this.response - ); - } - //if no status was set - if (!this.response.code || !this.response.status) { - //make it okay - this.response.status = StatusCode.OK; - } - //if the status was incomplete (309) - return status.code !== StatusCode.ABORT.code; - } - - /** - * Runs the 'response' event and interprets - */ - public async shutdown() { - //default status - let status = StatusCode.OK; - try { //to allow plugins to handle the response - status = await this.route.emit( - 'response', - this.request, - this.response - ); - } catch(error) { - //if there is an error - //upgrade the error to an exception - const exception = Exception - .upgrade(error as Error) - .toResponse(); - //set the exception as the error - this.response.setError(exception); - //allow plugins to handle the error - status = await this.route.emit( - 'error', - this.request, - this.response - ); - } - //if the status was incomplete (309) - return status.code !== StatusCode.ABORT.code; - } -} \ No newline at end of file diff --git a/packages/ingest/src/runtime/http/Queue.ts b/packages/ingest/src/runtime/http/Queue.ts deleted file mode 100644 index df1277a..0000000 --- a/packages/ingest/src/runtime/http/Queue.ts +++ /dev/null @@ -1,11 +0,0 @@ -//stackpress -import type { UnknownNest } from '@stackpress/types/dist/types'; -import TaskQueue from '@stackpress/types/dist/TaskQueue'; -//common -import type Response from '../../Response'; -//local -import type { RouteContext } from './types'; - -export default class Queue - extends TaskQueue<[ RouteContext, Response ]> -{} \ No newline at end of file diff --git a/packages/ingest/src/runtime/http/Route.ts b/packages/ingest/src/runtime/http/Route.ts deleted file mode 100644 index 2614d35..0000000 --- a/packages/ingest/src/runtime/http/Route.ts +++ /dev/null @@ -1,286 +0,0 @@ -//modules -import { Readable } from 'stream'; -import * as cookie from 'cookie'; -//stackpress -import type { Method, UnknownNest } from '@stackpress/types/dist/types'; -//common -import type { - IM, - SR, - RouteOptions, - CookieOptions, - LoaderResponse, - IMRequestInitializer, - SRResponseInitializer -} from '../../types'; -import Factory from '../../Factory'; -import Request from '../../Request'; -import Response from '../../Response'; -import Exception from '../../Exception'; -import { - isHash, - formDataToObject, - objectFromQuery, - readableStreamToReadable -} from '../../helpers'; -//local -import type { RouteAction } from './types'; -import Queue from './Queue'; -import Plugin from './Plugin'; -import { imToURL } from './helpers'; - -export default class Route - extends Factory -{ - /** - * Loads the plugins and returns the factory - */ - public static async bootstrap( - options: RouteOptions = {} - ) { - const factory = new Route(options); - return await factory.bootstrap(); - } - - //body size - protected _size: number; - //cookie options - protected _cookie: CookieOptions; - - /** - * Sets up the route - */ - public constructor(options: RouteOptions = {}) { - const { - size = 0, - cookie = { path: '/' }, - ...config - } = options; - super({ key: 'client', ...config }); - this._size = size; - this._cookie = cookie; - } - - /** - * Handles entry file requests - * - * NOTE: groupings are by exact event name/pattern match - * it doesn't take into consideration an event trigger - * can match multiple patterns. For example the following - * wont be grouped together. - * - * ie. GET /user/:id and GET /user/search - */ - public async handle( - route: string, - actions: Set, - im: IM, - sr: SR - ) { - //initialize the request - const req = this.request({ resource: im }); - const res = this.response({ resource: sr }); - const queue = this.queue(actions); - const ctx = req.fromRoute(route); - //load the body - await req.load(); - //bootstrap the plugins - await this.bootstrap(); - //hook the plugins - await Plugin.hook(this, queue, ctx, res); - //if the response was not sent by now, - if (!res.sent) { - //send the response - res.dispatch(); - } - return sr; - } - - /** - * Sets up the queue - */ - public queue(actions: Set) { - const emitter = new Queue(); - actions.forEach(action => emitter.add(action)); - return emitter; - } - - /** - * Sets up the request - */ - public request(init?: IMRequestInitializer>) { - if (!init) { - return new Request>({ context: this }); - } - const im = init.resource; - //set context - init.context = this; - //set method - init.method = init.method - || (im.method?.toUpperCase() || 'GET') as Method; - //set the type - init.mimetype = init.mimetype - || im.headers['content-type'] - || 'text/plain'; - //set the headers - init.headers = init.headers || Object.fromEntries( - Object.entries(im.headers).filter( - ([key, value]) => typeof value !== 'undefined' - ) - ) as Record; - //set session - init.session = init.session || cookie.parse( - im.headers.cookie as string || '' - ) as Record; - //set url - const url = imToURL(im); - init.url = init.url || url; - //set query - init.query = init.query - || objectFromQuery(url.searchParams.toString()); - //make request - const req = new Request>(init); - const size = this.config('server', 'bodySize') || this._size; - req.loader = loader(im, size); - return req; - } - - /** - * Sets up the response - */ - public response(init?: SRResponseInitializer) { - if (!init) { - return new Response(); - } - const sr = init.resource; - const res = new Response(init); - const cookie = this.config('cookie') || this._cookie; - res.dispatcher = dispatcher(sr, cookie); - return res; - } -} - -/** - * Request body loader - */ -export function loader(resource: IM, size = 0) { - return (req: Request) => { - return new Promise(resolve => { - //if the body is cached - if (req.body !== null) { - resolve(undefined); - } - - //we can only request the body once - //so we need to cache the results - let body = ''; - resource.on('data', chunk => { - body += chunk; - Exception.require( - !size || body.length <= size, - `Request exceeds ${size}` - ); - }); - resource.on('end', () => { - resolve({ body, post: formDataToObject(req.mimetype, body) }); - }); - }); - } -}; - -/** - * Response dispatcher - */ -export function dispatcher( - resource: SR, - options: CookieOptions = { path: '/' } -) { - return (res: Response) => { - return new Promise(resolve => { - //set code and status - resource.statusCode = res.code; - resource.statusMessage = res.status; - //write cookies - for (const [name, entry] of res.session.revisions.entries()) { - if (entry.action === 'remove') { - resource.setHeader( - 'Set-Cookie', - cookie.serialize(name, '', { ...options, expires: new Date(0) }) - ); - } else if (entry.action === 'set' - && typeof entry.value !== 'undefined' - ) { - const { value } = entry; - const values = Array.isArray(value) ? value : [ value ]; - for (const value of values) { - resource.setHeader( - 'Set-Cookie', - cookie.serialize(name, value, options) - ); - } - } - } - //write headers - for (const [ name, value ] of res.headers.entries()) { - resource.setHeader(name, value); - } - //set content type - if (res.mimetype) { - resource.setHeader('Content-Type', res.mimetype); - } - //if body is a valid response - if (typeof res.body === 'string' - || Buffer.isBuffer(res.body) - || res.body instanceof Uint8Array - ) { - resource.end(res.body); - //if it's a node stream - } else if (res.body instanceof Readable) { - res.body.pipe(resource); - //if it's a web stream - } else if (res.body instanceof ReadableStream) { - //convert to node stream - readableStreamToReadable(res.body).pipe(resource); - //if body is an object or array - } else if (isHash(res.body) || Array.isArray(res.body)) { - resource.setHeader('Content-Type', 'application/json'); - resource.end(JSON.stringify({ - code: res.code, - status: res.status, - results: res.body, - error: res.error, - errors: res.errors.size > 0 ? res.errors.get() : undefined, - total: res.total > 0 ? res.total : undefined, - stack: res.stack ? res.stack : undefined - })); - } else if (res.code && res.status) { - resource.setHeader('Content-Type', 'application/json'); - resource.end(JSON.stringify({ - code: res.code, - status: res.status, - error: res.error, - errors: res.errors.size > 0 ? res.errors.get() : undefined, - stack: res.stack ? res.stack : undefined - })); - } - //type Body = string | Buffer | Uint8Array - // | Record | unknown[] - - //we cased for all possible types so it's - //better to not infer the response body - resolve(); - }); - } -}; - -export function bootstrap( - options: RouteOptions = {} -) { - return Route.bootstrap(options); -} - -export function route( - options: RouteOptions = {} -) { - return new Route(options); -} \ No newline at end of file diff --git a/packages/ingest/src/runtime/http/index.ts b/packages/ingest/src/runtime/http/index.ts deleted file mode 100644 index 14a0d0f..0000000 --- a/packages/ingest/src/runtime/http/index.ts +++ /dev/null @@ -1,39 +0,0 @@ -//modules -import * as cookie from 'cookie'; -//common -import Context from '../../Context'; -import Exception from '../../Exception'; -import Request from '../../Request'; -import Response from '../../Response'; -import { ReadSession, WriteSession } from '../../Session'; -export type * from '../../types'; -export * from '../../helpers'; -//local -import Route, { - loader, - dispatcher, - route, - bootstrap -} from './Route'; -import Queue from './Queue'; -import Plugin from './Plugin'; - -export type * from './types'; -export * from './helpers'; - -export { - cookie, - Context, - Exception, - Request, - Response, - ReadSession, - WriteSession, - Queue, - Plugin, - Route, - loader, - dispatcher, - route, - bootstrap -}; \ No newline at end of file diff --git a/packages/ingest/src/runtime/http/types.ts b/packages/ingest/src/runtime/http/types.ts deleted file mode 100644 index e1b0a5a..0000000 --- a/packages/ingest/src/runtime/http/types.ts +++ /dev/null @@ -1,16 +0,0 @@ -//stackpress -import type { UnknownNest } from '@stackpress/types/dist/types'; -//common -import type Context from '../../Context'; -import type Request from '../../Request'; -import type Response from '../../Response'; -//local -import type Route from './Route'; - -export type RouteAction = ( - req: Context>, - res: Response -) => void | boolean | Promise; - -export type RouteRequest = Request>; -export type RouteContext = Context>; \ No newline at end of file diff --git a/packages/ingest/src/types.ts b/packages/ingest/src/types.ts index 102a85a..3960770 100644 --- a/packages/ingest/src/types.ts +++ b/packages/ingest/src/types.ts @@ -1,76 +1,90 @@ //modules -import type { IncomingMessage, ServerResponse } from 'http'; -import type { Readable } from 'stream'; +import type { + IncomingMessage, + ServerResponse, + ServerOptions as NodeServerOptions, + Server as NodeServer +} from 'node:http'; +import type { Readable } from 'node:stream'; //stackpress import type { + Event, Method, - Trace, - NestedObject + Trace, + RouterMap, + RouterAction, + NestedObject, + UnknownNest, + FileSystem } from '@stackpress/types/dist/types'; -import type FileSystem from '@stackpress/types/dist/filesystem/FileSystem'; +import type EventEmitter from '@stackpress/types/dist/event/EventEmitter'; //local -import type Context from './Context'; -import type Factory from './Factory'; import type Request from './Request'; import type Response from './Response'; +import type Router from './Router'; +import type Server from './Server'; import type { WriteSession } from './Session'; +export { UnknownNest }; + //--------------------------------------------------------------------// -// Generic Types +// Node Types -//a generic class constructor -export type Constructor = { new (): T }; +export { NodeServer, NodeServerOptions }; + +export type NodeRequest = globalThis.Request; +export type NodeResponse = globalThis.Response; +export type NodeOptResponse = NodeResponse|undefined; //--------------------------------------------------------------------// -// HTTP Types +// Payload Types -export type IM = IncomingMessage; -export type SR = ServerResponse; +export type Body = string | Buffer | Uint8Array | Readable | ReadableStream + | Record | Array; //--------------------------------------------------------------------// -// Fetch Types +// Response Types -export type FetchRequest = globalThis.Request; -export type FetchResponse = globalThis.Response; +export type ResponseDispatcher = (res: Response) => Promise; -//--------------------------------------------------------------------// -// Payload Types +export type ResponseInitializer = { + body?: Body, + headers?: Headers, + mimetype?: string, + resource?: S +}; -export type FactoryContext = Context; -export type Req = Context; -export type Res = Response; +export type ResponseErrorOptions = { + error: string, + errors?: NestedObject, + event?: Event>, + stack?: Trace[], + code?: number, + status?: string +} + +//--------------------------------------------------------------------// +// Request Types export type Headers = Record | Map; -export type Body = string | Buffer | Uint8Array | Readable | ReadableStream - | Record | Array; export type Data = Map | NestedObject; export type Query = string | Map | NestedObject; export type Session = Record | Map; export type Post = Record | Map; -export type LoaderResponse = { body?: Body, post?: Post }; +export type LoaderResults = { body?: Body, post?: Post }; +export type RequestLoader = ( + req: Request +) => Promise; export type CallableSession = ( (name: string) => string|string[]|undefined ) & WriteSession; -export type RequestLoader = (req: Request) => Promise; -export type ResponseDispatcher = (res: Response) => Promise; - -export type ContextInitializer = { - args?: Array | Set, - params?: Record | Map -}; - -export type ResponseInitializer = { - body?: Body, - headers?: Headers, - mimetype?: string, - resource?: SR|FetchResponse -}; -export type RequestInitializer = { +export type RequestInitializer = { + resource: R, body?: Body, - context?: C, + context?: X, headers?: Headers, mimetype?: string, data?: Data, @@ -78,29 +92,8 @@ export type RequestInitializer = { query?: Query, post?: Post, session?: Session, - url?: string|URL, - resource?: IM|FetchRequest -}; -export type IMRequestInitializer = RequestInitializer & { - resource: IM -}; -export type SRResponseInitializer = ResponseInitializer & { - resource: SR + url?: string|URL }; -export type FetchRequestInitializer = RequestInitializer & { - resource: FetchRequest -}; -export type FetchResponseInitializer = ResponseInitializer & { - resource: FetchResponse -}; - -export type ResponseErrorOptions = { - error: string, - errors?: NestedObject, - stack?: Trace[], - code?: number, - status?: string -} //--------------------------------------------------------------------// // Session Types @@ -122,10 +115,42 @@ export type CookieOptions = { secure?: boolean; }; +//--------------------------------------------------------------------// +// HTTP Types + +export type IM = IncomingMessage; +export type SR = ServerResponse; + +export type HTTPResponse = Response; +export type HTTPRequest< + C extends UnknownNest = UnknownNest +> = Request>; +export type HTTPRouter< + C extends UnknownNest = UnknownNest +> = Router>; +export type HTTPServer< + C extends UnknownNest = UnknownNest +> = Server; + +//--------------------------------------------------------------------// +// Fetch Types + +export type FetchResponse = Response; +export type FetchRequest< + C extends UnknownNest = UnknownNest +> = Request>; +export type FetchRouter< + C extends UnknownNest = UnknownNest +> = Router>; +export type FetchServer< + C extends UnknownNest = UnknownNest +> = Server; + //--------------------------------------------------------------------// // Loader Types export type ConfigLoaderOptions = { + cache?: boolean, cwd?: string, fs?: FileSystem, filenames?: string[] @@ -137,12 +162,51 @@ export type PluginLoaderOptions = ConfigLoaderOptions & { plugins?: string[] }; -export type RouteOptions = PluginLoaderOptions & { - cookie?: CookieOptions, - size?: number -} +//--------------------------------------------------------------------// +// Router Types + +export type RouterQueueArgs< + R = unknown, + S = unknown, + X = unknown +> = [ Request, Response ]; + +export type RouterEntry< + R = unknown, + S = unknown, + X = unknown +> = string|RouterAction, Response>; + +export type EntryTask = { entry: string, priority: number }; + +export type RouterEmitter< + R = unknown, + S = unknown, + X = unknown +> = EventEmitter, Response>>; //--------------------------------------------------------------------// -// Factory Types +// Server Types + +export type ServerHandler< + C extends UnknownNest = UnknownNest, + R = unknown, + S = unknown +> = (ctx: Server, req: R, res: S) => Promise; + +export type ServerGateway = (options: NodeServerOptions) => NodeServer; + +export type ServerOptions< + C extends UnknownNest = UnknownNest, + R = unknown, + S = unknown +> = PluginLoaderOptions & { + handler?: ServerHandler, + gateway?: (server: Server) => ServerGateway +}; -export type FactoryEvents = Record; \ No newline at end of file +export type ServerRequest< + C extends UnknownNest = UnknownNest, + R = unknown, + S = unknown +> = Request>; \ No newline at end of file diff --git a/packages/ingest/tests/Context.test.ts b/packages/ingest/tests/Context.test.ts deleted file mode 100644 index eef86a2..0000000 --- a/packages/ingest/tests/Context.test.ts +++ /dev/null @@ -1,241 +0,0 @@ -import { describe, it } from 'mocha'; -import { expect } from 'chai'; -import Request from '../src/Request'; -import RequestContext from '../src/Context'; - -describe('Context Tests', () => { - it('Should initialize', () => { - const request = new Request({ - method: 'POST', - url: 'http://localhost/foo/bar?bar=zoo', - mimetype: 'text/json', - body: { foo: 'bar' }, - headers: { 'Content-Type': 'text/json' }, - session: { foo: 'bar' }, - data: { foo: 'bar' }, - post: { foo: 'bar' }, - }); - const context = request.fromRoute('/foo/:name'); - - expect(context.method).to.equal('POST'); - expect(context.url.href).to.equal('http://localhost/foo/bar?bar=zoo'); - - expect(context.mimetype).to.equal('text/json'); - expect(context.type).to.equal('object'); - expect((context.body as Record)?.foo).to.contain('bar'); - - expect(context.headers.size).to.equal(1); - expect(context.session.size).to.equal(1); - - expect(context.data.size).to.equal(3); - expect(context.query.size).to.equal(1); - expect(context.post.size).to.equal(1); - - expect(context.data('name')).to.equal('bar'); - expect(context.data('foo')).to.equal('bar'); - expect(context.query('bar')).to.equal('zoo'); - expect(context.post('foo')).to.equal('bar'); - }); - - it('Should sync', () => { - const request = new Request({ - method: 'POST', - url: 'http://localhost/foo/bar?bar=zoo', - mimetype: 'text/json', - body: { foo: 'bar' }, - headers: { 'Content-Type': 'text/json' }, - session: { foo: 'bar' }, - data: { foo: 'bar' }, - post: { foo: 'bar' }, - }); - const context = request.fromRoute('/foo/:name'); - - context.query.set('blah', 'zeh'); - expect(context.query('blah')).to.equal('zeh'); - expect(request.query('blah')).to.equal('zeh'); - - context.post.set('blah', 'zeh'); - expect(context.post('blah')).to.equal('zeh'); - expect(request.post('blah')).to.equal('zeh'); - - context.data.set('blah', 'zeh'); - expect(context.data('blah')).to.equal('zeh'); - expect(request.data('blah')).to.equal('zeh'); - }); - - it('Should initialize with different argument types', () => { - const request = new Request({ - method: 'GET', - url: 'http://localhost/test/arg1/arg2', - data: { existingKey: 'existingValue' } - }); - - // Test with Set arguments - const setArgs = new Set(['arg1', 'arg2']); - let context = request.fromRoute('/test/*/**'); - expect(context.args.has('arg1')).to.be.true; - expect(context.args.has('arg2')).to.be.true; - - // Test with Array arguments - request.url.pathname = '/test/arg3/arg4'; - context = request.fromRoute('/test/*/**'); - expect(context.args.has('arg3')).to.be.true; - expect(context.args.has('arg4')).to.be.true; - - // Test with no matches - request.url.pathname = '/no-match'; - context = request.fromRoute('/test/*/**'); - expect(context.args.size).to.equal(0); - - // Test with null arguments - context = new RequestContext(request, { args: null as any }); - expect(context.args.size).to.equal(0); - - // Test with non-array/set arguments - context = new RequestContext(request, { args: 123 as any }); - expect(context.args.size).to.equal(0); - - // Test with empty Set - context = new RequestContext(request, { args: new Set() }); - expect(context.args.size).to.equal(0); - - // Test with empty Array - context = new RequestContext(request, { args: [] }); - expect(context.args.size).to.equal(0); - - // Test with Set containing non-string values - const nonStringSet = new Set([1, 2, 3]); - context = new RequestContext(request, { - args: Array.from(nonStringSet).map(String) - }); - expect(context.args.size).to.equal(3); - expect(context.args.has('1')).to.be.true; - expect(context.args.has('2')).to.be.true; - expect(context.args.has('3')).to.be.true; - }); - - it('Should initialize with different parameter types', () => { - const request = new Request({ - method: 'GET', - url: 'http://localhost/test/value1/value2', - data: { existingKey: 'existingValue' } - }); - - // Test with Map parameters - const mapParams = new Map([['key1', 'value1'], ['key2', 'value2']]); - let context = request.fromRoute('/test/:key1/:key2'); - expect(context.params('key1')).to.equal('value1'); - expect(context.params('key2')).to.equal('value2'); - expect(context.data('key1')).to.equal('value1'); - expect(context.data('key2')).to.equal('value2'); - - // Test with object parameters - request.url.pathname = '/test/value3/value4'; - context = request.fromRoute('/test/:key3/:key4'); - expect(context.params('key3')).to.equal('value3'); - expect(context.params('key4')).to.equal('value4'); - expect(context.data('key3')).to.equal('value3'); - expect(context.data('key4')).to.equal('value4'); - - // Test with no matches - request.url.pathname = '/no-match'; - context = request.fromRoute('/test/:key1/:key2'); - expect(context.params.size).to.equal(0); - - // Test with null parameters - context = new RequestContext(request, { params: null as any }); - expect(context.params.size).to.equal(0); - - // Test with non-map/object parameters - context = new RequestContext(request, { params: 123 as any }); - expect(context.params.size).to.equal(0); - - // Test with empty Map - context = new RequestContext(request, { params: new Map() }); - expect(context.params.size).to.equal(0); - - // Test with empty object - context = new RequestContext(request, { params: {} }); - expect(context.params.size).to.equal(0); - - // Test with Map containing non-string values - const nonStringMap = new Map([['key1', 1], ['key2', true]]); - context = new RequestContext(request, { - params: new Map(Array.from(nonStringMap.entries()).map(([k, v]) => [k, String(v)])) - }); - expect(context.params.size).to.equal(2); - expect(context.params('key1')).to.equal('1'); - expect(context.params('key2')).to.equal('true'); - - // Test with object containing non-string values - context = new RequestContext(request, { - params: { key1: String(1), key2: String(true) } - }); - expect(context.params.size).to.equal(2); - expect(context.params('key1')).to.equal('1'); - expect(context.params('key2')).to.equal('true'); - }); - - it('Should handle loaded state correctly', async () => { - // Create a request with a body - const request = new Request({ - method: 'POST', - url: 'http://localhost/test', - body: 'test body', - mimetype: 'text/plain' - }); - await request.load(); - const context = request.fromRoute('/test'); - expect(context.loaded).to.be.true; - - // Create a request without a body - const emptyRequest = new Request({ - method: 'GET', - url: 'http://localhost/test' - }); - const emptyContext = emptyRequest.fromRoute('/test'); - expect(emptyContext.loaded).to.be.false; - }); - - it('Should handle data sync correctly', () => { - const request = new Request({ - method: 'GET', - url: 'http://localhost/test', - data: { key1: 'existing1', key2: 'existing2' } - }); - - // Test when param key exists in data (shouldn't override) - let context = new RequestContext(request, { - params: new Map([ - ['key1', 'param1'], // exists in data - ['key3', 'param3'] // doesn't exist in data - ]) - }); - - expect(context.data('key1')).to.equal('existing1'); // unchanged - expect(context.data('key2')).to.equal('existing2'); // unchanged - expect(context.data('key3')).to.equal('param3'); // added - - // Test with empty params (no data sync needed) - context = new RequestContext(request, { - params: new Map() - }); - expect(context.data('key1')).to.equal('existing1'); - expect(context.data('key2')).to.equal('existing2'); - - // Test with undefined params (no data sync needed) - context = new RequestContext(request); - expect(context.data('key1')).to.equal('existing1'); - expect(context.data('key2')).to.equal('existing2'); - - // Test with null data in request - const emptyRequest = new Request({ - method: 'GET', - url: 'http://localhost/test' - }); - context = new RequestContext(emptyRequest, { - params: new Map([['key1', 'value1']]) - }); - expect(context.data('key1')).to.equal('value1'); - }); -}); \ No newline at end of file diff --git a/packages/ingest/tests/Request.test.ts b/packages/ingest/tests/Request.test.ts index e29ad89..238ad83 100644 --- a/packages/ingest/tests/Request.test.ts +++ b/packages/ingest/tests/Request.test.ts @@ -1,7 +1,6 @@ import { describe, it } from 'mocha'; import { expect } from 'chai'; import Request from '../src/Request'; -import { map } from '@stackpress/types/dist/helpers'; describe('Request Tests', () => { it('Should be empty', () => { @@ -124,28 +123,6 @@ describe('Request Tests', () => { expect(arrayBody.type).to.equal('array'); }); - it('Should handle pattern matching', () => { - const request = new Request({ - url: 'http://example.com/user/123' - }); - - const context1 = request.fromPattern('/user/([0-9]+)/'); - expect(context1?.args?.values().next().value).to.equal('123'); - - const context2 = request.fromPattern(new RegExp('user/([0-9]+)')); - expect(context2?.args?.values().next().value).to.equal('123'); - }); - - it('Should handle route parameters', () => { - const request = new Request({ - url: 'http://unknownhost/users/123/posts/456' - }); - - const context = request.fromRoute('/users/:id/posts/:postId'); - expect(context?.params.get('id')).to.equal('123'); - expect(context?.params.get('postId')).to.equal('456'); - }); - it('Should handle body loading', async () => { const request = new Request(); request.loader = async (req) => ({ diff --git a/packages/ingest/tests/helpers.test.ts b/packages/ingest/tests/helpers.test.ts index 575c5a2..badf7bb 100644 --- a/packages/ingest/tests/helpers.test.ts +++ b/packages/ingest/tests/helpers.test.ts @@ -7,10 +7,10 @@ import { objectFromJson, eventParams, routeParams, - withUnknownHost, - readableStreamToReadable, - readableToReadableStream + withUnknownHost } from '../src/helpers'; +import { readableStreamToReadable } from '../src/http/helpers'; +import { readableToReadableStream } from '../src/fetch/helpers'; import { Readable } from 'stream'; describe('helpers', () => { diff --git a/yarn.lock b/yarn.lock index c0768ce..95c1a2a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -151,126 +151,6 @@ dependencies: "@jridgewell/trace-mapping" "0.3.9" -"@esbuild/aix-ppc64@0.24.0": - version "0.24.0" - resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.24.0.tgz#b57697945b50e99007b4c2521507dc613d4a648c" - integrity sha512-WtKdFM7ls47zkKHFVzMz8opM7LkcsIp9amDUBIAWirg70RM71WRSjdILPsY5Uv1D42ZpUfaPILDlfactHgsRkw== - -"@esbuild/android-arm64@0.24.0": - version "0.24.0" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.24.0.tgz#1add7e0af67acefd556e407f8497e81fddad79c0" - integrity sha512-Vsm497xFM7tTIPYK9bNTYJyF/lsP590Qc1WxJdlB6ljCbdZKU9SY8i7+Iin4kyhV/KV5J2rOKsBQbB77Ab7L/w== - -"@esbuild/android-arm@0.24.0": - version "0.24.0" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.24.0.tgz#ab7263045fa8e090833a8e3c393b60d59a789810" - integrity sha512-arAtTPo76fJ/ICkXWetLCc9EwEHKaeya4vMrReVlEIUCAUncH7M4bhMQ+M9Vf+FFOZJdTNMXNBrWwW+OXWpSew== - -"@esbuild/android-x64@0.24.0": - version "0.24.0" - resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.24.0.tgz#e8f8b196cfdfdd5aeaebbdb0110983460440e705" - integrity sha512-t8GrvnFkiIY7pa7mMgJd7p8p8qqYIz1NYiAoKc75Zyv73L3DZW++oYMSHPRarcotTKuSs6m3hTOa5CKHaS02TQ== - -"@esbuild/darwin-arm64@0.24.0": - version "0.24.0" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.24.0.tgz#2d0d9414f2acbffd2d86e98253914fca603a53dd" - integrity sha512-CKyDpRbK1hXwv79soeTJNHb5EiG6ct3efd/FTPdzOWdbZZfGhpbcqIpiD0+vwmpu0wTIL97ZRPZu8vUt46nBSw== - -"@esbuild/darwin-x64@0.24.0": - version "0.24.0" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.24.0.tgz#33087aab31a1eb64c89daf3d2cf8ce1775656107" - integrity sha512-rgtz6flkVkh58od4PwTRqxbKH9cOjaXCMZgWD905JOzjFKW+7EiUObfd/Kav+A6Gyud6WZk9w+xu6QLytdi2OA== - -"@esbuild/freebsd-arm64@0.24.0": - version "0.24.0" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.0.tgz#bb76e5ea9e97fa3c753472f19421075d3a33e8a7" - integrity sha512-6Mtdq5nHggwfDNLAHkPlyLBpE5L6hwsuXZX8XNmHno9JuL2+bg2BX5tRkwjyfn6sKbxZTq68suOjgWqCicvPXA== - -"@esbuild/freebsd-x64@0.24.0": - version "0.24.0" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.24.0.tgz#e0e2ce9249fdf6ee29e5dc3d420c7007fa579b93" - integrity sha512-D3H+xh3/zphoX8ck4S2RxKR6gHlHDXXzOf6f/9dbFt/NRBDIE33+cVa49Kil4WUjxMGW0ZIYBYtaGCa2+OsQwQ== - -"@esbuild/linux-arm64@0.24.0": - version "0.24.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.24.0.tgz#d1b2aa58085f73ecf45533c07c82d81235388e75" - integrity sha512-TDijPXTOeE3eaMkRYpcy3LarIg13dS9wWHRdwYRnzlwlA370rNdZqbcp0WTyyV/k2zSxfko52+C7jU5F9Tfj1g== - -"@esbuild/linux-arm@0.24.0": - version "0.24.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.24.0.tgz#8e4915df8ea3e12b690a057e77a47b1d5935ef6d" - integrity sha512-gJKIi2IjRo5G6Glxb8d3DzYXlxdEj2NlkixPsqePSZMhLudqPhtZ4BUrpIuTjJYXxvF9njql+vRjB2oaC9XpBw== - -"@esbuild/linux-ia32@0.24.0": - version "0.24.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.24.0.tgz#8200b1110666c39ab316572324b7af63d82013fb" - integrity sha512-K40ip1LAcA0byL05TbCQ4yJ4swvnbzHscRmUilrmP9Am7//0UjPreh4lpYzvThT2Quw66MhjG//20mrufm40mA== - -"@esbuild/linux-loong64@0.24.0": - version "0.24.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.24.0.tgz#6ff0c99cf647504df321d0640f0d32e557da745c" - integrity sha512-0mswrYP/9ai+CU0BzBfPMZ8RVm3RGAN/lmOMgW4aFUSOQBjA31UP8Mr6DDhWSuMwj7jaWOT0p0WoZ6jeHhrD7g== - -"@esbuild/linux-mips64el@0.24.0": - version "0.24.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.24.0.tgz#3f720ccd4d59bfeb4c2ce276a46b77ad380fa1f3" - integrity sha512-hIKvXm0/3w/5+RDtCJeXqMZGkI2s4oMUGj3/jM0QzhgIASWrGO5/RlzAzm5nNh/awHE0A19h/CvHQe6FaBNrRA== - -"@esbuild/linux-ppc64@0.24.0": - version "0.24.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.24.0.tgz#9d6b188b15c25afd2e213474bf5f31e42e3aa09e" - integrity sha512-HcZh5BNq0aC52UoocJxaKORfFODWXZxtBaaZNuN3PUX3MoDsChsZqopzi5UupRhPHSEHotoiptqikjN/B77mYQ== - -"@esbuild/linux-riscv64@0.24.0": - version "0.24.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.24.0.tgz#f989fdc9752dfda286c9cd87c46248e4dfecbc25" - integrity sha512-bEh7dMn/h3QxeR2KTy1DUszQjUrIHPZKyO6aN1X4BCnhfYhuQqedHaa5MxSQA/06j3GpiIlFGSsy1c7Gf9padw== - -"@esbuild/linux-s390x@0.24.0": - version "0.24.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.24.0.tgz#29ebf87e4132ea659c1489fce63cd8509d1c7319" - integrity sha512-ZcQ6+qRkw1UcZGPyrCiHHkmBaj9SiCD8Oqd556HldP+QlpUIe2Wgn3ehQGVoPOvZvtHm8HPx+bH20c9pvbkX3g== - -"@esbuild/linux-x64@0.24.0": - version "0.24.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.24.0.tgz#4af48c5c0479569b1f359ffbce22d15f261c0cef" - integrity sha512-vbutsFqQ+foy3wSSbmjBXXIJ6PL3scghJoM8zCL142cGaZKAdCZHyf+Bpu/MmX9zT9Q0zFBVKb36Ma5Fzfa8xA== - -"@esbuild/netbsd-x64@0.24.0": - version "0.24.0" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.24.0.tgz#1ae73d23cc044a0ebd4f198334416fb26c31366c" - integrity sha512-hjQ0R/ulkO8fCYFsG0FZoH+pWgTTDreqpqY7UnQntnaKv95uP5iW3+dChxnx7C3trQQU40S+OgWhUVwCjVFLvg== - -"@esbuild/openbsd-arm64@0.24.0": - version "0.24.0" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.0.tgz#5d904a4f5158c89859fd902c427f96d6a9e632e2" - integrity sha512-MD9uzzkPQbYehwcN583yx3Tu5M8EIoTD+tUgKF982WYL9Pf5rKy9ltgD0eUgs8pvKnmizxjXZyLt0z6DC3rRXg== - -"@esbuild/openbsd-x64@0.24.0": - version "0.24.0" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.24.0.tgz#4c8aa88c49187c601bae2971e71c6dc5e0ad1cdf" - integrity sha512-4ir0aY1NGUhIC1hdoCzr1+5b43mw99uNwVzhIq1OY3QcEwPDO3B7WNXBzaKY5Nsf1+N11i1eOfFcq+D/gOS15Q== - -"@esbuild/sunos-x64@0.24.0": - version "0.24.0" - resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.24.0.tgz#8ddc35a0ea38575fa44eda30a5ee01ae2fa54dd4" - integrity sha512-jVzdzsbM5xrotH+W5f1s+JtUy1UWgjU0Cf4wMvffTB8m6wP5/kx0KiaLHlbJO+dMgtxKV8RQ/JvtlFcdZ1zCPA== - -"@esbuild/win32-arm64@0.24.0": - version "0.24.0" - resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.24.0.tgz#6e79c8543f282c4539db684a207ae0e174a9007b" - integrity sha512-iKc8GAslzRpBytO2/aN3d2yb2z8XTVfNV0PjGlCxKo5SgWmNXx82I/Q3aG1tFfS+A2igVCY97TJ8tnYwpUWLCA== - -"@esbuild/win32-ia32@0.24.0": - version "0.24.0" - resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.24.0.tgz#057af345da256b7192d18b676a02e95d0fa39103" - integrity sha512-vQW36KZolfIudCcTnaTpmLQ24Ha1RjygBo39/aLkM2kmjkWmZGEJ5Gn9l5/7tzXA42QGIoWbICfg6KLLkIw6yw== - -"@esbuild/win32-x64@0.24.0": - version "0.24.0" - resolved "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.0.tgz" - integrity sha512-7IAFPrjSQIJrGsK6flwg7NFmwBoSTyF3rl7If0hNUFQU4ilTsEPL6GuMuU9BfIWVVGuRnuIidkSMC+c0Otu8IA== - "@inquirer/checkbox@^4.0.2": version "4.0.2" resolved "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.0.2.tgz" @@ -460,21 +340,39 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" -"@stackpress/types@0.2.11": - version "0.2.11" - resolved "https://registry.yarnpkg.com/@stackpress/types/-/types-0.2.11.tgz#fc3d8b61fa17fe435467f465da122a1b186dd4f1" - integrity sha512-qpygvVQW7JmXOD+INyvYEwr7fPsGSSloQpM5/BP6aHHQrReLg6TOJEUJefITKc91Tvijh3oRA7PYEMKzY034NQ== +"@peculiar/asn1-schema@^2.3.13", "@peculiar/asn1-schema@^2.3.8": + version "2.3.13" + resolved "https://registry.yarnpkg.com/@peculiar/asn1-schema/-/asn1-schema-2.3.13.tgz#ec8509cdcbc0da3abe73fd7e690556b57a61b8f4" + integrity sha512-3Xq3a01WkHRZL8X04Zsfg//mGaA21xlL4tlVn4v2xGT0JStiztATRkMwa5b+f/HXmY2smsiLXYK46Gwgzvfg3g== dependencies: - "@inquirer/prompts" "7.1.0" + asn1js "^3.0.5" + pvtsutils "^1.3.5" + tslib "^2.6.2" + +"@peculiar/json-schema@^1.1.12": + version "1.1.12" + resolved "https://registry.yarnpkg.com/@peculiar/json-schema/-/json-schema-1.1.12.tgz#fe61e85259e3b5ba5ad566cb62ca75b3d3cd5339" + integrity sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w== + dependencies: + tslib "^2.0.0" -"@ts-morph/common@~0.25.0": - version "0.25.0" - resolved "https://registry.npmjs.org/@ts-morph/common/-/common-0.25.0.tgz" - integrity sha512-kMnZz+vGGHi4GoHnLmMhGNjm44kGtKUXGnOvrKmMwAuvNjM/PgKVGfUnL7IDvK7Jb2QQ82jq3Zmp04Gy+r3Dkg== +"@peculiar/webcrypto@^1.4.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@peculiar/webcrypto/-/webcrypto-1.5.0.tgz#9e57174c02c1291051c553600347e12b81469e10" + integrity sha512-BRs5XUAwiyCDQMsVA9IDvDa7UBR9gAvPHgugOeGng3YN6vJ9JYonyDc0lNczErgtCWtucjR5N7VtaonboD/ezg== dependencies: - minimatch "^9.0.4" - path-browserify "^1.0.1" - tinyglobby "^0.2.9" + "@peculiar/asn1-schema" "^2.3.8" + "@peculiar/json-schema" "^1.1.12" + pvtsutils "^1.3.5" + tslib "^2.6.2" + webcrypto-core "^1.8.0" + +"@stackpress/types@0.3.6": + version "0.3.6" + resolved "https://registry.yarnpkg.com/@stackpress/types/-/types-0.3.6.tgz#fcab1c387befe3fda4ec74b791e5862f999eb9bc" + integrity sha512-fAuTlbjofTOxMo0YBu4p0Z9T3j+zMTkg8qoSQMLs78oveQZj0P93ZjpIXucWOQNLsLaPj64Wxgu3N5ChCevpQw== + dependencies: + "@inquirer/prompts" "7.1.0" "@tsconfig/node10@^1.0.7": version "1.0.11" @@ -518,6 +416,41 @@ dependencies: undici-types "~6.19.8" +"@whatwg-node/events@^0.0.3": + version "0.0.3" + resolved "https://registry.yarnpkg.com/@whatwg-node/events/-/events-0.0.3.tgz#13a65dd4f5893f55280f766e29ae48074927acad" + integrity sha512-IqnKIDWfXBJkvy/k6tzskWTc2NK3LcqHlb+KHGCrjOCH4jfQckRX0NAiIcC/vIqQkzLYw2r2CTSwAxcrtcD6lA== + +"@whatwg-node/fetch@^0.8.1": + version "0.8.8" + resolved "https://registry.yarnpkg.com/@whatwg-node/fetch/-/fetch-0.8.8.tgz#48c6ad0c6b7951a73e812f09dd22d75e9fa18cae" + integrity sha512-CdcjGC2vdKhc13KKxgsc6/616BQ7ooDIgPeTuAiE8qfCnS0mGzcfCOoZXypQSz73nxI+GWc7ZReIAVhxoE1KCg== + dependencies: + "@peculiar/webcrypto" "^1.4.0" + "@whatwg-node/node-fetch" "^0.3.6" + busboy "^1.6.0" + urlpattern-polyfill "^8.0.0" + web-streams-polyfill "^3.2.1" + +"@whatwg-node/node-fetch@^0.3.6": + version "0.3.6" + resolved "https://registry.yarnpkg.com/@whatwg-node/node-fetch/-/node-fetch-0.3.6.tgz#e28816955f359916e2d830b68a64493124faa6d0" + integrity sha512-w9wKgDO4C95qnXZRwZTfCmLWqyRnooGjcIwG0wADWjw9/HN0p7dtvtgSvItZtUyNteEvgTrd8QojNEqV6DAGTA== + dependencies: + "@whatwg-node/events" "^0.0.3" + busboy "^1.6.0" + fast-querystring "^1.1.1" + fast-url-parser "^1.1.3" + tslib "^2.3.1" + +"@whatwg-node/server@0.6.7": + version "0.6.7" + resolved "https://registry.yarnpkg.com/@whatwg-node/server/-/server-0.6.7.tgz#14f5d0aca49308759d64fc7faa3cbfc20162a1ee" + integrity sha512-M4zHWdJ6M1IdcxnZBdDmiUh1bHQ4gPYRxzkH0gh8Qf6MpWJmX6I/MNftqem3GNn+qn1y47qqlGSed7T7nzsRFw== + dependencies: + "@whatwg-node/fetch" "^0.8.1" + tslib "^2.3.1" + acorn-walk@^8.1.1: version "8.3.4" resolved "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz" @@ -604,6 +537,15 @@ arrify@^1.0.0: resolved "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz" integrity sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA== +asn1js@^3.0.5: + version "3.0.5" + resolved "https://registry.yarnpkg.com/asn1js/-/asn1js-3.0.5.tgz#5ea36820443dbefb51cc7f88a2ebb5b462114f38" + integrity sha512-FVnvrKJwpt9LP2lAMl8qZswRNm3T4q9CON+bxldk2iwk3FFpuwhx2FfinyitizWHsVYyaY+y5JzDR0rCMV5yTQ== + dependencies: + pvtsutils "^1.3.2" + pvutils "^1.1.3" + tslib "^2.4.0" + assertion-error@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz" @@ -661,6 +603,13 @@ buffer-from@^1.0.0, buffer-from@^1.1.0: resolved "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz" integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== +busboy@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/busboy/-/busboy-1.6.0.tgz#966ea36a9502e43cdb9146962523b92f531f6893" + integrity sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA== + dependencies: + streamsearch "^1.1.0" + caching-transform@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/caching-transform/-/caching-transform-4.0.0.tgz" @@ -762,11 +711,6 @@ cliui@^7.0.2: strip-ansi "^6.0.0" wrap-ansi "^7.0.0" -code-block-writer@^13.0.3: - version "13.0.3" - resolved "https://registry.npmjs.org/code-block-writer/-/code-block-writer-13.0.3.tgz" - integrity sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg== - color-convert@^2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz" @@ -879,36 +823,6 @@ es6-error@^4.0.1: resolved "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz" integrity sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg== -esbuild@0.24.0: - version "0.24.0" - resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.24.0.tgz" - integrity sha512-FuLPevChGDshgSicjisSooU0cemp/sGXR841D5LHMB7mTVOmsEHcAxaH3irL53+8YDIeVNQEySh4DaYU/iuPqQ== - optionalDependencies: - "@esbuild/aix-ppc64" "0.24.0" - "@esbuild/android-arm" "0.24.0" - "@esbuild/android-arm64" "0.24.0" - "@esbuild/android-x64" "0.24.0" - "@esbuild/darwin-arm64" "0.24.0" - "@esbuild/darwin-x64" "0.24.0" - "@esbuild/freebsd-arm64" "0.24.0" - "@esbuild/freebsd-x64" "0.24.0" - "@esbuild/linux-arm" "0.24.0" - "@esbuild/linux-arm64" "0.24.0" - "@esbuild/linux-ia32" "0.24.0" - "@esbuild/linux-loong64" "0.24.0" - "@esbuild/linux-mips64el" "0.24.0" - "@esbuild/linux-ppc64" "0.24.0" - "@esbuild/linux-riscv64" "0.24.0" - "@esbuild/linux-s390x" "0.24.0" - "@esbuild/linux-x64" "0.24.0" - "@esbuild/netbsd-x64" "0.24.0" - "@esbuild/openbsd-arm64" "0.24.0" - "@esbuild/openbsd-x64" "0.24.0" - "@esbuild/sunos-x64" "0.24.0" - "@esbuild/win32-arm64" "0.24.0" - "@esbuild/win32-ia32" "0.24.0" - "@esbuild/win32-x64" "0.24.0" - escalade@^3.1.1, escalade@^3.2.0: version "3.2.0" resolved "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz" @@ -933,10 +847,24 @@ external-editor@^3.1.0: iconv-lite "^0.4.24" tmp "^0.0.33" -fdir@^6.4.2: - version "6.4.2" - resolved "https://registry.npmjs.org/fdir/-/fdir-6.4.2.tgz" - integrity sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ== +fast-decode-uri-component@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz#46f8b6c22b30ff7a81357d4f59abfae938202543" + integrity sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg== + +fast-querystring@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/fast-querystring/-/fast-querystring-1.1.2.tgz#a6d24937b4fc6f791b4ee31dcb6f53aeafb89f53" + integrity sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg== + dependencies: + fast-decode-uri-component "^1.0.1" + +fast-url-parser@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/fast-url-parser/-/fast-url-parser-1.1.3.tgz#f4af3ea9f34d8a271cf58ad2b3759f431f0b318d" + integrity sha512-5jOCVXADYNuRkKFzNJ0dCCewsZiYo0dz8QNYljkOpFC6r2U4OBmKtvm/Tsuh4w1YYdDqDb31a8TVhBJ2OJKdqQ== + dependencies: + punycode "^1.3.2" fill-range@^7.1.1: version "7.1.1" @@ -1350,13 +1278,6 @@ minimatch@^5.0.1, minimatch@^5.1.6: dependencies: brace-expansion "^2.0.1" -minimatch@^9.0.4: - version "9.0.5" - resolved "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz" - integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== - dependencies: - brace-expansion "^2.0.1" - minimist@^1.2.0, minimist@^1.2.6: version "1.2.8" resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz" @@ -1517,11 +1438,6 @@ package-hash@^4.0.0: lodash.flattendeep "^4.4.0" release-zalgo "^1.0.0" -path-browserify@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz" - integrity sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g== - path-exists@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz" @@ -1552,11 +1468,6 @@ picomatch@^2.0.4, picomatch@^2.2.1: resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== -picomatch@^4.0.2: - version "4.0.2" - resolved "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz" - integrity sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg== - pkg-dir@^4.1.0: version "4.2.0" resolved "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz" @@ -1571,6 +1482,23 @@ process-on-spawn@^1.0.0: dependencies: fromentries "^1.2.0" +punycode@^1.3.2: + version "1.4.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" + integrity sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ== + +pvtsutils@^1.3.2, pvtsutils@^1.3.5: + version "1.3.6" + resolved "https://registry.yarnpkg.com/pvtsutils/-/pvtsutils-1.3.6.tgz#ec46e34db7422b9e4fdc5490578c1883657d6001" + integrity sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg== + dependencies: + tslib "^2.8.1" + +pvutils@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/pvutils/-/pvutils-1.1.3.tgz#f35fc1d27e7cd3dfbd39c0826d173e806a03f5a3" + integrity sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ== + randombytes@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz" @@ -1698,6 +1626,11 @@ sprintf-js@~1.0.2: resolved "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz" integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== +streamsearch@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764" + integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== + string-width@^4.1.0, string-width@^4.2.0: version "4.2.3" resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" @@ -1752,14 +1685,6 @@ test-exclude@^6.0.0: glob "^7.1.4" minimatch "^3.0.4" -tinyglobby@^0.2.9: - version "0.2.10" - resolved "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.10.tgz" - integrity sha512-Zc+8eJlFMvgatPZTl6A9L/yht8QqdmUNtURHaKZLmKBE12hNPSrqNkUp2cs3M/UKmNVVAMFQYSjYIVHDjW5zew== - dependencies: - fdir "^6.4.2" - picomatch "^4.0.2" - tmp@^0.0.33: version "0.0.33" resolved "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz" @@ -1783,14 +1708,6 @@ ts-mocha@10.0.0: optionalDependencies: tsconfig-paths "^3.5.0" -ts-morph@24.0.0: - version "24.0.0" - resolved "https://registry.npmjs.org/ts-morph/-/ts-morph-24.0.0.tgz" - integrity sha512-2OAOg/Ob5yx9Et7ZX4CvTCc0UFoZHwLEJ+dpDPSUi5TgwwlTlX47w+iFRrEwzUZwYACjq83cgjS/Da50Ga37uw== - dependencies: - "@ts-morph/common" "~0.25.0" - code-block-writer "^13.0.3" - ts-node@10.9.2: version "10.9.2" resolved "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz" @@ -1834,6 +1751,11 @@ tsconfig-paths@^3.5.0: minimist "^1.2.6" strip-bom "^3.0.0" +tslib@^2.0.0, tslib@^2.3.1, tslib@^2.4.0, tslib@^2.6.2, tslib@^2.7.0, tslib@^2.8.1: + version "2.8.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" + integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== + type-detect@^4.0.0, type-detect@^4.1.0: version "4.1.0" resolved "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz" @@ -1874,6 +1796,11 @@ update-browserslist-db@^1.1.1: escalade "^3.2.0" picocolors "^1.1.0" +urlpattern-polyfill@^8.0.0: + version "8.0.2" + resolved "https://registry.yarnpkg.com/urlpattern-polyfill/-/urlpattern-polyfill-8.0.2.tgz#99f096e35eff8bf4b5a2aa7d58a1523d6ebc7ce5" + integrity sha512-Qp95D4TPJl1kC9SKigDcqgyM2VDVO4RiJc2d4qe5GrYm+zbIQCWWKAFaJNQ4BhdFeDGwBmAxqJBwWSJDb9T3BQ== + uuid@^8.3.2: version "8.3.2" resolved "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz" @@ -1884,6 +1811,22 @@ v8-compile-cache-lib@^3.0.1: resolved "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz" integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== +web-streams-polyfill@^3.2.1: + version "3.3.3" + resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz#2073b91a2fdb1fbfbd401e7de0ac9f8214cecb4b" + integrity sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw== + +webcrypto-core@^1.8.0: + version "1.8.1" + resolved "https://registry.yarnpkg.com/webcrypto-core/-/webcrypto-core-1.8.1.tgz#09d5bd8a9c48e9fbcaf412e06b1ff1a57514ce86" + integrity sha512-P+x1MvlNCXlKbLSOY4cYrdreqPG5hbzkmawbcXLKN/mf6DZW0SdNNkZ+sjwsqVkI4A4Ko2sPZmkZtCKY58w83A== + dependencies: + "@peculiar/asn1-schema" "^2.3.13" + "@peculiar/json-schema" "^1.1.12" + asn1js "^3.0.5" + pvtsutils "^1.3.5" + tslib "^2.7.0" + which-module@^2.0.0: version "2.0.1" resolved "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz"