├── 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 |
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 | 
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 |
--------------------------------------------------------------------------------