├── tests └── endpoints │ ├── server │ ├── .env │ ├── public │ │ ├── text.txt │ │ ├── image.jpg │ │ └── test │ │ │ └── archive.zip │ ├── routes │ │ ├── module │ │ │ ├── [m2] │ │ │ │ └── module.ts │ │ │ └── m1 │ │ │ │ └── module.ts │ │ ├── regular │ │ │ ├── response │ │ │ │ ├── html │ │ │ │ │ ├── basic │ │ │ │ │ │ ├── view.html │ │ │ │ │ │ └── index.ts │ │ │ │ │ └── standalone │ │ │ │ │ │ └── view.html │ │ │ │ ├── none │ │ │ │ │ ├── basic │ │ │ │ │ │ └── index.ts │ │ │ │ │ └── custom │ │ │ │ │ │ └── index.ts │ │ │ │ ├── text │ │ │ │ │ ├── basic │ │ │ │ │ │ └── index.ts │ │ │ │ │ └── custom │ │ │ │ │ │ └── index.ts │ │ │ │ ├── redirect │ │ │ │ │ ├── basic │ │ │ │ │ │ └── index.ts │ │ │ │ │ └── success │ │ │ │ │ │ └── index.ts │ │ │ │ └── json │ │ │ │ │ ├── custom │ │ │ │ │ └── index.ts │ │ │ │ │ └── basic │ │ │ │ │ └── index.ts │ │ │ ├── dynamic-paths │ │ │ │ └── [testID] │ │ │ │ │ └── [testID] │ │ │ │ │ └── page │ │ │ │ │ └── [pageID] │ │ │ │ │ └── index.ts │ │ │ └── index.ts │ │ └── ajv │ │ │ └── index.ts │ ├── src │ │ └── foo.schema.json │ ├── sherpa.module.ts │ ├── sherpa.server.ts │ └── package.json │ ├── modules │ ├── pass-primary-2 │ │ ├── public │ │ │ ├── text.txt │ │ │ └── image.jpg │ │ ├── routes │ │ │ ├── index.ts │ │ │ └── [id] │ │ │ │ └── path │ │ │ │ └── index.ts │ │ └── sherpa.module.ts │ └── pass-primary-1 │ │ ├── package.json │ │ ├── routes │ │ ├── test │ │ │ ├── view.html │ │ │ └── index.ts │ │ └── index.ts │ │ └── sherpa.module.ts │ └── suite │ ├── index.ts │ ├── model.ts │ ├── helpers.ts │ ├── bench.ts │ ├── suite.ts │ └── tester.ts ├── src ├── compiler │ ├── utilities │ │ ├── tooling │ │ │ ├── dot-env │ │ │ │ ├── tests │ │ │ │ │ ├── test1.env │ │ │ │ │ ├── test2.env │ │ │ │ │ └── test3.env │ │ │ │ ├── index.ts │ │ │ │ └── index.test.ts │ │ │ ├── exported-loader │ │ │ │ └── tests │ │ │ │ │ ├── test1.ts │ │ │ │ │ ├── test2.ts │ │ │ │ │ ├── test3.ts │ │ │ │ │ ├── test4.ts │ │ │ │ │ ├── test5.ts │ │ │ │ │ ├── test6.ts │ │ │ │ │ └── test7.ts │ │ │ ├── exported-variables │ │ │ │ ├── tests │ │ │ │ │ ├── test2.ts │ │ │ │ │ ├── test1.ts │ │ │ │ │ └── test3.ts │ │ │ │ ├── index.ts │ │ │ │ └── index.test.ts │ │ │ ├── index.ts │ │ │ └── type-validation │ │ │ │ └── index.ts │ │ ├── logger │ │ │ ├── model.ts │ │ │ └── index.ts │ │ ├── path │ │ │ ├── directory-structure │ │ │ │ ├── model.ts │ │ │ │ └── index.ts │ │ │ └── index.ts │ │ └── kv │ │ │ └── index.ts │ ├── bundler │ │ ├── index.ts │ │ └── platforms │ │ │ ├── local │ │ │ └── index.ts │ │ │ └── abstract.ts │ ├── structure │ │ ├── endpoint │ │ │ ├── load-module.ts │ │ │ └── load-functions.ts │ │ ├── assets │ │ │ ├── index.ts │ │ │ └── files.ts │ │ ├── index.ts │ │ ├── config-server │ │ │ └── index.ts │ │ └── config-module │ │ │ └── index.ts │ ├── models.ts │ └── index.ts ├── internal │ ├── index.ts │ ├── handler │ │ └── index.ts │ ├── response │ │ └── index.ts │ └── request │ │ └── index.ts ├── native │ ├── ajv │ │ └── index.ts │ ├── url │ │ └── index.ts │ ├── model.ts │ ├── request │ │ ├── index.ts │ │ └── utilities.ts │ ├── index.ts │ ├── response │ │ ├── status-text.ts │ │ └── index.ts │ ├── parameters │ │ └── index.ts │ └── headers │ │ └── index.ts ├── instantiate │ └── index.ts ├── cli │ └── utilities.ts ├── server-local │ └── index.ts └── server-development │ └── index.ts ├── .gitattributes ├── docs ├── assets │ └── logos │ │ ├── favicon.png │ │ ├── logo-dark.png │ │ ├── logo-light.png │ │ ├── logo-large-dark.png │ │ └── logo-large-light.png ├── structure.mdx ├── api │ ├── components │ │ ├── parameters.mdx │ │ ├── headers.mdx │ │ └── request.mdx │ └── cli.mdx ├── build │ ├── static-assets.mdx │ ├── server-config.mdx │ ├── miscellaneous.mdx │ ├── building-a-module.mdx │ ├── module-config.mdx │ └── routing.mdx ├── installation.mdx └── index.mdx ├── .gitignore ├── .github ├── ISSUE_TEMPLATE │ ├── new-community-module.md │ ├── feature_request.md │ └── bug_report.md └── workflows │ ├── publish.yml │ └── testing.yml ├── toolchain ├── jest.config.ts ├── tsconfig.json └── .eslintrc.cjs ├── index.ts ├── LICENSE ├── README.md ├── docs.json └── package.json /tests/endpoints/server/.env: -------------------------------------------------------------------------------- 1 | foo=3 -------------------------------------------------------------------------------- /tests/endpoints/server/public/text.txt: -------------------------------------------------------------------------------- 1 | hello world -------------------------------------------------------------------------------- /tests/endpoints/modules/pass-primary-2/public/text.txt: -------------------------------------------------------------------------------- 1 | hello world -------------------------------------------------------------------------------- /src/compiler/utilities/tooling/dot-env/tests/test1.env: -------------------------------------------------------------------------------- 1 | SINGLE_VARIABLE=value -------------------------------------------------------------------------------- /src/compiler/utilities/tooling/exported-loader/tests/test1.ts: -------------------------------------------------------------------------------- 1 | export const foo = 2; -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /src/compiler/utilities/tooling/exported-variables/tests/test2.ts: -------------------------------------------------------------------------------- 1 | export const foo = 2; -------------------------------------------------------------------------------- /src/compiler/utilities/tooling/exported-loader/tests/test2.ts: -------------------------------------------------------------------------------- 1 | const foo = 2; 2 | export default foo; -------------------------------------------------------------------------------- /src/compiler/utilities/tooling/exported-variables/tests/test1.ts: -------------------------------------------------------------------------------- 1 | const foo = 2 2 | export default foo; -------------------------------------------------------------------------------- /src/compiler/utilities/tooling/exported-loader/tests/test3.ts: -------------------------------------------------------------------------------- 1 | function foo() {} 2 | export default foo(); -------------------------------------------------------------------------------- /docs/assets/logos/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sellersindustry/SherpaJS/HEAD/docs/assets/logos/favicon.png -------------------------------------------------------------------------------- /docs/assets/logos/logo-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sellersindustry/SherpaJS/HEAD/docs/assets/logos/logo-dark.png -------------------------------------------------------------------------------- /docs/assets/logos/logo-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sellersindustry/SherpaJS/HEAD/docs/assets/logos/logo-light.png -------------------------------------------------------------------------------- /src/compiler/utilities/tooling/dot-env/tests/test2.env: -------------------------------------------------------------------------------- 1 | SINGLE_VARIABLE=value2 2 | VARIABLE_ONE=value_1 3 | VARIABLE_TWO=value_2 -------------------------------------------------------------------------------- /docs/assets/logos/logo-large-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sellersindustry/SherpaJS/HEAD/docs/assets/logos/logo-large-dark.png -------------------------------------------------------------------------------- /docs/assets/logos/logo-large-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sellersindustry/SherpaJS/HEAD/docs/assets/logos/logo-large-light.png -------------------------------------------------------------------------------- /tests/endpoints/server/public/image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sellersindustry/SherpaJS/HEAD/tests/endpoints/server/public/image.jpg -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/.vercel/ 2 | **/.sherpa/ 3 | **/.sherpa-dev/ 4 | **/sherpa.TS_VALIDATION_BUFFER.ts 5 | /node_modules 6 | /dist 7 | SHERPA-KV.json -------------------------------------------------------------------------------- /tests/endpoints/server/public/test/archive.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sellersindustry/SherpaJS/HEAD/tests/endpoints/server/public/test/archive.zip -------------------------------------------------------------------------------- /src/internal/index.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | export * from "./handler/index.js"; 4 | export * from "./request/index.js"; 5 | export * from "./response/index.js"; 6 | 7 | -------------------------------------------------------------------------------- /tests/endpoints/modules/pass-primary-2/public/image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sellersindustry/SherpaJS/HEAD/tests/endpoints/modules/pass-primary-2/public/image.jpg -------------------------------------------------------------------------------- /tests/endpoints/modules/pass-primary-1/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pass-primary-1", 3 | "version": "0.0.0", 4 | "type": "module", 5 | "exports": "./sherpa.module.ts" 6 | } -------------------------------------------------------------------------------- /tests/endpoints/modules/pass-primary-1/routes/test/view.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Hello 4 | 5 | 6 |

Hello, world!

7 | 8 | -------------------------------------------------------------------------------- /tests/endpoints/server/routes/module/[m2]/module.ts: -------------------------------------------------------------------------------- 1 | import PrimaryPass2 from "../../../../modules/pass-primary-2/sherpa.module"; 2 | 3 | export default PrimaryPass2.load({ 4 | test: true 5 | }); 6 | 7 | 8 | -------------------------------------------------------------------------------- /tests/endpoints/server/routes/regular/response/html/basic/view.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Hello 4 | 5 | 6 |

Hello, world!

7 | 8 | -------------------------------------------------------------------------------- /tests/endpoints/server/routes/module/m1/module.ts: -------------------------------------------------------------------------------- 1 | import PrimaryPass1 from "../../../../modules/pass-primary-1/sherpa.module"; 2 | 3 | 4 | export default PrimaryPass1.load({ 5 | test: "Hello World" 6 | }) 7 | 8 | -------------------------------------------------------------------------------- /tests/endpoints/server/routes/regular/response/html/standalone/view.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Hello 4 | 5 | 6 |

Hello, world!

7 | 8 | -------------------------------------------------------------------------------- /tests/endpoints/server/src/foo.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "properties": { 4 | "foo": { 5 | "type": "number" 6 | } 7 | }, 8 | "required": ["foo"], 9 | "additionalProperties": false 10 | } -------------------------------------------------------------------------------- /tests/endpoints/modules/pass-primary-1/routes/test/index.ts: -------------------------------------------------------------------------------- 1 | import { Response } from "../../../../../../index.js"; 2 | 3 | 4 | export function POST() { 5 | return Response.text("Hello World", { status: 201 }); 6 | } 7 | 8 | -------------------------------------------------------------------------------- /src/compiler/utilities/tooling/exported-loader/tests/test4.ts: -------------------------------------------------------------------------------- 1 | import { CreateModuleInterface, SherpaJS } from "../../../../../../index"; 2 | 3 | export default SherpaJS.New.module({ 4 | name: "", 5 | interface: CreateModuleInterface 6 | }); 7 | -------------------------------------------------------------------------------- /src/compiler/utilities/tooling/exported-loader/tests/test5.ts: -------------------------------------------------------------------------------- 1 | import { CreateModuleInterface, SherpaJS as Example } from "sherpa-core"; 2 | 3 | export default Example.New.module({ 4 | name: "", 5 | interface: CreateModuleInterface 6 | }); 7 | -------------------------------------------------------------------------------- /src/compiler/utilities/tooling/dot-env/tests/test3.env: -------------------------------------------------------------------------------- 1 | # This is a comment 2 | VARIABLE_ONE=1 3 | 4 | # Another comment 5 | VARIABLE_TWO=string 6 | 7 | # Empty line 8 | # Yet another comment 9 | VARIABLE_THREE=true 10 | VARIABLE_THREE=false 11 | # COMMENTED_OUT=false -------------------------------------------------------------------------------- /src/compiler/utilities/tooling/exported-variables/tests/test3.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | export function POST() { 4 | return "Hello World"; 5 | } 6 | 7 | export function GET() { 8 | return "Hello World"; 9 | } 10 | 11 | export function FOO() { 12 | return "Hello World"; 13 | } 14 | 15 | -------------------------------------------------------------------------------- /tests/endpoints/modules/pass-primary-2/routes/index.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from "../../../../../index.js"; 2 | 3 | 4 | export function GET(request:Request, context:unknown) { 5 | return Response.JSON({ 6 | request, 7 | context 8 | }); 9 | } 10 | 11 | -------------------------------------------------------------------------------- /src/compiler/utilities/tooling/exported-loader/tests/test6.ts: -------------------------------------------------------------------------------- 1 | // @SherpaJS IgnoreInvalidSource 2 | import { CreateModuleInterface, SherpaJS as Example } from "../../../../../../index"; 3 | 4 | 5 | export default Example.New.module({ 6 | name: "", 7 | interface: CreateModuleInterface 8 | }); 9 | -------------------------------------------------------------------------------- /tests/endpoints/modules/pass-primary-1/sherpa.module.ts: -------------------------------------------------------------------------------- 1 | // @SherpaJS IgnoreInvalidSource 2 | import { SherpaJS, CreateModuleInterface } from "../../../../index"; 3 | 4 | export default SherpaJS.New.module({ 5 | name: "pass-primary-1", 6 | interface: CreateModuleInterface<{ test:string }> 7 | }); 8 | -------------------------------------------------------------------------------- /tests/endpoints/modules/pass-primary-2/routes/[id]/path/index.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from "../../../../../../../index.js"; 2 | 3 | export function GET(request:Request, context:unknown) { 4 | return Response.JSON({ 5 | request, 6 | context 7 | }); 8 | } 9 | 10 | 11 | -------------------------------------------------------------------------------- /tests/endpoints/modules/pass-primary-2/sherpa.module.ts: -------------------------------------------------------------------------------- 1 | // @SherpaJS IgnoreInvalidSource 2 | import { SherpaJS, CreateModuleInterface } from "../../../../index"; 3 | 4 | export default SherpaJS.New.module({ 5 | name: "pass-primary-2", 6 | interface: CreateModuleInterface<{ test: boolean }> 7 | }); 8 | -------------------------------------------------------------------------------- /src/compiler/utilities/tooling/exported-loader/tests/test7.ts: -------------------------------------------------------------------------------- 1 | // @SherpaJS IgnoreInvalidSource 2 | import { SherpaJS as Example } from "../../../../../../index"; 3 | 4 | type foo = { bar: string }; 5 | 6 | export default Example.New.server({ 7 | context: { 8 | bar: "foo" 9 | } 10 | }); 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/new-community-module.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: New Community Module 3 | about: Submit a new community module 4 | title: New Community Module - [Module Name] 5 | labels: New Community Module 6 | assignees: SellersEvan 7 | 8 | --- 9 | 10 | Module Name: __ 11 | Module Description: __ 12 | Module GitHub: __ 13 | Notes: ___ 14 | -------------------------------------------------------------------------------- /tests/endpoints/server/sherpa.module.ts: -------------------------------------------------------------------------------- 1 | // @SherpaJS IgnoreInvalidSource 2 | import { CreateModuleInterface } from "../../../src/compiler/models"; 3 | import { SherpaJS } from "../../../index"; 4 | 5 | 6 | export default SherpaJS.New.module({ 7 | name: "pass-primary-2", 8 | interface: CreateModuleInterface<{ test: boolean }> 9 | }); 10 | -------------------------------------------------------------------------------- /tests/endpoints/server/routes/regular/dynamic-paths/[testID]/[testID]/page/[pageID]/index.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from "../../../../../../../../../../index.js"; 2 | 3 | export function GET(request:Request, context:unknown) { 4 | return Response.JSON({ 5 | request: request, 6 | context: context 7 | }); 8 | } 9 | 10 | -------------------------------------------------------------------------------- /tests/endpoints/server/sherpa.server.ts: -------------------------------------------------------------------------------- 1 | // @SherpaJS IgnoreInvalidSource 2 | import { SherpaJS } from "../../../index"; 3 | 4 | 5 | export default SherpaJS.New.server({ 6 | context: { 7 | foo: "bar", 8 | exampleNum: 3, 9 | exampleBool: true, 10 | exampleArray: [1, 2, 3], 11 | deeperNested: { 12 | example: "foo" 13 | } 14 | } 15 | }); 16 | 17 | 18 | -------------------------------------------------------------------------------- /tests/endpoints/server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sherpa-core", 3 | "author": "Sellers Industries", 4 | "version": "0.0.0", 5 | "description": "Module and Reusable Microservice Platform. Build and modularize custom API endpoints, inspired by NextJS APIs. Export to Vercel and ExpressJS.", 6 | "type": "module", 7 | "exports": "./sherpa.module.ts", 8 | "scripts": { 9 | 10 | }, 11 | "devDependencies": { 12 | 13 | }, 14 | "license": "ISC", 15 | "dependencies": { 16 | 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /toolchain/jest.config.ts: -------------------------------------------------------------------------------- 1 | import type { JestConfigWithTsJest } from "ts-jest"; 2 | 3 | const config: JestConfigWithTsJest = { 4 | verbose: true, 5 | roots: ["../"], 6 | transform: { 7 | "^.+\\.ts?$": [ 8 | "ts-jest", 9 | { 10 | useESM: true, 11 | }, 12 | ], 13 | }, 14 | extensionsToTreatAsEsm: [".ts"], 15 | moduleNameMapper: { 16 | "^(\\.{1,2}/.*)\\.js$": "$1", 17 | }, 18 | testPathIgnorePatterns : ["../node_modules/", "../tests/endpoints/"], 19 | }; 20 | 21 | export default config; 22 | -------------------------------------------------------------------------------- /tests/endpoints/server/routes/regular/response/none/basic/index.ts: -------------------------------------------------------------------------------- 1 | import { Response } from "../../../../../../../../index.js"; 2 | 3 | 4 | export function GET() { 5 | return new Response(); 6 | } 7 | 8 | 9 | export function POST() { 10 | return Response.new(); 11 | } 12 | 13 | 14 | export function PUT() { 15 | return Response.new(); 16 | } 17 | 18 | 19 | export function PATCH() { 20 | return Response.new(); 21 | } 22 | 23 | 24 | export function DELETE() { 25 | return Response.new(); 26 | } 27 | 28 | -------------------------------------------------------------------------------- /tests/endpoints/server/routes/ajv/index.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, AJV } from "../../../../../index.js"; 2 | import schemaFoo from "../../src/foo.schema.json"; 3 | const validatorFoo = AJV(schemaFoo); 4 | 5 | 6 | export function POST(request:Request) { 7 | try { 8 | if (!validatorFoo(request.body)) { 9 | return Response.text(JSON.stringify(validatorFoo.errors)); 10 | } 11 | return Response.text("OK"); 12 | } catch (e) { 13 | console.log(e); 14 | return Response.text(e.toString()); 15 | } 16 | } 17 | 18 | -------------------------------------------------------------------------------- /tests/endpoints/server/routes/regular/response/text/basic/index.ts: -------------------------------------------------------------------------------- 1 | import { Response } from "../../../../../../../../index.js"; 2 | 3 | 4 | export function GET() { 5 | return Response.text("Hello World"); 6 | } 7 | 8 | 9 | export function POST() { 10 | return Response.text("Hello World"); 11 | } 12 | 13 | 14 | 15 | export function PUT() { 16 | return Response.text("Hello World"); 17 | } 18 | 19 | 20 | export function PATCH() { 21 | return Response.text("Hello World"); 22 | } 23 | 24 | 25 | export function DELETE() { 26 | return Response.text("Hello World"); 27 | } 28 | 29 | -------------------------------------------------------------------------------- /tests/endpoints/server/routes/regular/response/redirect/basic/index.ts: -------------------------------------------------------------------------------- 1 | import { Response } from "../../../../../../../../index.js"; 2 | 3 | 4 | export function GET() { 5 | return Response.redirect("../success"); 6 | } 7 | 8 | 9 | export function POST() { 10 | return Response.redirect("../success"); 11 | } 12 | 13 | 14 | export function PUT() { 15 | return Response.redirect("../success"); 16 | } 17 | 18 | 19 | export function PATCH() { 20 | return Response.redirect("../success"); 21 | } 22 | 23 | 24 | export function DELETE() { 25 | return Response.redirect("../success"); 26 | } 27 | 28 | -------------------------------------------------------------------------------- /src/native/ajv/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2024 Sellers Industries, Inc. 3 | * distributed under the MIT License 4 | * 5 | * author: Evan Sellers 6 | * date: Wed May 22 2024 7 | * file: index.ts 8 | * project: SherpaJS - Module Microservice Platform 9 | * purpose: AJV Wrapper 10 | * 11 | */ 12 | 13 | 14 | import { ValidateFunction, Schema, JSONSchemaType } from "ajv"; 15 | 16 | 17 | export function AJV(schema:Schema|JSONSchemaType):ValidateFunction { 18 | return schema as ValidateFunction; 19 | } 20 | 21 | 22 | // The grace of the Lord Jesus Christ be with your spirit. Amen. 23 | // - Philippians 4:23 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Repository** 24 | Please link the repository you're working in, if possible. It's very helpful to help us figure out the issue. 25 | 26 | **Additional context** 27 | Add any other context about the problem here. 28 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2024 Sellers Industries, Inc. 3 | * distributed under the MIT License 4 | * 5 | * author: Evan Sellers 6 | * date: Mon Mar 04 2024 7 | * file: index.ts 8 | * project: SherpaJS - Module Microservice Platform 9 | * purpose: Entry 10 | * 11 | */ 12 | 13 | 14 | export * from "./src/native/index.js"; 15 | export type * from "./src/native/index.js"; 16 | 17 | import { New } from "./src/instantiate/index.js"; 18 | const SherpaJS = { 19 | New 20 | }; 21 | 22 | export { SherpaJS }; 23 | 24 | 25 | // For God so loved the world that he gave his one and only Son, that whoever 26 | // believes in him shall not perish but have eternal life. 27 | // - John 3:16 28 | -------------------------------------------------------------------------------- /tests/endpoints/suite/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2024 Sellers Industries, Inc. 3 | * distributed under the MIT License 4 | * 5 | * author: Evan Sellers 6 | * date: Thu May 16 2024 7 | * file: index.ts 8 | * project: SherpaJS - Module Microservice Platform 9 | * purpose: Endpoint Test Suite 10 | * 11 | */ 12 | 13 | 14 | import { Suite } from "./suite.js"; 15 | import { Method, BodyType, Body } from "../../../index.js"; 16 | import { equals, includes } from "./helpers.js"; 17 | 18 | 19 | export type { Body }; 20 | export { Suite, equals, includes, Method, BodyType }; 21 | 22 | 23 | // And he took bread, gave thanks and broke it, and gave it to them, saying, 24 | // "This is my body given for you; do this in remembrance of me." 25 | // - Luke 22:19 26 | -------------------------------------------------------------------------------- /tests/endpoints/server/routes/regular/response/redirect/success/index.ts: -------------------------------------------------------------------------------- 1 | import { Response, Request } from "../../../../../../../../index.js"; 2 | 3 | 4 | export function GET(request:Request, context:unknown) { 5 | return Response.JSON({ 6 | request, 7 | context 8 | }); 9 | } 10 | 11 | 12 | export function PUT(request:Request, context:unknown) { 13 | return Response.JSON({ 14 | request, 15 | context 16 | }); 17 | } 18 | 19 | 20 | export function PATCH(request:Request, context:unknown) { 21 | return Response.JSON({ 22 | request, 23 | context 24 | }); 25 | } 26 | 27 | 28 | export function DELETE(request:Request, context:unknown) { 29 | return Response.JSON({ 30 | request, 31 | context 32 | }); 33 | } 34 | 35 | -------------------------------------------------------------------------------- /toolchain/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2022", 4 | "module": "Node16", 5 | "moduleResolution": "Node16", 6 | "esModuleInterop": true, 7 | "sourceMap": true, 8 | "outDir": "../dist", 9 | "declaration": true 10 | }, 11 | "exclude": [ 12 | "node_modules", 13 | "./dist", 14 | "./sherpa", 15 | "./vercel", 16 | "./*.test.ts" 17 | ], 18 | "files": [ 19 | "../index.ts", 20 | "../src/cli/index.ts", 21 | "../src/compiler/index.ts", 22 | "../src/native/index.ts", 23 | "../src/internal/index.ts", 24 | "../src/instantiate/index.ts", 25 | "../src/server-local/index.ts", 26 | "../tests/endpoints/index.test.ts" 27 | ] 28 | } -------------------------------------------------------------------------------- /src/native/url/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2024 Sellers Industries, Inc. 3 | * distributed under the MIT License 4 | * 5 | * author: Evan Sellers 6 | * date: Tue Mar 19 2024 7 | * file: index.ts 8 | * project: SherpaJS - Module Microservice Platform 9 | * purpose: URL Utilities 10 | * 11 | */ 12 | 13 | 14 | export class OriginURL extends URL { 15 | 16 | constructor(input: string, base?:string|OriginURL|URL) { 17 | super(input, base ? base : "http://0.0"); 18 | this.pathname = this.pathname.endsWith("/") ? this.pathname.slice(0, -1) : this.pathname; 19 | } 20 | 21 | } 22 | 23 | 24 | // Paul said, "John's baptism was a baptism of repentance. He told the people 25 | // to believe in the one coming after him, that is, in Jesus." 26 | // - Acts 19:4 27 | -------------------------------------------------------------------------------- /src/compiler/utilities/logger/model.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2024 Sellers Industries, Inc. 3 | * distributed under the MIT License 4 | * 5 | * author: Evan Sellers 6 | * date: Sun Feb 11 2024 7 | * file: model.ts 8 | * project: SherpaJS - Module Microservice Platform 9 | * purpose: Logger Models 10 | * 11 | */ 12 | 13 | 14 | export enum Level { 15 | ERROR, 16 | WARN, 17 | INFO, 18 | DEBUG 19 | } 20 | 21 | 22 | export type Message = { 23 | text:string; 24 | level?:Level; 25 | content?:string; 26 | file?:MessageFile; 27 | }; 28 | 29 | 30 | export type MessageFile = { 31 | filepath:string; 32 | line?:number; 33 | character?:number; 34 | properties?:string[]; 35 | } 36 | 37 | 38 | // Whoever believes in the Son has eternal life, but whoever rejects the Son 39 | // will not see life, for God’s wrath remains on them. 40 | // - John 3:36 41 | -------------------------------------------------------------------------------- /tests/endpoints/modules/pass-primary-1/routes/index.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from "../../../../../index.js"; 2 | 3 | 4 | export function GET(request:Request, context:unknown) { 5 | return Response.JSON({ 6 | request, 7 | context 8 | }); 9 | } 10 | 11 | 12 | export function POST(request:Request, context:unknown) { 13 | return Response.JSON({ 14 | request, 15 | context 16 | }); 17 | } 18 | 19 | 20 | export function PUT(request:Request, context:unknown) { 21 | return Response.JSON({ 22 | request, 23 | context 24 | }); 25 | } 26 | 27 | 28 | export function PATCH(request:Request, context:unknown) { 29 | return Response.JSON({ 30 | request, 31 | context 32 | }); 33 | } 34 | 35 | 36 | export function DELETE(request:Request, context:unknown) { 37 | return Response.JSON({ 38 | request, 39 | context 40 | }); 41 | } 42 | 43 | -------------------------------------------------------------------------------- /src/native/model.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2024 Sellers Industries, Inc. 3 | * distributed under the MIT License 4 | * 5 | * author: Evan Sellers 6 | * date: Tue Mar 19 2024 7 | * file: model.ts 8 | * project: SherpaJS - Module Microservice Platform 9 | * purpose: IO (Request/Response) Models 10 | * 11 | */ 12 | 13 | 14 | export enum BodyType { 15 | JSON = "JSON", 16 | Text = "Text", 17 | HTML = "HTML", 18 | None = "None" 19 | } 20 | 21 | 22 | export const CONTENT_TYPE:Record = { 23 | [BodyType.JSON]: "application/json", 24 | [BodyType.Text]: "text/plain", 25 | [BodyType.HTML]: "text/html", 26 | [BodyType.None]: undefined 27 | } 28 | 29 | 30 | export type Body = Record|string|undefined; 31 | 32 | 33 | // For you were once darkness, but now you are light in the Lord. Live as 34 | // children of light. 35 | // - Ephesians 5:8 36 | -------------------------------------------------------------------------------- /tests/endpoints/suite/model.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2024 Sellers Industries, Inc. 3 | * distributed under the MIT License 4 | * 5 | * author: Evan Sellers 6 | * date: Thu May 16 2024 7 | * file: model.ts 8 | * project: SherpaJS - Module Microservice Platform 9 | * purpose: Endpoint Test Suite - Models 10 | * 11 | */ 12 | 13 | 14 | import { Method, Body } from "../../../index.js"; 15 | 16 | 17 | export type TestOptions = { 18 | method:Method, 19 | path:string, 20 | body?:Body 21 | } 22 | 23 | 24 | export type BenchOptions = { 25 | host:string; 26 | start?:string; 27 | setup?:string[]; 28 | teardown?:string[]; 29 | } 30 | 31 | 32 | export type TestResults = { 33 | name:string, 34 | success:boolean, 35 | message?:string, 36 | stack?:string 37 | }; 38 | 39 | 40 | // Whoever eats my flesh and drinks my blood remains in me, and I in them. 41 | // - John 6:56 42 | -------------------------------------------------------------------------------- /toolchain/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "ignorePatterns": [ 7 | "dist/", 8 | ".vercel/", 9 | ".sherpa/" 10 | ], 11 | "extends": [ 12 | "eslint:recommended", 13 | "plugin:@typescript-eslint/recommended" 14 | ], 15 | "overrides": [ 16 | { 17 | "env": { 18 | "node": true 19 | }, 20 | "files": [ 21 | ".eslintrc.{js,cjs}" 22 | ], 23 | "parserOptions": { 24 | "sourceType": "script" 25 | } 26 | } 27 | ], 28 | "parser": "@typescript-eslint/parser", 29 | "parserOptions": { 30 | "ecmaVersion": "latest", 31 | "sourceType": "module" 32 | }, 33 | "plugins": [ 34 | "@typescript-eslint" 35 | ], 36 | "rules": { 37 | "prefer-const": "off", 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout code 12 | uses: actions/checkout@v4 13 | - uses: actions/setup-node@v4 14 | with: 15 | node-version: 20 16 | - name: Install dependencies 17 | run: npm install 18 | - name: Run Tests 19 | run: npm test 20 | 21 | publish-npm: 22 | needs: build 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v3 26 | - uses: reedyuk/npm-version@1.1.1 27 | with: 28 | version: ${{github.event.release.tag_name}} 29 | - uses: actions/setup-node@v3 30 | with: 31 | node-version: 16 32 | registry-url: https://registry.npmjs.org/ 33 | - run: npm ci 34 | - run: npm publish 35 | env: 36 | NODE_AUTH_TOKEN: ${{secrets.npm_token}} 37 | -------------------------------------------------------------------------------- /src/compiler/utilities/path/directory-structure/model.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2024 Sellers Industries, Inc. 3 | * distributed under the MIT License 4 | * 5 | * author: Evan Sellers 6 | * date: Tue Mar 19 2024 7 | * file: model.ts 8 | * project: SherpaJS - Module Microservice Platform 9 | * purpose: Directory Structure Models 10 | * 11 | */ 12 | 13 | 14 | export type DirectoryStructure = { 15 | list: DirectoryStructureFile[]; 16 | tree: DirectoryStructureTree; 17 | } 18 | 19 | 20 | export type DirectoryStructureFile = { 21 | filename:string; 22 | filepath:{ 23 | absolute:string; 24 | relative:string; 25 | }; 26 | } 27 | 28 | 29 | export type DirectoryStructureTree = { 30 | files: DirectoryStructureFile[]; 31 | directories: { [key:string]:DirectoryStructureTree }; 32 | } 33 | 34 | 35 | // For everyone born of God overcomes the world. This is the victory that has 36 | // overcome the world, even our faith. 37 | // - 1 John 5:4 38 | -------------------------------------------------------------------------------- /src/native/request/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2024 Sellers Industries, Inc. 3 | * distributed under the MIT License 4 | * 5 | * author: Evan Sellers 6 | * date: Tue Mar 19 2024 7 | * file: index.ts 8 | * project: SherpaJS - Module Microservice Platform 9 | * purpose: Request Interface 10 | * 11 | */ 12 | 13 | 14 | import { Body, BodyType } from "../model.js"; 15 | import { Headers } from "../headers/index.js"; 16 | import { Parameters } from "../parameters/index.js"; 17 | import { Method } from "../../compiler/models.js"; 18 | 19 | 20 | export class IRequest { 21 | 22 | readonly url:string; 23 | readonly params:{ path:Parameters, query:Parameters }; 24 | readonly method:keyof typeof Method; 25 | readonly headers:Headers; 26 | 27 | readonly body:Body; 28 | readonly bodyType:keyof typeof BodyType; 29 | 30 | } 31 | 32 | 33 | // Hearing this, Jesus said to Jairus, "Don’t be afraid; just believe, and she 34 | // will be healed." 35 | // - Luke 8:50 36 | -------------------------------------------------------------------------------- /tests/endpoints/server/routes/regular/response/none/custom/index.ts: -------------------------------------------------------------------------------- 1 | import { Response, Headers as SherpaHeaders } from "../../../../../../../../index.js"; 2 | 3 | 4 | export function GET() { 5 | return new Response({ 6 | status: 401, 7 | headers: { 8 | "X-Foo": "bar" 9 | } 10 | }); 11 | } 12 | 13 | 14 | export function POST() { 15 | return Response.new({ 16 | status: 401, 17 | headers: new Headers({ 18 | "X-Foo": "bar" 19 | }) 20 | }); 21 | } 22 | 23 | 24 | export function PUT() { 25 | return Response.new({ 26 | status: 401, 27 | headers: new SherpaHeaders({ 28 | "X-Foo": "bar" 29 | }) 30 | }); 31 | } 32 | 33 | 34 | export function PATCH() { 35 | return Response.new({ 36 | status: 401, 37 | headers: [["X-Foo", "bar"]] 38 | }); 39 | } 40 | 41 | 42 | export function DELETE() { 43 | return Response.new({ 44 | status: 401, 45 | headers: new SherpaHeaders([["X-Foo", "bar"]]) 46 | }); 47 | } 48 | 49 | -------------------------------------------------------------------------------- /tests/endpoints/suite/helpers.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2024 Sellers Industries, Inc. 3 | * distributed under the MIT License 4 | * 5 | * author: Evan Sellers 6 | * date: Thu May 16 2024 7 | * file: helpers.ts 8 | * project: SherpaJS - Module Microservice Platform 9 | * purpose: Endpoint Test Suite - Verification Helpers 10 | * 11 | */ 12 | 13 | 14 | export class Fail extends Error { 15 | constructor(message:string) { 16 | super(message); 17 | } 18 | } 19 | 20 | 21 | export function equals(expect:unknown, actual:unknown) { 22 | if (expect != actual) { 23 | throw new Fail(`Expected "${expect}" but got "${actual}"`); 24 | } 25 | } 26 | 27 | 28 | export function includes(buffer:string, searchString:string) { 29 | if (!buffer.includes(searchString)) { 30 | throw new Fail(`Expected "${buffer.slice(0, 10)}..." to include "${searchString}"`); 31 | } 32 | } 33 | 34 | 35 | // Therefore we do not lose heart. Though outwardly we are wasting away, yet 36 | // inwardly we are being renewed day by day. 37 | // - 2 Corinthians 4:16 38 | -------------------------------------------------------------------------------- /tests/endpoints/server/routes/regular/index.ts: -------------------------------------------------------------------------------- 1 | import { Response } from "../../../../../index.js"; 2 | 3 | 4 | export function GET(request:Request, context:unknown) { 5 | return Response.JSON({ 6 | request: request, 7 | context: context, 8 | env: process.env 9 | }, { status: 201 }); 10 | } 11 | 12 | 13 | export function POST(request:Request, context:unknown) { 14 | return Response.JSON({ 15 | request: request, 16 | context: context 17 | }, { status: 201 }); 18 | } 19 | 20 | 21 | export function PUT(request:Request, context:unknown) { 22 | return Response.JSON({ 23 | request: request, 24 | context: context 25 | }, { status: 201 }); 26 | } 27 | 28 | 29 | export function PATCH(request:Request, context:unknown) { 30 | return Response.JSON({ 31 | request: request, 32 | context: context 33 | }, { status: 201 }); 34 | } 35 | 36 | 37 | export function DELETE(request:Request, context:unknown) { 38 | return Response.JSON({ 39 | request: request, 40 | context: context 41 | }, { status: 201 }); 42 | } 43 | 44 | -------------------------------------------------------------------------------- /tests/endpoints/server/routes/regular/response/text/custom/index.ts: -------------------------------------------------------------------------------- 1 | import { Response, Headers as SherpaHeaders } from "../../../../../../../../index.js"; 2 | 3 | 4 | export function GET() { 5 | return Response.text("foo-bar", { 6 | status: 401, 7 | headers: { 8 | "X-Foo": "bar" 9 | } 10 | }); 11 | } 12 | 13 | 14 | export function POST() { 15 | return Response.text("foo-bar", { 16 | status: 401, 17 | headers: new Headers({ 18 | "X-Foo": "bar" 19 | }) 20 | }); 21 | } 22 | 23 | 24 | export function PUT() { 25 | return Response.text("foo-bar", { 26 | status: 401, 27 | headers: new SherpaHeaders({ 28 | "X-Foo": "bar" 29 | }) 30 | }); 31 | } 32 | 33 | 34 | export function PATCH() { 35 | return Response.text("foo-bar", { 36 | status: 401, 37 | headers: [["X-Foo", "bar"]] 38 | }); 39 | } 40 | 41 | 42 | export function DELETE() { 43 | return Response.text("foo-bar", { 44 | status: 401, 45 | headers: new SherpaHeaders([["X-Foo", "bar"]]) 46 | }); 47 | } 48 | 49 | -------------------------------------------------------------------------------- /src/instantiate/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2024 Sellers Industries, Inc. 3 | * distributed under the MIT License 4 | * 5 | * author: Evan Sellers 6 | * date: Tue Mar 19 2024 7 | * file: index.ts 8 | * project: SherpaJS - Module Microservice Platform 9 | * purpose: Environment Instantiations 10 | * 11 | */ 12 | 13 | 14 | import { 15 | ModuleInterface, 16 | ModuleConfig, 17 | ModuleLoader, ServerConfig 18 | } from "../compiler/models.js"; 19 | 20 | 21 | export class New { 22 | 23 | 24 | static server(config:ServerConfig):ServerConfig { 25 | return config; 26 | } 27 | 28 | 29 | static module, Schema>(config:ModuleConfig):ModuleLoader { 30 | return { 31 | ...config, 32 | load: (context:Schema) => { 33 | return new config.interface(context); 34 | } 35 | }; 36 | } 37 | 38 | 39 | } 40 | 41 | 42 | // In the same way, faith by itself, if it is not accompanied by action, is dead. 43 | // - James 2:17 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Sellers Industry Solutions 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/compiler/utilities/tooling/exported-variables/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2024 Sellers Industries, Inc. 3 | * distributed under the MIT License 4 | * 5 | * author: Evan Sellers 6 | * date: Mon Apr 29 2024 7 | * file: index.ts 8 | * project: SherpaJS - Module Microservice Platform 9 | * purpose: Exported Loaders 10 | * 11 | */ 12 | 13 | 14 | import fs from "fs"; 15 | import { parse } from "es-module-lexer"; 16 | 17 | 18 | export type ExportedVariable = { 19 | name: string; 20 | } 21 | 22 | 23 | export async function getExportedVariables(filepath:string):Promise { 24 | let [, exports] = await parse(getBuffer(filepath)!); // note: await is required 25 | return exports.map(_export => { 26 | return { 27 | name: _export.n 28 | }; 29 | }); 30 | } 31 | 32 | 33 | function getBuffer(filepath:string):string { 34 | if (!fs.existsSync(filepath)) { 35 | return ""; 36 | } 37 | try { 38 | return fs.readFileSync(filepath, "utf8"); 39 | } catch { 40 | return ""; 41 | } 42 | } 43 | 44 | 45 | // As water reflects the face, so one's life reflects the heart. 46 | // - Proverbs 27:19 47 | -------------------------------------------------------------------------------- /src/native/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2024 Sellers Industries, Inc. 3 | * distributed under the MIT License 4 | * 5 | * author: Evan Sellers 6 | * date: Mon Mar 04 2024 7 | * file: index.ts 8 | * project: SherpaJS - Module Microservice Platform 9 | * purpose: Environment SDK 10 | * 11 | */ 12 | 13 | 14 | import { AJV } from "./ajv/index.js"; 15 | import { Parameters } from "./parameters/index.js"; 16 | import { Headers } from "./headers/index.js"; 17 | import { Body, BodyType } from "./model.js"; 18 | import { IResponse, Options as ResponseOptions } from "./response/index.js"; 19 | import { IRequest } from "./request/index.js"; 20 | import { CreateModuleInterface, Method, ModuleInterface } from "../compiler/models.js"; 21 | 22 | 23 | export { 24 | AJV, 25 | Headers, 26 | Parameters, 27 | Method, 28 | BodyType, 29 | CreateModuleInterface, 30 | ModuleInterface, 31 | IRequest as Request, 32 | IRequest, 33 | IResponse as Response, 34 | IResponse 35 | } 36 | 37 | 38 | export type { 39 | Body, 40 | ResponseOptions 41 | } 42 | 43 | 44 | // Now faith is confidence in what we hope for and assurance about what 45 | // we do not see. 46 | // - Hebrews 11:1 47 | -------------------------------------------------------------------------------- /tests/endpoints/server/routes/regular/response/json/custom/index.ts: -------------------------------------------------------------------------------- 1 | import { Response, Headers as SherpaHeaders } from "../../../../../../../../index.js"; 2 | 3 | 4 | export function GET() { 5 | return Response.JSON({ 6 | "foo": "bar" 7 | }, { 8 | status: 401, 9 | headers: { 10 | "X-Foo": "bar" 11 | } 12 | }); 13 | } 14 | 15 | 16 | export function POST() { 17 | return Response.JSON({ 18 | "foo": "bar" 19 | }, { 20 | status: 401, 21 | headers: new Headers({ 22 | "X-Foo": "bar" 23 | }) 24 | }); 25 | } 26 | 27 | 28 | export function PUT() { 29 | return Response.JSON({ 30 | "foo": "bar" 31 | }, { 32 | status: 401, 33 | headers: new SherpaHeaders({ 34 | "X-Foo": "bar" 35 | }) 36 | }); 37 | } 38 | 39 | 40 | export function PATCH() { 41 | return Response.JSON({ 42 | "foo": "bar" 43 | }, { 44 | status: 401, 45 | headers: [["X-Foo", "bar"]] 46 | }); 47 | } 48 | 49 | 50 | export function DELETE() { 51 | return Response.JSON({ 52 | "foo": "bar" 53 | }, { 54 | status: 401, 55 | headers: new SherpaHeaders([["X-Foo", "bar"]]) 56 | }); 57 | } 58 | 59 | -------------------------------------------------------------------------------- /tests/endpoints/server/routes/regular/response/html/basic/index.ts: -------------------------------------------------------------------------------- 1 | import { Response } from "../../../../../../../../index.js"; 2 | 3 | 4 | export function POST() { 5 | return Response.JSON({ 6 | "string": "food", 7 | "number": 3, 8 | "boolean": true, 9 | "object": { 10 | "numbers": [3, -4] 11 | } 12 | }, { 13 | status: 201 14 | }); 15 | } 16 | 17 | 18 | export function PUT() { 19 | return Response.JSON({ 20 | "string": "food", 21 | "number": 3, 22 | "boolean": true, 23 | "object": { 24 | "numbers": [3, -4] 25 | } 26 | }, { 27 | status: 201 28 | }); 29 | } 30 | 31 | 32 | export function PATCH() { 33 | return Response.JSON({ 34 | "string": "food", 35 | "number": 3, 36 | "boolean": true, 37 | "object": { 38 | "numbers": [3, -4] 39 | } 40 | }, { 41 | status: 201 42 | }); 43 | } 44 | 45 | 46 | export function DELETE() { 47 | return Response.JSON({ 48 | "string": "food", 49 | "number": 3, 50 | "boolean": true, 51 | "object": { 52 | "numbers": [3, -4] 53 | } 54 | }, { 55 | status: 201 56 | }); 57 | } 58 | 59 | -------------------------------------------------------------------------------- /tests/endpoints/server/routes/regular/response/json/basic/index.ts: -------------------------------------------------------------------------------- 1 | import { Response } from "../../../../../../../../index.js"; 2 | 3 | 4 | export function GET() { 5 | return Response.JSON({ 6 | "string": "food", 7 | "number": 3, 8 | "boolean": true, 9 | "object": { 10 | "numbers": [3, -4] 11 | } 12 | }); 13 | } 14 | 15 | 16 | export function POST() { 17 | return Response.JSON({ 18 | "string": "food", 19 | "number": 3, 20 | "boolean": true, 21 | "object": { 22 | "numbers": [3, -4] 23 | } 24 | }); 25 | } 26 | 27 | 28 | export function PUT() { 29 | return Response.JSON({ 30 | "string": "food", 31 | "number": 3, 32 | "boolean": true, 33 | "object": { 34 | "numbers": [3, -4] 35 | } 36 | }); 37 | } 38 | 39 | 40 | export function PATCH() { 41 | return Response.JSON({ 42 | "string": "food", 43 | "number": 3, 44 | "boolean": true, 45 | "object": { 46 | "numbers": [3, -4] 47 | } 48 | }); 49 | } 50 | 51 | 52 | export function DELETE() { 53 | return Response.JSON({ 54 | "string": "food", 55 | "number": 3, 56 | "boolean": true, 57 | "object": { 58 | "numbers": [3, -4] 59 | } 60 | }); 61 | } 62 | 63 | -------------------------------------------------------------------------------- /src/compiler/utilities/tooling/exported-variables/index.test.ts: -------------------------------------------------------------------------------- 1 | import { Path } from "../../path/index"; 2 | import { getExportedVariables } from "./index"; 3 | 4 | 5 | const DIRNAME = Path.getDirectory(import.meta.url); 6 | 7 | 8 | describe("Tooling Exported Variables", () => { 9 | 10 | 11 | test("Non-Existent File", async () => { 12 | let file = Path.join(DIRNAME, "./tests/foo.ts"); 13 | let res = await getExportedVariables(file); 14 | expect(res).toEqual([]); 15 | }); 16 | 17 | 18 | test("Standard Default Export", async () => { 19 | let file = Path.join(DIRNAME, "./tests/test1.ts"); 20 | let res = await getExportedVariables(file); 21 | expect(res).toEqual([{ 22 | name: "default" 23 | }]); 24 | }); 25 | 26 | 27 | test("Standard Single Export", async () => { 28 | let file = Path.join(DIRNAME, "./tests/test2.ts"); 29 | let res = await getExportedVariables(file); 30 | expect(res).toEqual([{ 31 | name: "foo" 32 | }]); 33 | }); 34 | 35 | 36 | test("Standard Multiple Export", async () => { 37 | let file = Path.join(DIRNAME, "./tests/test3.ts"); 38 | let res = await getExportedVariables(file); 39 | expect(res).toEqual([{ 40 | name: "POST" 41 | }, { 42 | name: "GET" 43 | }, { 44 | name: "FOO" 45 | }]); 46 | }); 47 | 48 | 49 | }); 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |

5 | 6 | 7 |

SherpaJS

8 | 9 |

10 | 11 | 12 |

13 | 14 | 15 |

16 | 17 | 18 | --- 19 | 20 | 21 | ## Getting Started 22 | SherpaJS is a modular and agnostic serverless JavaScript web framework, that allows developers to easily build backend serverless web applications. 23 | 24 | * Check out the [documentation](https://docs.page/sellersindustry/SherpaJS) 25 | * Check out the [server template example](https://github.com/sellersindustry/SherpaJS-template-server) 26 | * Check out the [module template example](https://github.com/sellersindustry/SherpaJS-template-module) 27 | 28 | 29 |
30 | 31 | 32 | ## Contributing 33 | Any help is very much appreciated. Build some useful modules and [submit them to our community](https://github.com/sellersindustry/SherpaJS/issues/new/choose) module list. Even help with documentation or refactoring code is helpful. 34 | 35 | 36 |
37 | 38 | 39 | ### Credits 40 | - [Evan Sellers](https://github.com/SellersEvan) 41 | - Illustration by Icons 8 from Ouch! 42 | -------------------------------------------------------------------------------- /docs.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "SherpaJS", 3 | "description": "Module and Reusable Microservice Platform. Build and modularize custom API endpoints, inspired by NextJS APIs. Export to Vercel and ExpressJS.", 4 | "headerDepth": 5, 5 | "logo": "./assets/logos/favicon.png", 6 | "logoDark": "./assets/logos/favicon.png", 7 | "theme": "#0D95DC", 8 | "docsearch": { 9 | "appId": "EFDCDI47Q0", 10 | "apiKey": "eca2166b2d5a712b1371a892757a8299", 11 | "indexName": "page" 12 | }, 13 | "sidebar": [[ 14 | "Getting Started", 15 | [ 16 | ["Overview", "/"], 17 | ["Installation", "/installation"], 18 | ["Project Structure", "/structure"] 19 | ] 20 | ], [ 21 | "Building Your Application", 22 | [ 23 | ["Routing", "/build/routing"], 24 | ["Endpoints", "/build/endpoints"], 25 | ["Static Assets", "/build/static-assets"], 26 | ["Server Config", "/build/server-config"], 27 | ["Module Config", "/build/module-config"], 28 | ["Building a Module", "/build/building-a-module"], 29 | ["Miscellaneous", "/build/miscellaneous"] 30 | ] 31 | ], [ 32 | "API Reference", 33 | [ 34 | ["Response", "/api/components/response"], 35 | ["Request", "/api/components/request"], 36 | ["Headers", "/api/components/headers"], 37 | ["Parameters", "/api/components/parameters"], 38 | ["SherpaJS CLI", "/api/cli"] 39 | ] 40 | ]] 41 | } -------------------------------------------------------------------------------- /src/compiler/bundler/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2024 Sellers Industries, Inc. 3 | * distributed under the MIT License 4 | * 5 | * author: Evan Sellers 6 | * date: Sun Feb 11 2024 7 | * file: index.ts 8 | * project: SherpaJS - Module Microservice Platform 9 | * purpose: Bundler Selector 10 | * 11 | */ 12 | 13 | import { BuildOptions, BundlerType, Structure } from "../models.js"; 14 | import { Local } from "./platforms/local/index.js"; 15 | import { Vercel } from "./platforms/vercel/index.js"; 16 | import { Logger } from "../utilities/logger/index.js"; 17 | import { Message } from "../utilities/logger/model.js"; 18 | import { Bundler } from "./platforms/abstract.js"; 19 | 20 | 21 | export function NewBundler(structure:Structure, options:BuildOptions, errors?:Message[]):Bundler { 22 | if (options.bundler === BundlerType.Vercel) { 23 | return new Vercel(structure, options, errors); 24 | } else if (options.bundler === BundlerType.local) { 25 | return new Local(structure, options, errors); 26 | } else { 27 | Logger.raise({ text: `Invalid bundler "${options.bundler}"` }); 28 | return undefined as unknown as Bundler; 29 | } 30 | } 31 | 32 | 33 | export function clean(filepath:string) { 34 | let structure = { assets: {}, endpoints: {}, server: {} } as Structure; 35 | let options = { bundler: BundlerType.local, input: filepath, output: filepath }; 36 | new Vercel(structure, options).clean(); 37 | new Local(structure, options).clean(); 38 | } 39 | 40 | 41 | // A cheerful heart is good medicine, but a crushed spirit dries up the bones. 42 | // - Proverbs 17:22 43 | -------------------------------------------------------------------------------- /src/native/request/utilities.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2024 Sellers Industries, Inc. 3 | * distributed under the MIT License 4 | * 5 | * author: Evan Sellers 6 | * date: Wed Mar 20 2024 7 | * file: utilities.ts 8 | * project: SherpaJS - Module Microservice Platform 9 | * purpose: Request Utilities 10 | * 11 | */ 12 | 13 | 14 | import { Segment } from "../../compiler/models.js"; 15 | 16 | 17 | export class RequestUtilities { 18 | 19 | 20 | static isDynamicURL(segments:Segment[]):boolean { 21 | return segments.some((segment) => { 22 | return segment.isDynamic; 23 | }); 24 | } 25 | 26 | 27 | static getDynamicURL(segments:Segment[]):string { 28 | return segments.map((segment) => { 29 | return segment.isDynamic ? `[${segment.name}]` : segment.name; 30 | }).join("/"); 31 | } 32 | 33 | 34 | static compareURL(segmentsA:Segment[], segmentsB:Segment[]):number { 35 | if (segmentsA.length == 0 || segmentsB.length == 0) { 36 | return segmentsA.length - segmentsB.length; 37 | } 38 | if (segmentsA[0].isDynamic && !segmentsB[0].isDynamic) { 39 | return 1; 40 | } 41 | if (!segmentsA[0].isDynamic && segmentsB[0].isDynamic) { 42 | return -1; 43 | } 44 | if (segmentsA[0]["name"] != segmentsB[0]["name"]) { 45 | return segmentsA[0]["name"].localeCompare(segmentsB[0]["name"]); 46 | } 47 | return this.compareURL(segmentsA.slice(1), segmentsB.slice(1)); 48 | } 49 | 50 | 51 | } 52 | 53 | 54 | // Produce fruit in keeping with repentance. 55 | // - Matthew 3:8 56 | -------------------------------------------------------------------------------- /src/internal/handler/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2024 Sellers Industries, Inc. 3 | * distributed under the MIT License 4 | * 5 | * author: Evan Sellers 6 | * date: Sun Feb 11 2024 7 | * file: index.ts 8 | * project: SherpaJS - Module Microservice Platform 9 | * purpose: Request Handler 10 | * 11 | */ 12 | 13 | 14 | import { Context, Method } from "../../compiler/models.js"; 15 | import { Response, IResponse, IRequest } from "../../native/index.js"; 16 | 17 | 18 | type callback = (request:IRequest, context:Context) => Promise|IResponse|undefined; 19 | type endpoints = { 20 | [key in keyof typeof Method]: undefined|callback; 21 | }; 22 | 23 | 24 | export async function Handler(endpoints:endpoints, view:string|undefined, context:Context, request:IRequest):Promise { 25 | if (view && request.method == Method.GET) { 26 | try { 27 | return Response.HTML(decodeURIComponent(view)); 28 | } catch (error) { 29 | return Response.text(error.message, { status: 500 }); 30 | } 31 | } else if (endpoints[request.method]) { 32 | try { 33 | let response = await (endpoints[request.method] as callback)(request, context); 34 | if (!response) { 35 | return Response.new({ status: 200 }); 36 | } 37 | return response; 38 | } catch (error) { 39 | return Response.text(error.message, { status: 500 }); 40 | } 41 | } else { 42 | return Response.new({ status: 405 }); 43 | } 44 | } 45 | 46 | 47 | // Those who accepted his message were baptized, and about three thousand were 48 | // added to their number that day. 49 | // - Acts 2:41 50 | -------------------------------------------------------------------------------- /.github/workflows/testing.yml: -------------------------------------------------------------------------------- 1 | name: Testing 2 | 3 | on: 4 | pull_request: 5 | branches: ["*"] 6 | 7 | jobs: 8 | 9 | 10 | unit-test: 11 | strategy: 12 | matrix: 13 | os: [ubuntu-latest, windows-latest, macos-latest] 14 | node-version: [20.x, 22.x] 15 | 16 | name: Unit Tests (${{ matrix.os }}, NodeJS ${{ matrix.node-version }}) 17 | runs-on: ${{ matrix.os }} 18 | 19 | steps: 20 | - name: Checkout code 21 | uses: actions/checkout@v4 22 | 23 | - name: Install Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v4 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | 28 | - name: Install dependencies 29 | run: npm install 30 | 31 | - name: Run Unit Tests 32 | run: npm run test-unit 33 | 34 | 35 | endpoint-test: 36 | strategy: 37 | matrix: 38 | os: [ubuntu-latest, windows-latest, macos-latest] 39 | 40 | name: Endpoint Tests (${{ matrix.os }}) 41 | runs-on: ${{ matrix.os }} 42 | 43 | steps: 44 | - name: Checkout code 45 | uses: actions/checkout@v4 46 | 47 | - uses: actions/setup-node@v4 48 | with: 49 | node-version: 20 50 | 51 | - name: Install dependencies 52 | run: npm install 53 | 54 | - name: Run Endpoint Tests 55 | run: npm run test-endpoints 56 | 57 | 58 | linting: 59 | name: Linting 60 | runs-on: ubuntu-latest 61 | 62 | steps: 63 | - name: Checkout code 64 | uses: actions/checkout@v4 65 | 66 | - uses: actions/setup-node@v4 67 | with: 68 | node-version: 20 69 | 70 | - name: Install dependencies 71 | run: npm install 72 | 73 | - name: Run linting 74 | run: npm run lint 75 | -------------------------------------------------------------------------------- /docs/structure.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Project Structure 3 | description: An overview of the SherpaJS project structure 4 | --- 5 | 6 | # Project Structure 7 | An overview of the SherpaJS project structure. 8 | 9 | 10 |
11 | 12 | 13 | ## Top-level Directories 14 | Top-level directories are used to organize endpoints and static files. 15 | | | | 16 | |---|---| 17 | | [`routes`](/build/routes) | Endpoint routes | 18 | | [`public`](/build/static-assets) | Static assets, *optional only if you want to server public assets* | 19 | 20 | 21 |
22 | 23 | 24 | ## Top-level Files 25 | Top-level files are used to configure SherpaJS and manage the environment. 26 | | | | 27 | |---|---| 28 | | [`sherpa.server.ts`](/build/server-config) | SherpaJS server config | 29 | | [`sherpa.module.ts`](/build/module-config) | SherpaJS module config, *optional only if creating a module* | 30 | | [`.env`](/build/miscellaneous#environment-variables) | Environment Varibles File | 31 | 32 | 33 | 34 |
35 | 36 | 37 | ## Routing Conventions 38 | The naming conventions for inside the `routes` directory, allows you to define 39 | routes usings segments and different types of endpoints. View and functions 40 | endpoints may persist together, but no other files should be present in your 41 | `routes` directory. 42 | 43 | | | | | 44 | |---|---|---| 45 | | [`index`](/build/endpoints#function-endpoints) | `.ts` `.js` | Functions endpoint | 46 | | [`module`](/build/endpoints#module-endpoints) | `.ts` `.js` | Module endpoint | 47 | | [`view`](/build/endpoints#view-endpoints) | `.html` | View endpoint | 48 | | [`/folder_name`](/build/routes#regular-route) | | Regular route segment, *any folder_name* | 49 | | [`/[param_name]`](/build/routes#dynamic-route) | | Dynamic route segment, *any param_name* | 50 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sherpa-core", 3 | "author": "Sellers Industries", 4 | "version": "0.0.0", 5 | "description": "Module and Reusable Microservice Platform. Build and modularize custom API endpoints, inspired by NextJS APIs. Export to Vercel and ExpressJS.", 6 | "exports": { 7 | "./package.json": "./package.json", 8 | ".": { 9 | "import": "./dist/index.js", 10 | "types": "./dist/index.d.js" 11 | }, 12 | "./internal": { 13 | "import": "./dist/src/internal/index.js", 14 | "types": "./dist/src/internal/index.d.js" 15 | }, 16 | "./server-local": { 17 | "import": "./dist/src/server-local/index.js", 18 | "types": "./dist/src/server-local/index.d.js" 19 | } 20 | }, 21 | "type": "module", 22 | "bin": { 23 | "sherpa": "./dist/src/cli/index.js" 24 | }, 25 | "engines": { 26 | "node": ">=20" 27 | }, 28 | "scripts": { 29 | "prepare": "npm run build", 30 | "build": "tsc --build --force ./toolchain/tsconfig.json", 31 | "lint": "eslint . --ext .ts -c ./toolchain/.eslintrc.cjs", 32 | "test": "npm run test-unit && npm run test-endpoints", 33 | "test-unit": "node --experimental-vm-modules node_modules/jest/bin/jest.js -c ./toolchain/jest.config.ts", 34 | "test-endpoints": "npm run build && node ./dist/tests/endpoints/index.test.js" 35 | }, 36 | "devDependencies": { 37 | "@types/checksum": "^0.1.35", 38 | "@types/eslint": "^8.56.2", 39 | "@types/jest": "^29.5.12", 40 | "@typescript-eslint/eslint-plugin": "^6.20.0", 41 | "@typescript-eslint/parser": "^6.20.0", 42 | "eslint": "^8.56.0", 43 | "jest": "^29.7.0", 44 | "ts-jest": "^29.1.2" 45 | }, 46 | "license": "ISC", 47 | "dependencies": { 48 | "@offen/esbuild-plugin-jsonschema": "^1.1.0", 49 | "@types/node": "^20.12.4", 50 | "ajv": "^8.13.0", 51 | "checksum": "^1.0.0", 52 | "chokidar": "^3.6.0", 53 | "colorette": "^2.0.20", 54 | "commander": "^11.1.0", 55 | "es-module-lexer": "^1.5.0", 56 | "esbuild": "^0.19.10", 57 | "parse-imports": "^1.1.2", 58 | "stacktracey": "^2.1.8", 59 | "typescript": "^5.3.3" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/cli/utilities.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2024 Sellers Industries, Inc. 3 | * distributed under the MIT License 4 | * 5 | * author: Evan Sellers 6 | * date: Thu Mar 28 2024 7 | * file: utilities.ts 8 | * project: SherpaJS - Module Microservice Platform 9 | * purpose: CLI Utilities 10 | * 11 | */ 12 | 13 | 14 | import fs from "fs"; 15 | import path from "path"; 16 | import { Path } from "../compiler/utilities/path/index.js"; 17 | import { Level, Message } from "../compiler/utilities/logger/model.js"; 18 | 19 | 20 | export function getEnvironmentFiles(input:string):string[] { 21 | let envFilepath = Path.join(input, ".env"); 22 | if (fs.existsSync(envFilepath)) { 23 | return [envFilepath]; 24 | } 25 | return []; 26 | } 27 | 28 | 29 | export function getAbsolutePath(filepath:string|undefined, fallback:string):string { 30 | if (!filepath) { 31 | filepath = fallback; 32 | } 33 | return Path.unix(path.resolve(filepath)); 34 | } 35 | 36 | 37 | export function getKeyValuePairs(values:string[]|undefined):{ logs:Message[], values:Record } { 38 | if (!values) { 39 | return { logs: [], values: {} }; 40 | } 41 | 42 | let logs:Message[] = []; 43 | let result:Record = {}; 44 | for (let entry of values) { 45 | let [key, value] = entry.split("="); 46 | if (!key || !value) { 47 | logs.push({ 48 | level: Level.ERROR, 49 | text: `Invalid key/value pair: "${entry}"`, 50 | content: `The key and value must be separated by an equal sign. Ex. "key=value".` 51 | }); 52 | continue; 53 | } 54 | result[key] = value; 55 | } 56 | return { logs: logs, values: result }; 57 | } 58 | 59 | 60 | export function getVersion():string { 61 | try { 62 | let filepath = Path.join(Path.getRootDirectory(), "package.json"); 63 | return JSON.parse(fs.readFileSync(filepath, "utf8")).version; 64 | } catch (error) { 65 | return "n/a"; 66 | } 67 | } 68 | 69 | 70 | // If anyone acknowledges that Jesus is the Son of God, God lives in them and 71 | // they in God. 72 | // - 1 John 4:15 73 | -------------------------------------------------------------------------------- /src/compiler/structure/endpoint/load-module.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2024 Sellers Industries, Inc. 3 | * distributed under the MIT License 4 | * 5 | * author: Evan Sellers 6 | * date: Fri May 10 2024 7 | * file: load-module.ts 8 | * project: SherpaJS - Module Microservice Platform 9 | * purpose: Get Endpoint Module 10 | * 11 | */ 12 | 13 | 14 | import { EndpointTree, ModuleInterface, Segment } from "../../models.js"; 15 | import { Logger } from "../../utilities/logger/index.js"; 16 | import { Level, Message } from "../../utilities/logger/model.js"; 17 | import { Path } from "../../utilities/path/index.js"; 18 | import { Tooling } from "../../utilities/tooling/index.js"; 19 | import { getComponents } from "../index.js"; 20 | 21 | 22 | export async function getEndpointModule(functionsFilepath:string, segments:Segment[]):Promise<{ logs:Message[], endpoints?:EndpointTree }> { 23 | let logs:Message[] = []; 24 | 25 | let { module, logs: logsModule } = await Tooling.getExportedLoader(functionsFilepath, "Module Loader", ".load"); 26 | logs.push(...logsModule); 27 | if (!module) return { logs }; 28 | 29 | let moduleLoader:ModuleInterface; 30 | try { 31 | moduleLoader = await Tooling.getDefaultExport(functionsFilepath) as ModuleInterface; 32 | } catch (e) { 33 | return { 34 | logs: [{ 35 | level: Level.ERROR, 36 | text: "Module Loader failed to parse.", 37 | content: e.message, 38 | file: { filepath: functionsFilepath } 39 | }] 40 | }; 41 | } 42 | 43 | let entry = Path.resolve(module.filepath, Path.getDirectory(functionsFilepath)) as string; 44 | let components = await getComponents(entry, moduleLoader.context, functionsFilepath, segments, false); 45 | let typeErrors = await Tooling.typeValidation(functionsFilepath, "Module Loader"); 46 | logs.push(...components.logs, ...typeErrors); 47 | if (Logger.hasError(logs)) { 48 | return { logs }; 49 | } 50 | return { ...components, logs }; 51 | } 52 | 53 | 54 | // After the Lord Jesus had spoken to them, he was taken up into heaven and 55 | // he sat at the right hand of God. 56 | // - Mark 16:19 57 | -------------------------------------------------------------------------------- /src/native/response/status-text.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2024 Sellers Industries, Inc. 3 | * distributed under the MIT License 4 | * 5 | * author: Evan Sellers 6 | * date: Tue Mar 19 2024 7 | * file: status-text.ts 8 | * project: SherpaJS - Module Microservice Platform 9 | * purpose: Response Status Text 10 | * 11 | */ 12 | 13 | 14 | export const STATUS_TEXT:Record = { 15 | 100: "Continue", 16 | 101: "Switching Protocols", 17 | 102: "Processing", 18 | 103: "Early Hints", 19 | 200: "OK", 20 | 201: "Created", 21 | 202: "Accepted", 22 | 203: "Non-Authoritative Information", 23 | 204: "No Content", 24 | 205: "Reset Content", 25 | 206: "Partial Content", 26 | 207: "Multi-Status", 27 | 208: "Already Reported", 28 | 226: "IM Used", 29 | 300: "Multiple Choices", 30 | 301: "Moved Permanently", 31 | 302: "Found", 32 | 303: "See Other", 33 | 304: "Not Modified", 34 | 307: "Temporary Redirect", 35 | 308: "Permanent Redirect", 36 | 400: "Bad Request", 37 | 401: "Unauthorized", 38 | 402: "Payment Required", 39 | 403: "Forbidden", 40 | 404: "Not Found", 41 | 405: "Method Not Allowed", 42 | 406: "Not Acceptable", 43 | 407: "Proxy Authentication Required", 44 | 408: "Request Timeout", 45 | 409: "Conflict", 46 | 410: "Gone", 47 | 411: "Length Required", 48 | 412: "Precondition Failed", 49 | 413: "Payload Too Large", 50 | 414: "URI Too Long", 51 | 415: "Unsupported Media Type", 52 | 416: "Range Not Satisfiable", 53 | 417: "Expectation Failed", 54 | 418: "I'm a Teapot 🫖", 55 | 421: "Misdirected Request", 56 | 422: "Unprocessable Entity", 57 | 423: "Locked", 58 | 424: "Failed Dependency", 59 | 425: "Too Early", 60 | 426: "Upgrade Required", 61 | 428: "Precondition Required", 62 | 429: "Too Many Requests", 63 | 431: "Request Header Fields Too Large", 64 | 451: "Unavailable For Legal Reasons", 65 | 500: "Internal Server Error", 66 | 501: "Not Implemented", 67 | 502: "Bad Gateway", 68 | 503: "Service Unavailable", 69 | 504: "Gateway Timeout", 70 | 505: "HTTP Version Not Supported", 71 | 506: "Variant Also Negotiates", 72 | 507: "Insufficient Storage", 73 | 508: "Loop Detected", 74 | 510: "Not Extended", 75 | 511: "Network Authentication Required", 76 | }; 77 | 78 | 79 | // As Scripture says, "Anyone who believes in him will never be put to shame." 80 | // - Romans 10:11 81 | -------------------------------------------------------------------------------- /src/compiler/utilities/tooling/dot-env/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2024 Sellers Industries, Inc. 3 | * distributed under the MIT License 4 | * 5 | * author: Evan Sellers 6 | * date: Thu Mar 28 2024 7 | * file: dot-env.ts 8 | * project: SherpaJS - Module Microservice Platform 9 | * purpose: Environment Variables 10 | * 11 | */ 12 | 13 | 14 | import fs from "fs"; 15 | import { BuildOptions, EnvironmentVariables } from "../../../models.js"; 16 | 17 | 18 | export function getEnvironmentVariables(options:BuildOptions|undefined):EnvironmentVariables { 19 | if (!options) { 20 | return {}; 21 | } 22 | return parseAll({ 23 | ...process.env as EnvironmentVariables, 24 | ...getFiles(options).map(filepath => parseFile(filepath)).reduce((a, b) => { return { ...a, ...b } }, {}), 25 | ...options.developer?.environment?.variables, 26 | "SHERPA_PLATFORM": options.bundler.toString() 27 | }); 28 | } 29 | 30 | 31 | function parseFile(filepath:string):EnvironmentVariables { 32 | if (!fs.existsSync(filepath)) { 33 | return {}; 34 | } 35 | return Object.fromEntries(fs.readFileSync(filepath, "utf8").split("\n").map(entry => { 36 | return entry.trim(); 37 | }).filter(entry => { 38 | return !entry.startsWith("#") && entry != "" && entry.includes("="); 39 | }).map(entry => { 40 | let sections = entry.split("="); 41 | return [sections[0].trim(), sections[1].trim()]; 42 | })); 43 | } 44 | 45 | 46 | function getFiles(options:BuildOptions):string[] { 47 | if (!options?.developer?.environment?.files) { 48 | return []; 49 | } 50 | return options.developer.environment.files.filter((filepath) => { 51 | return fs.existsSync(filepath); 52 | }); 53 | } 54 | 55 | 56 | function parseAll(values:Record):Record { 57 | return Object.fromEntries(Object.entries(values).map(([key, value]) => { 58 | return [key, parse(value)]; 59 | })); 60 | } 61 | 62 | 63 | function parse(value:string|number|boolean):string|number|boolean { 64 | if (typeof value === "number" || typeof value === "boolean") { 65 | return value; 66 | } else if (value == "true") { 67 | return true; 68 | } else if (value == "false") { 69 | return false; 70 | } else if (/^\d+$/.test(value)) { 71 | return parseInt(value); 72 | } 73 | return value; 74 | } 75 | 76 | 77 | // For you were once darkness, but now you are light in the Lord. Live as 78 | // children of light. 79 | // - Ephesians 5:8 80 | -------------------------------------------------------------------------------- /src/compiler/utilities/path/directory-structure/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2024 Sellers Industries, Inc. 3 | * distributed under the MIT License 4 | * 5 | * author: Evan Sellers 6 | * date: Tue Mar 19 2024 7 | * file: index.ts 8 | * project: SherpaJS - Module Microservice Platform 9 | * purpose: Directory Structure 10 | * 11 | */ 12 | 13 | 14 | import fs from "fs"; 15 | import { Path } from "../index.js"; 16 | import { 17 | DirectoryStructure as DirStruct, 18 | DirectoryStructureFile as File, 19 | DirectoryStructureTree as Tree 20 | } from "./model.js"; 21 | 22 | 23 | export function getDirectoryStructure(directory:string):DirStruct { 24 | let tree = getDirectoryStructureTree(directory, ""); 25 | return { 26 | tree: tree, 27 | list: flattenTreeStructure(tree) 28 | } 29 | } 30 | 31 | 32 | function getDirectoryStructureTree(dirAbsolute:string, dirRelative:string):Tree { 33 | return { 34 | files: getFiles(dirAbsolute, dirRelative), 35 | directories: getDirectories(dirAbsolute).reduce((directories, dirname) => { 36 | return { 37 | ...directories, 38 | [dirname]: getDirectoryStructureTree( 39 | Path.join(dirAbsolute, dirname), 40 | Path.join(dirRelative, dirname) 41 | ) 42 | }; 43 | }, {}) 44 | } 45 | } 46 | 47 | 48 | function getFiles(dirAbsolute:string, dirRelative:string):File[] { 49 | return fs.readdirSync(dirAbsolute).filter((filename:string) => { 50 | return fs.statSync(Path.join(dirAbsolute, filename)).isFile(); 51 | }).map((filename) => { 52 | return { 53 | filename: filename, 54 | filepath: { 55 | absolute: Path.join(dirAbsolute, filename), 56 | relative: Path.join(dirRelative, filename) 57 | } 58 | } 59 | }); 60 | } 61 | 62 | 63 | function getDirectories(dirAbsolute:string):string[] { 64 | return fs.readdirSync(dirAbsolute).filter((dirname:string) => { 65 | return fs.statSync(Path.join(dirAbsolute, dirname)).isDirectory(); 66 | }); 67 | } 68 | 69 | 70 | function flattenTreeStructure(structure:Tree):File[] { 71 | return [ 72 | ...structure.files, 73 | ...Object.keys(structure.directories).map((dirName) => { 74 | return flattenTreeStructure(structure.directories[dirName]) 75 | }).flat() 76 | ] 77 | } 78 | 79 | 80 | // For it is with your heart that you believe and are justified, and it is with 81 | // your mouth that you profess your faith and are saved. 82 | // - Romans 10:10 83 | -------------------------------------------------------------------------------- /src/compiler/utilities/logger/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2024 Sellers Industries, Inc. 3 | * distributed under the MIT License 4 | * 5 | * author: Evan Sellers 6 | * date: Sun Feb 11 2024 7 | * file: index.ts 8 | * project: SherpaJS - Module Microservice Platform 9 | * purpose: Logger 10 | * 11 | */ 12 | 13 | 14 | import { cyan, dim, magenta, red, yellow } from "colorette" 15 | import { Message, Level, MessageFile } from "./model.js"; 16 | 17 | 18 | export class Logger { 19 | 20 | 21 | public static display(message:Message[]|Message) { 22 | if (Array.isArray(message)) { 23 | message.forEach((m) => this.display(m)); 24 | } else { 25 | console.log(`${this.levelToString(message.level)} ${message.text}`); 26 | if (message.content) { 27 | console.log(` ${message.content}`); 28 | } 29 | if (message.file) { 30 | console.log(` ${this.fileToString(message.file)}`); 31 | } 32 | } 33 | } 34 | 35 | 36 | public static error(message:Message):Message { 37 | return { ...message, level: Level.ERROR }; 38 | } 39 | 40 | 41 | public static raise(message:Message) { 42 | this.display({ ...message, level: Level.ERROR }); 43 | this.exit(); 44 | } 45 | 46 | 47 | public static exit() { 48 | console.log("EXITED"); 49 | process.exit(1); 50 | } 51 | 52 | 53 | public static hasError(messages:Message[]):boolean { 54 | return messages.some(message => message.level == Level.ERROR); 55 | } 56 | 57 | 58 | private static levelToString(level:Level=Level.INFO):string { 59 | if (level == Level.ERROR) { 60 | return red(`[ERROR]`); 61 | } else if (level == Level.WARN) { 62 | return yellow(`[WARNING]`); 63 | } else if (level == Level.DEBUG) { 64 | return magenta(`[DEBUG]`); 65 | } else { 66 | return cyan(`[INFO] `); 67 | } 68 | } 69 | 70 | 71 | private static fileToString(file?:MessageFile):string { 72 | if (!file) return ""; 73 | let properties = file.properties ? file.properties.join(".") + " " : ""; 74 | let path = file.filepath + (file.line ? ":" + file.line : (file.character) ? ":" + file.character : ""); 75 | return dim(`at ${properties}(${path})`); 76 | } 77 | 78 | 79 | } 80 | 81 | 82 | export type { Message as Log }; 83 | export { Level as LogLevel }; 84 | 85 | 86 | // Who is it that overcomes the world? Only the one who believes that Jesus is 87 | // the Son of God. 88 | // - 1 John 5:5 89 | -------------------------------------------------------------------------------- /docs/api/components/parameters.mdx: -------------------------------------------------------------------------------- 1 | # Parameters 2 | The `Parameters` class represents a collection of key-value pairs used for 3 | representing URL parameters. It provides methods for working with URL 4 | parameters. Paramaters are automatically parsed to there expected types, this 5 | includes numbers and booleans. This class is very similar to the standard 6 | [URLSearchParams class](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams) 7 | used in web APIs. 8 | 9 | When parameters come from the [`Request`](/api/components/request) class, from a 10 | URL they are automatically parsed (booleans and numbers) and seperated by commas. 11 | See [request examples](/api/components/request#basic-get-request), for more 12 | information. 13 | 14 | 15 |
16 | 17 | 18 | ## Constructor 19 | 20 | 21 | ### constructor(init) 22 | `constructor()` 23 | Creates a new instance of the `Parameters` class. 24 | 25 | 26 |
27 | 28 | 29 | ## Methods 30 | 31 | 32 | ### has(key) 33 | `has(key: string): boolean` \ 34 | Checks if a parameter with the specified key exists. 35 | * `key` - The key of the parameter to check. 36 | 37 | 38 |
39 | 40 | 41 | ### get(key) 42 | `get(key: string): parameterValue | undefined` \ 43 | Returns the first value of the parameter with the specified key. 44 | * `key` (string): The key of the parameter. 45 | 46 | 47 |
48 | 49 | 50 | ### getAll() 51 | `getAll(key: string): parameterValue[] | undefined` \ 52 | Returns all values of the parameter with the specified key. 53 | * `key` (string): The key of the parameter. 54 | 55 | 56 | 57 |
58 | 59 | 60 | ### keys() 61 | `keys(): string[]` \ 62 | Returns an array of all the keys present in the parameters. 63 | 64 | 65 |
66 | 67 | 68 | ### toJSON() 69 | `toJSON(): Record` \ 70 | Returns a JSON representation of the parameters. 71 | 72 | 73 |
74 | 75 | 76 | ## Examples 77 | The following examples, illustrate how to construct a `Paramaters` class, append 78 | and retreive values from the object. 79 | 80 | ```typescript 81 | import { Paramaters } from "sherpa-core"; 82 | const _params = new Paramaters(); 83 | 84 | _params.append("topics", "AI"); 85 | _params.get("topics"); // returns "AI" 86 | _params.getAll("topics"); // returns ["AI"] 87 | 88 | _params.append("topics", "code"); 89 | _params.get("topics"); // returns "AI" 90 | _params.getAll("topics"); // returns ["AI", "code"] 91 | 92 | _params.append("page", "1"); 93 | _params.get("page"); // returns 1 94 | _params.getAll("page"); // returns [1] 95 | 96 | _params.append("isCool", "true"); 97 | _params.get("isCool"); // returns true 98 | _params.getAll("isCool"); // returns [true] 99 | ``` -------------------------------------------------------------------------------- /docs/build/static-assets.mdx: -------------------------------------------------------------------------------- 1 | # Static Assets 2 | SherpaJS servers can serve static assets such as images, CSS, JavaScript 3 | files, and other resources that do not change frequently. These assets are 4 | placed in a dedicated folder within your project and are served directly to 5 | clients when requested. 6 | 7 | This static asset directory is optional. Static assets are not transferred from 8 | modules. 9 | 10 | 11 |
12 | 13 | 14 | ## Defining Static Assets Directory 15 | The default directory for static assets in a SherpaJS project is the `public` 16 | folder. Any files placed in this folder are accessible via a URL path that 17 | mirrors the directory structure within `public`. 18 | 19 | 20 |
21 | 22 | 23 | ## Serving Static Assets 24 | To serve static assets, simply place the files within the `public` 25 | directory of your project. For example: 26 | 27 | ```plaintext 28 | /your-project 29 | │ 30 | ├── /public 31 | │ ├── /images 32 | │ │ └── logo.png 33 | │ ├── /css 34 | │ │ └── styles.css 35 | │ └── /js 36 | │ └── script.js 37 | │ 38 | ├── /routes 39 | │ └── view.html 40 | └── sherpa.server.ts 41 | ``` 42 | 43 | In this structure: 44 | * `logo.png` will be accessible at `/images/logo.png` 45 | * `styles.css` will be accessible at `/css/styles.css` 46 | * `script.js` will be accessible at `/js/script.js` 47 | * `view.html` will be accessible at `/` 48 | 49 | 50 |
51 | 52 | 53 | ## Examples 54 | 55 | 56 | ### View Endpoints Example 57 | If you want to reference these static assets in your view endpoint or other 58 | files, use the appropriate URL path. For example, in an HTML file: 59 | 60 | ```html title="/routes/view.html" 61 | 62 | 63 | 64 | 65 | 66 | My SherpaJS App 67 | 68 | 69 | 70 |
71 | Logo 72 |
73 |
74 |

Welcome to My SherpaJS App

75 |

This is a simple example of serving static assets.

76 | 77 |
78 | 79 | 80 | ``` 81 | 82 | 83 |
84 | 85 | 86 | ## Security Considerations 87 | * **Access Control** - Ensure that sensitive files are not placed in the 88 | `public` directory as they will be publicly accessible. 89 | 90 | 91 |
92 | 93 | 94 | By organizing your static assets efficiently and using SherpaJS's built-in 95 | capabilities, you can serve these resources effectively to enhance the 96 | performance and usability of your web applications. -------------------------------------------------------------------------------- /src/internal/response/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2024 Sellers Industries, Inc. 3 | * distributed under the MIT License 4 | * 5 | * author: Evan Sellers 6 | * date: Mon Mar 04 2024 7 | * file: transformer.ts 8 | * project: SherpaJS - Module Microservice Platform 9 | * purpose: SherpaJS Response to Native 10 | * 11 | */ 12 | 13 | 14 | import { OriginURL } from "../../native/url/index.js"; 15 | import { BodyType } from "../../native/model.js"; 16 | import { IRequest } from "../../native/request/index.js"; 17 | import { IResponse } from "../../native/response/index.js"; 18 | import { ServerResponse as LocalResponse } from "http"; 19 | const VercelResponse = Response; 20 | type VercelResponseType = Response; 21 | 22 | 23 | export function ResponseLocal(request:IRequest, response:IResponse, nativeResponse:LocalResponse) { 24 | applyRedirectHeaders(request, response); 25 | nativeResponse.statusCode = response.status; 26 | nativeResponse.statusMessage = response.statusText; 27 | 28 | for (let [key, value] of response.headers.entries()) { 29 | if (value) { 30 | nativeResponse.setHeader(key, value); 31 | } 32 | } 33 | 34 | nativeResponse.end(getBody(response)); 35 | } 36 | 37 | 38 | export function ResponseVercel(request:IRequest, response:IResponse):VercelResponseType { 39 | applyRedirectHeaders(request, response); 40 | return new VercelResponse(getBody(response), { 41 | headers: response.headers, 42 | status: response.status, 43 | statusText: response.statusText 44 | }); 45 | } 46 | 47 | 48 | function getBody(response:IResponse):string|undefined { 49 | switch (response.bodyType) { 50 | case BodyType.JSON: 51 | return JSON.stringify(response.body); 52 | default: 53 | return response.body as string; 54 | } 55 | return undefined; 56 | } 57 | 58 | 59 | function applyRedirectHeaders(request:IRequest, response:IResponse) { 60 | if (response.headers.has("Location")) { 61 | let host = request.headers.get("host") as string; 62 | let protocol = host.toLowerCase().includes("localhost") ? "http" : "https"; 63 | let origin = `${protocol}://${host}`; 64 | let url = new OriginURL(request.url, origin); 65 | let path = url.origin + url.pathname; 66 | // NOTE: a base url is required, trailing "/" required for proper "./" and "../" 67 | path = !path.endsWith("/") ? `${path}/` : path; 68 | response.headers.set("Location", new OriginURL(response.headers.get("Location") as string, path).href); 69 | } 70 | } 71 | 72 | 73 | // We have come to share in Christ, if indeed we hold our original conviction 74 | // firmly to the very end. 75 | // - Hebrews 3:14 76 | 77 | -------------------------------------------------------------------------------- /src/compiler/utilities/kv/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2024 Sellers Industries, Inc. 3 | * distributed under the MIT License 4 | * 5 | * author: Evan Sellers 6 | * date: Mon Apr 29 2024 7 | * file: index.ts 8 | * project: SherpaJS - Module Microservice Platform 9 | * purpose: Key Value Cache 10 | * 11 | */ 12 | 13 | 14 | import fs from "fs"; 15 | import { Path } from "../path/index.js"; 16 | 17 | 18 | const FILENAME = "SHERPA-KV.json"; 19 | const FILEPATH = Path.join(Path.getRootDirectory(), FILENAME); 20 | 21 | 22 | type KVStructure = Record>; 23 | 24 | 25 | export class KV { 26 | 27 | private namespace:string; 28 | 29 | constructor(namespace:string) { 30 | this.namespace = namespace; 31 | } 32 | 33 | 34 | getNamespace():string { 35 | return this.namespace; 36 | } 37 | 38 | 39 | get(key:string):unknown { 40 | return KV.load()[this.namespace][key].data; 41 | } 42 | 43 | 44 | has(key:string):boolean { 45 | if (!KV.load()[this.namespace]) { 46 | return false; 47 | } 48 | return Object.keys(KV.load()[this.namespace]).includes(key); 49 | } 50 | 51 | 52 | set(key:string, value:unknown) { 53 | let data = KV.load(); 54 | if (!data[this.namespace]) { 55 | data[this.namespace] = {}; 56 | } 57 | data[this.namespace][key] = { 58 | data: value, 59 | timestamp: new Date().toISOString() 60 | }; 61 | KV.store(data); 62 | } 63 | 64 | 65 | delete(key:string) { 66 | let data = KV.load(); 67 | delete data[this.namespace][key]; 68 | if (Object.keys(data[this.namespace]).length === 0) { 69 | delete data[this.namespace]; 70 | } 71 | KV.store(data); 72 | } 73 | 74 | 75 | getKeys():string[] { 76 | return Object.keys(KV.load()[this.namespace]); 77 | } 78 | 79 | 80 | getValues():unknown[] { 81 | return Object.values(KV.load()[this.namespace]).map(o => o["data"]); 82 | } 83 | 84 | 85 | static getFilepath():string { 86 | return FILEPATH; 87 | } 88 | 89 | 90 | private static load():KVStructure { 91 | try { 92 | return JSON.parse(fs.readFileSync(this.getFilepath(), "utf8")); 93 | } catch (error) { 94 | return {}; 95 | } 96 | } 97 | 98 | 99 | private static store(data:KVStructure) { 100 | fs.writeFileSync(this.getFilepath(), JSON.stringify(data, null, 4), "utf8"); 101 | } 102 | 103 | 104 | } 105 | 106 | 107 | // He replied, "Blessed rather are those who hear the word of God and obey it." 108 | // - Luke 11:28 109 | -------------------------------------------------------------------------------- /src/compiler/utilities/path/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2024 Sellers Industries, Inc. 3 | * distributed under the MIT License 4 | * 5 | * author: Evan Sellers 6 | * date: Mon Mar 04 2024 7 | * file: index.ts 8 | * project: SherpaJS - Module Microservice Platform 9 | * purpose: File Utilities 10 | * 11 | */ 12 | 13 | 14 | import fs from "fs"; 15 | import path from "path"; 16 | import { getDirectoryStructure } from "./directory-structure/index.js"; 17 | import { DirectoryStructure } from "./directory-structure/model.js"; 18 | 19 | 20 | export class Path { 21 | 22 | 23 | public static getFilename(filepath:string):string { 24 | return path.basename(filepath); 25 | } 26 | 27 | 28 | public static getName(filepath:string):string { 29 | return path.basename(filepath, path.extname(filepath)); 30 | } 31 | 32 | 33 | public static getExtension(filepath:string):string { 34 | return path.extname(filepath).slice(1).toUpperCase(); 35 | } 36 | 37 | 38 | public static unix(filepath:string):string { 39 | return filepath 40 | .replace("file://", "") 41 | .replace(/^\/?[A-Za-z]:/, "") 42 | .replace(/\\/g, "/"); 43 | } 44 | 45 | 46 | public static getDirectory(filepath:string):string { 47 | return this.unix(path.dirname(filepath)); 48 | } 49 | 50 | 51 | public static join(...paths:string[]):string { 52 | return this.unix(path.join(...paths)); 53 | } 54 | 55 | 56 | public static getRootDirectory() { 57 | return this.join(this.getDirectory(this.unix(import.meta.url)), "../../../../../"); 58 | } 59 | 60 | 61 | public static resolve(filepath:string, resolveDir:string):string|undefined { 62 | let npm = Path.join(resolveDir, "../../../node_modules", filepath); 63 | if (fs.existsSync(npm)) { 64 | return npm; 65 | } 66 | return Path.getDirectory(Path.join(resolveDir, filepath)); 67 | } 68 | 69 | 70 | public static resolveExtension(filepath:string, filename:string, extensions:string[]):string|undefined { 71 | for (let type of extensions) { 72 | let _filename = filename + "." + type.toLowerCase(); 73 | let _filepath = Path.join(filepath, _filename); 74 | if (fs.existsSync(_filepath)) { 75 | return this.unix(_filepath); 76 | } 77 | } 78 | return undefined; 79 | } 80 | 81 | 82 | public static getDirectoryStructure(filepath:string):DirectoryStructure { 83 | return getDirectoryStructure(filepath); 84 | } 85 | 86 | 87 | 88 | public static isAbsolute(filepath:string):boolean { 89 | return path.isAbsolute(filepath); 90 | } 91 | 92 | 93 | } 94 | 95 | 96 | // Be on your guard; stand firm in the faith; be courageous; be strong. 97 | // - 1 Corinthians 16:13 98 | -------------------------------------------------------------------------------- /src/compiler/structure/assets/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2024 Sellers Industries, Inc. 3 | * distributed under the MIT License 4 | * 5 | * author: Evan Sellers 6 | * date: Fri May 3 2024 7 | * file: index.ts 8 | * project: SherpaJS - Module Microservice Platform 9 | * purpose: Get Assets 10 | * 11 | */ 12 | 13 | 14 | import { Path } from "../../utilities/path/index.js"; 15 | import { Segment, AssetTree, Asset } from "../../models.js"; 16 | import { DirectoryStructureTree } from "../../utilities/path/directory-structure/model.js"; 17 | import { Message } from "../../utilities/logger/model.js"; 18 | import { getAssetFiles } from "./files.js"; 19 | import { RequestUtilities } from "../../../native/request/utilities.js"; 20 | 21 | 22 | export function getAssets(entry:string):{ assets:AssetTree, logs:Message[] } { 23 | let { files, logs } = getAssetFiles(entry); 24 | return { 25 | assets: getAssetTree(files.tree), 26 | logs 27 | }; 28 | } 29 | 30 | 31 | export function flattenAssets(assetTree?:AssetTree):Asset[] { 32 | if (!assetTree) return []; 33 | if (assetTree["filepath"]) return [assetTree as unknown as Asset]; 34 | 35 | let assetList:Asset[] = []; 36 | if (assetTree["."]) { 37 | assetList.push(...assetTree["."] as Asset[]); 38 | } 39 | 40 | let segments = Object.keys(assetTree).filter(segment => segment != "."); 41 | assetList.push(...segments.map(segment => flattenAssets(assetTree[segment] as AssetTree)).flat()); 42 | return assetList.flat().sort(sortAssets); 43 | } 44 | 45 | 46 | function sortAssets(assetA:Asset, assetB:Asset):number { 47 | return RequestUtilities.compareURL(assetA.segments, assetB.segments); 48 | } 49 | 50 | 51 | function getAssetTree(assetTree:DirectoryStructureTree, segments:Segment[]=[]):AssetTree { 52 | let assets:AssetTree = {}; 53 | if (assetTree.files.length > 0) { 54 | assets["."] = assetTree.files.map(file => { 55 | return { 56 | filepath: file.filepath.absolute, 57 | filename: Path.getFilename(file.filepath.absolute), 58 | segments: segments, 59 | path: RequestUtilities.getDynamicURL(segments) + "/" + Path.getFilename(file.filepath.absolute) 60 | } 61 | }); 62 | } 63 | 64 | for (let segmentName of Object.keys(assetTree.directories)) { 65 | let segmentKey = `/${segmentName}`; 66 | if (assets[segmentKey]) { 67 | throw new Error(`Overlapping asset segment: "${segmentKey}".`); 68 | } 69 | assets[segmentKey] = getAssetTree( 70 | assetTree.directories[segmentName], 71 | [...segments, { name: segmentName, isDynamic: false }] 72 | ); 73 | } 74 | 75 | return assets; 76 | } 77 | 78 | 79 | // When God raised up his servant, he sent him first to you to bless you by 80 | // turning each of you from your wicked ways. 81 | // - Acts 3:26 82 | -------------------------------------------------------------------------------- /src/compiler/structure/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2024 Sellers Industries, Inc. 3 | * distributed under the MIT License 4 | * 5 | * author: Evan Sellers 6 | * date: Sun Feb 11 2024 7 | * file: index.ts 8 | * project: SherpaJS - Module Microservice Platform 9 | * purpose: Structure 10 | * 11 | */ 12 | 13 | 14 | import { Message } from "../utilities/logger/model.js" 15 | import { getModuleConfig } from "./config-module/index.js"; 16 | import { getServerConfig } from "./config-server/index.js"; 17 | import { flattenAssets, getAssets } from "./assets/index.js"; 18 | import { 19 | Context, CreateModuleInterface, EndpointTree, 20 | ModuleConfigFile, Segment, Structure, 21 | } from "../models.js" 22 | import { flattenEndpoints, getEndpoints } from "./endpoint/index.js"; 23 | 24 | 25 | export async function getStructure(entry:string):Promise { 26 | let logs:Message[] = []; 27 | 28 | let { server, logs: logsServer } = await getServerConfig(entry); 29 | logs.push(...logsServer); 30 | if (!server) return { logs }; 31 | 32 | let { endpoints, logs: logsEndpoints } = await getComponents(entry, server.instance.context, server.filepath, [], true); 33 | logs.push(...logsEndpoints); 34 | if (!endpoints) return { logs }; 35 | 36 | let { assets, logs: logsAssets } = getAssets(entry); 37 | logs.push(...logsAssets); 38 | 39 | return { 40 | logs, 41 | endpoints: { 42 | tree: endpoints, 43 | list: flattenEndpoints(endpoints) 44 | }, 45 | assets: { 46 | tree: assets, 47 | list: flattenAssets(assets) 48 | }, 49 | server 50 | }; 51 | } 52 | 53 | 54 | export async function getComponents(entry:string, context:Context, contextFilepath:string, segments:Segment[], isRoot:boolean):Promise<{ logs:Message[], endpoints?:EndpointTree }> { 55 | let logs:Message[] = []; 56 | 57 | let { module, logs: logsModule } = await getModule(entry, context, contextFilepath, isRoot); 58 | logs.push(...logsModule); 59 | if (!module) return { logs }; 60 | 61 | let { endpoints, logs: endpointFileLogs } = await getEndpoints(module.entry, module, segments); 62 | logs.push(...endpointFileLogs); 63 | if (!endpoints) return { logs }; 64 | 65 | return { endpoints, logs }; 66 | } 67 | 68 | 69 | async function getModule(entry:string, context:Context, contextFilepath:string, isRoot:boolean):Promise<{ logs:Message[], module?:ModuleConfigFile }> { 70 | if (isRoot) { 71 | return { 72 | logs: [], 73 | module: { 74 | entry, 75 | filepath: entry, 76 | context: context, 77 | contextFilepath: contextFilepath, 78 | instance: { 79 | name: ".", 80 | interface: CreateModuleInterface 81 | } 82 | } 83 | } 84 | } 85 | return await getModuleConfig(entry, context, contextFilepath); 86 | } 87 | 88 | 89 | // Jesus answered, "The work of God is this: to believe in the one he has sent." 90 | // - John 6:29 91 | -------------------------------------------------------------------------------- /docs/installation.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Installation 3 | description: Installing SherpaJS and creating a new SherpaJS Server 4 | --- 5 | 6 | 7 | # Installation 8 | Installing SherpaJS and creating a new SherpaJS Server. 9 | 10 | 11 |
12 | 13 | 14 | ## Quick Installation 15 | We recommend starting a new SherpaJS server by downloading the 16 | [SherpaJS Server Template](https://github.com/sellersindustry/SherpaJS-template-server). 17 | This template will have all the required structure and example 18 | endpoints setup for you. After download the project, install all the 19 | dependencies by running: 20 | ```bash 21 | npm install 22 | ``` 23 | 24 | You can start the development server with `npm run dev`. 25 | 26 | Now your project is all setup, explore around the project files and get 27 | acquainted with the [SherpaJS Project Structure](/structure) and 28 | [SherpaJS CLI](/api/cli). 29 | 30 | 31 |
32 | 33 | 34 | ## Manual Installation 35 | Creating a new server is extremely easy and can be done within a couple of 36 | minutes. To manually create a new server start by create a new NodeJS project 37 | with `npm init`. Then install the SherpaJS Core package: 38 | 39 | ```bash 40 | npm install sherpa-core 41 | ``` 42 | 43 |
44 | 45 | 46 | ### Updating `package.json` 47 | Open the `package.json` file in the root directory of your project, and update 48 | the following properties. 49 | 50 | ```json title="package.json" 51 | { 52 | "type": "module", 53 | "scripts": { 54 | "build": "sherpa build -b Vercel", 55 | "build-local": "sherpa build -b local", 56 | "start": "sherpa start", 57 | "dev": "sherpa dev" 58 | } 59 | } 60 | ``` 61 | 62 | 63 |
64 | 65 | 66 | ### Creating Server Config 67 | Create a new Typescript file for your [server configuration](/build/server-config) 68 | in the root directory of your project named `sherpa.server.ts`. 69 | 70 | ```typescript title="sherpa.server.ts" 71 | import { SherpaJS } from "sherpa-core"; 72 | 73 | export default SherpaJS.New.server({ 74 | context: { // optional, provide a context that are provided to endpoints 75 | example: "foo" 76 | } 77 | }); 78 | ``` 79 | 80 | 81 |
82 | 83 | 84 | ### Creating Endpoints 85 | SherpaJS uses a directory-based structure for routing, similar to NextJS. Start 86 | by creating a `/routes` directory and place a new enpoint `index.ts` file in the 87 | directory. This will be processed at the root `/` of your application. 88 | 89 | ```typescript title="/routes/index.ts" 90 | import { Request, Response, Context } from "sherpa-core"; 91 | 92 | export function GET(request:Request, context:Context) { 93 | return Response.text("Hello World!"); 94 | } 95 | ``` 96 | 97 | Learn more about [routing](/build/rounting) and [endpoints](/build/endpoints). 98 | 99 | 100 |
101 | 102 | 103 | ### Creating Static Assets (Optional) 104 | Create a `/public` directory in the root directory of your project to store 105 | static assets such as images, stylings and more. 106 | 107 | Learn more about [static assets](/build/static-assets). 108 | -------------------------------------------------------------------------------- /docs/build/server-config.mdx: -------------------------------------------------------------------------------- 1 | # Server Configuration 2 | Sherpa servers are configured using a `sherpa.server.ts` file, where you define 3 | the structure and behavior of your server. This configuration file is located 4 | at the root of your project and serves as the entry point for your Sherpa server. 5 | 6 | 7 |
8 | 9 | 10 | ## Config File 11 | The configuration file must be located at `sherpa.server.ts` and have a 12 | default export of the config using the `SherpaJS.New.server` function as follows: 13 | 14 | ```typescript title="sherpa.server.ts" 15 | import SherpaJS from "sherpa-core"; 16 | 17 | export default SherpaJS.New.server({ 18 | content: { 19 | serverSecret: process.env.SERVER_SECRET, 20 | allowThingy: true 21 | } 22 | }); 23 | ``` 24 | 25 | 26 | When you load modules they have there own context, that is provided 27 | by the module loader, and do not have access to your server context. 28 | 29 | 30 | 31 | 32 |
33 | 34 | 35 | ## Config Structure 36 | 37 | ### Context 38 | A property that allows you to define a context object. Contexts are 39 | provided to endpoints and can contain any additional data or settings needed 40 | for request processing. 41 | 42 | 43 |
44 | 45 | 46 | ## Server Config Type 47 | The server configuration type is structured as: 48 | 49 | ```typescript 50 | { 51 | context: T; 52 | } 53 | ``` 54 | 55 | where `T` can be any type, allowing for flexible and customizable 56 | server configurations. 57 | 58 | 59 |
60 | 61 | 62 | ## Examples 63 | 64 | ### Basic Example 65 | A basic configuration where the context contains simple settings: 66 | 67 | ```typescript title="sherpa.server.ts" 68 | import { SherpaJS } from "sherpa-core"; 69 | 70 | export default SherpaJS.New.server({ 71 | context: { 72 | serverSecret: "foo", 73 | allowThingy: true 74 | } 75 | }); 76 | ``` 77 | 78 | The context object is then available within endpoints. 79 | ```typescript title="routes/index.ts" 80 | import { Request, Response } from "sherpa-core"; 81 | 82 | 83 | export function GET(request:Request, context:unknown) { 84 | consoe.log(context.serverSecret); // returns "foo" 85 | consoe.log(context.allowThingy); // returns true 86 | return Response.new(); 87 | } 88 | ``` 89 | 90 | 91 |
92 | 93 | 94 | ### Typed Example 95 | A more detailed configuration using a typed context for additional type 96 | safety and clarity: 97 | 98 | ```typescript title="sherpa.server.ts" 99 | import { SherpaJS } from "sherpa-core"; 100 | 101 | type ConfigExample = { serverSecret: string; allowThingy: boolean }; 102 | 103 | export default SherpaJS.New.server({ 104 | context: { 105 | serverSecret: "foo", 106 | allowThingy: true 107 | } 108 | }); 109 | ``` 110 | 111 | The context object is then available within endpoints. 112 | ```typescript title="routes/index.ts" 113 | import { Request, Response } from "sherpa-core"; 114 | 115 | 116 | export function GET(request:Request, context:ConfigExample) { 117 | consoe.log(context.serverSecret); // returns "foo" 118 | consoe.log(context.allowThingy); // returns true 119 | return Response.new(); 120 | } 121 | ``` 122 | -------------------------------------------------------------------------------- /src/native/parameters/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2024 Sellers Industries, Inc. 3 | * distributed under the MIT License 4 | * 5 | * author: Evan Sellers 6 | * date: Tue Mar 26 2024 7 | * file: index.ts 8 | * project: SherpaJS - Module Microservice Platform 9 | * purpose: Parameters 10 | * 11 | */ 12 | 13 | 14 | 15 | import { Segment } from "../../compiler/models.js"; 16 | import { OriginURL } from "../url/index.js"; 17 | 18 | 19 | type parameterValue = string|number|boolean; 20 | type parameters = { [key:string]:parameterValue[] }; 21 | 22 | 23 | export class Parameters { 24 | 25 | private parameters:parameters = {}; 26 | 27 | 28 | public has(key:string):boolean { 29 | return this.parameters[key] != undefined; 30 | } 31 | 32 | 33 | public get(key:string):parameterValue|undefined { 34 | if (!this.has(key) || this.parameters[key].length == 0) { 35 | return undefined; 36 | } 37 | return this.parameters[key][0] as parameterValue; 38 | } 39 | 40 | 41 | public getAll(key:string):parameterValue[]|undefined { 42 | if (!this.has(key)) { 43 | return undefined; 44 | } 45 | return this.parameters[key] as parameterValue[]; 46 | } 47 | 48 | 49 | public keys():string[] { 50 | return Object.keys(this.parameters); 51 | } 52 | 53 | 54 | public toJSON():Record { 55 | return { ...this.parameters }; 56 | } 57 | 58 | 59 | static getPathParams(url:string, segments:Segment[]):Parameters { 60 | let params = new Parameters(); 61 | new OriginURL(url).pathname.split("/").filter((o) => o != "").forEach((value:string, index:number) => { 62 | if (segments[index].isDynamic) { 63 | params.append(segments[index].name, value, false); 64 | } 65 | }); 66 | return params; 67 | } 68 | 69 | 70 | static getQueryParams(url:string):Parameters { 71 | let params = new Parameters(); 72 | new OriginURL(url).searchParams.forEach((value, key) => { 73 | params.append(key, value); 74 | }); 75 | return params; 76 | } 77 | 78 | 79 | private append(key:string, value:string, useDelimiter:boolean=true) { 80 | if (!this.has(key)) { 81 | this.parameters[key] = []; 82 | } 83 | this.parameters[key].push(...this.parse(value, useDelimiter)); 84 | } 85 | 86 | 87 | private parse(value:string, useDelimiter:boolean):parameterValue[] { 88 | if (useDelimiter && value.includes(",")) { 89 | return value.split(",").map((subValue) => this.parse(subValue, useDelimiter)).flat(); 90 | } else { 91 | if (value == "true") { 92 | return [true]; 93 | } else if (value == "false") { 94 | return [false]; 95 | } else if (/^\d+$/.test(value)) { 96 | return [parseInt(value)]; 97 | } 98 | return [value]; 99 | } 100 | } 101 | 102 | 103 | } 104 | 105 | 106 | // Blessed are those whose ways are blameless, who walk according to the law of 107 | // the Lord. 108 | // - Psalm 119:1 109 | -------------------------------------------------------------------------------- /docs/api/cli.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: SherpaJS CLI 3 | description: The SherpaJS CLI allows you to develop, build, start your application, and more. 4 | --- 5 | 6 | # SherpaJS CLI 7 | 8 | The SherpaJS command-line interface (CLI) allows you to develop, build, start your application, and 9 | more. 10 | 11 | 12 |
13 | 14 | 15 | ## Installation 16 | To install the SherpaJS CLI, simply run the following command: 17 | 18 | ```bash 19 | npm install sherpa-core -g 20 | ``` 21 | 22 | This command will globally install the SherpaJS core package, enabling you to 23 | utilize its features across your system. Once installed, you can easily run 24 | the SherpaJS command-line interface (CLI) using the following command: 25 | 26 | ```bash 27 | sherpa 28 | ``` 29 | 30 | 31 |
32 | 33 | 34 | ## Commands 35 | CLI for SherpaJS - Modular Microservices Framework 36 | 37 | ```bash 38 | sherpa [options] [command] 39 | ``` 40 | 41 | **Options:** 42 | - `-V`, `--version` output the version number 43 | - `-h`, `--help` display help for command 44 | 45 | **Commands:** 46 | - `build [options]` Creates a production build of your application 47 | - `start [options]` Start a production build of your application 48 | - `clean [options]` Removes all build directories of your application 49 | - `dev [options]` Start a server in development mode with hot-reload 50 | - `help [command]` display help for command 51 | 52 | 53 |
54 | 55 | 56 | ### Build Command 57 | Creates a production build of your application. 58 | ```bash 59 | sherpa build [options] 60 | ``` 61 | 62 | **Options:** 63 | - `-i`, `--input ` path to SherpaJS server, defaults to current directory 64 | - `-o`, `--output ` path to server output, defaults to input directory 65 | - `-b`, `--bundler ` platform bundler ("**Vercel**", "*local**", *default: "local"*) 66 | - `-v`, `--variable [key values...]` Specify optional environment variables as key=value pairs Ex. `foo=bar test="1234 HI"` 67 | - `--dev` enable development mode, does not minify output 68 | - `-h`, `--help` display help for command 69 | 70 | 71 | 72 |
73 | 74 | 75 | ### Start Command 76 | Start a production build of your application. Ensure you have created a local 77 | build, with [\"sherpa build\"](/api/cli#build-command) first. 78 | ```bash 79 | sherpa start [options] 80 | ``` 81 | 82 | **Options:** 83 | - `-i`, `--input ` path to SherpaJS server, defaults to current directory 84 | - `-p`, `--port ` port number (default: "3000") 85 | - `-h`, `--help` display help for command 86 | 87 | 88 |
89 | 90 | 91 | ### Clean Command 92 | Removes all build directories of your application. 93 | ```bash 94 | sherpa clean [options] 95 | ``` 96 | 97 | **Options:** 98 | - `-i`, `--input ` path to SherpaJS build directories, defaults to current directory 99 | - `-h`, `--help` display help for command 100 | 101 | 102 |
103 | 104 | 105 | ### Dev Command 106 | Start a server in development mode with hot-reload. 107 | ```bash 108 | sherpa dev [options] 109 | ``` 110 | 111 | **Options:** 112 | - `-i`, `--input ` path to SherpaJS server, defaults to current directory 113 | - `-o`, `--output ` path to server output, defaults to input directory 114 | - `-v`, `--variable [key values...]` Specify optional environment variables as key=value pairs Ex. `foo=bar test="1234 HI"` 115 | - `-p`, `--port ` port number (default: "3000") 116 | - `-h`, `--help` display help for command 117 | 118 | -------------------------------------------------------------------------------- /docs/build/miscellaneous.mdx: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | 3 | 4 | ## Ajv JSON Schema Validator 5 | [Ajv](https://ajv.js.org/), one of the best JSON schema validators, is 6 | extremely useful in the development of backend APIs for data validation. But Ajv 7 | is not supported by many edge environments, including Vercel, because of it's 8 | usage of `eval`. However, the SherpaJS compiler will pre-compile standalone Ajv 9 | schema, so schema validation can be done on the edge. 10 | 11 |
12 | 13 | ### Example Static Schema 14 | Start by creating a `schema.json` file containing the desired schema. 15 | ```json title="src/foo.schema.json" 16 | { 17 | "type": "object", 18 | "properties": { 19 | "foo": { 20 | "type": "number" 21 | } 22 | }, 23 | "required": ["foo"], 24 | "additionalProperties": false 25 | } 26 | ``` 27 | 28 | Then import your schema (in any script), and write the schema in the `AJV` 29 | function, provided by Sherpa Core. 30 | ```typescript title="routes/index.ts" 31 | import { Request, Response, AJV } from "sherpa-core"; 32 | import schemaFoo from "../../src/foo.schema.json"; 33 | const validatorFoo = AJV(schemaFoo); 34 | 35 | export function POST(request:Request) { 36 | try { 37 | if (!validatorFoo(request.body)) { 38 | return Response.text(JSON.stringify(validatorFoo.errors)); 39 | } 40 | return Response.text("OK"); 41 | } catch (e) { 42 | console.log(e); 43 | return Response.text(e.toString()); 44 | } 45 | } 46 | ``` 47 | 48 | 49 |
50 | 51 | 52 | ## Environment Variables 53 | Environment variables are a key part of configuring your SherpaJS application. 54 | They allow you to set various configuration options and secrets without 55 | hardcoding them into your application's codebase. 56 | 57 | 58 |
59 | 60 | 61 | ### Loading Environment Variables 62 | SherpaJS uses the `.env` file to load environment variables. This file should 63 | be placed at the root of your project. When the system is compiled, the 64 | variables defined in this file are automatically loaded into your server's 65 | environment. 66 | 67 | Any environment variables provided by hosting services (such as Vercel or AWS) 68 | are automatically included in your build. 69 | 70 | Environment varibles can also be added during build with the 71 | [build command](/api/cli#build-command). 72 | 73 | 74 |
75 | 76 | 77 | ### Automatic Parsing 78 | SherpaJS automatically parses environment variables to their appropriate types: 79 | - Strings remain as strings 80 | - Booleans are converted to `true` or `false` 81 | - Numbers are converted to numeric values 82 | 83 | 84 |
85 | 86 | 87 | ### Example .env File 88 | Here is an example of a `.env` file: 89 | 90 | ```shell title=".env" 91 | PORT=3000 # number 92 | DATABASE_URL=mongodb://localhost:27017/mydatabase # string 93 | JWT_SECRET=myverysecretkey # string 94 | ENABLE_FEATURE_X=true # boolean 95 | MAX_CONNECTIONS=100 # number 96 | ``` 97 | 98 |
99 | 100 | 101 | ### Best Practices 102 | - **Security**: Never commit your `.env` file to version control. Add it 103 | to your `.gitignore` file. 104 | - **Defaults**: Provide sensible default values for environment variables 105 | in your code. 106 | - **Validation**: Validate the presence and format of critical environment 107 | variables at the start of your application to avoid runtime errors. 108 | 109 | -------------------------------------------------------------------------------- /tests/endpoints/suite/bench.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2024 Sellers Industries, Inc. 3 | * distributed under the MIT License 4 | * 5 | * author: Evan Sellers 6 | * date: Thu May 16 2024 7 | * file: bench.ts 8 | * project: SherpaJS - Module Microservice Platform 9 | * purpose: Endpoint Test Suite - Test Benches 10 | * 11 | */ 12 | 13 | 14 | import { Path } from "../../../src/compiler/utilities/path/index.js"; 15 | import { ChildProcess, spawn } from "child_process"; 16 | 17 | 18 | export class Bench { 19 | 20 | 21 | private name:string; 22 | private host:string; 23 | private setupCmds:string[]; 24 | private teardownCmds:string[]; 25 | private startCmd:string|undefined; 26 | private server:ChildProcess; 27 | 28 | 29 | constructor(name:string, host:string, start?:string, setup:string[]=[], teardown:string[]=[]) { 30 | this.name = name; 31 | this.host = host; 32 | this.setupCmds = setup; 33 | this.teardownCmds = teardown; 34 | this.startCmd = start; 35 | } 36 | 37 | 38 | public getName():string { 39 | return this.name; 40 | } 41 | 42 | 43 | public getHost():string { 44 | return this.host; 45 | } 46 | 47 | 48 | public async setup() { 49 | for (const command of this.setupCmds) { 50 | await this.execute(command); 51 | } 52 | } 53 | 54 | 55 | public async start() { 56 | if (this.startCmd) { 57 | let args = this.getArguments(this.startCmd); 58 | this.server = spawn(args[0], args.slice(1), { cwd: this.getCWD() }); 59 | await this.wait(1000); 60 | } 61 | } 62 | 63 | 64 | public async teardown() { 65 | if (this.server) { 66 | this.server.kill(); 67 | } 68 | for (const command of this.teardownCmds) { 69 | await this.execute(command); 70 | } 71 | } 72 | 73 | 74 | private async execute(command:string):Promise { 75 | return new Promise((resolve) => { 76 | let args = this.getArguments(command); 77 | let process = spawn(args[0], args.slice(1), { cwd: this.getCWD(), shell: true }); 78 | 79 | process.stderr.on("data", (data) => { 80 | throw new Error(`Failed to execute command "${command}" for "${this.name}" test bench.\n${args.join(" ")}\n${data}`); 81 | }); 82 | 83 | process.on("close", (code) => { 84 | if (code === 0) { 85 | resolve(); 86 | } else { 87 | throw new Error(`Failed to execute command "${command}" for "${this.name}" test bench.\n${args.join(" ")}\ncode: ${code}`); 88 | } 89 | }); 90 | }); 91 | } 92 | 93 | 94 | private getCWD():string { 95 | return Path.join(Path.getDirectory(import.meta.url), "../../../../tests/endpoints/server"); 96 | } 97 | 98 | 99 | private getArguments(command:string):string[] { 100 | return command.replace(/^%sherpa-cli%/, "node ../../../dist/src/cli/index.js").split(" "); 101 | } 102 | 103 | 104 | private async wait(ms:number):Promise { 105 | return new Promise((resolve) => { 106 | setTimeout(() => { 107 | resolve(); 108 | }, ms); 109 | }); 110 | } 111 | 112 | 113 | } 114 | 115 | 116 | // If I give all I possess to the poor and give over my body to hardship that 117 | // I may boast, but do not have love, I gain nothing. 118 | // - 1 Corinthians 13:3 119 | -------------------------------------------------------------------------------- /docs/api/components/headers.mdx: -------------------------------------------------------------------------------- 1 | # Headers 2 | 3 | The `Headers` class represents a set of HTTP headers and provides methods and 4 | properties for working with them. It is very similar to the standard 5 | [Headers class](https://developer.mozilla.org/en-US/docs/Web/API/Headers) 6 | used in web APIs. 7 | 8 | 9 |
10 | 11 | 12 | ## Constructor 13 | 14 | ### constructor(init) 15 | `constructor(init?: HeadersInit)` 16 | Creates a new instance of the `Headers` class. 17 | * `init` (optional): An optional parameter that can be an instance of 18 | `Headers`, Web API `Headers`, an array of key-value pairs, or a record of 19 | header fields and values. 20 | 21 | 22 |
23 | 24 | 25 | 26 | ## Methods 27 | 28 | 29 | ### append(name, value) 30 | `append(name: string, value: string): void` \ 31 | Appends a new value onto an existing header's value, or adds the header if it 32 | does not already exist. 33 | * `name` - The name of the header. 34 | * `value` - The value to append to the header. 35 | 36 | 37 |
38 | 39 | 40 | ### has(name) 41 | `has(name: string): boolean` \ 42 | Checks if a header with a specified name exists. 43 | * `name` - The name of the header to check. 44 | 45 | 46 |
47 | 48 | 49 | ### get(name) 50 | `get(name: string): string | null` \ 51 | Returns the first value of the specified header. 52 | * `name` - The name of the header. 53 | 54 | 55 |
56 | 57 | 58 | ### set(name, value) 59 | `set(name: string, value: string): void` \ 60 | Sets a new value for an existing header, or adds the header if it does 61 | not already exist. 62 | * `name` - The name of the header. 63 | * `value` - The new value to set for the header. 64 | 65 | 66 |
67 | 68 | 69 | ### delete(name) 70 | `delete(name: string): void` \ 71 | Removes a header and its value from the set of headers. 72 | * `name` - The name of the header to delete. 73 | 74 | 75 |
76 | 77 | 78 | ### toJSON() 79 | `toJSON(): Record` \ 80 | Returns a JSON representation of the headers. 81 | 82 | 83 |
84 | 85 | 86 | ### toString() 87 | `toString(): string` \ 88 | Returns a string representation of the headers in JSON format. 89 | 90 | 91 |
92 | 93 | 94 | ### getSetCookie() 95 | `getSetCookie(): string[]` \ 96 | Returns an array of values of the 'Set-Cookie' header. 97 | 98 | 99 |
100 | 101 | 102 | ### forEach(callbackFn) 103 | `forEach(callbackfn: (value: string, key: string, iterable: Headers) => void): void` \ 104 | Executes a provided function once per each header present in the set of headers. 105 | * `callbackfn` - A function to execute for each header. 106 | * `thisArg` - An optional value to use as `this` when executing the callback function. 107 | 108 | 109 |
110 | 111 | 112 | ### keys() 113 | `keys(): IterableIterator` \ 114 | Returns an iterator of all the header names. 115 | 116 | 117 |
118 | 119 | 120 | ### values() 121 | `values(): IterableIterator` \ 122 | Returns an iterator of all the header values. 123 | 124 | 125 |
126 | 127 | 128 | ### entries() 129 | `entries(): IterableIterator<[string, string]>` \ 130 | Returns an iterator of all the header names and values as key-value pairs. 131 | 132 | 133 |
134 | 135 | 136 | ## Examples 137 | The following examples, illustrate how to construct a `Headers` class, append 138 | and retreive values from the object. 139 | 140 | ```typescript 141 | import { Headers } from "sherpa-core"; 142 | const _headers = new Headers(); 143 | 144 | _headers.append("Content-Type", "application/json"); 145 | _headers.get("Content-Type"); // returns "application/json" 146 | ``` 147 | 148 | -------------------------------------------------------------------------------- /src/compiler/structure/config-server/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2024 Sellers Industries, Inc. 3 | * distributed under the MIT License 4 | * 5 | * author: Evan Sellers 6 | * date: Sun Feb 11 2024 7 | * file: index.ts 8 | * project: SherpaJS - Module Microservice Platform 9 | * purpose: Server Config Structure 10 | * 11 | */ 12 | 13 | 14 | import { 15 | ServerConfig, ServerConfigFile, 16 | FILE_EXTENSIONS, FILENAME 17 | } from "../../models.js"; 18 | import { Path } from "../../utilities/path/index.js"; 19 | import { Tooling } from "../../utilities/tooling/index.js"; 20 | import { Level, Message } from "../../utilities/logger/model.js"; 21 | import { getModuleConfig } from "../config-module/index.js"; 22 | 23 | 24 | export async function getServerConfig(entry:string):Promise<{ logs:Message[], server?:ServerConfigFile }> { 25 | let logs:Message[] = []; 26 | 27 | let { filepath, logs: logsFilepath } = getFilepath(entry); 28 | logs.push(...logsFilepath); 29 | if (!filepath) return { logs }; 30 | 31 | let { instance, logs: logsInstance } = await getInstance(filepath); 32 | logs.push(...logsInstance); 33 | if (!instance) return { logs }; 34 | 35 | logs.push(...await verifyModuleConfig(entry)); 36 | logs.push(...await Tooling.typeValidation(filepath, "Server config")); 37 | 38 | return { 39 | server: { 40 | filepath: filepath, 41 | instance: instance 42 | }, 43 | logs 44 | } 45 | } 46 | 47 | 48 | function getFilepath(entry:string):{ logs:Message[], filepath?:string } { 49 | let filepath = Path.resolveExtension( 50 | entry, 51 | FILENAME.CONFIG.SERVER, 52 | FILE_EXTENSIONS.CONFIG.SERVER 53 | ); 54 | if (filepath) { 55 | return { filepath, logs: [] }; 56 | } 57 | return { 58 | logs: [{ 59 | level: Level.ERROR, 60 | text: "Server config file could not be found.", 61 | content: `Must have server config, "${FILENAME.CONFIG.SERVER}" ` 62 | + `of type "${FILE_EXTENSIONS.CONFIG.SERVER.join("\", \"")}".`, 63 | file: { filepath: entry } 64 | }] 65 | }; 66 | } 67 | 68 | 69 | async function getInstance(filepath:string):Promise<{ logs:Message[], instance?:ServerConfig }> { 70 | let { module, logs } = await Tooling.getExportedLoader(filepath, "Server Config", "SherpaJS.New.server", "sherpa-core"); 71 | if (!module) { 72 | return { logs }; 73 | } 74 | try { 75 | return { 76 | logs: await Tooling.typeValidation(filepath, "Server Config"), 77 | instance: await Tooling.getDefaultExport(filepath) as ServerConfig 78 | } 79 | } catch (e) { 80 | return { 81 | logs: [{ 82 | level: Level.ERROR, 83 | text: "Server config file could not be loaded.", 84 | content: e.message, 85 | file: { filepath: filepath } 86 | }] 87 | }; 88 | } 89 | } 90 | 91 | 92 | async function verifyModuleConfig(entry:string):Promise { 93 | if (hasModuleConfig(entry)) { 94 | return (await getModuleConfig(entry, undefined, "null")).logs; 95 | } 96 | return []; 97 | } 98 | 99 | 100 | function hasModuleConfig(entry:string):boolean { 101 | return Path.resolveExtension( 102 | entry, 103 | FILENAME.CONFIG.MODULE, 104 | FILE_EXTENSIONS.CONFIG.MODULE 105 | ) != undefined; 106 | } 107 | 108 | 109 | // Therefore I tell you, whatever you ask for in prayer, believe that you have 110 | // received it, and it will be yours. 111 | // - Mark 11:24 112 | -------------------------------------------------------------------------------- /src/compiler/structure/assets/files.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2024 Sellers Industries, Inc. 3 | * distributed under the MIT License 4 | * 5 | * author: Evan Sellers 6 | * date: Fri May 3 2024 7 | * file: files.ts 8 | * project: SherpaJS - Module Microservice Platform 9 | * purpose: Get Asset File Structure 10 | * 11 | */ 12 | 13 | 14 | import fs from "fs"; 15 | import { Path } from "../../utilities/path/index.js"; 16 | import { 17 | DirectoryStructure, DirectoryStructureFile, 18 | DirectoryStructureTree 19 | } from "../../utilities/path/directory-structure/model.js"; 20 | import { Level, Message } from "../../utilities/logger/model.js"; 21 | import { Logger } from "../../utilities/logger/index.js"; 22 | 23 | 24 | const MEGABYTES = 10e6; 25 | const REGEX_SEGMENT = /^([a-zA-Z0-9-]+)$/; 26 | 27 | 28 | export const EMPTY_ASSET_STRUCTURE:DirectoryStructure = { 29 | list: [], 30 | tree: { 31 | files: [], 32 | directories: {} 33 | } 34 | }; 35 | 36 | 37 | export function getAssetFiles(entry:string):{ logs:Message[], files:DirectoryStructure } { 38 | let directory = Path.join(entry, "public"); 39 | 40 | if (!fs.existsSync(directory)) { 41 | return { logs: [], files: EMPTY_ASSET_STRUCTURE }; 42 | } 43 | 44 | let files = Path.getDirectoryStructure(directory); 45 | let logs = getDiagnostics(files, directory); 46 | 47 | if (Logger.hasError(logs)) { 48 | return { logs, files }; 49 | } 50 | 51 | return { logs, files: files }; 52 | } 53 | 54 | 55 | function getDiagnostics(assets:DirectoryStructure, filepath:string):Message[] { 56 | let errors:Message[] = []; 57 | for (let file of assets.list) { 58 | errors.push(...validateFileSize(file)); 59 | errors.push(...validateFileExtension(file)); 60 | } 61 | errors.push(...validateSegments(assets.tree, filepath)); 62 | return errors; 63 | } 64 | 65 | 66 | function validateFileExtension(file:DirectoryStructureFile):Message[] { 67 | if (!file.filename.includes(".")) { 68 | return [{ 69 | level: Level.ERROR, 70 | text: "File has no extension.", 71 | file: { filepath: file.filepath.absolute } 72 | }]; 73 | } 74 | return []; 75 | } 76 | 77 | 78 | function validateFileSize(file:DirectoryStructureFile):Message[] { 79 | let bytes = fs.statSync(file.filepath.absolute).size; 80 | if (bytes > 100 * MEGABYTES) { 81 | return [{ 82 | level: Level.ERROR, 83 | text: "File is too large.", 84 | content: "Files must be less than 100MB.", 85 | file: { filepath: file.filepath.absolute } 86 | }]; 87 | } 88 | return []; 89 | } 90 | 91 | 92 | function validateSegments(structure:DirectoryStructureTree, filepath:string):Message[] { 93 | let errors:Message[] = []; 94 | for (let segmentName of Object.keys(structure.directories)) { 95 | let _filepath = Path.join(filepath, segmentName); 96 | errors.push(...validateSegmentName(segmentName, _filepath)); 97 | errors.push(...validateSegments(structure.directories[segmentName], _filepath)); 98 | } 99 | return errors; 100 | } 101 | 102 | 103 | function validateSegmentName(segment:string, filepath?:string):Message[] { 104 | if (REGEX_SEGMENT.test(segment)) 105 | return []; 106 | return [{ 107 | level: Level.ERROR, 108 | text: `Invalid asset segment route "${segment}".`, 109 | content: "Asset segment routes should only contain letters, numbers, and dashes.", 110 | file: filepath ? { filepath: filepath } : undefined 111 | }]; 112 | } 113 | 114 | 115 | // Since we live by the Spirit, let us keep in step with the Spirit. 116 | // - Galatians 5:25 117 | -------------------------------------------------------------------------------- /src/compiler/utilities/tooling/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2024 Sellers Industries, Inc. 3 | * distributed under the MIT License 4 | * 5 | * author: Evan Sellers 6 | * date: Mon Mar 04 2024 7 | * file: index.ts 8 | * project: SherpaJS - Module Microservice Platform 9 | * purpose: Source Code Utilities 10 | * 11 | */ 12 | 13 | 14 | import vm from "vm"; 15 | import path from "path"; 16 | import { BuildOptions } from "../../models.js"; 17 | import { build, BuildOptions as ESBuildOptions } from "esbuild"; 18 | import { Message } from "../logger/model.js"; 19 | import { typeValidation } from "./type-validation/index.js"; 20 | import { getEnvironmentVariables } from "./dot-env/index.js"; 21 | import { ExportLoaderModule, getExportedLoader } from "./exported-loader/index.js"; 22 | import { ExportedVariable, getExportedVariables } from "./exported-variables/index.js"; 23 | import jsonschemaPlugin from "@offen/esbuild-plugin-jsonschema"; 24 | 25 | 26 | export type { ExportLoaderModule, ExportedVariable }; 27 | export const DEFAULT_ESBUILD_TARGET:Partial = { 28 | format: "esm", 29 | target: "es2022", 30 | platform: "node", 31 | bundle: true, 32 | allowOverwrite: true, 33 | treeShaking: true, 34 | minify: true, 35 | footer: { 36 | js: "// Generated by SherpaJS" 37 | } 38 | }; 39 | 40 | 41 | export class Tooling { 42 | 43 | 44 | static async getExportedVariables(filepath:string):Promise { 45 | return await getExportedVariables(filepath); 46 | } 47 | 48 | 49 | static async getExportedLoader(filepath:string, fileTypeName:string, prototype?:string, source?:string):Promise<{ logs:Message[], module?:ExportLoaderModule }> { 50 | return await getExportedLoader(filepath, fileTypeName, prototype, source); 51 | } 52 | 53 | 54 | static async hasExportedLoader(filepath:string):Promise { 55 | return (await this.getExportedLoader(filepath, "N/A")).module != undefined; 56 | } 57 | 58 | 59 | static async typeValidation(filepath:string, fileTypeName:string):Promise { 60 | return await typeValidation(filepath, fileTypeName); 61 | } 62 | 63 | 64 | static async getDefaultExport(filepath:string):Promise { 65 | let result = await build({ 66 | ...DEFAULT_ESBUILD_TARGET, 67 | format: "cjs", 68 | entryPoints: [filepath], 69 | plugins: [jsonschemaPlugin()], 70 | write: false 71 | }); 72 | 73 | let code = result.outputFiles[0].text; 74 | let context = vm.createContext({ process, module: { exports: {} }, require: () => { return {}; }}); 75 | vm.runInContext(code, context); 76 | return context.module.exports.default; 77 | } 78 | 79 | 80 | static async build(props:{ buffer:string, output:string, resolve?:string, options?:BuildOptions, esbuild?:Partial }) { 81 | await build({ 82 | ...DEFAULT_ESBUILD_TARGET, 83 | ...props.options?.developer?.bundler?.esbuild, 84 | ...props.esbuild, 85 | plugins: [jsonschemaPlugin()], 86 | stdin: { 87 | contents: props.buffer, 88 | resolveDir: props.resolve ? path.resolve(props.resolve) : undefined, 89 | loader: "ts", 90 | }, 91 | outfile: path.resolve(props.output), 92 | define: { 93 | "global": "window", 94 | "process.env": JSON.stringify(getEnvironmentVariables(props.options)) 95 | } 96 | }); 97 | } 98 | 99 | 100 | } 101 | 102 | 103 | // Whoever believes and is baptized will be saved, but whoever does not 104 | // believe will be condemned. 105 | // - Mark 16:16 106 | -------------------------------------------------------------------------------- /docs/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Overview 3 | description: Instant Open Source docs with zero configuration. 4 | --- 5 | 6 | ![SherpaJS Logo](/assets/logos/logo-large-dark.png) 7 | 8 | ## What is SherpaJS? 9 | SherpaJS is a **modular and agnostic serverless JavaScript web framework,** that 10 | allows developers to easily build backend serverless web applications. Using a 11 | directory-based structure, inspired by NextJS, developers can define 12 | endpoints or import modules to execute code. Modules allow developers to easily 13 | reuse sets of endpoints and deploy them on their own servers - for example 14 | analytics, authentication, and release systems ([Community Modules](/#community-modules)). 15 | SherpaJS servers can then be compiled to a variety of different serverless 16 | platforms including Vercel Serverless and local Server 17 | ([Supported Platforms](/#supported-platforms)). 18 | 19 | 20 | This project is in early development, so it is possible for you to run 21 | into issues. If you run into any issues please just create a 22 | [new issue](https://github.com/sellersindustry/SherpaJS/issues) and 23 | link your code. Feel free to debug or update the code! 24 | 25 | 26 | 27 |
28 | 29 | 30 | ## Primary Features 31 | The main features of SherpaJS include: 32 | 33 | * **Routing** - A file-based routing system that supports dynamic routes, 34 | loading modules, and rendering HTML. 35 | * **Modules** - Build and utilize pre-build sets of endpoints that can 36 | be configured and loaded at specific routes. 37 | * **Agnostic Deployment** - Build your serverless applications to numerous 38 | serverless platforms including Vercel. *AWS Lambda coming soon.* 39 | * **Rendering** - Render static HTML at specific routes. *Templated HTML and 40 | Vue coming later.* 41 | * **Static Files** - Serve static files and access static files from endpoints. 42 | 43 | We are constantly adding new features and have plenty of amazing things planned 44 | for the future including: documentation generation, SDK client generation, and 45 | admin panels. 46 | 47 | 48 |
49 | 50 | 51 | ## Community Modules 52 | A list of some of the community modules for SherpaJS, with more coming all 53 | the time! If you make a module, 54 | [let us know](https://github.com/sellersindustry/SherpaJS/issues)! 55 | 56 | | Module | Description | 57 | |---|---| 58 | | [Static Flags](https://github.com/sellersindustry/SherpaJS-static-flags) | Create static flags of booleans, strings, or numbers | 59 | | [Events](https://github.com/sellersindustry/SherpaJS-events) | Create event sending endpoints for analytics platforms like PostHog using [Metadapter Events](https://github.com/sellersindustry/metadapter-event) | 60 | 61 | 62 |
63 | 64 | 65 | ## Supported Platforms 66 | The current platforms that SherpaJS supports, with more to come in the future! 67 | * Vercel Edge Functions 68 | * Local 69 | 70 | 71 |
72 | 73 | 74 | ## Deploy a Server 75 | SherpaJS can compile to various different web platforms, with more to come 76 | later. Want to support a new framework? [Submit a Ticket](https://github.com/sellersindustry/SherpaJS/issues). 77 | See the [build command](/api/cli#build-command) to compile to each platform. 78 | 79 | 80 |
81 | 82 | 83 | ### Vercel Edge Functions 84 | Building to Vercel will generate a Vercel serverless server in the `.vercel` 85 | directory relative to your output. When your SherpaJS server repository is 86 | deployed Vercel this folder will automatically be deployed. Ensure your build 87 | command is set to build SherpaJS with the Vercel bundler. 88 | 89 | 90 |
91 | 92 | 93 | ### Local Server 94 | Building to local server will generate a NodeJS server, that utilizes the built 95 | in HTTP service. This server will be located at the `.sherpa/index.js` relative 96 | to your output. You can start the server using 97 | [`sherpa start`](/api/cli#start-command). 98 | -------------------------------------------------------------------------------- /src/compiler/bundler/platforms/local/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2024 Sellers Industries, Inc. 3 | * distributed under the MIT License 4 | * 5 | * author: Evan Sellers 6 | * date: Sun Feb 11 2024 7 | * file: index.ts 8 | * project: SherpaJS - Module Microservice Platform 9 | * purpose: Local Bundler 10 | * 11 | */ 12 | 13 | 14 | import { Endpoint } from "../../../models.js"; 15 | import { Tooling } from "../../../utilities/tooling/index.js"; 16 | import { Path } from "../../../utilities/path/index.js"; 17 | import { Bundler, View } from "../abstract.js"; 18 | import { RequestUtilities } from "../../../../native/request/utilities.js"; 19 | 20 | 21 | export class Local extends Bundler { 22 | 23 | 24 | async build() { 25 | await super.build(); 26 | await Tooling.build({ 27 | buffer: this.getBuffer(), 28 | output: Path.join(this.getFilepath(), "index.js"), 29 | resolve: this.options.input, 30 | options: this.options, 31 | esbuild: { 32 | platform: "node", 33 | } 34 | }); 35 | 36 | } 37 | 38 | 39 | private getBuffer() { 40 | return ` 41 | import path from "path"; 42 | import { ServerLocal } from "${Path.join(Path.getRootDirectory(), "dist/src/server-local/index.js")}"; 43 | import { Handler, RequestLocal, ResponseLocal } from "${Path.join(Path.getRootDirectory(), "dist/src/internal/index.js")}"; 44 | 45 | const portArg = process.argv[2]; 46 | const port = portArg && !isNaN(parseInt(portArg)) ? parseInt(portArg) : 3000; 47 | const silent = process.argv.includes("--silent-startup"); 48 | const server = new ServerLocal(port, silent); 49 | const dirname = import.meta.dirname; 50 | ${this.endpoints.list.map((endpoint:Endpoint, index:number) => { 51 | return ` 52 | ${endpoint.filepath ? 53 | `import * as endpoint_${index} from "${endpoint.filepath}";` : 54 | `const endpoint_${index} = {};` 55 | } 56 | ${this.views[index] ? 57 | `const view_${index} = "${encodeURIComponent((this.views[index] as View).html)}";` : 58 | `const view_${index} = "";` 59 | } 60 | import import_context_${index} from "${endpoint.module.contextFilepath}"; 61 | 62 | const context_${index} = import_context_${index}.context; 63 | const segments_${index} = ${JSON.stringify(endpoint.segments)}; 64 | const url_${index} = "${RequestUtilities.getDynamicURL(endpoint.segments)}"; 65 | server.addEndpoint(url_${index}, async (nativeRequest, nativeResponse) => { 66 | let req = await RequestLocal(nativeRequest, segments_${index}); 67 | let res = await Handler(endpoint_${index}, ${`view_${index}`}, context_${index}, req); 68 | ResponseLocal(req, res, nativeResponse); 69 | }); 70 | 71 | `; 72 | }).join("\n")} 73 | 74 | ${this.assets.list.map((asset, index) => { 75 | return ` 76 | const asset_url_${index} = "${RequestUtilities.getDynamicURL(asset.segments)}${asset.segments.length > 0 ? "/" : ""}${asset.filename}"; 77 | const asset_filepath_${index} = path.join(dirname, "public", asset_url_${index}); 78 | server.addAsset(asset_url_${index}, asset_filepath_${index}); 79 | `; 80 | }).join("\n")} 81 | 82 | server.start(); 83 | `; 84 | } 85 | 86 | 87 | } 88 | 89 | 90 | // A wise son brings joy to his father, but a foolish son brings grief to his mother. 91 | // - Proverbs 10:1 92 | -------------------------------------------------------------------------------- /src/native/headers/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2024 Sellers Industries, Inc. 3 | * distributed under the MIT License 4 | * 5 | * author: Evan Sellers 6 | * date: Mon Mar 25 2024 7 | * file: headers.ts 8 | * project: SherpaJS - Module Microservice Platform 9 | * purpose: IO Headers Class 10 | * 11 | */ 12 | 13 | 14 | export type HeadersInit = string[][] | Record> | IHeaders | Headers; 15 | 16 | 17 | class IHeaders implements Headers { 18 | 19 | private headers:Record; 20 | 21 | constructor(init?:HeadersInit) { 22 | this.headers = {}; 23 | if (init) { 24 | if (init instanceof Headers || init instanceof IHeaders) { 25 | init.forEach((value:string, name:string) => { 26 | this.set(name, value); 27 | }); 28 | } else if (Array.isArray(init)) { 29 | init.forEach(([name, value]) => { 30 | this.append(name, value); 31 | }); 32 | } else { 33 | Object.entries(init).forEach(([name, value]) => { 34 | if (Array.isArray(value)) { 35 | value.forEach(v => this.append(name, v)); 36 | } else { 37 | this.set(name, value as string); 38 | } 39 | }); 40 | } 41 | } 42 | } 43 | 44 | 45 | append(name:string, value:string):void { 46 | let normalizedName = name.toLowerCase(); 47 | this.headers[normalizedName] = this.headers[normalizedName] 48 | ? `${this.headers[normalizedName]}, ${value}` 49 | : value; 50 | } 51 | 52 | 53 | has(name:string):boolean { 54 | return this.get(name) != null; 55 | } 56 | 57 | 58 | get(name:string):string|null { 59 | return this.headers[name.toLowerCase()] || null; 60 | } 61 | 62 | 63 | set(name:string, value:string):void { 64 | this.headers[name.toLowerCase()] = value; 65 | } 66 | 67 | 68 | delete(name:string): void { 69 | delete this.headers[name.toLowerCase()]; 70 | } 71 | 72 | 73 | toJSON():Record { 74 | return { ...this.headers }; 75 | } 76 | 77 | 78 | toString():string { 79 | return JSON.stringify(this.headers); 80 | } 81 | 82 | 83 | getSetCookie():string[] { 84 | return Object.entries(this.headers) 85 | .filter(([name]) => name.toLowerCase() === "set-cookie") 86 | .flatMap(([, value]) => value.split(", ")); 87 | } 88 | 89 | 90 | forEach(callbackfn:(value: string, key: string, iterable: Headers) => void, thisArg?:unknown):void { 91 | for (const [name, value] of Object.entries(this.headers)) { 92 | callbackfn.call(thisArg, value, name, this); 93 | } 94 | } 95 | 96 | 97 | keys():IterableIterator { 98 | return Object.keys(this.headers)[Symbol.iterator]() as IterableIterator; 99 | } 100 | 101 | 102 | values():IterableIterator { 103 | return Object.values(this.headers)[Symbol.iterator]() as IterableIterator; 104 | } 105 | 106 | 107 | entries():IterableIterator<[string, string]> { 108 | return Object.entries(this.headers)[Symbol.iterator]() as IterableIterator<[string, string]>; 109 | } 110 | 111 | 112 | *[Symbol.iterator]():IterableIterator<[string, string]> { 113 | for (const [name, value] of Object.entries(this.headers)) { 114 | yield [name, value]; 115 | } 116 | } 117 | 118 | 119 | } 120 | 121 | 122 | export { IHeaders as Headers }; 123 | 124 | 125 | // I will never forget your precepts, for by them you have preserved my life. 126 | // - Psalm 119:93 127 | -------------------------------------------------------------------------------- /src/compiler/utilities/tooling/dot-env/index.test.ts: -------------------------------------------------------------------------------- 1 | import { Path } from "../../../utilities/path/index"; 2 | import { BundlerType } from "../../../models"; 3 | import { getEnvironmentVariables } from "./index"; 4 | 5 | 6 | const DIRNAME = Path.getDirectory(import.meta.url); 7 | 8 | 9 | describe("Tooling Dot Environment", () => { 10 | 11 | 12 | test("Native Environment Variables", () => { 13 | process.env.Test = "example"; 14 | let envVars = getEnvironmentVariables({ 15 | input: "--", 16 | output: "--", 17 | bundler: BundlerType.local 18 | }); 19 | expect(envVars).toHaveProperty("Test", "example"); 20 | }); 21 | 22 | 23 | test("Additional Environment Variables", () => { 24 | process.env.Test = "example"; 25 | let envVars = getEnvironmentVariables({ 26 | input: "--", 27 | output: "--", 28 | bundler: BundlerType.local, 29 | developer: { 30 | environment: { 31 | variables: { 32 | "TEST_2": "12foo-bar", 33 | "TEST_3": "true", 34 | "TEST_4": 123432, 35 | "TEST_5": "123432", 36 | } 37 | } 38 | } 39 | }); 40 | expect(envVars).toHaveProperty("Test", "example"); 41 | expect(envVars).toHaveProperty("TEST_2", "12foo-bar"); 42 | expect(envVars).toHaveProperty("TEST_3", true); 43 | expect(envVars).toHaveProperty("TEST_4", 123432); 44 | expect(envVars).toHaveProperty("TEST_5", 123432); 45 | }); 46 | 47 | 48 | test("Non-Existent Environment File", () => { 49 | process.env.Test12 = "Example12"; 50 | process.env.Test13 = "Example13"; 51 | let envVars = getEnvironmentVariables({ 52 | input: "--", 53 | output: "--", 54 | bundler: BundlerType.local, 55 | developer: { 56 | environment: { 57 | variables: { 58 | "Test13": "OVERRIDE", 59 | "FOO": "BAR", 60 | "BAR": "43", 61 | }, 62 | files: [ "/foo/test.env" ] 63 | } 64 | } 65 | }); 66 | expect(envVars).toHaveProperty("Test12", "Example12"); 67 | expect(envVars).toHaveProperty("Test13", "OVERRIDE"); 68 | expect(envVars).toHaveProperty("FOO", "BAR"); 69 | expect(envVars).toHaveProperty("BAR", 43); 70 | }); 71 | 72 | 73 | test("Simple Environment File", () => { 74 | process.env.Test12 = "Example12"; 75 | let envVars = getEnvironmentVariables({ 76 | input: "--", 77 | output: "--", 78 | bundler: BundlerType.local, 79 | developer: { 80 | environment: { 81 | variables: { 82 | "FOO": "BAR" 83 | }, 84 | files: [ 85 | Path.join(DIRNAME, "./tests/test1.env") 86 | ] 87 | } 88 | } 89 | }); 90 | expect(envVars).toHaveProperty("Test12", "Example12"); 91 | expect(envVars).toHaveProperty("FOO", "BAR"); 92 | expect(envVars).toHaveProperty("SINGLE_VARIABLE", "value"); 93 | }); 94 | 95 | 96 | test("Simple Environment File Override", () => { 97 | let envVars = getEnvironmentVariables({ 98 | input: "--", 99 | output: "--", 100 | bundler: BundlerType.local, 101 | developer: { 102 | environment: { 103 | variables: { 104 | "FOO": "BAR", 105 | "SINGLE_VARIABLE": "OVERRIDE" 106 | }, 107 | files: [ 108 | Path.join(DIRNAME, "./tests/test1.env"), 109 | Path.join(DIRNAME, "./tests/test2.env") 110 | ] 111 | } 112 | } 113 | }); 114 | expect(envVars).toHaveProperty("FOO", "BAR"); 115 | expect(envVars).toHaveProperty("SINGLE_VARIABLE", "OVERRIDE"); 116 | expect(envVars).toHaveProperty("VARIABLE_ONE", "value_1"); 117 | expect(envVars).toHaveProperty("VARIABLE_TWO", "value_2"); 118 | }); 119 | 120 | 121 | test("Complex Environment File", () => { 122 | let envVars = getEnvironmentVariables({ 123 | input: "--", 124 | output: "--", 125 | bundler: BundlerType.local, 126 | developer: { 127 | environment: { 128 | files: [ 129 | Path.join(DIRNAME, "./tests/test3.env") 130 | ] 131 | } 132 | } 133 | }); 134 | expect(envVars).toHaveProperty("VARIABLE_ONE", 1); 135 | expect(envVars).toHaveProperty("VARIABLE_TWO", "string"); 136 | expect(envVars).toHaveProperty("VARIABLE_THREE", false); 137 | expect(envVars).not.toHaveProperty("COMMENTED_OUT", "false"); 138 | }); 139 | 140 | }); 141 | 142 | -------------------------------------------------------------------------------- /src/compiler/structure/endpoint/load-functions.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2024 Sellers Industries, Inc. 3 | * distributed under the MIT License 4 | * 5 | * author: Evan Sellers 6 | * date: Fri Apr 19 2024 7 | * file: load-functions.ts 8 | * project: SherpaJS - Module Microservice Platform 9 | * purpose: Get Endpoint Functions 10 | * 11 | */ 12 | 13 | 14 | import { ExportedVariable, Tooling } from "../../utilities/tooling/index.js"; 15 | import { Level, Message } from "../../utilities/logger/model.js"; 16 | import { 17 | EndpointTree, ModuleConfigFile, Segment, 18 | EXPORT_VARIABLES, EXPORT_VARIABLES_METHODS, Method, 19 | FILENAME 20 | } from "../../models.js"; 21 | import { RequestUtilities } from "../../../native/request/utilities.js"; 22 | 23 | 24 | export async function getEndpointFunctions(module:ModuleConfigFile, functionsFilepath:string|undefined, viewFilepath:string|undefined, segments:Segment[]):Promise<{ logs:Message[], endpoints?:EndpointTree }> { 25 | let logs:Message[] = []; 26 | let variables:ExportedVariable[] = []; 27 | let hasView:boolean = viewFilepath != undefined; 28 | 29 | if (functionsFilepath != undefined) { 30 | variables = await Tooling.getExportedVariables(functionsFilepath); 31 | logs.push(...validateExports(functionsFilepath, variables, hasView)); 32 | if (!hasView && !hasExportMethodHandlers(variables)) { 33 | return { logs }; 34 | } 35 | } 36 | 37 | if (hasView) { 38 | variables.push({ name: Method.GET }); 39 | } 40 | 41 | return { 42 | logs: logs, 43 | endpoints: { 44 | ".": { 45 | filepath: functionsFilepath as string, 46 | viewFilepath: viewFilepath, 47 | methods: getExportedMethods(variables), 48 | module: module, 49 | segments: segments, 50 | path: RequestUtilities.getDynamicURL(segments) 51 | } 52 | } 53 | } 54 | } 55 | 56 | 57 | function validateExports(filepath:string, variables:ExportedVariable[], hasView:boolean):Message[] { 58 | let logs:Message[] = []; 59 | if (variables.length == 1 && variables[0].name == "default") { 60 | logs.push({ 61 | level: Level.ERROR, 62 | text: `Invalid export "default" cannot be used with a functions endpoint.`, 63 | content: `You might be trying to use a module endpoint, if this is the case, name the file "${FILENAME.ENDPOINT.MODULE}".`, 64 | file: { filepath: filepath } 65 | }); 66 | return logs; 67 | } 68 | for (let variable of variables) { 69 | if (!EXPORT_VARIABLES.includes(variable.name)) { 70 | logs.push({ 71 | level: Level.WARN, 72 | text: `Invalid Export "${variable.name}" will be ignored.`, 73 | content: `The only valid exports are: "${EXPORT_VARIABLES.join("\", \"")}".`, 74 | file: { filepath: filepath } 75 | }); 76 | } 77 | if (hasView && variable.name == Method.GET) { 78 | logs.push({ 79 | level: Level.ERROR, 80 | text: `Invalid Export "GET" cannot be used with a view.`, 81 | file: { filepath: filepath } 82 | }); 83 | } 84 | } 85 | if (!hasView && !hasExportMethodHandlers(variables)) { 86 | logs.push({ 87 | level: Level.WARN, 88 | text: "No Valid Exports. No route will be generated.", 89 | content: `The only valid exports are: "${EXPORT_VARIABLES.join("\", \"")}".`, 90 | file: { filepath: filepath } 91 | }); 92 | } 93 | return logs; 94 | } 95 | 96 | 97 | function getExportedMethods(variables:ExportedVariable[]):Method[] { 98 | return variables.filter(variable => { 99 | return EXPORT_VARIABLES_METHODS.includes(variable.name); 100 | }).map(variable => Method[variable.name as keyof typeof Method]); 101 | } 102 | 103 | 104 | function hasExportMethodHandlers(variables:ExportedVariable[]):boolean { 105 | return variables.some((variable) => EXPORT_VARIABLES_METHODS.includes(variable.name)); 106 | } 107 | 108 | 109 | // In the same way, faith by itself, if it is not accompanied by action, is dead. 110 | // - James 2:17 111 | -------------------------------------------------------------------------------- /src/internal/request/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2024 Sellers Industries, Inc. 3 | * distributed under the MIT License 4 | * 5 | * author: Evan Sellers 6 | * date: Mon Mar 04 2024 7 | * file: transformer.ts 8 | * project: SherpaJS - Module Microservice Platform 9 | * purpose: Native Request to SherpaJS Request 10 | * 11 | */ 12 | 13 | 14 | import { IRequest } from "../../native/request/index.js"; 15 | import { Body, BodyType } from "../../native/model.js"; 16 | import { Headers } from "../../native/headers/index.js"; 17 | import { Parameters } from "../../native/parameters/index.js"; 18 | import { IncomingMessage as LocalRequest } from "http"; 19 | import { Method, Segment } from "../../compiler/models.js"; 20 | import { OriginURL } from "../../native/url/index.js"; 21 | type VercelRequest = Request; 22 | 23 | 24 | export async function RequestLocal(req:LocalRequest, segments:Segment[]):Promise { 25 | if (!req.url || !req.method) { 26 | throw new Error("Missing URL and Methods"); 27 | } 28 | let headers = new Headers(req.headers as HeadersInit); 29 | let { body, bodyType } = await parseBodyLocal(req, headers); 30 | return { 31 | url: new OriginURL(req.url).pathname, 32 | params: { 33 | path: Parameters.getPathParams(req.url, segments), 34 | query: Parameters.getQueryParams(req.url), 35 | }, 36 | method: req.method.toUpperCase() as keyof typeof Method, 37 | headers: headers, 38 | body: body, 39 | bodyType: bodyType 40 | } 41 | } 42 | 43 | 44 | async function parseBodyLocal(req:LocalRequest, headers:Headers):Promise<{ body:Body, bodyType:BodyType }> { 45 | return new Promise((resolve) => { 46 | if ((req.method as string).toUpperCase() == Method.GET) { 47 | resolve({ body: undefined, bodyType: BodyType.None }); 48 | return; 49 | } 50 | 51 | let body:Body = ""; 52 | let bodyType = BodyType.Text; 53 | 54 | req.on("data", (chunk: Buffer) => { 55 | body += chunk.toString(); 56 | }); 57 | 58 | req.on("end", () => { 59 | let contentType = (headers.get("Content-Type") || "").toLowerCase(); 60 | if (!contentType || body == "") { 61 | resolve({ 62 | body: undefined, 63 | bodyType: BodyType.None 64 | }); 65 | } 66 | 67 | if (contentType == "application/json") { 68 | body = JSON.parse(body as string); 69 | bodyType = BodyType.JSON; 70 | } 71 | 72 | resolve({ 73 | body, 74 | bodyType 75 | }); 76 | }); 77 | 78 | req.on("error", () => { 79 | resolve({ body: undefined, bodyType: BodyType.None }); 80 | }); 81 | }); 82 | } 83 | 84 | 85 | export async function RequestVercel(req:VercelRequest, segments:Segment[]):Promise { 86 | let headers = new Headers(req.headers); 87 | let { body, bodyType } = await parseBodyVercel(req, headers); 88 | return { 89 | url: new OriginURL(req.url).pathname, 90 | params: { 91 | path: Parameters.getPathParams(req.url, segments), 92 | query: Parameters.getQueryParams(req.url), 93 | }, 94 | method: req.method.toUpperCase() as keyof typeof Method, 95 | headers: headers, 96 | body: body, 97 | bodyType: bodyType 98 | } 99 | } 100 | 101 | 102 | async function parseBodyVercel(req:VercelRequest, headers:Headers):Promise<{ body:Body, bodyType:BodyType }> { 103 | if (req.method.toUpperCase() == Method.GET) { 104 | return { body: undefined, bodyType: BodyType.None }; 105 | } 106 | let contentType = (headers.get("Content-Type") || "").toLowerCase(); 107 | if (!contentType) { 108 | return { body: undefined, bodyType: BodyType.None }; 109 | } 110 | if (contentType == "application/json") { 111 | return { body: await req.json(), bodyType: BodyType.JSON }; 112 | } 113 | return { body: await req.text(), bodyType: BodyType.Text }; 114 | } 115 | 116 | 117 | 118 | // Therefore I tell you, whatever you ask for in prayer, believe that you have 119 | // received it, and it will be yours. 120 | // - Mark 11:24 121 | -------------------------------------------------------------------------------- /src/compiler/models.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2024 Sellers Industries, Inc. 3 | * distributed under the MIT License 4 | * 5 | * author: Evan Sellers 6 | * date: Sun Feb 11 2024 7 | * file: models.ts 8 | * project: SherpaJS - Module Microservice Platform 9 | * purpose: Models 10 | * 11 | */ 12 | 13 | 14 | import { BuildOptions as ESBuildOptions } from "esbuild"; 15 | 16 | 17 | export const EXPORT_VARIABLES_METHODS = ["GET", "POST", "PATCH", "DELETE", "PUT"]; 18 | export const EXPORT_VARIABLES = [...EXPORT_VARIABLES_METHODS]; 19 | 20 | 21 | export const FILENAME = { 22 | "CONFIG": { 23 | "MODULE": "sherpa.module", 24 | "SERVER": "sherpa.server" 25 | }, 26 | "ENDPOINT": { 27 | "MODULE": "module", 28 | "FUNCTIONS": "index", 29 | "VIEW": "view" 30 | } 31 | } 32 | 33 | 34 | export const FILE_EXTENSIONS = { 35 | "CONFIG": { 36 | "MODULE": ["JS", "TS"], 37 | "SERVER": ["JS", "TS"] 38 | }, 39 | "ENDPOINT": { 40 | "MODULE": ["JS", "TS"], 41 | "FUNCTIONS": ["JS", "TS"], 42 | "VIEW": ["HTML"] 43 | } 44 | }; 45 | 46 | 47 | export type Context = unknown; 48 | 49 | 50 | export type ServerConfig = (T extends undefined ? { 51 | context?: unknown 52 | } : { 53 | context: T 54 | }); 55 | 56 | 57 | export type ServerConfigFile = { 58 | filepath:string; 59 | instance:ServerConfig; 60 | }; 61 | 62 | 63 | export interface ModuleInterface { 64 | context:Schema; 65 | } 66 | 67 | 68 | export class CreateModuleInterface implements ModuleInterface { 69 | context:Schema; 70 | constructor(context:Schema) { this.context = context; } 71 | } 72 | 73 | 74 | export type InstantiableModuleInterface, Schema> = { 75 | new (context:Schema):Interface; 76 | }; 77 | 78 | 79 | export type ModuleConfig, Schema> = { 80 | name:string; 81 | interface:InstantiableModuleInterface; 82 | }; 83 | 84 | 85 | export type ModuleLoader, Schema> = ModuleConfig & { 86 | load:(context:Schema) => Interface; 87 | }; 88 | 89 | 90 | export type ModuleConfigFile = { 91 | entry:string; 92 | filepath:string; 93 | instance:ModuleConfig, unknown>; 94 | context:Context; 95 | contextFilepath:string; 96 | }; 97 | 98 | 99 | export enum Method { 100 | GET="GET", 101 | PUT="PUT", 102 | POST="POST", 103 | PATCH="PATCH", 104 | DELETE="DELETE" 105 | } 106 | 107 | 108 | export type Segment = { 109 | name:string; 110 | isDynamic:boolean; 111 | } 112 | 113 | 114 | export type Endpoint = { 115 | filepath:string; 116 | viewFilepath?:string; 117 | methods:Method[]; 118 | module:ModuleConfigFile; 119 | segments:Segment[]; 120 | path:string; 121 | } 122 | 123 | 124 | export type EndpointTree = { 125 | [key:string]:EndpointTree|Endpoint; 126 | } 127 | 128 | 129 | export type EndpointStructure = { 130 | list:Endpoint[]; 131 | tree:EndpointTree; 132 | } 133 | 134 | 135 | export type Asset = { 136 | filepath:string; 137 | filename:string; 138 | segments:Segment[]; 139 | path:string; 140 | } 141 | 142 | 143 | export type AssetTree = { 144 | [key:string]:AssetTree|Asset[]; 145 | } 146 | 147 | 148 | export type AssetStructure = { 149 | list:Asset[]; 150 | tree:AssetTree; 151 | } 152 | 153 | 154 | export type Structure = { 155 | endpoints?:EndpointStructure; 156 | assets?:AssetStructure; 157 | server?:ServerConfigFile; 158 | } 159 | 160 | 161 | export enum BundlerType { 162 | Vercel = "Vercel", 163 | local = "local", 164 | } 165 | 166 | 167 | export type EnvironmentVariables = { [key:string]:string|number|boolean }; 168 | 169 | 170 | export type BuildOptions = { 171 | input:string; 172 | output:string; 173 | bundler:BundlerType; 174 | developer?:{ 175 | bundler?:{ 176 | esbuild?:Partial; 177 | }, 178 | environment?:{ 179 | variables?:EnvironmentVariables, 180 | files?:string[] 181 | } 182 | } 183 | } 184 | 185 | 186 | // Because you know that the testing of your faith produces perseverance. 187 | // - James 1:3 188 | -------------------------------------------------------------------------------- /src/native/response/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2024 Sellers Industries, Inc. 3 | * distributed under the MIT License 4 | * 5 | * author: Evan Sellers 6 | * date: Tue Mar 19 2024 7 | * file: index.ts 8 | * project: SherpaJS - Module Microservice Platform 9 | * purpose: Response builder 10 | * 11 | */ 12 | 13 | 14 | import { Headers, HeadersInit } from "../headers/index.js"; 15 | import { Body, BodyType, CONTENT_TYPE } from "../model.js"; 16 | import { STATUS_TEXT } from "./status-text.js"; 17 | 18 | 19 | export interface Options { 20 | headers:HeadersInit; 21 | status:number; 22 | } 23 | 24 | 25 | export class IResponse { 26 | 27 | readonly status:number; 28 | readonly statusText:string; 29 | readonly headers:Headers; 30 | 31 | readonly body:Body; 32 | readonly bodyType:keyof typeof BodyType; 33 | 34 | 35 | constructor(options?:Partial) { 36 | let _options = IResponse.defaultOptions(BodyType.None, options); 37 | this.status = _options.status; 38 | this.statusText = IResponse.getStatusText(_options.status); 39 | this.headers = _options.headers; 40 | this.body = undefined; 41 | this.bodyType = BodyType.None; 42 | } 43 | 44 | 45 | static new(options?:Partial):IResponse { 46 | return new IResponse(options); 47 | } 48 | 49 | 50 | static text(text:T, options?:Partial):IResponse { 51 | let _options = IResponse.defaultOptions(BodyType.Text, options); 52 | return { 53 | status: _options.status, 54 | statusText: IResponse.getStatusText(_options.status), 55 | headers: _options.headers, 56 | body: text.toString(), 57 | bodyType: BodyType.Text 58 | } 59 | } 60 | 61 | 62 | static JSON }>(JSON:T|Record, options?:Partial):IResponse { 63 | let _options = IResponse.defaultOptions(BodyType.JSON, options); 64 | let _isCallable = JSON.toJSON && typeof (JSON as Record).toJSON === "function"; 65 | return { 66 | status: _options.status, 67 | statusText: IResponse.getStatusText(_options.status), 68 | headers: _options.headers, 69 | body: _isCallable ? (JSON as { toJSON():Record }).toJSON() : JSON, 70 | bodyType: BodyType.JSON 71 | } 72 | } 73 | 74 | 75 | static HTML(html:string, options?:Partial):IResponse { 76 | let _options = IResponse.defaultOptions(BodyType.HTML, options); 77 | return { 78 | status: _options.status, 79 | statusText: IResponse.getStatusText(_options.status), 80 | headers: _options.headers, 81 | body: html, 82 | bodyType: BodyType.HTML 83 | } 84 | } 85 | 86 | 87 | static redirect(redirect:string):IResponse { 88 | let _options = IResponse.defaultOptions(BodyType.None, {}); 89 | if (!_options.headers.has("Location")) { 90 | _options.headers.set("Location", redirect); 91 | } 92 | return { 93 | status: 302, 94 | statusText: IResponse.getStatusText(302), 95 | headers: _options.headers, 96 | body: undefined, 97 | bodyType: BodyType.None 98 | } 99 | } 100 | 101 | 102 | private static defaultOptions(bodyType:BodyType, options?:Partial):{ status:number, headers:Headers } { 103 | let _options = { 104 | status: options?.status || 200, 105 | headers: new Headers(options?.headers || {}) 106 | }; 107 | if (!_options.headers.has("Content-Type")) { 108 | _options.headers.set("Content-Type", CONTENT_TYPE[bodyType] as string); 109 | } 110 | return _options; 111 | } 112 | 113 | 114 | private static getStatusText(status:number):string { 115 | let text = STATUS_TEXT[status]; 116 | if (!text) { 117 | throw new Error(`Status code "${status}" is invalid.`); 118 | } 119 | return text; 120 | } 121 | 122 | } 123 | 124 | 125 | // I write these things to you who believe in the name of the Son of God so 126 | // that you may know that you have eternal life. 127 | // - 1 John 5:13 128 | -------------------------------------------------------------------------------- /src/server-local/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2024 Sellers Industries, Inc. 3 | * distributed under the MIT License 4 | * 5 | * author: Evan Sellers 6 | * date: Tue Mar 19 2024 7 | * file: index.ts 8 | * project: SherpaJS - Module Microservice Platform 9 | * purpose: Local Server 10 | * 11 | */ 12 | 13 | 14 | import { 15 | IncomingMessage, ServerResponse, 16 | Server as HTTPServer, createServer 17 | } from "http"; 18 | import fs from "fs"; 19 | import { cyan, green, bold, magenta, gray, red } from "colorette" 20 | import { OriginURL } from "../native/url/index.js"; 21 | 22 | 23 | type handler = (request:IncomingMessage, response:ServerResponse) => Promise|undefined|Promise|void; 24 | type endpoint = { 25 | url:RegExp; 26 | handler:handler; 27 | }; 28 | 29 | 30 | export class ServerLocal { 31 | 32 | private readonly port: number; 33 | private readonly silentStartup:boolean; 34 | private server: HTTPServer|null; 35 | private endpoints:endpoint[]; 36 | 37 | 38 | constructor(port:number, silentStartup:boolean=false) { 39 | this.endpoints = []; 40 | this.port = port; 41 | this.server = null; 42 | this.silentStartup = silentStartup; 43 | } 44 | 45 | 46 | public start():void { 47 | if (this.server) { 48 | throw new Error("Server is already running"); 49 | } 50 | 51 | this.server = createServer(this.handleRequest.bind(this)); 52 | this.server.listen(this.port, () => { 53 | if (!this.silentStartup) { 54 | console.log(`${green("SherpaJS Server is started at")} ${cyan(`http://localhost:${bold(this.port)}`)}${green(".")}`); 55 | } 56 | }); 57 | } 58 | 59 | 60 | public stop():void { 61 | if (!this.server) { 62 | throw new Error("Server is not running"); 63 | } 64 | 65 | this.server.close(() => { 66 | console.log("Server has stopped"); 67 | }); 68 | this.server = null; 69 | } 70 | 71 | 72 | public addEndpoint(url:string, handler:handler):void { 73 | this.endpoints.push({ 74 | url: this.convertDynamicSegments(url), 75 | handler: handler 76 | }); 77 | } 78 | 79 | 80 | public addAsset(url:string, filepath:string):void { 81 | this.endpoints.push({ 82 | url: this.convertDynamicSegments(url), 83 | handler: (req:IncomingMessage, res:ServerResponse) => { 84 | fs.readFile(filepath, (error, data) => { 85 | if (error) { 86 | throw error; 87 | } 88 | res.writeHead(200); 89 | res.end(data); 90 | }); 91 | } 92 | }); 93 | } 94 | 95 | 96 | private convertDynamicSegments(url:string):RegExp { 97 | return new RegExp("^/" + url.replace(/\[([^/]+?)\]/g, () => { 98 | return `([^/]+)` 99 | }) + "(/)?$"); 100 | } 101 | 102 | 103 | private async handleRequest(req:IncomingMessage, res:ServerResponse):Promise { 104 | let url = req.url; 105 | if (!url) { 106 | res.writeHead(400); 107 | res.end(); 108 | return; 109 | } 110 | 111 | let endpoint = this.getEndpoint(url); 112 | if (!endpoint) { 113 | res.writeHead(404); 114 | res.end(); 115 | return; 116 | } 117 | 118 | let startTime = Date.now(); 119 | await endpoint.handler(req, res); 120 | let deltaTime = Date.now() - startTime; 121 | let method = req.method ? req.method.toUpperCase() : "UNKNOWN"; 122 | console.log(`${magenta(method)} ${req.url} ${res.statusCode} ${gray("in")} ${red(deltaTime)}${red("ms")}`); 123 | } 124 | 125 | 126 | private getEndpoint(url:string):endpoint|undefined { 127 | let _url = new OriginURL(url).pathname; 128 | for (let endpoint of this.endpoints) { 129 | if (endpoint.url.test(_url)) { 130 | return endpoint; 131 | } 132 | } 133 | return undefined; 134 | } 135 | 136 | 137 | } 138 | 139 | 140 | // Whoever has the Son has life; whoever does not have the Son of God does 141 | // not have life. 142 | // - 1 John 5:12 143 | -------------------------------------------------------------------------------- /docs/api/components/request.mdx: -------------------------------------------------------------------------------- 1 | # Request 2 | The `Request` class represents an HTTP request with various properties such 3 | as URL, parameters, method, headers, body, and body type. This object is passed 4 | to an endpoint when a request is made. 5 | 6 | 7 |
8 | 9 | 10 | ## Properties 11 | 12 | ### url 13 | * Type - `string` *(readonly)* 14 | * Description - The URL of the HTTP request. 15 | 16 | 17 |
18 | 19 | 20 | ### params 21 | * Type - `{ path: Parameters, query: Parameters }` *(readonly)* 22 | * Description - An object containing `path` and `query` parameters represented 23 | by instances of the [Parameters class](/api/components/parameters). 24 | 25 | 26 |
27 | 28 | 29 | ### method 30 | * Type - `keyof typeof Method` *(readonly)* 31 | * Description - The HTTP method of the request, represented as one of the keys 32 | of the [Method](/api/components/request#method-1) enum. 33 | 34 | 35 |
36 | 37 | 38 | ### headers 39 | * Type - `Headers` *(readonly)* 40 | * Description - The headers of the request, represented by an instance 41 | of the [Headers class](/api/components/headers). 42 | 43 | 44 |
45 | 46 | 47 | ### body 48 | * Type - `Body` *(readonly)* 49 | * Description - The body of the request, which can be `undefined`, a string, 50 | or a JSON object. The body will be automatically parsed from the request. 51 | 52 | 53 |
54 | 55 | 56 | ### bodyType 57 | * Type - `keyof typeof BodyType` *(readonly)* 58 | * Description: The type of the request body, represented as one of the keys of 59 | the [BodyType](/api/components/request#bodytypes-1) enum. 60 | 61 | 62 |
63 | 64 | 65 | ## Enums 66 | 67 | ### BodyType 68 | An enum representing the possible types of the request body: 69 | * `JSON` - The body is in JSON format. 70 | * `Text` - The body is in plain text format. 71 | * `HTML` - The body is in HTML format. 72 | * `None` - There is no body associated with the request. 73 | 74 | 75 |
76 | 77 | 78 | ### Method 79 | An enum representing the HTTP method type. 80 | * `GET` - HTTP [GET method](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/GET) request 81 | * `PUT` - HTTP [PUT method](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/PUT) request 82 | * `POST` - HTTP [POST method](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST) request 83 | * `PATCH` - HTTP [PATCH method](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/PATCH) request 84 | * `DELETE` - HTTP [DELETE method](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/DELETE) request 85 | 86 | 87 |
88 | 89 | 90 | ## Examples 91 | The following examples, illustrate how the `Request` object will be passed to 92 | an endpoint. 93 | 94 | 95 | ### Basic GET Request 96 | `GET /regular` \ 97 | `GET /regular?foo=1&foo=true,example` 98 | ```json 99 | { 100 | "url": "/regular", 101 | "params": { 102 | "path": {}, 103 | "query": { 104 | "foo": [ 105 | 1, 106 | true, 107 | "example" 108 | ] 109 | } 110 | }, 111 | "method": "GET", 112 | "headers": { 113 | "content-type": "application/json", 114 | "user-agent": "PostmanRuntime/7.37.3", 115 | "accept": "*/*", 116 | "postman-token": "62c2eb4b-8e06-4bbe-9c21-ed7755f08aeb", 117 | "host": "localhost:3000", 118 | "accept-encoding": "gzip, deflate, br", 119 | "connection": "keep-alive", 120 | "content-length": "18" 121 | }, 122 | "bodyType": "None" 123 | } 124 | ``` 125 | 126 | 127 |
128 | 129 | 130 | ### Dynamic POST Request 131 | `POST /user/[userID]` \ 132 | `POST /user/bob` 133 | ```json 134 | { 135 | "url": "/regular", 136 | "params": { 137 | "path": { 138 | "userID": "bob" 139 | }, 140 | "query": {} 141 | }, 142 | "method": "POST", 143 | "headers": { 144 | "content-type": "application/json", 145 | "user-agent": "PostmanRuntime/7.37.3", 146 | "accept": "*/*", 147 | "postman-token": "62c2eb4b-8e06-4bbe-9c21-ed7755f08aeb", 148 | "host": "localhost:3000", 149 | "accept-encoding": "gzip, deflate, br", 150 | "connection": "keep-alive", 151 | "content-length": "18" 152 | }, 153 | "bodyType": "JSON", 154 | "body": { 155 | "example": "hello world" 156 | } 157 | } 158 | ``` 159 | 160 | -------------------------------------------------------------------------------- /src/compiler/utilities/tooling/type-validation/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2024 Sellers Industries, Inc. 3 | * distributed under the MIT License 4 | * 5 | * author: Evan Sellers 6 | * date: Mon Mar 04 2024 7 | * file: index.ts 8 | * project: SherpaJS - Module Microservice Platform 9 | * purpose: Validate Typescript 10 | * 11 | */ 12 | 13 | import fs from "fs"; 14 | import ts from "typescript"; 15 | import checksum from "checksum"; 16 | import { KV as KVStorage } from "../../kv/index.js"; 17 | import { Path } from "../../path/index.js"; 18 | import { Level, Message } from "../../logger/model.js"; 19 | const KV = new KVStorage("TYPE-VALIDATION"); 20 | 21 | 22 | export async function typeValidation(filepath:string, fileTypeName:string):Promise { 23 | let cachedLogs = await getCache(filepath); 24 | if (cachedLogs) { 25 | return cachedLogs; 26 | } 27 | 28 | try { 29 | let logs = processDiagnostics(filepath, fileTypeName, getDiagnostics(filepath)); 30 | setCache(filepath, logs); 31 | return logs; 32 | } catch (error) { 33 | return [{ 34 | level: Level.ERROR, 35 | text: `Failed to parse ${fileTypeName}.`, 36 | content: error.message, 37 | file: { filepath: filepath } 38 | }]; 39 | } 40 | } 41 | 42 | 43 | function getDiagnostics(filepath:string):readonly ts.Diagnostic[] { 44 | let program = ts.createProgram({ 45 | rootNames: [filepath], 46 | options: { 47 | noEmit: true, 48 | lib: ["es2022"] 49 | }, 50 | host: { 51 | ...ts.createCompilerHost({}), 52 | writeFile: () => {} 53 | } 54 | }); 55 | return ts.getPreEmitDiagnostics(program); 56 | } 57 | 58 | 59 | function processDiagnostics(filepath:string, fileTypeName:string, diagnostic:readonly ts.Diagnostic[]):Message[] { 60 | return diagnostic.filter(diagnostic => { 61 | if (!diagnostic.start) { 62 | return false; 63 | } 64 | if (diagnostic.messageText.toString().includes("Cannot find namespace 'WebAssembly'")) { 65 | return false; 66 | } 67 | return true; 68 | }).map((diagnostic):Message => { 69 | let position = getLineNumber(filepath, diagnostic.start as number); 70 | let message = diagnostic.messageText.toString(); 71 | if (diagnostic.relatedInformation && diagnostic.relatedInformation.length > 0) { 72 | let context = diagnostic.relatedInformation[0].messageText; 73 | message += " " + context.toString(); 74 | } 75 | return { 76 | level: Level.WARN, 77 | text: `${fileTypeName} Type Error - ${message}`, 78 | file: { 79 | filepath: filepath, 80 | line: position.line, 81 | character: position.character 82 | } 83 | }; 84 | }); 85 | } 86 | 87 | 88 | function getLineNumber(filepath:string, position:number):ts.LineAndCharacter { 89 | let buffer = fs.readFileSync(filepath, "utf8"); 90 | let source = ts.createSourceFile(filepath, buffer, ts.ScriptTarget.Latest); 91 | return source.getLineAndCharacterOfPosition(position); 92 | } 93 | 94 | 95 | async function setCache(filepath:string, logs:Message[]):Promise { 96 | KV.set(getCacheKey(filepath), { 97 | filepath: filepath, 98 | checksum: await getChecksum(filepath), 99 | logs: logs 100 | }); 101 | } 102 | 103 | 104 | async function getCache(filepath:string):Promise { 105 | let key = getCacheKey(filepath); 106 | if (KV.has(key)) { 107 | let data = KV.get(key) as { filepath:string, checksum: string, logs: Message[] }; 108 | if (data.checksum == await getChecksum(filepath)) { 109 | return data.logs; 110 | } 111 | } 112 | return undefined; 113 | } 114 | 115 | 116 | function getCacheKey(filepath:string):string { 117 | return btoa(Path.unix(filepath)); 118 | } 119 | 120 | 121 | async function getChecksum(filepath:string):Promise { 122 | return new Promise((resolve) => { 123 | checksum.file(filepath, { algorithm: "sha1" }, (error, hash) => { 124 | resolve(hash); 125 | }); 126 | }); 127 | 128 | } 129 | 130 | 131 | // Because you know that the testing of your faith produces perseverance. 132 | // - James 1:3 133 | -------------------------------------------------------------------------------- /docs/build/building-a-module.mdx: -------------------------------------------------------------------------------- 1 | # Building a Module 2 | Building a SherpaJS module is straightforward and can be accomplished in just a 3 | few steps. Modules allow you to encapsulate and share functionality across 4 | multiple SherpaJS applications. Here's how to get started: 5 | 6 | 7 |
8 | 9 | 10 | ## Quick Installation 11 | We recommend starting with the 12 | [SherpaJS Module Template](https://github.com/sellersindustry/SherpaJS-template-module), 13 | which provides a pre-configured structure and example endpoints. After 14 | downloading the template, install all dependencies by running: 15 | 16 | ```sh 17 | npm install 18 | ``` 19 | 20 | You can start the development server with `npm run dev`. 21 | 22 | Now your project is all setup, explore around the project files and get 23 | acquainted with the [SherpaJS Project Structure](/structure) and 24 | [SherpaJS CLI](/api/cli). 25 | 26 | 27 |
28 | 29 | 30 | ## Manual Installation 31 | Creating a new module is extremely easy and can be done within a couple of 32 | minutes. To manually create a new module start by create a new NodeJS project 33 | with `npm init`. Then install the SherpaJS Core package: 34 | 35 | ```sh 36 | npm install sherpa-core 37 | ``` 38 | 39 | 40 |
41 | 42 | 43 | 44 | ### Updating `package.json` 45 | Open the `package.json` file in the root directory of your project, and 46 | update the following properties. 47 | 48 | ```json title="package.json" 49 | { 50 | "type": "module", 51 | "exports": "./sherpa.module.ts", 52 | "scripts": { 53 | "build": "sherpa build -b Vercel", 54 | "build-local": "sherpa build -b local", 55 | "start": "sherpa start", 56 | "dev": "sherpa dev", 57 | "test": "echo \"no test specified\"" 58 | } 59 | } 60 | ``` 61 | 62 | 63 |
64 | 65 | ### Creating Module Config 66 | Create a [module config](/build/module-config) file named `sherpa.module.ts` in 67 | the root directory of your module. This file will default export a module 68 | configuration. 69 | 70 | ```typescript title="sherpa.module.ts" 71 | import { SherpaJS, CreateModuleInterface } from "sherpa-core"; 72 | 73 | export type Schema = { foo: boolean, bar: string }; 74 | 75 | export default SherpaJS.New.module({ 76 | name: "example-module", 77 | interface: CreateModuleInterface() 78 | }); 79 | ``` 80 | 81 | [Learn more about module configs](/build/module-config). 82 | 83 | 84 |
85 | 86 | 87 | ### Creating Server Config 88 | Create a new Typescript file for your [server configuration](/build/server-config) 89 | in the root directory of your project named `sherpa.server.ts`. *Technically a 90 | server config is not required to create a module, but it is to test the module*. 91 | 92 | ```typescript title="sherpa.server.ts" 93 | import { SherpaJS } from "sherpa-core"; 94 | import { Schema } from "../sherpa.module.ts"; 95 | 96 | export default SherpaJS.New.server({ 97 | context: { 98 | example: "foo" 99 | } 100 | }); 101 | ``` 102 | 103 | The context you provide the server should match the context schema for your module. 104 | 105 | 106 | ### Creating Endpoints 107 | SherpaJS uses a directory-based structure for routing, similar to NextJS. Start 108 | by creating a `/routes` directory and place a new enpoint `index.ts` file in the 109 | directory. This will be processed at the root `/` of your application. 110 | 111 | ```typescript title="routes/index.ts" 112 | import { Request, Response } from "sherpa-core"; 113 | import { Schema } from "../sherpa.module.ts" 114 | 115 | export function GET(request:Request, context:Schema) { 116 | return Response.text("Hello World!"); 117 | } 118 | ``` 119 | 120 | Learn more about [routing](/build/rounting) and [endpoints](/build/endpoints). 121 | 122 | 123 |
124 | 125 | 126 | ### Details 127 | While the routes of your module are available on a server when the module is 128 | loaded, other assets like the `public` directory are not available. Keep this 129 | in mind when designing your module. 130 | 131 | 132 |
133 | 134 | 135 | ### Share Your Module 136 | Share your module with the world by deploying it as an NPM package and 137 | submitting a 138 | [new issue](https://github.com/sellersindustry/SherpaJS/issues/new/choose) 139 | to get it listed as a SherpaJS Community module. 140 | 141 | **Important Notes** 142 | * Ensure your module is deployed as an NPM package. 143 | * Include documentation on how to set it up, including required properties. 144 | * Link to the SherpaJS documentation for users to understand how to set it up. 145 | 146 | Thank you for supporting SherpaJS! 🎉🥳 147 | 148 | -------------------------------------------------------------------------------- /tests/endpoints/suite/suite.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2024 Sellers Industries, Inc. 3 | * distributed under the MIT License 4 | * 5 | * author: Evan Sellers 6 | * date: Thu May 16 2024 7 | * file: suite.ts 8 | * project: SherpaJS - Module Microservice Platform 9 | * purpose: Endpoint Test Suite - Suite 10 | * 11 | */ 12 | 13 | 14 | import { Tester } from "./tester.js"; 15 | import { BenchOptions, TestOptions, TestResults } from "./model.js"; 16 | import { bold, green, red, gray } from "colorette"; 17 | import { Bench } from "./bench.js"; 18 | 19 | 20 | export class Suite { 21 | 22 | 23 | private tests:Tester[] = []; 24 | private benches:Bench[] = []; 25 | private results:{ [name:string]:TestResults[] } = {}; 26 | 27 | 28 | test(name:string, options:TestOptions):Tester { 29 | let test = new Tester( 30 | name, 31 | options.method, 32 | options.path, 33 | options.body 34 | ); 35 | this.tests.push(test); 36 | return test; 37 | } 38 | 39 | 40 | bench(name:string, options:BenchOptions):Bench { 41 | let bench = new Bench( 42 | name, 43 | options.host, 44 | options.start, 45 | options.setup || [], 46 | options.teardown || [] 47 | ); 48 | this.benches.push(bench); 49 | return bench; 50 | } 51 | 52 | 53 | async run() { 54 | for await (const bench of this.benches) { 55 | if (this.results[bench.getName()]) { 56 | throw new Error(`Test bench name duplicate: "${bench.getName()}"`); 57 | } 58 | this.results[bench.getName()] = []; 59 | 60 | await bench.setup(); 61 | await bench.start(); 62 | for await (const test of this.tests) { 63 | this.results[bench.getName()].push(await test.invoke(bench.getHost())); 64 | } 65 | await bench.teardown(); 66 | } 67 | 68 | this.display(); 69 | } 70 | 71 | 72 | private display() { 73 | for (let bench of this.benches) { 74 | let _passed = this.results[bench.getName()].filter((result) => result.success).length; 75 | let _failed = this.results[bench.getName()].filter((result) => !result.success).length; 76 | let _total = this.results[bench.getName()].length; 77 | 78 | console.log("\n============ " + bold(bench.getName()) + " ============="); 79 | 80 | for (let test of this.results[bench.getName()]) { 81 | if (test.success) { 82 | console.log(`${green("√")} ${gray(test.name)}`); 83 | } else { 84 | console.log(`${red("×")} ${gray(test.name)}`); 85 | if (test.message) { 86 | console.log(` ${red(test.message)}\n`); 87 | } 88 | if (test.stack) { 89 | for (let line of test.stack.split("\n")) { 90 | console.log(` ${gray(line)}`); 91 | } 92 | } 93 | console.log(""); 94 | } 95 | } 96 | 97 | console.log(`${bold("Tests:")} ${green(`${_passed} passed,`)} ${red(`${_failed} failed,`)} total ${_total}`); 98 | } 99 | 100 | let passed = 0; 101 | let failed = 0; 102 | let total = 0; 103 | 104 | console.log("\n============ " + bold("Overview") + " ============="); 105 | for (let bench of this.benches) { 106 | let _passed = this.results[bench.getName()].filter((result) => result.success).length; 107 | let _failed = this.results[bench.getName()].filter((result) => !result.success).length; 108 | let _total = this.results[bench.getName()].length; 109 | passed += _passed; 110 | failed += _failed; 111 | total += _total; 112 | console.log(`${bold(`${bench.getName()}:`)} ${green(`${_passed} passed,`)} ${red(`${_failed} failed,`)} total ${_total}`); 113 | } 114 | console.log(`${bold("All Tests:")} ${green(`${passed} passed,`)} ${red(`${failed} failed,`)} total ${total}`); 115 | if (failed > 0) { 116 | process.exit(1); 117 | } 118 | } 119 | 120 | 121 | } 122 | 123 | 124 | // Wives, submit yourselves to your own husbands as you do to the Lord. For the 125 | // husband is the head of the wife as Christ is the head of the church, his 126 | // body, of which he is the Savior. 127 | // - Ephesians 5:22-23 128 | -------------------------------------------------------------------------------- /src/compiler/bundler/platforms/abstract.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2024 Sellers Industries, Inc. 3 | * distributed under the MIT License 4 | * 5 | * author: Evan Sellers 6 | * date: Sun Feb 11 2024 7 | * file: abstract.ts 8 | * project: SherpaJS - Module Microservice Platform 9 | * purpose: Abstract Bundler 10 | * 11 | */ 12 | 13 | 14 | import fs from "fs"; 15 | import { AssetStructure, BuildOptions, EndpointStructure, ServerConfigFile, Structure } from "../../models.js"; 16 | import { Logger } from "../../utilities/logger/index.js"; 17 | import { Path } from "../../utilities/path/index.js"; 18 | import { Message } from "../../utilities/logger/model.js"; 19 | import { RequestUtilities } from "../../../native/request/utilities.js"; 20 | 21 | 22 | export type View = { 23 | html:string, 24 | filepath:string, 25 | }; 26 | 27 | 28 | export abstract class Bundler { 29 | 30 | 31 | protected options:BuildOptions; 32 | protected assets:AssetStructure; 33 | protected endpoints:EndpointStructure; 34 | protected sever:ServerConfigFile; 35 | protected views:(View|undefined)[]; 36 | protected errors:Message[]|undefined; 37 | 38 | 39 | constructor(endpoints:Structure, options:BuildOptions, errors?:Message[]) { 40 | this.endpoints = endpoints.endpoints as EndpointStructure; 41 | this.assets = endpoints.assets as AssetStructure; 42 | this.sever = endpoints.server as ServerConfigFile; 43 | this.options = options; 44 | this.errors = errors; 45 | } 46 | 47 | 48 | getFilepath():string { 49 | return Path.join(this.options.output, ".sherpa"); 50 | } 51 | 52 | 53 | getFilepathAssets():string { 54 | return Path.join(this.getFilepath(), "public"); 55 | } 56 | 57 | 58 | async build() { 59 | await this.clean(); 60 | this.createBuildDirectory(); 61 | this.createManifest(); 62 | this.createViews(); 63 | this.createAssets(); 64 | } 65 | 66 | 67 | async clean():Promise { 68 | try { 69 | if (fs.existsSync(this.getFilepath())) { 70 | return new Promise((resolve) => { 71 | fs.rm(this.getFilepath(), { recursive: true }, (error) => { 72 | if (error) { 73 | throw error; 74 | } 75 | resolve(); 76 | }); 77 | }); 78 | } 79 | } catch (error) { 80 | Logger.raise({ 81 | text: "Failed to clean build directory.", 82 | file: { 83 | filepath: this.getFilepath() 84 | } 85 | }); 86 | } 87 | } 88 | 89 | 90 | private createBuildDirectory() { 91 | fs.mkdirSync(this.getFilepath()); 92 | } 93 | 94 | 95 | private createManifest() { 96 | let filepath = Path.join(this.getFilepath(), "sherpa.manifest.json"); 97 | let data = { 98 | created: new Date().toISOString(), 99 | options: this.options, 100 | server: this.sever, 101 | endpoints: this.endpoints, 102 | assets: this.assets, 103 | errors: this.errors 104 | }; 105 | fs.writeFileSync(filepath, JSON.stringify(data, null, 4)); 106 | } 107 | 108 | 109 | private createViews() { 110 | this.views = []; 111 | for (let endpoint of this.endpoints.list) { 112 | if (endpoint.viewFilepath) { 113 | let html = fs.readFileSync(endpoint.viewFilepath, "utf8"); 114 | this.views.push({ html, filepath: endpoint.viewFilepath }); 115 | } else { 116 | this.views.push(undefined); 117 | } 118 | } 119 | } 120 | 121 | 122 | private createAssets() { 123 | let destination = this.getFilepathAssets(); 124 | if (!fs.existsSync(destination)) { 125 | fs.mkdirSync(destination, { recursive: true, }); 126 | } 127 | 128 | for (let asset of this.assets.list) { 129 | let route = RequestUtilities.getDynamicURL(asset.segments); 130 | 131 | let _destination = Path.join(destination, route); 132 | if (!fs.existsSync(_destination)) { 133 | fs.mkdirSync(_destination, { recursive: true, }); 134 | } 135 | 136 | fs.copyFileSync(asset.filepath, Path.join(_destination, asset.filename)); 137 | } 138 | } 139 | 140 | 141 | } 142 | 143 | 144 | // I rejoice in following your statutes as one rejoices in great riches. 145 | // - Psalm 119:14 146 | -------------------------------------------------------------------------------- /src/server-development/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2024 Sellers Industries, Inc. 3 | * distributed under the MIT License 4 | * 5 | * author: Evan Sellers 6 | * date: Mon May 13 2024 7 | * file: index.ts 8 | * project: SherpaJS - Module Microservice Platform 9 | * purpose: Local Development Server 10 | * 11 | */ 12 | 13 | 14 | import fs from "fs"; 15 | import chokidar from "chokidar"; 16 | import { BuildOptions, Compiler } from "../compiler/index.js"; 17 | import { Path } from "../compiler/utilities/path/index.js"; 18 | import { ChildProcessWithoutNullStreams, spawn } from "child_process"; 19 | import { Logger } from "../compiler/utilities/logger/index.js"; 20 | import { cyan, green, red } from "colorette"; 21 | 22 | 23 | export class ServerDevelopment { 24 | 25 | 26 | private readonly options:BuildOptions; 27 | private readonly port:number|undefined; 28 | private server:ChildProcessWithoutNullStreams; 29 | private initial:boolean; 30 | 31 | 32 | constructor (options:BuildOptions, port?:number) { 33 | this.options = options; 34 | this.port = port; 35 | this.initial = true; 36 | this.makeTempDir(); 37 | this.start(); 38 | } 39 | 40 | 41 | private start() { 42 | this.refresh(); 43 | chokidar.watch(this.options.input, { 44 | persistent: true, 45 | ignored: /(^|[/\\])\../, // note: . files are ignored 46 | }).on("change", (path, stats) => { 47 | if (path && stats) { 48 | this.refresh(); 49 | } 50 | }); 51 | } 52 | 53 | 54 | private async refresh() { 55 | if (!this.initial) { 56 | console.log(cyan("SherpaJS detected change, rebuilding...")); 57 | } 58 | 59 | this.removeTempDir(); 60 | this.makeTempDir(); 61 | 62 | let { success, logs } = await Compiler.build({ 63 | ...this.options, 64 | output: this.getTempDir() 65 | }, false); 66 | 67 | if (success) { 68 | this.copyFromTempDir(); 69 | if (this.server) { 70 | this.server.kill(); 71 | } 72 | this.server = spawn("node", [ 73 | Path.join(this.options.output, "/.sherpa/index.js"), 74 | this.port ? this.port.toString() : "", 75 | !this.initial ? "--silent-startup" : "" 76 | ]); 77 | this.server.stdout.on("data", (data) => console.log(data.toString().replace("\n", ""))); 78 | this.server.stderr.on("data", (data) => console.log(data.toString().replace("\n", ""))); 79 | this.server.on("close", (data) => { if (data) console.log(data.toString().replace("\n", "") )}); 80 | if (!this.initial) { 81 | console.log(green("SherpaJS Server Rebuilt Successfully.")); 82 | } else { 83 | this.initial = false; 84 | } 85 | } else { 86 | this.removeTempDir(); 87 | Logger.display(logs); 88 | console.log(red("SherpaJS Failed to Build Server.") + " See logs for more information.") 89 | } 90 | } 91 | 92 | 93 | private copyFromTempDir() { 94 | let filepath = Path.join(this.options.output, ".sherpa"); 95 | if (fs.existsSync(filepath)) { 96 | fs.rmSync(filepath, { recursive: true }); 97 | } 98 | this.copyDirectory( 99 | Path.join(this.getTempDir(), ".sherpa"), 100 | Path.join(this.options.output, ".sherpa") 101 | ) 102 | this.removeTempDir(); 103 | } 104 | 105 | 106 | private copyDirectory(source:string, destination:string) { 107 | if (!fs.existsSync(destination)) { 108 | fs.mkdirSync(destination, { recursive: true }); 109 | } 110 | 111 | fs.readdirSync(source).forEach(file => { 112 | let sourcePath = Path.join(source, file); 113 | let destPath = Path.join(destination, file); 114 | 115 | if (fs.statSync(sourcePath).isDirectory()) { 116 | this.copyDirectory(sourcePath, destPath); 117 | } else { 118 | fs.copyFileSync(sourcePath, destPath); 119 | } 120 | }); 121 | } 122 | 123 | 124 | private makeTempDir() { 125 | let filepath = this.getTempDir(); 126 | if (!fs.existsSync(filepath)) { 127 | fs.mkdirSync(filepath); 128 | } 129 | } 130 | 131 | 132 | private removeTempDir() { 133 | let filepath = this.getTempDir(); 134 | if (fs.existsSync(filepath)) { 135 | fs.rmSync(filepath, { recursive: true }); 136 | } 137 | } 138 | 139 | 140 | private getTempDir() { 141 | return Path.join(this.options.output, ".sherpa-dev"); 142 | } 143 | 144 | 145 | } 146 | 147 | 148 | // Teach me your way, Lord, that I may rely on your faithfulness; give me an 149 | // undivided heart, that I may fear your name. 150 | // - Psalm 86:11 151 | -------------------------------------------------------------------------------- /tests/endpoints/suite/tester.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2024 Sellers Industries, Inc. 3 | * distributed under the MIT License 4 | * 5 | * author: Evan Sellers 6 | * date: Thu May 16 2024 7 | * file: tester.ts 8 | * project: SherpaJS - Module Microservice Platform 9 | * purpose: Endpoint Test Suite - Test 10 | * 11 | */ 12 | 13 | 14 | import StackTracey from "stacktracey"; 15 | import { 16 | Method, Body, BodyType, 17 | IResponse, Headers as IHeaders 18 | } from "../../../index.js"; 19 | import { Fail } from "./helpers.js"; 20 | import { TestResults } from "./model.js"; 21 | 22 | 23 | export class Tester { 24 | 25 | 26 | private name:string; 27 | private method:Method; 28 | private url:string; 29 | private body?:Body; 30 | private handler?:(request:IResponse) => void; 31 | 32 | 33 | constructor(name:string, method:Method, url:string, body?:Body) { 34 | this.name = name; 35 | this.method = method; 36 | this.url = url; 37 | this.body = body; 38 | } 39 | 40 | 41 | expect(fn:(request:IResponse) => void) { 42 | this.handler = fn; 43 | } 44 | 45 | 46 | async invoke(host:string):Promise { 47 | try { 48 | this.handler!(await this.getResponse(host)); 49 | return { name: this.name, success: true }; 50 | } catch (error) { 51 | console.log(error); 52 | let stack = new StackTracey(error.stack).items.map((e) => e.beforeParse).join("\n"); 53 | if (error instanceof Fail) { 54 | return { 55 | name: this.name, 56 | success: false, 57 | message: error.message.toString(), 58 | stack: stack 59 | } 60 | } 61 | return { 62 | name: this.name, 63 | success: false, 64 | message: `JS Error: ${error.message}`, 65 | stack: stack 66 | } 67 | } 68 | } 69 | 70 | 71 | private async getResponse(host:string):Promise { 72 | let { body, contentType } = this.getRequestBody(this.body); 73 | return await this.cast(await fetch(new URL(this.url, host).toString(), { 74 | method: this.method, 75 | body: body, 76 | headers: { 77 | "Content-Type": contentType 78 | } 79 | })); 80 | } 81 | 82 | 83 | private getRequestBody(body:Body):{ body:string|undefined, contentType:string } { 84 | let contentType:string; 85 | let rawBody:string; 86 | if (!body) { 87 | return { body: undefined, contentType: "" }; 88 | } else if (typeof body === "object") { 89 | contentType = "application/json"; 90 | rawBody = JSON.stringify(body); 91 | } else if (typeof body === "string") { 92 | contentType = "text/plain"; 93 | rawBody = body as string; 94 | } else { 95 | throw new Error("Unsupported body type: " + typeof body); 96 | } 97 | return { body: rawBody, contentType: contentType }; 98 | } 99 | 100 | 101 | private async cast(response:Response):Promise { 102 | let headers = new IHeaders(response.headers); 103 | let { body, bodyType } = await this.getResponseBody(response, headers); 104 | return { 105 | status: response.status, 106 | statusText: response.statusText, 107 | headers: headers, 108 | body: body, 109 | bodyType: bodyType 110 | }; 111 | } 112 | 113 | 114 | private async getResponseBody(response:Response, headers:IHeaders):Promise<{ body:Body, bodyType:BodyType }> { 115 | let contentType = (headers.get("Content-Type") || "").toLowerCase(); 116 | let body = await response.text(); 117 | if (!contentType || body == "") { 118 | return { 119 | body: undefined, 120 | bodyType: BodyType.None 121 | }; 122 | } else if (contentType.startsWith("application/json")) { 123 | return { 124 | body: JSON.parse(body as string), 125 | bodyType: BodyType.JSON 126 | }; 127 | } else if (contentType.startsWith("text/html")) { 128 | return { 129 | body: body, 130 | bodyType: BodyType.HTML 131 | }; 132 | } else if (contentType.startsWith("text/plain")) { 133 | return { 134 | body: body, 135 | bodyType: BodyType.Text 136 | } 137 | } else { 138 | throw new Error(`Unsupported content type: ${contentType}`); 139 | } 140 | } 141 | 142 | 143 | } 144 | 145 | 146 | // You, God, are my God, earnestly I seek you; I thirst for you, my whole being 147 | // longs for you, in a dry and parched land where there is no water. 148 | // - Psalm 63:1 149 | -------------------------------------------------------------------------------- /src/compiler/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2024 Sellers Industries, Inc. 3 | * distributed under the MIT License 4 | * 5 | * author: Evan Sellers 6 | * date: Mon Mar 04 2024 7 | * file: index.ts 8 | * project: SherpaJS - Module Microservice Platform 9 | * purpose: Compiler 10 | * 11 | */ 12 | 13 | 14 | import fs from "fs"; 15 | import { green, red } from "colorette"; 16 | import { getStructure } from "./structure/index.js"; 17 | import { Logger } from "./utilities/logger/index.js"; 18 | import { NewBundler } from "./bundler/index.js"; 19 | import { BuildOptions, BundlerType } from "./models.js"; 20 | import { Level, Message } from "./utilities/logger/model.js"; 21 | import { Path } from "./utilities/path/index.js"; 22 | 23 | 24 | export { BundlerType }; 25 | export type { BuildOptions }; 26 | 27 | 28 | export class Compiler { 29 | 30 | 31 | public static async build(options:BuildOptions, verbose:boolean=true):Promise<{ success:boolean, logs:Message[] }> { 32 | let errorsOptions = this.validateBuildOptions(options); 33 | if (errorsOptions.length) { 34 | return this.display({ logs: errorsOptions, verbose, success: false }); 35 | } 36 | 37 | let logs:Message[] = []; 38 | try { 39 | let structure = await getStructure(options.input); 40 | logs.push(...structure.logs); 41 | 42 | if (!structure.endpoints || !structure.server || !structure.assets) { 43 | logs.push({ 44 | level: Level.ERROR, 45 | text: "Failed to generate endpoints." 46 | }); 47 | return this.display({ logs, verbose, success: false }); 48 | } 49 | 50 | if (Logger.hasError(logs)) { 51 | return this.display({ logs, verbose, success: false }); 52 | } 53 | 54 | await NewBundler(structure, options, logs).build(); 55 | } catch (error) { 56 | logs.push({ 57 | level: Level.ERROR, 58 | text: "Failed to bundle SherpaJS Server", 59 | content: error.message 60 | }); 61 | console.log(error.stack); 62 | return this.display({ logs, verbose, success: false }); 63 | } 64 | return this.display({ logs, verbose, success: true }); 65 | } 66 | 67 | 68 | public static clean(filepath:string) { 69 | [ 70 | ".sherpa", 71 | ".sherpa-dev", 72 | ".vercel" 73 | ].forEach(dirName => { 74 | let dirPath = Path.join(filepath, dirName); 75 | if (fs.existsSync(dirPath)) { 76 | fs.rmSync(dirPath, { recursive: true, force: true }); 77 | } 78 | }) 79 | } 80 | 81 | 82 | private static validateBuildOptions(options:BuildOptions):Message[] { 83 | let errors:Message[] = []; 84 | errors.push(...this.validateFilepath(options.input, "Input")); 85 | errors.push(...this.validateFilepath(options.output, "Output")); 86 | if (options?.developer?.environment?.files) { 87 | errors.push(...options.developer.environment.files.map(filepath => { 88 | return this.validateFilepath(filepath, "Environment File"); 89 | }).flat()); 90 | } 91 | return errors; 92 | } 93 | 94 | 95 | private static validateFilepath(filepath:string, name:string):Message[] { 96 | if (!Path.isAbsolute(filepath)) { 97 | return [{ 98 | level: Level.ERROR, 99 | text: `${name} path is not an absolute path.`, 100 | file: { filepath: filepath } 101 | }]; 102 | } 103 | if (!fs.existsSync(filepath)) { 104 | return [{ 105 | level: Level.ERROR, 106 | text: `${name} path does not exist.`, 107 | file: { filepath: filepath } 108 | }]; 109 | } 110 | return []; 111 | } 112 | 113 | 114 | private static display(output:{ logs:Message[], success:boolean, verbose:boolean }):{ success:boolean, logs:Message[] } { 115 | if (output.verbose) { 116 | if (output.logs.length == 0) { 117 | console.log("No Build Logs.") 118 | } else { 119 | console.log("============ Build Logs ============") 120 | Logger.display(output.logs); 121 | console.log(""); 122 | } 123 | if (output.success) { 124 | console.log(green("SherpaJS Successfully Built Server!")); 125 | } else { 126 | console.log(red("SherpaJS Failed to Build Server.") + " See logs for more information.") 127 | } 128 | } 129 | return { logs: output.logs, success: output.success }; 130 | } 131 | 132 | 133 | } 134 | 135 | 136 | // I write these things to you who believe in the name of the Son of God so 137 | // that you may know that you have eternal life. 138 | // - 1 John 5:13 139 | -------------------------------------------------------------------------------- /docs/build/module-config.mdx: -------------------------------------------------------------------------------- 1 | # Module Configuration 2 | Sherpa modules are configured using a `sherpa.module.ts` file, where you define 3 | the structure and behavior of your module. This configuration file is located 4 | at the root of your project and serves as the entry point for your Sherpa module. 5 | 6 | 7 | ## Config File 8 | The configuration file must be located at `sherpa.module.ts` and have a 9 | default export of the config using the `SherpaJS.New.module` function as 10 | follows. You can export a class using `CreateModuleInterface` that acts as a 11 | wrapper for validating the context when the module is loaded. 12 | 13 | 14 |
15 | 16 | 17 | ### Basic Configuration 18 | This basic configuration sets up a module with a simple interface 19 | definition. The context schema is defined inline using the 20 | `CreateModuleInterface` function. 21 | 22 | ```typescript title="sherpa.module.ts" 23 | import { SherpaJS, CreateModuleInterface } from "sherpa-core"; 24 | 25 | export default SherpaJS.New.module({ 26 | name: "example-module", 27 | interface: CreateModuleInterface<{ foo: boolean, bar: string }>() 28 | }); 29 | ``` 30 | 31 | 32 |
33 | 34 | 35 | ### Class-Based Configuration 36 | Alternatively, you can export any interface class with a constructor that 37 | takes the context as a parameter and sets `this.context` within your class. 38 | You can attach additional methods to interact with your module. 39 | 40 | These interfaces can be called by other functions and endpoints inside your 41 | codebase, see the 42 | [calling module interfaces example](/build/module-config#module-interface-call-example). 43 | 44 | ```typescript title="sherpa.module.ts" 45 | import { SherpaJS, ModuleInterface } from "sherpa-core"; 46 | 47 | export default SherpaJS.New.module({ 48 | name: "example-module", 49 | interface: class Example implements ModuleInterface { 50 | context: { foo: number, bar: string }; 51 | constructor(context: { foo: number, bar: string }) { 52 | this.context = context; 53 | } 54 | } 55 | }); 56 | ``` 57 | 58 | 59 | You can also export any additional attributes from this file as needed. The 60 | module config file, `sherpa.module.ts` should be the main script defined 61 | in your `package.json`. 62 | 63 | 64 | 65 |
66 | 67 | 68 | ## Config Structure 69 | 70 | ### name 71 | The name of the module. 72 | 73 | 74 |
75 | 76 | 77 | ### interface 78 | A class that has a constructor with `context:[TYPE]` parameter and a 79 | property `context: [TYPE]`. You can also use `CreateModuleInterface` to 80 | generate this class. 81 | 82 | 83 |
84 | 85 | 86 | ## Module Config Type 87 | The module configuration type is structured as: 88 | 89 | ```typescript 90 | { 91 | name:string; 92 | interface:T; 93 | } 94 | ``` 95 | 96 | where `T` is a class that implements implements `ModuleInterface`. Thus, it 97 | has a `context` and a `constructor` that takes `context` as a parameter. 98 | 99 | ```typescript 100 | { 101 | context:Schema; 102 | constructor(context:Schema) { this.context = context; } 103 | } 104 | ``` 105 | 106 | 107 |
108 | 109 | 110 | ## Example Config 111 | 112 | ### Basic Example 113 | A basic configuration using the `CreateModuleInterface` function. 114 | 115 | ```typescript title="example-module/sherpa.module.ts" 116 | import { SherpaJS, CreateModuleInterface } from "sherpa-core"; 117 | 118 | export default SherpaJS.New.module({ 119 | name: "example-module", 120 | interface: CreateModuleInterface<{ foo: boolean, bar: string }>() 121 | }); 122 | ``` 123 | 124 | Loading the module from a server endpoints, [learn more](/build/endpoints). 125 | ```typescript title="route/module.ts" 126 | import ExampleModule from "../example-module/sherpa.module.ts"; 127 | 128 | export default ExampleModule.load({ 129 | foo: true, 130 | bar: "hello" 131 | }) 132 | ``` 133 | 134 | 135 | 136 |
137 | 138 | 139 | ### Module Interface Call Example 140 | 141 | A basic configuration using the `CreateModuleInterface` function. 142 | 143 | ```typescript title="example-module/sherpa.module.ts" 144 | import { SherpaJS, CreateModuleInterface } from "sherpa-core"; 145 | 146 | export type Schema = { foo: number, bar: string }; 147 | 148 | export default SherpaJS.New.module({ 149 | name: "example-module", 150 | interface: class Example implements ModuleInterface { 151 | 152 | context:Schema; 153 | 154 | constructor(context:Schema) { 155 | this.context = context; 156 | } 157 | 158 | getFoo():number { 159 | return this.context.foo; 160 | } 161 | 162 | } 163 | }); 164 | ``` 165 | 166 | Loading the a module from a server endpoints, 167 | [learn more](/build/endpoints). 168 | ```typescript title="route/example/module.ts" 169 | import ExampleModule from "../../example-module/sherpa.module.ts"; 170 | 171 | export default ExampleModule.load({ 172 | foo: 3, 173 | bar: "hello" 174 | }) 175 | ``` 176 | 177 | Calling the loaded module instance from another endpoint... 178 | ```typescript title="route/index.ts" 179 | import { Request, Response } from "sherpa-core"; 180 | import example from "./example/module.ts"; 181 | 182 | export function GET() { 183 | return Response.text(example.getFoo()); // returns 3 184 | } 185 | 186 | ``` 187 | 188 | -------------------------------------------------------------------------------- /docs/build/routing.mdx: -------------------------------------------------------------------------------- 1 | # Routing 2 | SherpaJS provides a flexible and intuitive way to define endpoints and handle 3 | incoming requests within your microservice architecture. Drawing inspiration 4 | by Next.js, SherpaJS routes follow a directory-based structure located in 5 | the `/routes` directory of your module. 6 | 7 | 8 |
9 | 10 | 11 | ## Terminology 12 | 13 | ### Route Component Tree 14 | * **Routes**: A hierarchical structure for all the segments and endpoints 15 | of the server. 16 | * **Subroutes**: A subsection of the route structure. 17 | * **Root**: The first level of segments and endpoints in a route or subroute. 18 | 19 | 20 |
21 | 22 | 23 | ### URL Anatomy 24 | * **Segment**: The directory name part of the URL path delimited by slashes. 25 | * **URL Path**: The part of the URL that comes after the domain, composed of segments. 26 | * **Endpoint**: The final destination in the route structure where the request 27 | is handled, typically defined in `index.ts` files. 28 | 29 | 30 |
31 | 32 | 33 | ## Structure of Routes 34 | In the `/routes` directory, you can create directories to organize your 35 | routes. Each endpoint within a route is represented by a file typically named 36 | `index.ts`, [learn more about other types of endpoints](/build/endpoints). 37 | 38 | 39 |
40 | 41 | 42 | ### Example Route Structure 43 | ```plaintext 44 | /routes 45 | │ 46 | ├── /users 47 | │ └── index.ts // Endpoint logic for "/users" 48 | │ 49 | │ 50 | ├── /example 51 | │ ├── index.ts // Endpoint logic for "/example" 52 | │ ├── /subroute 53 | │ │ └── index.ts // Endpoint logic for "/example/subroute" 54 | │ 55 | ├── /[id] 56 | │ └── index.ts // Endpoint logic for "/[id]" access "[id]" with request.params.path.get("id") 57 | │ 58 | ├── /products 59 | │ ├── index.ts // Endpoint logic for "/products" 60 | │ ├── /[productID] 61 | │ │ └── index.ts // Endpoint logic for "/products/[productID]" access "[productID]" with request.params.path.get("productID") 62 | │ └── /category 63 | │ └── index.ts // Endpoint logic for "/products/category" 64 | ``` 65 | 66 | 67 |
68 | 69 | 70 | ## Defining Segment routes 71 | Routes in SherpaJS are created using a file-system based router where folders 72 | define segment routes, and files inside these folders define the endpoint logic. 73 | 74 | 75 |
76 | 77 | 78 | ### Basic Segment Route 79 | In SherpaJS, the basic segment routes are straightforward. Each folder 80 | represents a route segment, and the `index.ts` file within the folder 81 | contains the endpoint logic, [or other types of endpoints](/build/endpoints). 82 | 83 | ```typescript title="routes/index.ts" 84 | import { Request, Response } from "sherpa-core"; 85 | 86 | export function GET(request:Request) { 87 | return Response.text("Hello World!"); 88 | } 89 | ``` 90 | 91 | 92 |
93 | 94 | 95 | ### Nested Segment Route 96 | To create a nested segment route, nest folders inside each other. For example, to 97 | create a `/dashboard/settings` route, you would nest two folders like so: 98 | 99 | ```plaintext 100 | /routes 101 | │ 102 | ├── /dashboard 103 | │ ├── index.ts // Endpoint logic for "/dashboard" 104 | │ └── /settings 105 | │ └── index.ts // Endpoint logic for "/dashboard/settings" 106 | ``` 107 | 108 | ```typescript title="routes/dashboard/settings/index.ts" 109 | import { Request, Response } from "sherpa-core"; 110 | 111 | export function GET(request:Request) { 112 | return Response.text("Hello World!"); 113 | } 114 | ``` 115 | 116 | 117 |
118 | 119 | 120 | ### Dynamic Segment Route 121 | To define a dynamic segment route, name a directory using square brackets, such 122 | as `[id]`. Within a dynamic segment route directory, you can access the parameter 123 | value from the request object in your endpoint logic. For example, if you have 124 | a dynamic segement route named `[id]`, you can access the parameter 125 | using `request.params.path.get("id")`. 126 | 127 | ```plaintext 128 | /routes 129 | │ 130 | ├── /products 131 | │ └── /[id] 132 | │ └── index.ts // Endpoint logic for "/products/[id]" 133 | ``` 134 | 135 | ```typescript title="routes/products/[id]/index.ts" 136 | import { Request, Response } from "sherpa-core"; 137 | 138 | export function GET(request:Request) { 139 | return Response.new(request.params.path.get("id")); 140 | } 141 | ``` 142 | 143 | 144 |
145 | 146 | 147 | ## Restrictions 148 | The SherpaJS routing system has some restrictions when it comes to defining 149 | routes, these restrictions help to keep routing simple, consistent, and ensure 150 | best practices. The SherpaJS compiler will prevent you from compiling if you 151 | violate any of these restrictions. 152 | * Multiple [dynamic segement routes](/build/routing#dynamic-segment-route) 153 | are not allowed in a single segement route. 154 | * Additional segment routes or endpoints are not allowed as the segment where 155 | a [Module Endpoint](/build/endpoints#module-endpoints) is loaded. 156 | * [Function Endpoints](/build/endpoints#function-endpoints) cannot have a 157 | GET method when paired with a [View Endpoint](/build/endpoints#view-endpoints). 158 | 159 | 160 |
161 | 162 | 163 | ## Next Steps 164 | Now that you understand the fundamentals of routing in SherpaJS, you can start 165 | creating endpoints for your application. \ 166 | 167 | [Learn more about Endpoints](/build/endpoints) 168 | -------------------------------------------------------------------------------- /src/compiler/structure/config-module/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2024 Sellers Industries, Inc. 3 | * distributed under the MIT License 4 | * 5 | * author: Evan Sellers 6 | * date: Sun Feb 11 2024 7 | * file: index.ts 8 | * project: SherpaJS - Module Microservice Platform 9 | * purpose: Module Config Structure 10 | * 11 | */ 12 | 13 | 14 | import { 15 | ModuleConfigFile, ModuleConfig, 16 | Context, ModuleInterface, 17 | FILENAME, FILE_EXTENSIONS 18 | } from "../../models.js"; 19 | import fs from "fs"; 20 | import { Path } from "../../utilities/path/index.js"; 21 | import { Tooling } from "../../utilities/tooling/index.js"; 22 | import { Level, Message } from "../../utilities/logger/model.js"; 23 | 24 | 25 | export async function getModuleConfig(entry:string, context:Context, contextFilepath:string):Promise<{ logs:Message[], module?:ModuleConfigFile }> { 26 | let logs:Message[] = []; 27 | 28 | let { filepath, logs: logsFilepath } = getFilepath(entry); 29 | logs.push(...logsFilepath); 30 | if (!filepath) return { 31 | logs: logsFilepath }; 32 | 33 | let { instance, logs: logsInstance } = await getInstance(filepath); 34 | if (!instance) return { logs: logsInstance }; 35 | 36 | logs.push(...lint(entry)); 37 | 38 | return { 39 | module: { 40 | entry: entry, 41 | filepath: filepath, 42 | context: context, 43 | contextFilepath: contextFilepath, 44 | instance: instance 45 | }, 46 | logs 47 | } 48 | } 49 | 50 | 51 | function getFilepath(entry:string):{ logs:Message[], filepath?:string } { 52 | let filepath = Path.resolveExtension( 53 | entry, 54 | FILENAME.CONFIG.MODULE, 55 | FILE_EXTENSIONS.CONFIG.MODULE 56 | ); 57 | if (filepath) { 58 | return { filepath, logs: [] }; 59 | } 60 | return { 61 | logs: [{ 62 | level: Level.ERROR, 63 | text: "Module config file could not be found.", 64 | content: `Must have module config, "${FILENAME.CONFIG.MODULE}" ` 65 | + `of type "${FILE_EXTENSIONS.CONFIG.MODULE.join("\", \"")}".`, 66 | file: { filepath: entry } 67 | }] 68 | }; 69 | } 70 | 71 | 72 | async function getInstance(filepath:string):Promise<{ logs:Message[], instance?:ModuleConfig, unknown> }> { 73 | let { module, logs } = await Tooling.getExportedLoader(filepath, "Module Config", "SherpaJS.New.module", "sherpa-core"); 74 | if (!module) { 75 | return { logs }; 76 | } 77 | try { 78 | return { 79 | logs: await Tooling.typeValidation(filepath, "Module Config"), 80 | instance: await Tooling.getDefaultExport(filepath) as ModuleConfig, unknown> 81 | } 82 | } catch (e) { 83 | return { 84 | logs: [{ 85 | level: Level.ERROR, 86 | text: "Module config file could not be loaded.", 87 | content: e.message, 88 | file: { filepath: filepath } 89 | }] 90 | }; 91 | } 92 | } 93 | 94 | 95 | function lint(entry:string):Message[] { 96 | return [ 97 | ...lintPackageJSON(entry) 98 | ]; 99 | } 100 | 101 | 102 | function lintPackageJSON(entry:string):Message[] { 103 | try { 104 | let logs:Message[] = []; 105 | let filepath = Path.join(entry, "package.json"); 106 | if (!fs.existsSync(filepath)) { 107 | return []; 108 | } 109 | 110 | let packageJSON = JSON.parse(fs.readFileSync(filepath, "utf8")); 111 | if (packageJSON.type !== "module") { 112 | logs.push({ 113 | level: Level.WARN, 114 | text: `package.json is not configured properly.`, 115 | content: `Ensure the "type" attribute is set to "module".`, 116 | file: { filepath: filepath } 117 | }); 118 | } 119 | logs.push(...lintPackageExports(filepath, entry, packageJSON)); 120 | 121 | return logs; 122 | } catch { 123 | return []; 124 | } 125 | } 126 | 127 | 128 | function lintPackageExports(filepath:string, entry:string, packageJSON:Record):Message[] { 129 | for (let exportFilepath of getAllExported(packageJSON.exports as string|string[]|Record)) { 130 | let expectedFilepath = Path.resolveExtension(entry, FILENAME.CONFIG.MODULE, FILE_EXTENSIONS.CONFIG.MODULE); 131 | if (expectedFilepath == Path.join(entry, exportFilepath)) { 132 | return []; 133 | } 134 | } 135 | return [{ 136 | level: Level.WARN, 137 | text: `package.json is not configured properly.`, 138 | content: `Ensure the "exports" attribute contains the "${FILENAME.CONFIG.MODULE}" file.`, 139 | file: { filepath: filepath } 140 | }]; 141 | } 142 | 143 | 144 | function getAllExported(exports:string|string[]|Record):string[] { 145 | if (typeof exports === "string") { 146 | return [exports]; 147 | } else if (Array.isArray(exports)) { 148 | return exports; 149 | } else if (typeof exports === "object") { 150 | return Object.keys(exports).map(o => getAllExported(exports[o] as string|string[]|Record)).flat(3); 151 | } 152 | return []; 153 | } 154 | 155 | 156 | // Whoever believes and is baptized will be saved, but whoever does not believe 157 | // will be condemned. 158 | // - Mark 16:16 159 | --------------------------------------------------------------------------------