/wcd

Next.js + TypeScript

Primary LanguageTypeScriptMIT LicenseMIT

🐜 Windy City Devs LLC 🐜

  • 07/03/20
{
	"scripts": {
		"tsc": "tsc -p tsconfig-cjs.json",
		"prod": "rm -rf .next && next build && tsc --project tsconfig.server.json",
		"production": "run-p prod:productionserver prod:server",
		"prod:productionserver": "node ./.next/production-server/server/index.js",
		"prod:server": "node ./.next/server/init-server.js",
		"build:prod": "cross-env run-s prebuild build export start",
		"client": "next",
		"analyze": "cross-env ANALYZE=true npm run build",
		"analyze:server": "cross-env BUNDLE_ANALYZE=server npm run build",
		"analyze:browser": "cross-env BUNDLE_ANALYZE=browser npm run build",
		"env-scripts": "ts-node ./scripts/env-check.js",
		"watch": "./node_modules/.bin/tsc-watch --onSuccess \" exit\"",
		"vercel:build": "run-p serve next:build",
		"next:build": "rm -rf .next && cross-env run-p serve vercel:build",
		"prestart": "npm run export",
		"localprod": "next start",
		"p:build": "concurrently --kill-others --success first \"next build\" \"cross-env npm run serve\" --compiler-options=\"{\\\"module\\\": \\\"commonjs\\\"}\"",
		"build:client": "next build",
		"conc:build": "npm run p:build",
		"build": "run-p built:server conc:build",
		"post:build": "run-p built:server localprod",
		"built:server": "tsc -p tsconfig.server.json",
		"start": "serve out",
		"seed": "ts-node --compiler-options=\"{\\\"module\\\": \\\"commonjs\\\"}\" server/cloud/seed.ts",
		"clear": "ts-node --compiler-options=\"{\\\"module\\\": \\\"commonjs\\\"}\" server/cloud/clear.ts",
		"serve": "ts-node --compiler-options=\"{\\\"module\\\": \\\"commonjs\\\"}\" --project tsconfig.server.json server/index.ts",
		"exe:dev": "ts-node --transpile-only --compiler-options=\"{\\\"module\\\": \\\"commonjs\\\"}\"",
		"dev": "concurrently --kill-others \"npm run serve\" \"next\" --compiler-options=\"{\\\"module\\\": \\\"commonjs\\\"}\"",
		"export": "next export",
		"vercel:export": "npm run export",
		"server:start": "node ./bin/server/index.js",
		"client:start": "next start",
		"heroku:start": "run-p server:start client:start"
	}
}

Measuring performance

Toggle Script Messages to exit on success

Next Examples

Consider incorporating

  • swr.now.sh - stale-while-revalidate - TypeScript ready - SSR support - hmm...

Updating Post data in ./cloud/seed.ts

  • each time an update is made run the following in order
npm run clear
npm run seed
  • then double check to ensure that the data was pulled from the cloud correctly
npm run dev

Deploy with Vercel

{
	"start": "node ./.next/production-server/server/index.js"
}

Deploy with heroku

{
	"heroku-run-build-script": "true"
}

New Next Methods

TypeScript & Styled Components Next.js Example

ESLint+TS in Next -- Analyzing Overhead Performance Cost Incurrence

Overview

  • ESLint - JS Linter - parses source code into an AST (Abstract Syntax Tree), a JS-AST - JS-AST used by plugins to create assertions (lint rules)
  • TypeScript - JS Superset, static code analyzer - TS compiler parsers code into a TS-AST - TS-AST - TS-AST !== JS-AST - Why? TS is a superset, it supports additional properties - annotations, decorators, generics, interfaces, enums, modules, tuples, etc
  • Consider the following
const x: number = 1;
  • when compiler parses TS→TS-AST, the "number" type annotation is represented
  • ESLint cannot handle TS-AST alone
  • ESLint is comprised of several libraries, more moving parts - ships with built-in parser, espree (estree in TS)
// from node_modules\@typescript-eslint\typescript-estree\dist\convert-comments.d.ts
import * as ts from "typescript";
import { TSESTree } from "./ts-estree";
/**
 * Convert all comments for the given AST.
 * @param ast the AST object
 * @param code the TypeScript code
 * @returns the converted ESTreeComment
 * @private
 */
export declare function convertComments(
	ast: ts.SourceFile,
	code: string
): TSESTree.Comment[];
//# sourceMappingURL=convert-comments.d.ts.map
  • use in a .ts file
import {
	parse,
	parseAndGenerateServices
} from "@typescript-eslint/typescript-estree";

const code = `string of code to be parsed into an AST`;
// parses code with options provided, returns ESTree-compatible AST
const ast1 = parse(code, {
	loc: true,
	range: true
});

// accepts additional options relative to parse
// can generate an AST containing type information
const ast2 = parseAndGenerateServices(code, {
	filePath: "/some/path",
	loc: true,
	project: "../tsconfig.json",
	range: true
});

References

TS → TS-AST

  • TypeScript-ESLint converts TS-AST into an ESLint AST - ESLint runs rules against AST generated by ESLint-JS parser → Espree - Espree: https://github.com/eslint/espree

Extensions to Install

  • vscode-styled-components
  • styled-components-snippets

Getting Started with Next

  • pages dir is required
    • indicates to Next framework where app pages are located
  • public dir is required
    • indicates to Next framework where static resources are located
mkdir pages public && npm init -y
npm i --save react react-dom && npm i --save-dev typescript @types/react @types/react-dom @types/node
  • next, set up scripts in package.json
{
    "scripts": {
        "dev": "next",
        "build": "next build",
        "start": "next start"
    }
}
  • then, configure a TypeScript dev environment
touch tsconfig.json
  • now start the dev script
npm run dev
  • why?

    • Next automatically populates tsconfig w/ default values upon launching dev
  • Now, create index.tsx in the pages dir

cd pages && touch index.tsx && cd ..
  • Note: must export all page components as default functions when using the next framework
import React from "react";
import Head from "next/head";

export default function Front() {
	return (
		<>
			<Head>
				<title>Website Landing Page</title>
			</Head>
			<main>Next is Now</main>
		</>
	);
}

styled-components

Custom _app Component

const MyApp = ({ Component, pageProps }) => {
	return (
		<ThemeProvider theme={theme}>
			<GlobalStyle theme={theme} />
			<Head>
				<title>Next.ts</title>
			</Head>
			<Header />
			<main className="main">
				<Center>
					<Component {...pageProps} />
				</Center>
			</main>
			<Footer />
		</ThemeProvider>
	);
};

export default MyApp;

Custom _document Component

  • augments html and body declarations for appication
  • includes initial props for expressing asynchronous server-rendering data requirements
  • The following are required imports from 'next/document' for the page to be properly rendered
<Html></Html>
<Head />
<Main />
<NextScript />

Creating the Feed, Post, and Section Components

  • This can showcase recent projects, projects in progress, example websites for specific usecases (eg restaurants), etc
  • starting from the root directory
cd components && mkdir Feed && cd Feed && touch index.tsx && cd ../..
  • a section component and its corresponding style file must also be created as follows
  • again, starting from the root directory
cd components && mkdir Section && cd Section && touch index.tsx && cd ../Style && mkdir Section && cd Section && touch index.ts && cd ../../..
  • Post, another component, and its corresponding style file must also be created
cd components && mkdir Post && cd Post && touch index.tsx && cd ../Style && mkdir Post && cd Post && touch index.ts && cd ../../..
  • whew lad, bash proficiency -> optimal efficiency
    • always try to start in the root and end in the root (best practice)

useRouter Hook from next/router

import React, { FunctionComponent } from "react";
import { useRouter } from "next/router";

const Post: FunctionComponent = () => {
	const { pathname, query } = useRouter();

	return (
		<div>
			Pathname: {pathname};<br />
			Post Id: {query.id}
		</div>
	);
};

export default Post;

Abbreviations

Enter dangerouslySetInnerHTML

  • used in ./components/PostBody/index.tsx for simplicity sake
    • Add text preprocessing to avoid leaving an opening for XSS attacks (REVISIT)
import React, { FunctionComponent } from "react";
import Link from "next/link";
import { Post } from "../../shared";
import {
	ContentPostBody,
	FigurePostBody,
	MetaPostBody,
	TitlePostBody
} from "../Style";

interface PostBodyProps {
	post: Post;
}

export const PostBody: FunctionComponent<PostBodyProps> = ({ post }) => {
	return (
		<div>
			<TitlePostBody>{post.title}</TitlePostBody>
			<FigurePostBody>
				<img src={post.image} alt={post.title} />
			</FigurePostBody>

			<ContentPostBody dangerouslySetInnerHTML={{ __html: post.content }} />

			<MetaPostBody>
				<span>{post.date}</span>
				<span>&middot;</span>
				<Link href="/category/[id]" as={`/category/${post.category}`}>
					<a>{post.category}</a>
				</Link>
				<span>&middot;</span>
				<a href={post.source}>Source</a>
			</MetaPostBody>
		</div>
	);
};

Adding a secondary build tsconfig

TypeScript + Babbel

Public Folder houses Static Assets

Git rename local and remote branch

module.exports = {
	parser: "@typescript-eslint/parser",
	extends: [
		"eslint:recommended",
		"plugin:@typescript-eslint/recommended",
		"plugin:react/recommended",
		"prettier/@typescript-eslint",
		"plugin:prettier/recommended",
		"eslint-config-prettier"
	],
	parserOptions: {
		ecmaVersion: 2020,
		sourceType: "module",
		ecmaFeatures: {
			jsx: true
		}
	},
	plugins: ["@typescript-eslint", "prettier"],
	env: {
		node: true,
		es6: true,
		browser: true
	},
	rules: {
		// could set "indent": [true, "tabs", 1]
		"react/prop-types": "off",
		indent: "off",
		"@typescript-eslint/indent": "off",
		"@typescript-eslint/explicit-function-return-type": "off",
		"prettier/prettier": "error"
	},
	settings: {
		react: {
			version: "detect"
		}
	}
};


{
	"presets": [
		[
			"next/babel",
			{
				"preset-env": {},
				"transform-runtime": {},
				"styled-jsx": {},
				"class-properties": {}
			}
		]
	],
	"plugins": [
		[
			"babel-plugin-styled-components",
			{
				"ssr": true,
				"displayName": true,
				"preprocess": false
			}
		]
	]
}
	"babel": {
		"presets": [
			"babel-preset-react-app",
			"next/babel",
			"@zeit/next-typescript/babel"
		]
	},