TheoryOfNekomata 10 месяцев назад
Сommit
45f9853dd5
43 измененных файлов: 5881 добавлений и 0 удалений
  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 Просмотреть файл

@@ -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 Просмотреть файл

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

+ 6
- 0
TODO.md Просмотреть файл

@@ -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 Просмотреть файл

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

+ 107
- 0
packages/cli/.gitignore Просмотреть файл

@@ -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 Просмотреть файл

@@ -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 Просмотреть файл

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

+ 9
- 0
packages/cli/src/cli.ts Просмотреть файл

@@ -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 Просмотреть файл

@@ -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 Просмотреть файл

@@ -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 Просмотреть файл

@@ -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 Просмотреть файл

@@ -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 Просмотреть файл

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

+ 335
- 0
packages/cli/src/packages/cli-wrapper.ts Просмотреть файл

@@ -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 Просмотреть файл

@@ -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 Просмотреть файл

@@ -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 Просмотреть файл

@@ -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 Просмотреть файл

@@ -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 Просмотреть файл

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

+ 107
- 0
packages/core/.gitignore Просмотреть файл

@@ -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 Просмотреть файл

@@ -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 Просмотреть файл

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

+ 34
- 0
packages/core/src/index.ts Просмотреть файл

@@ -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 Просмотреть файл

@@ -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 Просмотреть файл

@@ -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 Просмотреть файл

@@ -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 Просмотреть файл

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

+ 107
- 0
packages/web-api/.gitignore Просмотреть файл

@@ -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 Просмотреть файл

@@ -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 Просмотреть файл

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

+ 7
- 0
packages/web-api/src/config.ts Просмотреть файл

@@ -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 Просмотреть файл

@@ -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 Просмотреть файл

@@ -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 Просмотреть файл

@@ -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 Просмотреть файл

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

+ 36
- 0
packages/web-api/src/routes.ts Просмотреть файл

@@ -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 Просмотреть файл

@@ -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 Просмотреть файл

@@ -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 Просмотреть файл

@@ -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 Просмотреть файл

@@ -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 Просмотреть файл

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

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

+ 4279
- 0
pnpm-lock.yaml
Разница между файлами не показана из-за своего большого размера
Просмотреть файл


+ 2
- 0
pnpm-workspace.yaml Просмотреть файл

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

Загрузка…
Отмена
Сохранить