@maskedeng-tom/ssrsx
TypeScript icon, indicating that this package has built-in type declarations

0.5.10 • Public • Published

Server Side Renderer with tsx

npm version License: MIT


Table of Contents


Basic Usage with Koa

install

npm install @maskedeng-tom/ssrsx
npm install koa @types/koa

tsconfig.json

  • change jsx and jsxImportSource to react-jsx and jsx respectively.
// tsconfig.json
{
  "compilerOptions": {
    "target": "es2016",
    "jsx": "react-jsx",             // !important
    "jsxImportSource": "ssrsxjsx",  // !important
    "module": "CommonJS",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true
  }
}

server side

// index.tsx
import Koa from 'koa';
import { ssrsxKoa } from '@maskedeng-tom/ssrsx';

const App = () => {
  return <>
    <div>
      Hello Ssrsx world !
    </div>
  </>;
};

const app = new Koa();

app.use(ssrsxKoa({
  development: true,
  app: <App/>
}));

app.listen(3000);

start application

npm install
npm run start

and access to http://localhost:3000/


with express

npm install @maskedeng-tom/ssrsx
npm install express @types/express
// tsconfig.json
{
  "compilerOptions": {
    "target": "es2016",
    "jsx": "react-jsx",       // !important
    "jsxImportSource": "jsx", // !important
    "module": "CommonJS",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true
  }
}
// index.tsx
import express from 'express';
import { ssrsxExpress } from '@maskedeng-tom/ssrsx';

const App = () => {
  return <>
    <div>
      Hello Ssrsx world !
    </div>
  </>;
};

const app = express();

app.use(ssrsxExpress({
  development: true,
  app: <App/>
}));

app.listen(3000);

with Router

// index.tsx
import Koa from 'koa';
import { ssrsxKoa, Router, Routes, Route, Link } from '@maskedeng-tom/ssrsx';

const Page1 = () => {
  return <div>
    <div>Page1</div>
    <div><Link to="/page2">Link to Page2</Link></div>
    <div><Link to="/">Top</Link></div>
  </div>;
};

const Page2 = () => {
  return <div>
    <div>Page2</div>
    <div><Link to="/page1">Link to Page1</Link></div>
    <div><Link to="/">Top</Link></div>
  </div>;
};

const App = () => {
  return <html lang="en">
    <head>
      <meta charSet="UTF-8"/>
      <title>Ssrsx</title>
    </head>
    <body>
      <div>
        <Router>
          <Routes>
            <Route path="/">
              <div>
                <div>Hello Ssrsx world !</div>
                <div><Link to="/page1">Link to Page1</Link></div>
                <div><Link to="/page2">Link to Page2</Link></div>
              </div>
            </Route>
            <Route path="page1"><Page1/></Route>
            <Route path="page2"><Page2/></Route>
          </Routes>
        </Router>
      </div>
    </body>
  </html>;
};

const app = new Koa();

app.use(ssrsxKoa({
  development: true,
  app: <App/>
}));

app.listen(3000);

with Client script

create client script folder

mkdir src
mkdir src/client

client script

  • [client module name].js is a client script file.
// src/client/test.js
const onClick = (e: Event) => {
  alert('from js://onClick@test');
};

export { onClick }; // need export! important!

server side

  • js:// is a protocol to call client script function from ssrsx.

    js://[exported function name]@[client module name]

// index.tsx
import Koa from 'koa';
import { ssrsxKoa, useGlobalStyle } from '@maskedeng-tom/ssrsx';

const App = () => {

  useGlobalStyle({
    '.clickable': {
      cursor: 'pointer',
      color: 'blue',
      textDecoration: 'underline',
    },
  });

  return <html lang="en">
    <head>
      <meta charSet="UTF-8"/>
      <title>Ssrsx</title>
    </head>
    <body>
      <div>

        <div onClick="alert('inline js')" className="clickable">
          Run inline js
        </div>
        <div onClick="js://onClick@test" className="clickable" >
          Run outside client js (src/client/test.client.js -&gt; onClick)
        </div>

      </div>
    </body>
  </html>;
};

const app = new Koa();

app.use(ssrsxKoa({
  development: true,
  clientRoot: 'src/client', // client script root
  app: <App/>
}));

app.listen(3000);

with jQuery

This is a sample using jQuery. External libraries are loaded using requirejs. Specify the root folder of the client script in clientRoot and the path to place modules such as jQuery in requireJsRoot.

install jQuery and create client script folder

npm install @maskedeng-tom/ssrsx
npm install koa @types/koa
npm install jquery @types/jquery

mkdir src
mkdir src/client

copy jQuery script to client script folder

cp node_modules/jquery/dist/jquery.min.js src/client

client script with jQuery

// src/client/test.client.js
import $ from 'jquery';

const onClick = (e: Event) => {
  const input = $('#username');
  alert(input.val());
};

export { onClick };

server side with jQuery

// index.tsx
import Koa from 'koa';
import { ssrsxKoa } from '@maskedeng-tom/ssrsx';

const App = () => {
  return <html lang="en">
    <head>
      <meta charSet="UTF-8"/>
      <title>Ssrsx</title>
    </head>
    <body>
      <div>

        <div>
          <input type="text" id="username" name="username" value="foo"/>
        </div>

        <button type="text" onClick="js://test.onClick">
          Show input tag value!
        </button>

      </div>
    </body>
  </html>;
};

const app = new Koa();

app.use(ssrsxKoa({
  development: true,
  clientRoot: 'src/client',
  requireJsRoot: 'src/client',  // for requirejs
  requireJsPaths: {             // for requirejs.config paths
    'jquery': 'jquery.min',     // define for jquery (cut '.js' extension)
  },
  app: <App/>
}));

app.listen(3000);

with jQuery from CDN

If you want to use a CDN, specify the same version of jQuery as the one you installed with npm install jquery.

// index.tsx
...
app.use(ssrsxKoa({
  development: true,
  clientRoot: 'src/client',
  requireJsRoot: 'src/client',
  requireJsPaths: {
    'jquery': 'https://code.jquery.com/jquery-3.7.1.min',
  },
  app: <App/>
}));
...

use POST method

You can get the data sent by the POST method by using the useBody function. and You need to add a body parser to get the data sent by the POST method.

// index.tsx
import Koa from 'koa';
import bodyParser from 'koa-bodyparser';  // add body parser
import { ssrsxKoa, Router, Routes, Route, Link, useBody } from '../';

////////////////////////////////////////////////////////////////////////////////

const LoginCheck = () => {
  // post body data
  const body = useBody<{username: string, password: string}>();
  //
  return <>
    <h1>Login Post Result</h1>
    <div>
      <div>Username: {body.username}</div>
      <div>Password: {body.password}</div>
    </div>
    <Link to="/">Top</Link>
  </>;
};

const LoginForm = () => {
  return <>
    <h1>Login Form</h1>
    <form method="post" action="/login">
      <div>
        <label>
          username: <input type="text" name="username" />
        </label>
      </div>
      <div>
        <label>
          password: <input type="password" name="password" />
        </label>
      </div>
      <button type="submit">Login</button>
    </form>
  </>;
};

const App = () => {
  return <html lang="en">
    <head>
      <meta charSet="utf-8"/>
      <title>Ssrsx</title>
    </head>
    <body>
      <Router>
        <Routes>
          <Route path="/"><LoginForm /></Route>
          <Route path="/login"><LoginCheck /></Route>
        </Routes>
      </Router>
    </body>
  </html>;
};

const app = new Koa();

// body parser
app.use(bodyParser());

app.use(ssrsxKoa({
  development: true,
  clientRoot: 'test/client',
  app: <App/>
}));

app.listen(3000);

express body parser

// index.tsx
...
// body parser
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
...

use session

// index.tsx
import Koa from 'koa';
import bodyParser from 'koa-bodyparser';  // add body parser
import session from 'koa-session';        // add session  
//
import { ssrsxKoa, ssrsxExpress, useSearch } from '../';
import { Router, Routes, Route, Link, Navigate, useBody, useSession } from '../';

////////////////////////////////////////////////////////////////////////////////

interface SessionContext {
  username?: string;
}

const Authorized = () => {
  // session
  const session = useSession<SessionContext>();
  if(!session.username){
    // not authorized
    return <Navigate to="/"/>;
  }
  // authorized
  return <>
    <h1>Authorized</h1>
    <div>
      <div>Authorized Username: {session.username}</div>
    </div>
    <Link to="/logout">Logout</Link>
  </>;
};

const Login = () => {
  // session
  const session = useSession<SessionContext>();
  // post body data
  const body = useBody<{username: string, password: string}>();
  // check username and password
  if(body.username === 'admin' && body.password === 'admin'){
    session.username = body.username;
    return <Navigate to="/authorized"/>;
  }
  // authorization failed
  return <Navigate to="/?message=authorization_failed"/>;
};

const Logout = () => {
  // set session
  const session = useSession<SessionContext>();
  // session clear
  session.username = undefined;
  // redirect to top
  return <Navigate to="/"/>;
};

const LoginForm = () => {
  // get search(query parameter) data (?message=...)
  const search = useSearch<{message: string}>();
  // set session
  const session = useSession<SessionContext>();
  if(session.username){
    // already authorized
    return <Navigate to="/authorized"/>;
  }
  // login form
  return <>
    <h1>Login Form</h1>
    <form method="post" action="/login">
      <div>
        <label>
          username: <input type="text" name="username" />
        </label>
      </div>
      <div>
        <label>
          password: <input type="password" name="password" />
        </label>
      </div>
      {
        search.message && <div>{search.message}</div>
      }
      <button type="submit">Login</button>
    </form>
  </>;
};

const App = () => {
  return <html lang="en">
    <head>
      <meta charSet="utf-8"/>
      <title>Ssrsx</title>
    </head>
    <body>
      <Router>
        <Routes>
          <Route path="/"><LoginForm /></Route>
          <Route path="/login"><Login /></Route>
          <Route path="/logout"><Logout /></Route>
          <Route path="/authorized"><Authorized /></Route>
        </Routes>
      </Router>
    </body>
  </html>;
};

const app = new Koa();

app.keys = ['your custom secret'];  // session key
app.use(session(app));              // add session

app.use(bodyParser());

app.use(ssrsxKoa({
  development: true,
  clientRoot: 'test/client',
  app: <App/>
}));

app.listen(3000);

with CSP (Content Security Policy)

Setting CSP (Content Security Policy) requires allowing ws:// to communicate with WebSocket for ssrsx HotReload (development: true).

Also, because inline scripts are used internally, you need to allow 'unsafe-inline' or 'nonce-${nonce}'.

CSP with Koa

// index.tsx
import helmet from 'koa-helmet';
import crypto from 'crypto';
...
if(process.env.NODE_ENV === 'production'){
  app.use(helmet());
  app.use((ctx, next) => {
    // set nonce to state
    ctx.state.nonce = crypto.randomBytes(16).toString('base64');
    //
    return helmet.contentSecurityPolicy({ directives: {
      defaultSrc: ['\'self\'','ws'],        // add 'ws'
      connectSrc: ['\'self\'','ws://*:*'],  // add 'ws://*:*'
      scriptSrc: [
        '\'self\'',
        `'nonce-${ctx.state.nonce}'`,       // add 'nonce-??' or 'unsafe-inline'
      ],
    }})(ctx, next) as Koa.Middleware;
  });
}

CSP with express

// index.tsx
import helmet from 'helmet';
import crypto from 'crypto';
...
if(process.env.NODE_ENV === 'production'){
  app.use(helmet());
  app.use((req, res, next) => {
    // set nonce to locals
    (res as express.Response).locals.nonce = crypto.randomBytes(16).toString('base64');
    //
    helmet.contentSecurityPolicy({ directives: {
      defaultSrc: ['\'self\'','ws'],        // add 'ws'
      connectSrc: ['\'self\'','ws://*:*'],  // add 'ws://*:*'
      scriptSrc: [
        '\'self\'',
        // add 'nonce-??' or 'unsafe-inline'
        (req, res) => `'nonce-${(res as express.Response).locals?.nonce}'`,
      ],
    }})(req, res, next);
  });
}
...

User Context

User context can be set with the context property of the ssrsx(Koa or Express) function.

The context function is called every time it is rendered.

You can use the useContext function to get the user context value.

for Koa

interface UserContext {
  lang: string;
}

const App = () => {
  const user = useContext<{UserContext}>();
  return <div>Lang: {user.lang}</div>;
};

...

app.use(ssrsxKoa({
  ...
  context: (server): UserContext => {
    return {
      lang: 'en',
    };
  },
}));

for Express

interface UserContext {
  lang: string;
}

const App = () => {
  const user = useContext<{UserContext}>();
  return <div>Lang: {user.lang}</div>;
};

...

app.use(ssrsxExpress({
  ...
  context: (server): UserContext => {
    return {
      lang: 'en',
    };
  },
}));

async Component

In Ssrsx, you cannot perform asynchronous processing using useState or useEffect etc. , but you can create asynchronous components.

The ssrsx context function is called every time it is rendered, so when specifying something like a database instance, create the instance externally and specify it in the context function.

import { Redis } from 'ioredis';

interface UserContext {
  redis: Redis;
}

const isAuthorized = async (username: string, password: string) => {
  const context = useContext<UserContext>();
  const value = await context.redis.get(username);
  return value === password;
};

// async component
const LoginCheck = async () => {
  // post body data
  const body = useBody<{username: string, password: string}>();

  // check login
  const isLogin = await isAuthorized(body.username, body.password);
  if(!isLogin){
    return <Navigate to="/"/>;
  }

  return <>
    <h1>Login OK !</h1>
    <div>
      <div>Username: {body.username}</div>
      <div>Password: {body.password}</div>
    </div>
    <Link to="/">Top</Link>
  </>;
};

...

// create redis instance
const redis = new Redis();

app.use(ssrsxKoa({
  ...
  context: (ctx, next): UserContext => {
    return {
      redis,
    };
  },
}));

Contributing

CONTRIBUTING.mdをお読みください。ここには行動規範やプルリクエストの提出手順が詳細に記載されています。

  1. フォークする
  2. フィーチャーブランチを作成する:git checkout -b my-new-feature
  3. 変更を追加:git add .
  4. 変更をコミット:git commit -am 'Add some feature'
  5. ブランチをプッシュ:git push origin my-new-feature
  6. プルリクエストを提出 😎

Please read CONTRIBUTING.md for details on our code of conduct, and the process for submitting pull requests to us.

  1. Fork it!
  2. Create your feature branch: git checkout -b my-new-feature
  3. Add your changes: git add .
  4. Commit your changes: git commit -am 'Add some feature'
  5. Push to the branch: git push origin my-new-feature
  6. Submit a pull request 😎

Credits

昨今の複雑化していく開発現場にシンプルな力を! 💪

Simplify the complex development landscape of today! 💪


Authors

Maskedeng Tom - Initial work - Maskedeng Tom

😄 プロジェクト貢献者リスト 😄

See also the list of contributors who participated in this project.


Show your support

お役に立った場合はぜひ ⭐ を!

Please ⭐ this repository if this project helped you!


License

MIT License © Maskedeng Tom

TODOs

Package Sidebar

Install

npm i @maskedeng-tom/ssrsx

Weekly Downloads

1,566

Version

0.5.10

License

MIT

Unpacked Size

227 kB

Total Files

123

Last publish

Collaborators

  • maskedeng-tom