Selaa lähdekoodia

Initial commit

master
TheoryOfNekomata 10 kuukautta sitten
commit
45f9853dd5
43 muutettua tiedostoa jossa 5881 lisäystä ja 0 poistoa
  1. +11
    -0
      .editorconfig
  2. +3
    -0
      .gitignore
  3. +6
    -0
      TODO.md
  4. +9
    -0
      packages/cli/.eslintrc
  5. +107
    -0
      packages/cli/.gitignore
  6. +73
    -0
      packages/cli/package.json
  7. +3
    -0
      packages/cli/pridepack.json
  8. +9
    -0
      packages/cli/src/cli.ts
  9. +22
    -0
      packages/cli/src/commands.ts
  10. +11
    -0
      packages/cli/src/index.ts
  11. +82
    -0
      packages/cli/src/modules/adder/adder.controller.ts
  12. +21
    -0
      packages/cli/src/modules/adder/adder.service.ts
  13. +2
    -0
      packages/cli/src/modules/adder/index.ts
  14. +335
    -0
      packages/cli/src/packages/cli-wrapper.ts
  15. +19
    -0
      packages/cli/src/packages/write-stream.ts
  16. +68
    -0
      packages/cli/test/index.test.ts
  17. +24
    -0
      packages/cli/tsconfig.eslint.json
  18. +24
    -0
      packages/cli/tsconfig.json
  19. +9
    -0
      packages/core/.eslintrc
  20. +107
    -0
      packages/core/.gitignore
  21. +66
    -0
      packages/core/package.json
  22. +3
    -0
      packages/core/pridepack.json
  23. +34
    -0
      packages/core/src/index.ts
  24. +16
    -0
      packages/core/test/index.test.ts
  25. +24
    -0
      packages/core/tsconfig.eslint.json
  26. +24
    -0
      packages/core/tsconfig.json
  27. +9
    -0
      packages/web-api/.eslintrc
  28. +107
    -0
      packages/web-api/.gitignore
  29. +71
    -0
      packages/web-api/package.json
  30. +3
    -0
      packages/web-api/pridepack.json
  31. +7
    -0
      packages/web-api/src/config.ts
  32. +20
    -0
      packages/web-api/src/index.ts
  33. +60
    -0
      packages/web-api/src/modules/adder/adder.controller.ts
  34. +21
    -0
      packages/web-api/src/modules/adder/adder.service.ts
  35. +2
    -0
      packages/web-api/src/modules/adder/index.ts
  36. +36
    -0
      packages/web-api/src/routes.ts
  37. +11
    -0
      packages/web-api/src/server.ts
  38. +87
    -0
      packages/web-api/test/index.test.ts
  39. +24
    -0
      packages/web-api/tsconfig.eslint.json
  40. +24
    -0
      packages/web-api/tsconfig.json
  41. +6
    -0
      packages/web-api/vite.config.ts
  42. +4279
    -0
      pnpm-lock.yaml
  43. +2
    -0
      pnpm-workspace.yaml

+ 11
- 0
.editorconfig Näytä tiedosto

@@ -0,0 +1,11 @@
root = true

[*]
charset = utf-8
end_of_line = lf
indent_size = tab
indent_style = tab
insert_final_newline = true
max_line_length = 120
tab_width = 2
trim_trailing_whitespace = true

+ 3
- 0
.gitignore Näytä tiedosto

@@ -0,0 +1,3 @@
node_modules
.DS_Store
.idea/

+ 6
- 0
TODO.md Näytä tiedosto

@@ -0,0 +1,6 @@
- We could make a common code generation strategy between
CLI and Web API since we have unified the code structure.
- What this means is we just need to declare the connection
from the core SDK to the CLI or Web API functions, then we
can run some sort of code generation, so we don't have to
bother writing the Web API/CLI code manually.

+ 9
- 0
packages/cli/.eslintrc Näytä tiedosto

@@ -0,0 +1,9 @@
{
"root": true,
"extends": [
"lxsmnsyc/typescript"
],
"parserOptions": {
"project": "./tsconfig.eslint.json"
}
}

+ 107
- 0
packages/cli/.gitignore Näytä tiedosto

@@ -0,0 +1,107 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*

# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json

# Runtime data
pids
*.pid
*.seed
*.pid.lock

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage
*.lcov

# nyc test coverage
.nyc_output

# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# Bower dependency directory (https://bower.io/)
bower_components

# node-waf configuration
.lock-wscript

# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release

# Dependency directories
node_modules/
jspm_packages/

# TypeScript v1 declaration files
typings/

# TypeScript cache
*.tsbuildinfo

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/

# Optional REPL history
.node_repl_history

# Output of 'npm pack'
*.tgz

# Yarn Integrity file
.yarn-integrity

# dotenv environment variables file
.env
.env.production
.env.development

# parcel-bundler cache (https://parceljs.org/)
.cache

# Next.js build output
.next

# Nuxt.js build / generate output
.nuxt
dist

# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and *not* Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public

# vuepress build output
.vuepress/dist

# Serverless directories
.serverless/

# FuseBox cache
.fusebox/

# DynamoDB Local files
.dynamodb/

# TernJS port file
.tern-port

.npmrc

+ 73
- 0
packages/cli/package.json Näytä tiedosto

@@ -0,0 +1,73 @@
{
"name": "@modal-sh/cli",
"version": "0.0.0",
"files": [
"dist",
"src"
],
"engines": {
"node": ">=12"
},
"license": "UNLICENSED",
"keywords": [
"pridepack"
],
"devDependencies": {
"@types/node": "^18.14.1",
"@types/yargs": "^17.0.24",
"eslint": "^8.35.0",
"eslint-config-lxsmnsyc": "^0.5.0",
"pridepack": "2.4.4",
"tslib": "^2.5.0",
"typescript": "^4.9.5",
"vitest": "^0.28.1"
},
"scripts": {
"prepublishOnly": "pridepack clean && pridepack build",
"build": "pridepack build",
"type-check": "pridepack check",
"lint": "pridepack lint",
"clean": "pridepack clean",
"watch": "pridepack watch",
"start": "pridepack start",
"dev": "pridepack dev",
"test": "vitest"
},
"private": true,
"description": "CLI app.",
"repository": {
"url": "",
"type": "git"
},
"homepage": "",
"bugs": {
"url": ""
},
"author": "TheoryOfNekomata <allan.crisostomo@outlook.com>",
"publishConfig": {
"access": "restricted"
},
"types": "./dist/types/index.d.ts",
"main": "./dist/cjs/production/index.js",
"module": "./dist/esm/production/index.js",
"exports": {
".": {
"development": {
"require": "./dist/cjs/development/index.js",
"import": "./dist/esm/development/index.js"
},
"require": "./dist/cjs/production/index.js",
"import": "./dist/esm/production/index.js",
"types": "./dist/types/index.d.ts"
}
},
"typesVersions": {
"*": {}
},
"dependencies": {
"@clack/prompts": "^0.6.3",
"@modal-sh/core": "workspace:*",
"pino": "^8.14.1",
"yargs": "^17.7.2"
}
}

+ 3
- 0
packages/cli/pridepack.json Näytä tiedosto

@@ -0,0 +1,3 @@
{
"target": "es2018"
}

+ 9
- 0
packages/cli/src/cli.ts Näytä tiedosto

@@ -0,0 +1,9 @@
import { Cli, CliOptions } from './packages/cli-wrapper';

export const createCli = (options: CliOptions) => {
const cli = new Cli(options);

// TODO setup CLI here

return cli;
};

+ 22
- 0
packages/cli/src/commands.ts Näytä tiedosto

@@ -0,0 +1,22 @@
import { Cli } from './packages/cli-wrapper';
import { AdderController, AdderControllerImpl } from './modules/adder';

export const addCommands = (cli: Cli) => {
const adderController: AdderController = new AdderControllerImpl();

return cli
.command({
aliases: ['a'],
command: 'add',
parameters: ['<a> <b>'],
describe: 'Add two numbers',
handler: adderController.addNumbers,
})
.command({
aliases: ['s'],
command: 'subtract',
parameters: ['<a> <b>'],
describe: 'Subtract two numbers',
handler: adderController.subtractNumbers,
});
};

+ 11
- 0
packages/cli/src/index.ts Näytä tiedosto

@@ -0,0 +1,11 @@
import { addCommands } from '@/commands';
import { createCli } from '@/cli';
import { hideBin } from 'yargs/helpers';

const cli = createCli({
name: 'cli',
});

addCommands(cli);

cli.run(hideBin(process.argv));

+ 82
- 0
packages/cli/src/modules/adder/adder.controller.ts Näytä tiedosto

@@ -0,0 +1,82 @@
import {
AdderService,
AdderServiceImpl,
ArgumentOutOfRangeError,
InvalidArgumentTypeError,
} from './adder.service';
import { CommandHandler } from '../../packages/cli-wrapper';

export interface AdderController {
addNumbers: CommandHandler;
subtractNumbers: CommandHandler;
}

export class AdderControllerImpl implements AdderController {
constructor(
private readonly adderService: AdderService = new AdderServiceImpl(),
) {
// noop
}

readonly addNumbers: CommandHandler = (params) => {
if (!params.interactive) {
const checkArgs = params.args as Record<string, unknown>;
if (typeof checkArgs.a === 'undefined') {
params.logger.error('Missing required argument: a');
return -1;
}
if (typeof checkArgs.b === 'undefined') {
params.logger.error('Missing required argument: b');
return -1;
}
}

const { a, b } = params.args;
try {
const response = this.adderService.addNumbers({ a: Number(a), b: Number(b) });
params.logger.info(response);
} catch (errorRaw) {
const error = errorRaw as Error;
params.logger.error(error.message);
if (error instanceof InvalidArgumentTypeError) {
return -1;
}
if (error instanceof ArgumentOutOfRangeError) {
return -2;
}
return -3;
}
return 0;
}

readonly subtractNumbers: CommandHandler = (params) => {
if (!params.interactive) {
const checkArgs = params.args as Record<string, unknown>;
if (typeof checkArgs.a === 'undefined') {
params.logger.error('Missing required argument: a');
return -1;
}
if (typeof checkArgs.b === 'undefined') {
params.logger.error('Missing required argument: b');
return -1;
}
}

const { a, b } = params.args;
try {
const response = this.adderService.addNumbers({ a: Number(a), b: -(Number(b)) });
params.logger.info(response);
} catch (errorRaw) {
const error = errorRaw as Error;
params.logger.error(error.message);
if (error instanceof InvalidArgumentTypeError) {
return -1;
}
if (error instanceof ArgumentOutOfRangeError) {
return -2;
}
return -3;
}
return 0;
}
}

+ 21
- 0
packages/cli/src/modules/adder/adder.service.ts Näytä tiedosto

@@ -0,0 +1,21 @@
import {
add,
AddFunctionOptions,
ArgumentOutOfRangeError,
InvalidArgumentTypeError,
} from '@modal-sh/core';

export interface AdderService {
addNumbers(options: AddFunctionOptions): number;
}

export class AdderServiceImpl implements AdderService {
addNumbers(options: AddFunctionOptions) {
return add(options);
}
}

export {
ArgumentOutOfRangeError,
InvalidArgumentTypeError,
};

+ 2
- 0
packages/cli/src/modules/adder/index.ts Näytä tiedosto

@@ -0,0 +1,2 @@
export * from './adder.controller';
export * from './adder.service';

+ 335
- 0
packages/cli/src/packages/cli-wrapper.ts Näytä tiedosto

@@ -0,0 +1,335 @@
import yargs, { Argv } from 'yargs';
import * as clack from '@clack/prompts';
import { DummyWriteStream } from './write-stream';
import pino, {LogFn} from 'pino';
import * as util from 'util';

export interface Logger extends pino.BaseLogger {}

export interface CommandHandlerArgs {
self: any;
interactive: boolean;
send: (message: number) => void;
logger: Logger;
args: any;
}

export type CommandHandler = (args: CommandHandlerArgs) => void | number | Promise<number> | Promise<void>;

export interface CommandArgs {
aliases?: string[];
command: string;
parameters: string[];
describe: string;
handler: CommandHandler;
interactiveMode?: boolean;
}

type PromptValueArgs = [Record<string, string>, ...never[]]
| [string, string]
| [[string, string], ...never[]]
| [[string, string][], ...never[]];

export interface TestModeResult {
exitCode?: number;
stdout: DummyWriteStream;
stderr: DummyWriteStream;
}

interface InteractiveModeOptions {
option: string;
alias: string;
intro: string;
cancelled: string;
outro: string;
describe: string;
}

export interface CliOptions {
name: string;
interactiveMode?: Partial<InteractiveModeOptions>;
logger?: Logger | boolean;
}

export class Cli {
private testMode = false;

private promptValues = {} as Record<string, unknown>;

private testModeResult: TestModeResult = {
exitCode: undefined as number | undefined,
stdout: new DummyWriteStream(),
stderr: new DummyWriteStream(),
};

private readonly cli: Argv;

constructor(private readonly options: CliOptions) {
this.cli = yargs()
.scriptName(options.name)
.help()
.fail(false)
.strict(false);
}

private exit(code: number) {
if (this.testMode) {
this.testModeResult.exitCode = code;
return;
}
process.exit(code);
}

private async prompt(messages: Record<string, string>, interactiveModeOptions: InteractiveModeOptions) {
const {
intro,
cancelled,
outro,
} = interactiveModeOptions;
const messagesEntries = Object.entries(messages);

if (messagesEntries.length > 0) {
clack.intro(intro);
const values = await clack.group(
Object.fromEntries(
messagesEntries.map(([key, value]) => [key, () => clack.text({ message: value })])
),
{
onCancel: () => {
clack.cancel(cancelled);
this.exit(1);
}
}
);
clack.outro(outro);
return values;
}

return {};
}

private async generateCommandArgs(
context: any,
originalCommandArgs: Record<string, unknown>,
interactive: boolean,
interactiveModeOptions: InteractiveModeOptions,
) {
if (!interactive) {
return originalCommandArgs;
}

// TODO properly filter attributes
const clackGroup = context.optional
.filter((optional: { cmd: string[] }) => {
const {
'$0': _$0,
i: _i,
interactive: _interactive,
test: _test,
_: __,
...rest
} = originalCommandArgs;

return !Object.keys(rest).includes(optional.cmd[0]);
})
.reduce(
(group: Record<string, string>, optional: { cmd: string[], describe: string }) => ({
...group,
[optional.cmd[0]]: optional.describe ?? optional.cmd[0],
}),
{} as Record<string, string>,
);


const promptedArgs = this.testMode
? this.promptValues
: await this.prompt(clackGroup, interactiveModeOptions);

return {
...originalCommandArgs,
...promptedArgs,
};
}

private buildHandler(handlerFn: Function, interactiveModeOptions: InteractiveModeOptions) {
const thisCli = this;
return async function handler(this: any, commandArgs: Record<string, unknown>) {
const self = this;
const { option, alias } = interactiveModeOptions;
const interactiveLong = option in commandArgs ? Boolean(commandArgs.interactive) : false;
const interactiveShort = alias in commandArgs ? Boolean(commandArgs.i) : false;
const interactive = interactiveLong || interactiveShort;
const buildArgs = await thisCli.generateCommandArgs(
self,
commandArgs,
interactive,
interactiveModeOptions,
);

const stdoutLogFn: LogFn = (...args: unknown[]) => {
const stream = thisCli.testMode ? thisCli.testModeResult.stdout : process.stdout;
const [arg0, arg1, ...etcArgs] = args;
if (typeof arg0 === 'string' || typeof arg0 === 'number') {
if (arg1) {
if (etcArgs.length > 0) {
stream.write(util.format(`${arg0}\n`, arg1, ...etcArgs));
} else {
stream.write(util.format(`${arg0}\n`, arg1));
}
} else {
stream.write(util.format(`${arg0}\n`));
}
} else if (typeof arg0 === 'object' && (typeof arg1 === 'string' || typeof arg1 === 'number')) {
if (etcArgs.length > 0) {
stream.write(util.format(`${arg1}\n`, ...etcArgs));
} else {
stream.write(util.format(`${arg1}\n`));
}
}
};

const stderrLogFn: LogFn = (...args: unknown[]) => {
const stream = thisCli.testMode ? thisCli.testModeResult.stderr : process.stderr;
const [arg0, arg1, ...etcArgs] = args;
if (typeof arg0 === 'string' || typeof arg0 === 'number') {
if (arg1) {
if (etcArgs.length > 0) {
stream.write(util.format(`${arg0}\n`, arg1, ...etcArgs));
} else {
stream.write(util.format(`${arg0}\n`, arg1));
}
} else {
stream.write(util.format(`${arg0}\n`));
}
} else if (typeof arg0 === 'object' && (typeof arg1 === 'string' || typeof arg1 === 'number')) {
if (etcArgs.length > 0) {
stream.write(util.format(`${arg1}\n`, ...etcArgs));
} else {
stream.write(util.format(`${arg1}\n`));
}
}
};

const loggerFn = thisCli.options.logger;
const defaultLogger = {
level: 'info',
debug: stdoutLogFn,
info: stdoutLogFn,
warn: stdoutLogFn,
error: stderrLogFn,
fatal: stderrLogFn,
trace: stdoutLogFn,
silent: () => {},
} as Logger;
const loggerBooleanFn = typeof loggerFn === 'boolean' && loggerFn ? defaultLogger : {
level: 'silent',
debug: () => {},
info: () => {},
warn: () => {},
error: () => {},
fatal: () => {},
trace: () => {},
silent: () => {},
} as Logger;
const logger = typeof loggerFn === 'undefined' ? defaultLogger : (typeof loggerFn === 'function' ? loggerFn : loggerBooleanFn);

let exited = false;
const returnCode = await handlerFn({
self,
interactive,
logger,
send: (code: number) => {
exited = true;
thisCli.exit(code);
},
args: buildArgs,
});

if (!exited) {
thisCli.exit(returnCode ? returnCode : 0);
}
};
}

command({ parameters, command, interactiveMode = true, handler, ...args }: CommandArgs): Cli {
const {
option = 'interactive',
alias = 'i',
describe = 'Interactive mode',
intro = 'Please provide the following values:',
cancelled = 'Operation cancelled.',
outro = 'Thank you!',
} = this.options?.interactiveMode ?? {};

const commandArgs = {
...args,
command: `${command} ${parameters.join(' ').replace(/</g, '[').replace(/>/g, ']')}`,
usage: parameters.map((p) => `${command} ${p}`).join('\n'),
handler: this.buildHandler(handler, {
option,
alias,
intro,
cancelled,
outro,
describe,
}),
} as Record<string, unknown>

if (interactiveMode) {
commandArgs.options = {
...(commandArgs.options ?? {}),
[option]: {
alias,
describe,
type: 'boolean',
default: false,
hidden: true,
},
};
}

this.cli.command(commandArgs as unknown as Parameters<typeof this.cli.command>[0]);
return this;
}

async run(args: string[]): Promise<void> {
if (args.length > 0) {
await this.cli.parse(args);
return;
}
this.cli.showHelp();
}

test() {
this.testMode = true;
this.promptValues = {};
const thisCli = this;
return {
promptValue(...args: PromptValueArgs) {
const [testArg] = args;

if (typeof testArg === 'string') {
const [key, value] = args as [string, unknown];
thisCli.promptValues[key] = thisCli.promptValues[key] ?? value;
}

if (Array.isArray(testArg)) {
const [key, value] = testArg as [string, unknown];
thisCli.promptValues[key] = thisCli.promptValues[key] ?? value;
}

if (typeof testArg === 'object' && testArg !== null) {
Object.entries(testArg).forEach(([key, value]) => {
thisCli.promptValues[key] = thisCli.promptValues[key] ?? value;
});
}

return this;
},
async run(args: string[]): Promise<TestModeResult> {
await thisCli.cli.parse(args);
return thisCli.testModeResult;
},
};
}
}

+ 19
- 0
packages/cli/src/packages/write-stream.ts Näytä tiedosto

@@ -0,0 +1,19 @@
import {WriteStream} from 'tty';

export class DummyWriteStream extends WriteStream {
private bufferInternal = Buffer.from('');

constructor() {
super(0);
}

write(input: Uint8Array | string, _encoding?: BufferEncoding | ((err?: Error) => void), _cb?: (err?: Error) => void): boolean {
this.bufferInternal = Buffer.concat([this.bufferInternal, Buffer.from(input)]);
// noop
return true;
}

get buffer(): Buffer {
return this.bufferInternal;
}
}

+ 68
- 0
packages/cli/test/index.test.ts Näytä tiedosto

@@ -0,0 +1,68 @@
import { describe, it, expect, vi, beforeAll } from 'vitest';
import { createCli } from '../src/cli';
import { addCommands } from '../src/commands';
import { AdderServiceImpl, InvalidArgumentTypeError, ArgumentOutOfRangeError } from '../src/modules/adder';
import { Cli } from '../src/packages/cli-wrapper';

vi.mock('process');

describe('blah', () => {
let cli: Cli;
beforeAll(() => {
cli = createCli({
name: 'cli-test',
logger: false,
});
addCommands(cli);
});

it('returns result when successful', async () => {
const response = await cli.test().run(['add', '1', '2']);
expect(response.exitCode).toBe(0);
});

it('returns error when given invalid inputs', async () => {
vi.spyOn(AdderServiceImpl.prototype, 'addNumbers').mockImplementationOnce(() => {
throw new InvalidArgumentTypeError('Invalid input');
});

const response = await cli.test().run(['add', '1', '2']);
expect(response.exitCode).toBe(-1);
});

it('returns error when given out-of-range inputs', async () => {
vi.spyOn(AdderServiceImpl.prototype, 'addNumbers').mockImplementationOnce(() => {
throw new ArgumentOutOfRangeError('Out of range');
});

const response = await cli.test().run(['add', '1', '2']);
expect(response.exitCode).toBe(-2);
});

it('returns error when an unexpected error occurs', async () => {
vi.spyOn(AdderServiceImpl.prototype, 'addNumbers').mockImplementationOnce(() => {
throw new Error('Unexpected error');
});

const response = await cli.test().run(['add', '1', '2']);
expect(response.exitCode).toBe(-3);
});

it('returns error when given insufficient arguments', async () => {
const response = await cli.test().run(['add', '1']);
expect(response.exitCode).toBe(-1);
});

describe('on interactive mode', () => {
it('prompts values for insufficient arguments', async () => {
const response = await cli
.test()
.promptValue({
a: '1',
b: '2',
})
.run(['add', '-i']);
expect(response.exitCode).not.toBe(-1);
});
});
});

+ 24
- 0
packages/cli/tsconfig.eslint.json Näytä tiedosto

@@ -0,0 +1,24 @@
{
"exclude": ["node_modules"],
"include": ["src", "types", "test"],
"compilerOptions": {
"module": "ESNext",
"lib": ["ESNext"],
"importHelpers": true,
"declaration": true,
"sourceMap": true,
"rootDir": "./",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"moduleResolution": "node",
"jsx": "react",
"esModuleInterop": true,
"target": "es2018",
"paths": {
"@/*": ["./src/*"],
}
}
}

+ 24
- 0
packages/cli/tsconfig.json Näytä tiedosto

@@ -0,0 +1,24 @@
{
"exclude": ["node_modules"],
"include": ["src", "types"],
"compilerOptions": {
"module": "ESNext",
"lib": ["ESNext"],
"importHelpers": true,
"declaration": true,
"sourceMap": true,
"rootDir": "./src",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"moduleResolution": "node",
"jsx": "react",
"esModuleInterop": true,
"target": "es2018",
"paths": {
"@/*": ["./src/*"],
}
}
}

+ 9
- 0
packages/core/.eslintrc Näytä tiedosto

@@ -0,0 +1,9 @@
{
"root": true,
"extends": [
"lxsmnsyc/typescript"
],
"parserOptions": {
"project": "./tsconfig.eslint.json"
}
}

+ 107
- 0
packages/core/.gitignore Näytä tiedosto

@@ -0,0 +1,107 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*

# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json

# Runtime data
pids
*.pid
*.seed
*.pid.lock

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage
*.lcov

# nyc test coverage
.nyc_output

# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# Bower dependency directory (https://bower.io/)
bower_components

# node-waf configuration
.lock-wscript

# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release

# Dependency directories
node_modules/
jspm_packages/

# TypeScript v1 declaration files
typings/

# TypeScript cache
*.tsbuildinfo

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/

# Optional REPL history
.node_repl_history

# Output of 'npm pack'
*.tgz

# Yarn Integrity file
.yarn-integrity

# dotenv environment variables file
.env
.env.production
.env.development

# parcel-bundler cache (https://parceljs.org/)
.cache

# Next.js build output
.next

# Nuxt.js build / generate output
.nuxt
dist

# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and *not* Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public

# vuepress build output
.vuepress/dist

# Serverless directories
.serverless/

# FuseBox cache
.fusebox/

# DynamoDB Local files
.dynamodb/

# TernJS port file
.tern-port

.npmrc

+ 66
- 0
packages/core/package.json Näytä tiedosto

@@ -0,0 +1,66 @@
{
"name": "@modal-sh/core",
"version": "0.0.0",
"files": [
"dist",
"src"
],
"engines": {
"node": ">=12"
},
"license": "UNLICENSED",
"keywords": [
"pridepack"
],
"devDependencies": {
"@types/node": "^18.14.1",
"eslint": "^8.35.0",
"eslint-config-lxsmnsyc": "^0.5.0",
"pridepack": "2.4.4",
"tslib": "^2.5.0",
"typescript": "^4.9.5",
"vitest": "^0.28.1"
},
"scripts": {
"prepublishOnly": "pridepack clean && pridepack build",
"build": "pridepack build",
"type-check": "pridepack check",
"lint": "pridepack lint",
"clean": "pridepack clean",
"watch": "pridepack watch",
"start": "pridepack start",
"dev": "pridepack dev",
"test": "vitest"
},
"private": true,
"description": "Core library.",
"repository": {
"url": "",
"type": "git"
},
"homepage": "",
"bugs": {
"url": ""
},
"author": "TheoryOfNekomata <allan.crisostomo@outlook.com>",
"publishConfig": {
"access": "restricted"
},
"types": "./dist/types/index.d.ts",
"main": "./dist/cjs/production/index.js",
"module": "./dist/esm/production/index.js",
"exports": {
".": {
"development": {
"require": "./dist/cjs/development/index.js",
"import": "./dist/esm/development/index.js"
},
"require": "./dist/cjs/production/index.js",
"import": "./dist/esm/production/index.js",
"types": "./dist/types/index.d.ts"
}
},
"typesVersions": {
"*": {}
}
}

+ 3
- 0
packages/core/pridepack.json Näytä tiedosto

@@ -0,0 +1,3 @@
{
"target": "es2018"
}

+ 34
- 0
packages/core/src/index.ts Näytä tiedosto

@@ -0,0 +1,34 @@
export interface AddFunctionOptions {
a: number;
b: number;
}

export interface AddFunction {
(options: AddFunctionOptions): number;
}

export class InvalidArgumentTypeError extends TypeError {
constructor(message: string) {
super(message);
this.name = 'InvalidArgumentTypeError';
}
}

export class ArgumentOutOfRangeError extends RangeError {
constructor(message: string) {
super(message);
this.name = 'ArgumentOutOfRangeError';
}
}

export const add: AddFunction = (options: AddFunctionOptions): number => {
const { a, b } = options as unknown as Record<string, unknown>;
if (typeof a !== 'number' || typeof b !== 'number') {
throw new InvalidArgumentTypeError('a and b must be numbers');
}
if (!Number.isFinite(a) || !Number.isFinite(b)) {
throw new ArgumentOutOfRangeError('a and b must be finite numbers');
}

return a + b;
};

+ 16
- 0
packages/core/test/index.test.ts Näytä tiedosto

@@ -0,0 +1,16 @@
import { describe, it, expect } from 'vitest';
import { add } from '../src';

describe('blah', () => {
it('returns result', () => {
expect(add({ a: 1, b: 1 })).toEqual(2);
});

it('throws when given invalid argument types', () => {
expect(() => add({ a: '1' as unknown as number, b: 1 })).toThrow(TypeError);
});

it('throws when given out-of-range arguments', () => {
expect(() => add({ a: Infinity, b: 1 })).toThrow(RangeError);
});
});

+ 24
- 0
packages/core/tsconfig.eslint.json Näytä tiedosto

@@ -0,0 +1,24 @@
{
"exclude": ["node_modules"],
"include": ["src", "types", "test"],
"compilerOptions": {
"module": "ESNext",
"lib": ["ESNext"],
"importHelpers": true,
"declaration": true,
"sourceMap": true,
"rootDir": "./",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"moduleResolution": "node",
"jsx": "react",
"esModuleInterop": true,
"target": "es2018",
"paths": {
"@/*": ["./src/*"],
}
}
}

+ 24
- 0
packages/core/tsconfig.json Näytä tiedosto

@@ -0,0 +1,24 @@
{
"exclude": ["node_modules"],
"include": ["src", "types"],
"compilerOptions": {
"module": "ESNext",
"lib": ["ESNext"],
"importHelpers": true,
"declaration": true,
"sourceMap": true,
"rootDir": "./src",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"moduleResolution": "node",
"jsx": "react",
"esModuleInterop": true,
"target": "es2018",
"paths": {
"@/*": ["./src/*"],
}
}
}

+ 9
- 0
packages/web-api/.eslintrc Näytä tiedosto

@@ -0,0 +1,9 @@
{
"root": true,
"extends": [
"lxsmnsyc/typescript"
],
"parserOptions": {
"project": "./tsconfig.eslint.json"
}
}

+ 107
- 0
packages/web-api/.gitignore Näytä tiedosto

@@ -0,0 +1,107 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*

# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json

# Runtime data
pids
*.pid
*.seed
*.pid.lock

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage
*.lcov

# nyc test coverage
.nyc_output

# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# Bower dependency directory (https://bower.io/)
bower_components

# node-waf configuration
.lock-wscript

# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release

# Dependency directories
node_modules/
jspm_packages/

# TypeScript v1 declaration files
typings/

# TypeScript cache
*.tsbuildinfo

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/

# Optional REPL history
.node_repl_history

# Output of 'npm pack'
*.tgz

# Yarn Integrity file
.yarn-integrity

# dotenv environment variables file
.env
.env.production
.env.development

# parcel-bundler cache (https://parceljs.org/)
.cache

# Next.js build output
.next

# Nuxt.js build / generate output
.nuxt
dist

# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and *not* Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public

# vuepress build output
.vuepress/dist

# Serverless directories
.serverless/

# FuseBox cache
.fusebox/

# DynamoDB Local files
.dynamodb/

# TernJS port file
.tern-port

.npmrc

+ 71
- 0
packages/web-api/package.json Näytä tiedosto

@@ -0,0 +1,71 @@
{
"name": "@modal-sh/web-api",
"version": "0.0.0",
"files": [
"dist",
"src"
],
"engines": {
"node": ">=12"
},
"keywords": [
"pridepack"
],
"devDependencies": {
"@types/node": "^18.14.1",
"eslint": "^8.35.0",
"eslint-config-lxsmnsyc": "^0.5.0",
"pridepack": "2.4.4",
"tslib": "^2.5.0",
"typescript": "^4.9.5",
"vite": "^4.3.9",
"vite-tsconfig-paths": "^4.2.0",
"vitest": "^0.28.1"
},
"dependencies": {
"@modal-sh/core": "workspace:*",
"fastify": "^4.12.0"
},
"scripts": {
"prepublishOnly": "pridepack clean && pridepack build",
"build": "pridepack build",
"type-check": "pridepack check",
"lint": "pridepack lint",
"clean": "pridepack clean",
"watch": "pridepack watch",
"start": "pridepack start",
"dev": "pridepack dev",
"test": "vitest"
},
"private": true,
"description": "Web API.",
"repository": {
"url": "",
"type": "git"
},
"homepage": "",
"bugs": {
"url": ""
},
"author": "TheoryOfNekomata <allan.crisostomo@outlook.com>",
"publishConfig": {
"access": "restricted"
},
"types": "./dist/types/index.d.ts",
"main": "./dist/cjs/production/index.js",
"module": "./dist/esm/production/index.js",
"exports": {
".": {
"development": {
"require": "./dist/cjs/development/index.js",
"import": "./dist/esm/development/index.js"
},
"require": "./dist/cjs/production/index.js",
"import": "./dist/esm/production/index.js",
"types": "./dist/types/index.d.ts"
}
},
"typesVersions": {
"*": {}
}
}

+ 3
- 0
packages/web-api/pridepack.json Näytä tiedosto

@@ -0,0 +1,3 @@
{
"target": "es2018"
}

+ 7
- 0
packages/web-api/src/config.ts Näytä tiedosto

@@ -0,0 +1,7 @@
export namespace meta {
export const host = process.env.HOST ?? '0.0.0.0';

export const port = Number(process.env.PORT ?? 8080);

export const env = process.env.NODE_ENV ?? 'development';
}

+ 20
- 0
packages/web-api/src/index.ts Näytä tiedosto

@@ -0,0 +1,20 @@
import { createServer } from '@/server';
import { addRoutes } from '@/routes';
import * as config from '@/config';

const server = createServer({
logger: config.meta.env !== 'test',
});

addRoutes(server);

server.listen(
{ port: config.meta.port, host: config.meta.host },
(err, address) => {
if (err) {
server.log.error(err.message);
process.exit(1);
}
server.log.info(`server listening on ${address}`);
},
);

+ 60
- 0
packages/web-api/src/modules/adder/adder.controller.ts Näytä tiedosto

@@ -0,0 +1,60 @@
import { RouteHandlerMethod } from 'fastify';
import {
AdderService,
AdderServiceImpl,
ArgumentOutOfRangeError,
InvalidArgumentTypeError,
} from '@/modules/adder/adder.service';
import { constants } from 'http2';

export interface AdderController {
addNumbers: RouteHandlerMethod;
subtractNumbers: RouteHandlerMethod;
}

export class AdderControllerImpl implements AdderController {
constructor(
private readonly adderService: AdderService = new AdderServiceImpl(),
) {
// noop
}

readonly addNumbers: RouteHandlerMethod = async (request, reply) => {
const { a, b } = request.body as { a: number; b: number };
try {
const response = this.adderService.addNumbers({ a, b });
reply.send(response);
} catch (errorRaw) {
if (errorRaw instanceof InvalidArgumentTypeError) {
request.log.info(errorRaw);
reply.status(constants.HTTP_STATUS_BAD_REQUEST).send(errorRaw.message);
return;
}
if (errorRaw instanceof ArgumentOutOfRangeError) {
reply.status(constants.HTTP_STATUS_BAD_REQUEST).send(errorRaw.message);
return;
}
const error = errorRaw as Error;
reply.status(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR).send(error.message);
}
}

readonly subtractNumbers: RouteHandlerMethod = async (request, reply) => {
const { a, b } = request.body as { a: number; b: number };
try {
const response = this.adderService.addNumbers({ a, b: -b });
reply.send(response);
} catch (errorRaw) {
if (errorRaw instanceof InvalidArgumentTypeError) {
reply.status(constants.HTTP_STATUS_BAD_REQUEST).send(errorRaw.message);
return;
}
if (errorRaw instanceof ArgumentOutOfRangeError) {
reply.status(constants.HTTP_STATUS_BAD_REQUEST).send(errorRaw.message);
return;
}
const error = errorRaw as Error;
reply.status(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR).send(error.message);
}
}
}

+ 21
- 0
packages/web-api/src/modules/adder/adder.service.ts Näytä tiedosto

@@ -0,0 +1,21 @@
import {
add,
AddFunctionOptions,
ArgumentOutOfRangeError,
InvalidArgumentTypeError,
} from '@modal-sh/core';

export interface AdderService {
addNumbers(options: AddFunctionOptions): number;
}

export class AdderServiceImpl implements AdderService {
addNumbers(options: AddFunctionOptions) {
return add(options);
}
}

export {
ArgumentOutOfRangeError,
InvalidArgumentTypeError,
};

+ 2
- 0
packages/web-api/src/modules/adder/index.ts Näytä tiedosto

@@ -0,0 +1,2 @@
export * from './adder.controller';
export * from './adder.service';

+ 36
- 0
packages/web-api/src/routes.ts Näytä tiedosto

@@ -0,0 +1,36 @@
import { FastifyInstance } from 'fastify';
import { AdderController, AdderControllerImpl } from '@/modules/adder';

export const addRoutes = (server: FastifyInstance) => {
const adderController: AdderController = new AdderControllerImpl();

return server
.route({
method: 'POST',
url: '/add',
schema: {
body: {
type: 'object',
properties: {
a: { type: 'number' },
b: { type: 'number' },
}
}
},
handler: adderController.addNumbers,
})
.route({
method: 'POST',
url: '/subtract',
schema: {
body: {
type: 'object',
properties: {
a: { type: 'number' },
b: { type: 'number' },
}
}
},
handler: adderController.subtractNumbers,
});
};

+ 11
- 0
packages/web-api/src/server.ts Näytä tiedosto

@@ -0,0 +1,11 @@
import fastify, { FastifyServerOptions } from 'fastify';

export interface CreateServerOptions extends FastifyServerOptions {}

export const createServer = (options = {} as CreateServerOptions) => {
const server = fastify(options);

// TODO set up server stuff here

return server;
};

+ 87
- 0
packages/web-api/test/index.test.ts Näytä tiedosto

@@ -0,0 +1,87 @@
import { FastifyInstance } from 'fastify';
import {
describe,
it,
expect,
beforeAll,
afterAll,
vi,
} from 'vitest';
import { constants } from 'http2';
import { createServer } from '../src/server';
import { addRoutes } from '../src/routes';
import {
AdderServiceImpl,
ArgumentOutOfRangeError,
InvalidArgumentTypeError,
} from '../src/modules/adder';

describe('Example', () => {
let server: FastifyInstance;
const body = { a: 1, b: 2 };

beforeAll(() => {
server = createServer();
addRoutes(server);
});

afterAll(async () => {
await server.close();
});

it('returns result when successful', async () => {
const response = await server
.inject()
.post('/')
.body(body)
.headers({
'Accept': 'application/json',
});
expect(response.statusCode).toBe(constants.HTTP_STATUS_OK);
});

it('returns error when given invalid inputs', async () => {
vi.spyOn(AdderServiceImpl.prototype, 'addNumbers').mockImplementationOnce(() => {
throw new InvalidArgumentTypeError('Invalid input');
});

const response = await server
.inject()
.post('/')
.body(body)
.headers({
'Accept': 'application/json',
});
expect(response.statusCode).toBe(constants.HTTP_STATUS_BAD_REQUEST);
});

it('returns error when given out-of-range inputs', async () => {
vi.spyOn(AdderServiceImpl.prototype, 'addNumbers').mockImplementationOnce(() => {
throw new ArgumentOutOfRangeError('Out of range');
});

const response = await server
.inject()
.post('/')
.body(body)
.headers({
'Accept': 'application/json',
});
expect(response.statusCode).toBe(constants.HTTP_STATUS_BAD_REQUEST);
});

it('returns error when an unexpected error occurs', async () => {
vi.spyOn(AdderServiceImpl.prototype, 'addNumbers').mockImplementationOnce(() => {
throw new Error('Unexpected error');
});

const response = await server
.inject()
.post('/')
.body({ a: 1, b: 2 })
.headers({
'Accept': 'application/json',
});
expect(response.statusCode).toBe(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR);
});
});

+ 24
- 0
packages/web-api/tsconfig.eslint.json Näytä tiedosto

@@ -0,0 +1,24 @@
{
"exclude": ["node_modules"],
"include": ["src", "types", "test"],
"compilerOptions": {
"module": "ESNext",
"lib": ["DOM", "ESNext"],
"importHelpers": true,
"declaration": true,
"sourceMap": true,
"rootDir": "./",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"moduleResolution": "node",
"jsx": "react",
"esModuleInterop": true,
"target": "es2018",
"paths": {
"@/*": ["./src/*"],
}
}
}

+ 24
- 0
packages/web-api/tsconfig.json Näytä tiedosto

@@ -0,0 +1,24 @@
{
"exclude": ["node_modules"],
"include": ["src", "types"],
"compilerOptions": {
"module": "ESNext",
"lib": ["ESNext"],
"importHelpers": true,
"declaration": true,
"sourceMap": true,
"rootDir": "./src",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"moduleResolution": "node",
"jsx": "react",
"esModuleInterop": true,
"target": "es2018",
"paths": {
"@/*": ["./src/*"],
}
}
}

+ 6
- 0
packages/web-api/vite.config.ts Näytä tiedosto

@@ -0,0 +1,6 @@
import { defineConfig } from 'vite';
import tsconfigPaths from 'vite-tsconfig-paths';

export default defineConfig({
plugins: [tsconfigPaths()],
});

+ 4279
- 0
pnpm-lock.yaml
File diff suppressed because it is too large
Näytä tiedosto


+ 2
- 0
pnpm-workspace.yaml Näytä tiedosto

@@ -0,0 +1,2 @@
packages:
- 'packages/*'

Ladataan…
Peruuta
Tallenna