Coder Social home page Coder Social logo

boostio / boostnote-app Goto Github PK

View Code? Open in Web Editor NEW
3.7K 56.0 364.0 79.78 MB

Boost Note is a document driven project management tool that maximizes remote DevOps team velocity.

Home Page: https://boostnote.io

License: Other

HTML 0.06% TypeScript 98.65% JavaScript 0.55% CSS 0.21% Kotlin 0.20% Swift 0.33%
boostnote markdown notes-for-developer electron react typescript devops developer-tools wiki documentation-tool

boostnote-app's Introduction

TachiJS

Build Status codecov NPM download Supported by BoostIO

Tachi(ๅคชๅˆ€) https://en.wikipedia.org/wiki/Tachi

Highly testable dead simple web server written in Typescript

  • ๐Ÿ Highly testable. (all props in req and res are injectable so you don't have to mock at all.)
  • ๐Ÿ”ง Highly customizable.
  • ๐Ÿ’‰ Simple dependency injection.
  • โšก async/await request handler. (like Koa without any configurations.)
  • ๐Ÿญ Based on expressjs. (You can benefit from using this mature library)
  • โœ… Built-in request body validator.
  • ๐Ÿ“ Written in Typescript.

Why?

Nest.js looks nice. But its learning curve is too stiff.(TBH, I still don't know how to redirect dynamically.) Most of people probably do not need to know how Interceptor, Pipe and other things work. It might be good for some enterprize level projects.

But using raw expressjs is also quite painful. To test express apps, you have to use supertest or chai-http things. If you use them, you will lose debugging and error stack while testing because they send actual http request internally. Otherwise, you have to mock up all params, req, res and next, of RequestHandler of express.js.

To deal with the testing problem, inversify-express-utils could be a solution. But it does not support many decorators. To render with view engine like pug, we need to use res.render method. But the only solution is using @response decorator. It means you have to mock up Response in your test. So technically it is super hard to test routes rendering view engine.

Luckily, TachiJS tackles those problems. If you have other ideas, please create an issue!!

How to use

Install tachijs

npm i tachijs reflect-metadata

Add two compiler options, experimentalDecorators and emitDecoratorMetadata, to tsconfig.json.

{
  "compilerOptions": {
    ...
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    ...
  }
}

Quick start

import tachijs, { controller, httpGet } from 'tachijs'

@controller('/')
class HomeController() {
  // Define when this method should be used.
  @httpGet('/')
  index() {
    return {
      message: 'Hello, world!'
    }
  }
}

// Register `HomeController`
const app = tachijs({
  controllers: [HomeController]
})

// `app` is just an express application instance
app.listen(8000)

Now you can access http://localhost:8000/.

For other http methods, tachijs provides @httpPost, @httpPut, @httpPatch, @httpDelete, @httpOptions, @httpHead and @httpAll.

Configuring express app(Middlewares)

There are lots of ways to implement express middlewares.

Use before and after options

import bodyParser from 'body-parser'
import { ConfigSetter, NotFoundException } from 'tachijs'

const before: ConfigSetter = app => {
  app.use(bodyParser())
}

const after: ConfigSetter = app => {
  app.use('*', (req, res, next) => {
    next(new NotFoundException('Page does not exist.'))
  })

  const errorHandler: ErrorRequestHandler = (error, req, res, next) => {
    const { status = 500, message } = error
    res.status(status).json({
      status,
      message
    })
  }
  app.use(errorHandler)
}

const app = tachijs({
  before,
  after
})

app.listen(8000)

Without before or after options

Identically same to the above example.

import express from 'express'
import bodyParser from 'body-parser'
import { ConfigSetter, NotFoundException } from 'tachijs'

const app = express()
app.use(bodyParser())

tachijs({
  app
})

app.use('*', (req, res, next) => {
  next(new NotFoundException('Page does not exist.'))
})

const errorHandler: ErrorRequestHandler = (error, req, res, next) => {
  const { status = 500, message } = error
  res.status(status).json({
    status,
    message
  })
}
app.use(errorHandler)

app.listen(8000)

Apply middlewares to controllers and methods

Sometimes, you might want to apply middlewares to several methods only.

import { controller, httpGet, ForbiddenException } from 'tachijs'
import cors from 'cors'
import { RequestHandler } from 'express'

const onlyAdmin: RequestHandler = (req, res, next) => {
  if (!req.user.admin) {
    next(new ForbiddenException('Only admin users can access this api'))
    return
  }
  next()
}

// Apply `cors()` to controller. Now all methods will use the middleware.
@controller('/', [cors()])
class HomeController() {
  @httpGet('/')
  index() {
    return {
      message: 'Hello, world!'
    }
  }

  // Apply `onlyAdmin` to `admin` method. This middleware will be applied to this method only.
  @httpGet('/', [onlyAdmin])
  admin() {
    return {
      message: 'Hello, world!'
    }
  }
}

Configure router options

Tachijs will create and register a router for each controller.

So you can provide router options via @controller decorator.

@controller('/:name', [], {
  // Provide mergeParams option to express router.
  mergeParams: true
})
class HomeController {
  @httpGet('/hello')
  // Now routes in the controller can access params.
  index(@reqParams('name') name: string) {
    return `Hello, ${name}`
  }
}

Access req.params, req.query and req.body via decorators

You can access them via @reqParams, @reqQuery and @reqBody. (Don't forget to apply body-parser middleware)

import {
  controller,
  httpGet,
  httpPost,
  reqParams,
  reqQuery,
  reqBody
} from 'tachijs'

@controller('/posts')
class PostController() {
  @httpGet('/:postId')
  // `req.params.postId`
  async show(@reqParams('postId') postId: string) {
    const post = await Post.findById(postId)

    return {
      post
    }
  }

  @httpGet('/search')
  // `req.query.title`
  async search(@reqQuery('title') title: string = '') {
    const posts = await Post.find({
      title
    })

    return {
      posts
    }
  }

  @httpPost('/')
  // `req.body` (`@reqBody` does not accept property keys.)
  async create(@reqBody() body: unknown) {
    const validatedBody = validate(body)
    const post = await Post.create({
      ...validatedBody
    })

    return {
      post
    }
  }
}

We also provide reqHeaders, reqCookies and reqSession for req.headers, req.cookies and req.session. To know more, see our api documentation below.

Body validation

@reqBody supports validation via class-validator.

Please install class-validator package first.

npm install class-validator
import { IsString } from 'class-validator'

class PostDTO {
  @IsString()
  title: string

  @IsString()
  content: string
}


@controller('/posts')
class PostController() {
  @httpPost('/')
  // Tachijs can access `PostDTO` via reflect-metadata.
  async create(@reqBody() body: PostDTO) {
    // `body` is already validated and transformed into an instance of `PostDTO`.
    // So we don't need any extra validation.
    const post = await Post.create({
      ...body
    })

    return {
      post
    }
  }
}

Custom parameter decorators!

If you're using passport, you should want to access user data from req.user. @handlerParam decorator make it possible. The decorator gets a selector which accepts express's req, res and next. So all you need to do is decide what to return from thoes three parameters.

import { controller, httpGet, handlerParam } from 'tachijs'

@controller('/')
class HomeController {
  @httpGet('/')
  async showId(@handlerParam((req, res, next) => req.user) user: any) {
    doSomethingWithUser(user)

    return {
      ...
    }
  }
}

If you want reusable code, please try like the below.

import { controller, httpGet, handlerParam } from 'tachijs'

function reqUser() {
  // You can omit other next params, `res` and `next`, if you don't need for your selector.
  return handlerParam(req => req.user)
}

@controller('/')
class HomeController {
  @httpGet('/')
  async showId(@reqUser() user: any) {
    doSomethingWithUser(user)

    return {
      ...
    }
  }
}
Bind methods of req or res before exposing

You can also pass methods of req or res which are augmented by express module. Some of them might need the context of them. So please bind methods before exposing like the below example.

export function cookieSetter() {
  return handlerParam((req, res) => res.cookie.bind(res))
}
design:paramtype

Moreover, tachijs exposes metadata of parameters to forth argument. So you can make your custom validator for query with class-transformer-validator like below. (req.body is also using this.)

import { controller, httpGet, handlerParam } from 'tachijs'
import { IsString } from 'class-validator'
import { transformAndValidate } from 'class-transformer-validator'

function validatedQuery() {
  return handlerParam((req, res, next, meta) => {
    // meta.paramType is from `design:paramtypes`.
    // It is `Object` if the param type is unknown or any.
    return meta.paramType !== Object
      ? transformAndValidate(meta.paramType, req.query)
      : req.query
  })
}

// Validator class
class SearchQuery {
  @IsString()
  title: string
}

@controller('/')
class PostController {
  @httpGet('/search')
  // Provide the validator class to param type.
  // tachijs can access it via `reflect-metadata`.
  search(@validatedQuery() query: SearchQuery) {
    // Now `query` is type-safe
    // because it has been validated and transformed into an instance of SearchQuery.
    const { title } = query

    return {
      ...
    }
  }
}

To know more, see @handlerParam api documentation below.

Redirection, Rendering via pug and others...

Techinically, you don't have to access res to response data. But, if you want to redirect or render page via pug, you need to access res.redirect or res.render. Sadly, if you do, you have make mockup for res.

But, with tachijs, you can tackle this problem.

import { controller, httpGet, RedirectResult } from 'tachijs'

@controller('/')
class HomeController {
  @httpGet('/redirect')
  redirectToHome() {
    return new RedirectResult('/')
  }
}

Now, you can test your controller like the below example.

describe('HomeController#redirectToHome', () => {
  it('redirects to `/`', async () => {
    // Given
    const controller = new HomeController()

    // When
    const result = controller.redirectToHome()

    // Then
    expect(result).toBeInstanceOf(RedirectResult)
    expect(result).toMatchObject({
      location: '/'
    })
  })
})

There are other results too, EndResult, JSONResult, RenderResult, SendFileResult, SendResult, and SendStatusResult. Please see our api documentation below.

BaseController

If you need to use many types of result, you probably want BaseController. Just import it once, and your controller can instantiate results easily.

import { controller, httpGet, BaseController } from 'tachijs'

@controller('/')
// You have to extend your controller from `BaseController`
class HomeController extends BaseController {
  @httpGet('/redirect')
  redirectToHome() {
    // This is identically same to `return new RedirectResult('/')`
    return this.redirect('/')
  }
}

BaseController has methods for all build-in results, Please see our api documentation below.

BaseController#context

You may want to share some common methods via your own base controller. But, sadly, it is not possible to use decorators to get objects from req or res and services provided by @inject.

To make it possible, we introduce context. Which expose req, res and inject method via context if your controller is extended from BaseController.

interface Context {
  req: express.Request
  res: express.Response
  inject<S>(key: string): S
}
import { BaseController, controller, httpPost } from 'tachijs'

class MyBaseController extends BaseController {
  async getUserConfig() {
    // When unit testing, `context` is not defined.
    if (this.context == null) {
      return new UserConfig()
    }

    const { req, inject } = this.context

    // Now we can get the current user from `req`
    const currentUser = req.user

    // And inject any services from the container.
    const userConfigService = inject<UserConfigService>(
      ServiceTypes.UserConfigService
    )

    return userConfigService.findByUserId(userId)
  }
}

@controller('/')
class HomeController {
  @httpGet('/settings')
  settings() {
    const userConfig = await this.getUserConfig()

    return this.render('settings', {
      userConfig
    })
  }
}

#httpContext, #inject and #injector will be deprecated from v1.0.0. Please use #context

Customize result

If you want to have customized result behavior, you can do it with BaseResult. BaseResult is an abstract class which coerce you to define how to end the route by providing execute method. (Every built-in result is extended from BaseResult.)

Let's see our implementation of RedirectResult.

import express from 'express'
import { BaseResult } from './BaseResult'

export class RedirectResult extends BaseResult {
  constructor(
    public readonly location: string,
    public readonly status?: number
  ) {
    super()
  }

  // tachijs will provide all what you need and execute this method.
  async execute(
    req: express.Request,
    res: express.Response,
    next: express.NextFunction
  ) {
    if (this.status != null) return res.redirect(this.status, this.location)
    return res.redirect(this.location)
  }
}

Dependency injection

To make controllers more testable, tachijs provides dependency injection.

Let's think we have some mailing service, MailerService. While developing or testing, we probably don't want our server to send real e-mail everytime.

import tachijs, {
  controller,
  httpGet,
  httpPost,
  reqBody,
  inject,
  BaseController
} from 'tachijs'

// Create enum for service types
enum ServiceTypes {
  EmailService = 'EmailService',
  NotificationService = 'NotificationService'
}

// Abstract class coerce MailerService must have `sendEmail` method.
abstract class MailerService {
  abstract sendEmail(content: string): Promise<void>
}

// Mockup service for development and testing.
class MockEmailService extends MailerService {
  async sendEmail(content: string) {
    console.log(`Not sending email.... content: ${content}`)
  }
}

class EmailService extends MailerService {
  async sendEmail(content: string) {
    console.log(`Sending email.... content: ${content}`)
  }
}

interface Container {
  [ServiceTypes.EmailService]: typeof MailerService
}

const envIsDev = process.env.NODE_ENV === 'development'

// Swapping container depends on the current environment.
const container: Container = envIsDev
  ? {
      // In development env, don't send real e-mail because we use mockup.
      [ServiceTypes.EmailService]: MockEmailService
    }
  : {
      [ServiceTypes.EmailService]: EmailService
    }

@controller('/')
class HomeController extends BaseController {
  constructor(
    // Inject MailerService. The controller will get the one registered to the current container.
    @inject(ServiceTypes.EmailService) private mailer: MailerService
  ) {
    super()
  }

  @httpGet('/')
  home() {
    return `<form action='/notify' method='post'><input type='text' name='message'><button>Notify</button></form>`
  }

  @httpPost('/email')
  async sendEmail(@reqBody() body: any) {
    await this.mailer.sendEmail(body.message)

    return this.redirect('/')
  }
}

const server = tachijs({
  controllers: [HomeController],
  // Register container
  container
})

So you can test HomeController#sendEmail like the below example.

describe('HomeController#sendEmail', () => {
  it('sends email', async () => {
    // Given
    const spyFn = jest.fn()
    class TestEmailService extends MailerService {
      async sendEmail(content: string): Promise<void> {
        spyFn(content)
      }
    }
    const controller = new HomeController(new TestEmailService())

    // When
    const result = controller.sendEmail('hello')

    // Then
    expect(spyFn).toBeCalledWith('hello')
  })
})

Now we don't have to worry that our controller sending e-mail for each testing.

Furthermore, you can inject other services to your service as long as they exist in the container.

class NotificationService {
  constructor(
    // When NotificationService is instantiated, MailerService will be instantiated also by tachijs.
    @inject(ServiceTypes.EmailService) private mailer: MailerService
  ) {}

  async notifyWelcome() {
    await this.mailer.sendEmail('Welcome!')
  }
}
DI without tachijs

When some testing or just writing scripts using services, you might want to use DI without tachijs function. So we exposed Injector class which is used by tachijs.

enum ServiceTypes {
  NameService = 'NameService',
  MyService = 'MyService'
}
class NameService {
  getName() {
    return 'Test'
  }
}
class MyService {
  constructor(
    @inject(ServiceTypes.NameService) private nameService: NameService
  ) {}

  sayHello() {
    return `Hello, ${this.nameService.getName()}`
  }
}
const container = {
  [ServiceTypes.NameService]: NameService,
  [ServiceTypes.MyService]: MyService
}

// Create injector
const injector = new Injector(container)

// Instantiate by a key
const myService = injector.inject<MyService>(ServiceTypes.MyService)
// Instantiate by a constructor
const myService = injector.instantiate(MyService)

Bad practices

Please check this section too to keep your controllers testable.

Execute res.send or next inside of controllers or @handlerParam

Please don't do that. It just make your controller untestable. If you want some special behaviors after your methods are executed, please try to implement them with BaseResult.

Do

class HelloResult extends BaseResult {
  async execute(
    req: express.Request,
    res: express.Response,
    next: express.NextFunction
  ) {
    res.send('Hello')
  }
}

class HomePageController extends BaseController {
  @httpGet('/')
  index() {
    // Now we can test it by just checking the method returns an instance of `HelloResult`.
    return new HelloResult()
  }
}

Don't

class HomePageController {
  @httpGet('/')
  index(@handlerParam((req, res) => res) res: expressResponse) {
    // We have to make mock-up for express.Response to test
    res.send('Hello')
  }
}

Access BaseController#context in your descendant controllers

It is designed to be used inside of your base controller to make unit testing easy.

Do

class MyBaseController extends BaseController {
  doSomethingWithContext() {
    if (this.context == null) {
      // on unit testing
      return
    }
    // on live
  }
}

Don't

class HomePageController extends MyBaseController {
  @httpGet('/')
  index() {
    // We have to make mock-up everything to test
    this.context!.req....
  }
}

APIs

tachijs(options: TachiJSOptions): express.Application

Create and configure an express app.

TachiJSOptions

interface TachiJSOptions<C = {}> {
  app?: express.Application
  before?: ConfigSetter
  after?: ConfigSetter
  controllers?: any[]
  container?: C
}

type ConfigSetter = (app: express.Application) => void
  • app Optional. If you provide this option, tachijs will use it rather than creating new one.
  • before Optional. You can configure express app before registering controllers for applying middlewares.
  • after Optional. You can configure express app before registering controllers for error handling.
  • controllers Optional. Array of controller classes.
  • container Optional. A place for registered services. If you want to use DI, you have to register services to here first.

@controller(path: string, middlewares: RequestHandler[] = [], routerOptions: RouterOptions = {})

It marks class as a controller.

  • path Target path.
  • middlewares Optional. Array of middlewares.
  • routerOptions Optional. Express router options.

@httpMethod(method: string, path: string, middlewares: RequestHandler[] = [])

It marks method as a request handler.

  • method Target http methods, 'get', 'post', 'put', 'patch', 'delete', 'options', 'head' or 'all' are available. ('all' means any methods.)
  • path Target path.
  • middlewares Optional. Array of middlewares.

tachijs also provides shortcuts for @httpMethod.

  • @httpGet(path: string, middlewares: RequestHandler[] = [])
  • @httpPost(path: string, middlewares: RequestHandler[] = [])
  • @httpPut(path: string, middlewares: RequestHandler[] = [])
  • @httpPatch(path: string, middlewares: RequestHandler[] = [])
  • @httpDelete(path: string, middlewares: RequestHandler[] = [])
  • @httpOptions(path: string, middlewares: RequestHandler[] = [])
  • @httpHead(path: string, middlewares: RequestHandler[] = [])
  • @httpAll(path: string, middlewares: RequestHandler[] = [])

@handlerParam<T>(selector: HandlerParamSelector<T>)

  • selector selects a property from req, res, next or even our meta
export type HandlerParamSelector<T> = (
  req: express.Request,
  res: express.Response,
  next: express.NextFunction,
  meta: HandlerParamMeta<T>
) => T
interface HandlerParamMeta<T> {
  index: number
  selector: HandlerParamSelector<T>
  paramType: any
}
  • index Number index of the parameter.
  • selector Its selector.
  • paramType metadata from design:paramtypes.

@reqBody(validator?: any)

Inject req.body.

  • validator Optional. A class with decorators of class-validator. tachijs will validate req.body with it and transform req.body into the validator class. If validator is not given but the parameter has a class validator as its param type, tachijs will use it via reflect-metadata.
import { controller, httpPost, reqBody } from 'tachijs'

@controller('/post')
class PostController {
  @httpPost('/')
  // Identically same to `create(@reqBody(PostDTO) post: PostDTO)`
  create(@reqBody() post: PostDTO) {
    ...
  }
}

@reqParams(paramName?: string)

Inject req.params or its property.

  • paramName If it is given, req.params[paramName] will be injected.

@reqQuery(paramName?: string)

Inject req.query or its property.

  • paramName If it is given, req.query[paramName] will be injected.

@reqHeaders(paramName?: string)

Inject req.headers or its property.

  • paramName If it is given, req.headers[paramName] will be injected.

@reqCookies(paramName?: string)

Inject req.cookies or its property.

  • paramName If it is given, req.cookies[paramName] will be injected.

@reqSignedCookies(paramName?: string)

Inject req.signedCookies or its property.

  • paramName If it is given, req.signedCookies[paramName] will be injected.

@cookieSetter()

Inject res.cookie method to set cookie.

@cookieClearer()

Inject res.clearCookie method to clear cookie.

@reqSession(paramName?: string)

Inject req.session.

BaseController

A base for controller which have lots of helper methods for returning built-in results. Also, it allows another way to access properties of req, res and inject without any decorators.

  • #context tachijs will set req, res and inject method to this property. So, when unit testing, it is not defined.
    • #context.req Raw express request instance
    • #context.req Raw express response instance
    • #inject<S>(key: string): S A method to access a registered service by the given key. It is almost same to @inject decorator. (@inject<ServiceTypes.SomeService> someService: SomeService => const someService = this.inject<SomeService>(ServiceTypes.SomeService))
  • #end(data: any, encoding?: string, status?: number): EndResult
  • #json(data: any, status?: number): JSONResult
  • #redirect(location: string, status?: number): RedirectResult
  • #render(view: string, locals?: any, callback?: RenderResultCallback, status?: number): RenderResult
  • #sendFile(filePath: string, options?: any, callback?: SendFileResultCallback, status?: number): SendFileResult
  • #send(data: any, status?: number): SendResult
  • #sendStatus(status: number): SendStatusResult

Results

BaseResult

All of result classes must be extended from BaseResult because tachijs can recognize results by instanceof BaseResult.

It has only one abstract method which must be defined by descendant classes.

  • execute(req: express.Request, res: express.Response, next: express.NextFunction): Promise<any> tachijs will use this method to finalize response.

new EndResult(data: any, encoding?: string, status: number = 200)

tachijs will finalize response with res.status(status).end(data, encoding).

new JSONResult(data: any, status: number = 200)

tachijs will finalize response with res.status(status).json(data).

new NextResult(error?: any)

tachijs will finalize response with next(error).

new RedirectResult(location: string, status?: number)

tachijs will finalize response with res.redirect(location) (or res.redirect(status, location) if the status is given).

new RenderResult(view: string, locals?: any, callback?: RenderResultCallback, status: number = 200)

tachijs will finalize response with res.status(status).render(view, locals, (error, html) => callback(error, html, req, res, next))

type RenderResultCallback = (
  error: Error | null,
  html: string | null,
  req: express.Request,
  res: express.Response,
  next: express.NextFunction
) => void

new SendFileResult(filePath: string, options: any, callback?: SendFileResultCallback, status: number = 200)

tachijs will finalize response with res.status(status).sendFile(filePath, options, (error) => callback(error, req, res, next))

type SendFileResultCallback = (
  error: Error | null,
  req: express.Request,
  res: express.Response,
  next: express.NextFunction
) => void

new SendResult(data: any, status: number = 200)

tachijs will finalize response with res.status(status).send(data).

new SendStatusResult(status: number)

tachijs will finalize response with res.sendStatus(status).

@inject(key: string)

Inject a registered service in container by the given key.

class Injector

new Injector<C>(container: C)

Instantiate an injector with container

#instantiate(Constructor: any): any

Instantiate a service constructor. If the constructor has injected services, this method instantiate and inject them by #inject method.

#inject<S = any>(key: string): S

Instantiate a service by a key from Container. If there is no service for the given key, it will throws an error.

License

MIT ยฉ Junyoung Choi

boostnote-app's People

Contributors

alexandreaguido avatar butterycrumpet avatar davy-c avatar dependabot[bot] avatar ellekasai avatar esanhueza avatar flexo013 avatar funzin avatar georgeherby avatar guneskaan avatar hsuanxyz avatar igorsakalo avatar jhdcruz avatar jongkeun avatar komediruzecki avatar laudep avatar lexar-systems avatar marilari88 avatar matthfaria96 avatar oyeyipo avatar pfftdammitchris avatar pokidokika avatar rohjs avatar rokt33r avatar sarah-seo avatar saxc avatar sushantdhiman avatar tetsuya95 avatar xatier avatar zerox-dg avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

boostnote-app's Issues

Distributing and Auto updater

Support target

  • macOS
    • Homebrew
  • Windows
    • Scoop
    • Chocolatey
  • Linux(Ubuntu, Arch and Fedora ...suggest me if you want more)
    • Snap.io

About auto updater

I won't release the build with built-in auto-updater until the official version is ready.

Use Redux-saga

Dispatch method and database api should be called each data modification.
So, the current code is just like below.

  confirmCreating () {
    const { storageName, storageData } = this.props
    const { store } = this.context

    let count = 0
    let originalName = filenamify(this.state.newName, {replacement: '_'})
    let newName = originalName
    while (storageData.hasIn(['folders', newName])) {
      newName = `${originalName} (${++count})`
    }

    StorageManager
      .upsertFolder(storageName, newName)
      .then((res) => {
        store.dispatch({
          type: 'UPDATE_FOLDER',
          payload: {
            storageName,
            folderPath: newName
          }
        })
      })

    this.setState({
      isCreatingFolder: false
    }, () => {
      this.storageButton.focus()
    })
  }

https://github.com/CarbonStack/Inpad/blob/master/src/main/Nav/StorageSection.js#L101-L129

If we use redux-saga, we just dispatch an action without calling Database API from component.

  confirmCreating () {
    const { storageName, storageData } = this.props
    const { store } = this.context

    let count = 0
    let originalName = filenamify(this.state.newName, {replacement: '_'})
    let newName = originalName
    while (storageData.hasIn(['folders', newName])) {
      newName = `${originalName} (${++count})`
    }

    store.dispatch({
      type: 'ASYNC_UPDATE_FOLDER',
      payload: {
        storageName,
        folderPath: newName
      }
    })

    this.setState({
      isCreatingFolder: false
    }, () => {
      this.storageButton.focus()
    })
  }

It might be included in v1.0. (Not sure)

Central API for easier pluggin development

While developing this app we must keep a central API so that we can share it with the plugin developers. For example, when a developer develops a plugin that shows a message dialog he could call something like:

import Boost from 'boost';

Boost.window.showMessageDialog('Hello', 'from plugin', Boost.MessageDialogType.INFO);

I think we should plan out how the API should looks like a write a doc about it.

Reviving Plan

Sarah-seo confirmed that she can transfer ownership of this project to me.
I'm going to take over the refactoring from May. It would takes a week. So, this project will be revive again.

Hope we could see soon the new polished version of Inpad! :)

Syncing CouchDB

Inpad uses pouchDB. It is compatible with CouchDB and levelDB. It means it has tons of options for database.

First of all, I'm going to give an option to synchronize CouchDB.

MobX stores

  • Router
  • Data Store
  • Configuration Store

RouterStore

We're going to use https://github.com/alisd23/mobx-react-router now.
But it looks quite new. So, if it is not dependable, we might fork this project.

DataStore

This store will grab all data from PouchDB and map the data on MobX. PouchDB also has mapping and reducing features, but we have to store mappers and reducers to PouchDB. It makes hard to update those mappers and reducers.

ConfigurationStore

WIP...

Custom code mirror themes

Current theme doesn't seem to fit the app coloring.
I'll going to make custom theme.

  • For Default theme
  • For Dark theme

Improve the performance of Markdown preview

Current markdown preview will try to render again every time contents changed. This would be critical if users write a huge document.

Solution

  1. Make a queue rendering(Dispatch 500ms later)
  2. Render via web worker
  3. Terminate the worker if the content changed while rendering and make another queue again

Reduce package size

Size of node_modules is quite big.

  • Compile React stuffs and pouchDB via webpack and move them to devDependencies
  • Uglify compiled scripts
  • Discard from dep
    • react-desktop
  • Use minified script
    • lodash
    • moment
    • react-dom
    • pouchdb-browser
    • katex
    • immutable

Size list

8.0K	node_modules/bluebird-lst-c
8.0K	node_modules/strip-outer
 12K	node_modules/abbrev
 12K	node_modules/ansi-regex
 12K	node_modules/ansi-styles
 12K	node_modules/bindings
 12K	node_modules/brace-expansion
 12K	node_modules/buffer-shims
 12K	node_modules/character-entities-legacy
 12K	node_modules/character-reference-invalid
 12K	node_modules/decamelize
 12K	node_modules/detab
 12K	node_modules/emoji-regex
 12K	node_modules/escape-string-regexp
 12K	node_modules/filename-reserved-regex
 12K	node_modules/filenamify
 12K	node_modules/has-ansi
 12K	node_modules/has-flag
 12K	node_modules/has-unicode
 12K	node_modules/inflight
 12K	node_modules/irregular-plurals
 12K	node_modules/is-alphanumeric
 12K	node_modules/is-stream
 12K	node_modules/js-tokens
 12K	node_modules/mdast-comment-marker
 12K	node_modules/mdast-util-definitions
 12K	node_modules/mdast-util-heading-style
 12K	node_modules/normalize-uri
 12K	node_modules/npm-prefix
 12K	node_modules/object-assign
 12K	node_modules/once
 12K	node_modules/os-homedir
 12K	node_modules/os-tmpdir
 12K	node_modules/path-is-absolute
 12K	node_modules/pinkie-promise
 12K	node_modules/remark
 12K	node_modules/remark-html
 12K	node_modules/remark-slug
 12K	node_modules/repeat-string
 12K	node_modules/replace-ext
 12K	node_modules/shellsubstitute
 12K	node_modules/simple-get
 12K	node_modules/space-separated-tokens
 12K	node_modules/string.prototype.codepointat
 12K	node_modules/strip-ansi
 12K	node_modules/strip-markdown
 12K	node_modules/trim-lines
 12K	node_modules/trim-repeated
 12K	node_modules/trim-trailing-lines
 12K	node_modules/unherit
 12K	node_modules/unist-builder
 12K	node_modules/unist-util-generated
 12K	node_modules/unist-util-modify-children
 12K	node_modules/unist-util-position
 12K	node_modules/unist-util-stringify-position
 12K	node_modules/unist-util-visit
 12K	node_modules/untildify
 12K	node_modules/unzip-response
 12K	node_modules/vfile-location
 12K	node_modules/wrappy
 16K	node_modules/after
 16K	node_modules/argsarray
 16K	node_modules/bail
 16K	node_modules/balanced-match
 16K	node_modules/ccount
 16K	node_modules/character-entities-html4
 16K	node_modules/collapse-white-space
 16K	node_modules/color-name
 16K	node_modules/color-string
 16K	node_modules/comma-separated-tokens
 16K	node_modules/concat-map
 16K	node_modules/end-stream
 16K	node_modules/es6-promise-pool
 16K	node_modules/expand-template
 16K	node_modules/github-from-package
 16K	node_modules/graceful-readlink
 16K	node_modules/hast-util-is-element
 16K	node_modules/hast-util-whitespace
 16K	node_modules/hyphenate-style-name
 16K	node_modules/ieee754
 16K	node_modules/inherits
 16K	node_modules/ini
 16K	node_modules/is-alphabetical
 16K	node_modules/is-alphanumerical
 16K	node_modules/is-buffer
 16K	node_modules/is-decimal
 16K	node_modules/is-hexadecimal
 16K	node_modules/is-typedarray
 16K	node_modules/is-whitespace-character
 16K	node_modules/is-word-character
 16K	node_modules/jsonfile
 16K	node_modules/kebab-case
 16K	node_modules/level-write-stream
 16K	node_modules/longest-streak
 16K	node_modules/markdown-escapes
 16K	node_modules/mdast-util-compact
 16K	node_modules/mdast-util-to-string
 16K	node_modules/mime-types
 16K	node_modules/npmlog
 16K	node_modules/pinkie
 16K	node_modules/process-nextick-args
 16K	node_modules/remark-message-control
 16K	node_modules/scope-eval
 16K	node_modules/sliced
 16K	node_modules/state-toggle
 16K	node_modules/stringify-entities
 16K	node_modules/strip-json-comments
 16K	node_modules/supports-color
 16K	node_modules/trough
 16K	node_modules/unist-util-is
 16K	node_modules/util-extend
 16K	node_modules/vfile-sort
 16K	node_modules/x-is-string
 20K	node_modules/array-iterate
 20K	node_modules/assert-plus
 20K	node_modules/block-stream
 20K	node_modules/execspawn
 20K	node_modules/generate-function
 20K	node_modules/generate-object-property
 20K	node_modules/html-void-elements
 20K	node_modules/invariant
 20K	node_modules/json-stringify-safe
 20K	node_modules/jsonpointer
 20K	node_modules/ltgt
 20K	node_modules/markdown-table
 20K	node_modules/minimist
 20K	node_modules/ms
 20K	node_modules/oauth-sign
 20K	node_modules/osenv
 20K	node_modules/path-array
 20K	node_modules/prr
 20K	node_modules/pump
 20K	node_modules/simple-mime
 20K	node_modules/string_decoder
 20K	node_modules/stringstream
 20K	node_modules/unist-util-remove-position
 20K	node_modules/util-deprecate
 20K	node_modules/vfile
 24K	node_modules/aws-sign2
 24K	node_modules/caseless
 24K	node_modules/cryptiles
 24K	node_modules/deep-extend
 24K	node_modules/forever-agent
 24K	node_modules/fs.realpath
 24K	node_modules/github-markdown-css
 24K	node_modules/hoist-non-react-statics
 24K	node_modules/is-property
 24K	node_modules/isarray
 24K	node_modules/isstream
 24K	node_modules/lodash.pad
 24K	node_modules/lodash.padend
 24K	node_modules/lodash.padstart
 24K	node_modules/property-information
 24K	node_modules/punycode
 24K	node_modules/remark-emoji
 24K	node_modules/tar-fs
 24K	node_modules/tunnel-agent
 24K	node_modules/whatwg-fetch
 24K	node_modules/which
 28K	node_modules/bcrypt-pbkdf
 28K	node_modules/delegates
 28K	node_modules/electron-positioner
 28K	node_modules/encoding
 28K	node_modules/fast-future
 28K	node_modules/function-bind
 28K	node_modules/ghrepos
 28K	node_modules/ghutils
 28K	node_modules/has
 28K	node_modules/isexe
 28K	node_modules/isomorphic-fetch
 28K	node_modules/klaw
 28K	node_modules/match-at
 28K	node_modules/trim
 28K	node_modules/unified
 28K	node_modules/xtend
 32K	node_modules/aws4
 32K	node_modules/combined-stream
 32K	node_modules/d
 32K	node_modules/delayed-stream
 32K	node_modules/extend
 32K	node_modules/hast-util-sanitize
 32K	node_modules/immediate
 32K	node_modules/jsprim
 32K	node_modules/level-codec
 32K	node_modules/loose-envify
 32K	node_modules/mkdirp
 32K	node_modules/noop-logger
 32K	node_modules/parse-entities
 36K	node_modules/ansi
 36K	node_modules/double-ended-queue
 36K	node_modules/form-data
 36K	node_modules/graceful-fs
 36K	node_modules/load-plugin
 36K	node_modules/minimatch
 36K	node_modules/nopt
 36K	node_modules/symbol-observable
 40K	node_modules/asap
 40K	node_modules/chalk
 40K	node_modules/clone
 40K	node_modules/core-util-is
 40K	node_modules/gauge
 40K	node_modules/react-immutable-proptypes
 40K	node_modules/sntp
 40K	node_modules/spark-md5
 44K	node_modules/is-my-json-valid
 44K	node_modules/vuvuzela
 48K	node_modules/end-of-stream
 48K	node_modules/verror
 52K	node_modules/character-entities
 52K	node_modules/debug
 52K	node_modules/es6-symbol
 52K	node_modules/extsprintf
 52K	node_modules/getpass
 52K	node_modules/wrapped
 56K	node_modules/commander
 56K	node_modules/ecc-jsbn
 56K	node_modules/ghreleases
 60K	node_modules/es6-iterator
 60K	node_modules/node-fetch
 60K	node_modules/semver
 64K	node_modules/boom
 64K	node_modules/jsbn
 64K	node_modules/node-uuid
 68K	node_modules/errno
 68K	node_modules/level-iterator-stream
 68K	node_modules/rc
 72K	node_modules/color
 72K	node_modules/github-slugger
 76K	node_modules/bowser
 76K	node_modules/sprintf-js
 80K	node_modules/lie
 80K	node_modules/qs
 84K	node_modules/asynckit
 84K	node_modules/http-signature
 84K	node_modules/url-template
 88K	node_modules/asn1
 88K	node_modules/rimraf
 92K	node_modules/dashdash
 92K	node_modules/tar
 96K	node_modules/hast-util-to-html
 96K	node_modules/hoek
 96K	node_modules/readable-stream
100K	node_modules/fstream
100K	node_modules/jodid25519
100K	node_modules/ua-parser-js
108K	node_modules/har-validator
116K	node_modules/array-index
116K	node_modules/buffer
144K	node_modules/mdast-util-to-hast
152K	node_modules/react-redux
156K	node_modules/levelup
164K	node_modules/tweetnacl
168K	node_modules/mime-db
168K	node_modules/node-emoji
176K	node_modules/abstract-leveldown
184K	node_modules/argparse
196K	node_modules/hawk
196K	node_modules/promise
200K	node_modules/hyperquest
204K	node_modules/redux
204K	node_modules/write-stream
208K	node_modules/deferred-leveldown
208K	node_modules/remark-stringify
216K	node_modules/es6-promise
216K	node_modules/through2
220K	node_modules/bl
224K	node_modules/esprima
224K	node_modules/tough-cookie
232K	node_modules/json-schema
236K	node_modules/tar-stream
244K	node_modules/are-we-there-yet
248K	node_modules/jsonist
256K	node_modules/sshpk
268K	node_modules/sander
276K	node_modules/prebuild
276K	node_modules/remark-parse
320K	node_modules/remark-lint
360K	node_modules/js-yaml
396K	node_modules/iconv-lite
400K	node_modules/electron-auto-updater
404K	node_modules/nan
452K	node_modules/immutable
652K	node_modules/async
696K	node_modules/react
704K	node_modules/bluebird
864K	node_modules/source-map-support
964K	node_modules/fbjs
984K	node_modules/octicons
1.5M	node_modules/glamor
1.8M	node_modules/es5-ext
1.8M	node_modules/styled-components
1.9M	node_modules/node-gyp
2.0M	node_modules/node-ninja
2.4M	node_modules/react-dom
2.6M	node_modules/katex
2.6M	node_modules/lodash-es
2.8M	node_modules/moment
3.1M	node_modules/codemirror
4.0M	node_modules/pouchdb
4.2M	node_modules/core-js
4.8M	node_modules/lodash
7.5M	node_modules/leveldown

NoteList as a treeview

This is a common feature request for Boost. Instead of a list of notes, users want to categorize them using folder structure (tree view). Something like this:

image

Image and file support

Image will be stored as an attachment to PouchDB.
WebSQL also supports Blob data type.

To consider web app, attachments shouldn't be replicated to browser db because of the size limit. But, it could be loaded from the couchDB server directly.

Japanese IME popup overlays input texts

In v0.7.0, due to the replacement of the text editor module, pop-up of convert candidates provided by Japanese IME (both of OS X El Capitan original IME and Google Japanese IME) overlays original input texts, so that we can not see an appropriate cursor position.

screen shot 2016-10-17 at 14 59 12

Plugin system

We're going to make everything pluggable.

  • Make something like apm, atom package manager.
  • Make a server for the package manager.

We're going to work after releasing a prototype version.

Syncing using Simperium

Hi There, I know there was a ticket for this already (#24), but just thought I would suggest using Simperium. Its what Simplenote uses, and works very well cross-platform.

Thanks!

-Josh-

How can I backup notes?

Hello,

Is it possible to backup or move notes to other computer?
Could share information where notes are stored?

Best regards

Todo list for prototype app

This issue is just a todo list to show the progress of renewal app.

If you want to have some idea, please create a new issue.

  • Implement storage routing
  • Extract storage form
  • Implement folder routing
  • Implement note list
  • Create Note
  • Update note
  • Implement NotePage
  • Get title from contents
  • Destroy notes
  • Filter notes by routing
    • Implement folderNoteMap to data store
    • Implement tagNoteMap to data store
  • Validate storage name(Client)
  • Implement codemirror
  • Implement default previewer
  • Finalize SideNav
    • Context menu
    • Context menu separator
    • Separate Remove Folder and New Folder menu items
    • Implement Rename Folder menu item to context menu
    • Prompt dialog
    • MessageBox dialog #65
    • Show dialog when removing a folder #65
      • Don't show dialog if the folder is empty (Postponed)
    • Implement Context for storage
      • New Folder
      • Rename Storage (Postponed)
      • Remove Storage
    • Make storage and folder nav item distinguishable
    • Show Folder name rather than full path
    • Implement Context menu for side nav
      • Add a storage
    • Implement Hover style for Nav items
    • Keyboard navigation (Postponed)
  • Finalize NoteList
    • Implement SearchForm
    • Style New Note button
    • Style note list item
  • Finalize NoteDetail
    • Style Editor
    • Optional Codemirror
    • Optional Preview
  • Storage page
    • Present a roadmap about data syncing

Prototype Todo List

Key binding

  • Cmd + N / Ctrl + N to create note
  • Cmd + Shift + N / Ctrl + Shift + N to create folder
  • Cmd + Alt + F / Ctrl + Alt + F to focus serch input

TitleBar

  • Create a note button
  • Delete button
    • Delete folder when nav is focused
    • Delete note when list is focused
  • Searching Notes

Nav

  • Storage context menu
    • New folder
  • Folder context menu
    • Rename folder()
    • Delete folder
    • New folder
  • Keybinding
    • Up/Down to navigate
    • Enter to focus NoteList
    • โŒซ / Delete to delete folder

NoteList

  • Context menu
    • Delete a note
  • Draggable note(move note to other folder)
  • Keybinding
    • Up/Down to navigate
    • Esc to focus Nav
    • Enter to focus Detail
    • โŒซ / Delete to delete note

Detail

  • Tag Select
  • Esc to focus List

Editor

  • Render on Blur / Edit on Focus(like Boostnote does)
  • 2-pane live edit(like most common markdown note app does)

Database API

  • Create a note
  • Update a note
  • Delete a note
  • Create a folder
  • Rename a folder
  • Delete a folder(Include deleting notes)

Preferences window

  • Dark theme
  • Markdown stylesheet
    • Font size
    • Font family
  • Editor configuration
    • Font family
    • Font size
    • Indent size
    • Indent type(Spaces/Tabs)

Deployment

  • App Icon

Type definition of markdown processor(remark.js)

We're going to use Remark. https://github.com/remarkjs
But, it still doesn't have a proper definition yet.

So we have two options:

  1. Make type definitions for remark.js
  2. Rewrite in typescript https://github.com/Rokt33r/typed-remark/

The first option should be done quite soon. But, we might lose the power of typescript.
The second is a very hard way. I'd tried it and I found it is worth to do it. If we do, we could extend the library dependably. (Sadly, the original author should want to keep using pure Javascript.)

Until this issue is resolved, we're going to use marked(or any other library which has own type definitions)

Logo Ideas

I need some ideas for logo of this app.

screen shot 2016-11-20 at 4 03 18 pm

Trash can

Deleted notes should be put in trashcan so users can restore them. This feature should be implemented in db/Client.

Scroll to top when clicking checkbox of task item from preview

Markdown editor consist of CodeMirror and Iframe components. When scroll event occur from one component, it will tell another component.

Clicking checkbox in preview
-> update value of codemirror
-> reset scroll position
-> scroll event is fired from codemirror
-> preview(Iframe) adjust its scroll position to the reset one.

The fix will be shipped to v0.2.

[WIP] Abstract Syntax Tree Editor for Markdown

I'm going to make a new markdown editor which renders lively, but keep user to feel using common code editor.

How to archive?

  • Every component is rendered as a React component
  • If user selects a block, it will switch edit mode while other blocks still be rendered.

Mobile app

It will be built on React-Native. I don't think this would be hard.

Most libraries, like CodeMirror and pouchDB, already support the mobile.
I think this will be done about the next fall(the October of 2017).

Feature list

If you have other ideas which isn't listed here yet, please create a new issue for it. ๐Ÿ‘

General

  • Open in new window
  • Quick note assistant

Managing notes

  • Nested folder
  • Smart folder(Wiki/Predefined filter)
  • Tag
  • Trash can

Markdown

  • GFM
  • Latex
  • Diagram(Sequence / Flowchart)
  • Abstract Syntax Tree Editor(render as a block)

Cloud support

  • Replication
  • Original cloud service

Security

  • Encrypt content(Just like Notes does)

Scrap / Import

  • Web article(blog post, news article... whatever)
    • Medium
    • Stackoverflow
    • Slack post
    • Github wiki
  • Evernote
  • macOS Notes
  • Google Keep
  • Markdown file
  • Gist

Export

  • Gist
  • Slack Post
  • Facebook Post
  • Medium
  • Evernote
  • Markdown file
  • PDF file
  • HTML file

UI

  • Original codemirror theme

Nested folder

Folder name is restricted by filenamify.
It means / character is reserved.

I'm going to implement nested folder with /

If you want to put nested folder to the default folder, Notes, it can be done by renaming Notes/nested.

i18n support

I'm going to support English, Korean and Japanese.
If you can speak other language, please help me to add it!

Also, I'm still designing how to apply i18n support. If you have an idea, please suggest me!

Behavior improvement of Task list in notes

Todo

  • Show status of task list from NoteList
  • Clickable checkbox
  • Codemirror support

Show status of task list from NoteList

Like tags count, showing tasklist octicon and the status 1/3 would be good.
I'll upload the design later.

Clickable checkbox

By clicking rendered checkbox, user can set it done/undone without editing content.

Codemirror support

If user press Enter when writing task list, append an empty task and move the cursor.
If user press Enter again without editing the created task, escape the task list and let user write the plain body.

Lint markdown

I noticed the current markdown parser has its linter, remark-lint. I think it would be great to show the warning from the linter. By this, user can learn proper syntax of markdown.

Key-bindings

Specs
https://github.com/Sarah-Seo/Inpad/blob/master/docs/key-bindings.md

Main

  • main:new-note
  • main:new-folder
  • main:focus-search
  • main:find
  • main:hide-window
  • main:quit
  • main:delete
  • main:refresh
  • main:preferences
  • main:print

TitleBar

  • title:new-note
  • title:focus-search

Nav(Left Navigator)

  • nav:new-note
  • nav:new-folder
  • nav:focus-list
  • nav:up
  • nav:down
  • nav:delete

List

  • list:focus-editor
  • list:focus-nav
  • list:up
  • list:down
  • list:delete

Detail

  • detail:focus-editor
  • detail:focus-tag-select
  • detail:find
  • detail:print

Code Snippets like Boostnote

@amlcodes asks me to support another note format, code snippets, like Boostnote.

I'd used Boostnote quite ago. Although I thought that feature would be useful, I used Snippet notes quite rarely.

For now, I think the code fence of GFM would be enough.

This doesn't mean I won't give any chance. It would be useful in some cases.
But, the way of Boostnote does not work for me. Also, I think there are many other people like me.

Please comment here if you have better idea. ๐Ÿ˜

Store meta data of db

We should store meta data of db for safe syncing.

key: meta
value:

interface ClientMetaData {
  app: string // `boost`?
  version: string // Semver? 
}

By this we could have 2 benifits:

  1. Coerce users to use compatible db only.(To prevent syncing with db used by other apps)
  2. Implement migration

Extract db api to module

We should extract db/Client module from this repository to implement #44 and #43. Other good reason is we could manage changes in db api with semver.

Refactor SideNav

When switching folders, items in SideNav is rendered inefficiently because of path checking occurs in each list item.
screen shot 2018-12-21 at 12 38 43

It should be done when mapping list items. So, list items can know it doesn't have to be re-rendered.

Use Jest as a Test runner

We need a custom test runner for compiled code by webpack on electron.

Currently, I wrote the codes from scratch. It uses webpack HMR so runs test very fast.
But, the test runner has lack of functionality. It should be improved more.

Anyway, I'm going to mimic the api of Jest. If you have any good idea, please tell me. We should discuss more.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.