Rust WASM Starter (#112)

This commit is contained in:
Jake Runzer
2021-05-26 19:00:33 -07:00
committed by GitHub
parent 281366e5f5
commit 682e022d94
43 changed files with 6728 additions and 0 deletions

35
examples/rust-wasm/.gitignore vendored Normal file
View File

@@ -0,0 +1,35 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
node_modules
.pnp
.pnp.js
# testing
/coverage
# next.js
.next/
out/
# production
build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env.local
.env.development.local
.env.test.local
.env.production.local
# Rust
crate/pkg
crate/target

View File

@@ -0,0 +1,6 @@
{
"bracketSpacing": true,
"singleQuote": false,
"trailingComma": "all",
"arrowParens": "avoid"
}

View File

@@ -0,0 +1,24 @@
FROM rustlang/rust:nightly
# Install node
RUN curl -sL https://deb.nodesource.com/setup_14.x | bash - \
&& apt-get install -y nodejs \
&& npm i -g yarn
# Install wasm-pack
RUN curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
# Create app directory
WORKDIR /usr/src/app
# Bundle app source
COPY . .
# Install deps
RUN yarn
# Build
RUN yarn build
# Start
CMD [ "yarn", "start" ]

View File

@@ -0,0 +1,38 @@
---
title: WASM Rust
description: Game of life implemented in Rust and served with NextJS
tags:
- wasm
- rust
- next
- typescript
- tailwind
---
# WebAssembly Rust
This example is a implements [Conway's Game of
Life](https://rustwasm.github.io/book/game-of-life/introduction.html) tutorial
in Rust and WebAssembly. The frontend is served as a
[NextJS](https://nextjs.org/) app with TypeScript and Tailwind.
[![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/new?template=https%3A%2F%2Fgithub.com%2Frailwayapp%2Fexamples%2Ftree%2Fmaster%2Fexamples%2Frust-wasm)
## ✨ Features
- Rust
- WASM
- TypeScript
- NextJS
- Tailwind
## 💁‍♀️ How to use
- Ensure you have the [Rust toolchain](https://www.rust-lang.org/) setup
- Install [wasm-pack](https://rustwasm.github.io/wasm-pack/)
- Install deps with `yarn`
- Run app in development `yarn dev`
## 📝 Notes
This starter implements part of the official [Rust WASM tutorial](https://rustwasm.github.io/book/game-of-life/introduction.html).

View File

@@ -0,0 +1,25 @@
{
"name": "wasm-starter",
"private": true,
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"workspaces": [
"packages/*"
],
"scripts": {
"dev": "yarn build:wasm && concurrently -n crate,www -c magenta,cyan \"yarn dev:www\" \"yarn dev:wasm\" ",
"dev:www": "yarn workspace www dev",
"dev:wasm": "yarn workspace crate dev",
"build": "yarn build:wasm && yarn build:www",
"build:www": "yarn workspace www build",
"build:wasm": "yarn workspace crate build",
"start": "yarn workspace www start",
"clean": "wsrun --exclude-missing clean"
},
"devDependencies": {
"concurrently": "^6.1.0",
"wsrun": "^5.2.4",
"prettier": "^2.2.1"
}
}

View File

@@ -0,0 +1,6 @@
/target
**/*.rs.bk
Cargo.lock
bin/
pkg/
wasm-pack.log

View File

@@ -0,0 +1,36 @@
[package]
name = "crate"
version = "0.1.0"
authors = ["Jake Runzer <jakerunzer@gmail.com>"]
edition = "2018"
[lib]
crate-type = ["cdylib", "rlib"]
[features]
default = ["console_error_panic_hook"]
[dependencies]
wasm-bindgen = "0.2"
js-sys = "0.3.51"
cargo-watch = "7.8.0"
# The `console_error_panic_hook` crate provides better debugging of panics by
# logging them with `console.error`. This is great for development, but requires
# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for
# code size when deploying.
console_error_panic_hook = { version = "0.1.1", optional = true }
# `wee_alloc` is a tiny allocator for wasm that is only ~1K in code size
# compared to the default allocator's ~10K. It is slower than the default
# allocator, however.
#
# Unfortunately, `wee_alloc` requires nightly Rust when targeting wasm for now.
wee_alloc = { version = "0.4.2", optional = true }
[dev-dependencies]
wasm-bindgen-test = "0.2"
[profile.release]
# Tell `rustc` to optimize for small code size.
opt-level = "s"

View File

@@ -0,0 +1,25 @@
Copyright (c) 2018 Jake Runzer <jakerunzer@gmail.com>
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.

View File

@@ -0,0 +1,69 @@
<div align="center">
<h1><code>wasm-pack-template</code></h1>
<strong>A template for kick starting a Rust and WebAssembly project using <a href="https://github.com/rustwasm/wasm-pack">wasm-pack</a>.</strong>
<p>
<a href="https://travis-ci.org/rustwasm/wasm-pack-template"><img src="https://img.shields.io/travis/rustwasm/wasm-pack-template.svg?style=flat-square" alt="Build Status" /></a>
</p>
<h3>
<a href="https://rustwasm.github.io/docs/wasm-pack/tutorials/npm-browser-packages/index.html">Tutorial</a>
<span> | </span>
<a href="https://discordapp.com/channels/442252698964721669/443151097398296587">Chat</a>
</h3>
<sub>Built with 🦀🕸 by <a href="https://rustwasm.github.io/">The Rust and WebAssembly Working Group</a></sub>
</div>
## About
[**📚 Read this template tutorial! 📚**][template-docs]
This template is designed for compiling Rust libraries into WebAssembly and
publishing the resulting package to NPM.
Be sure to check out [other `wasm-pack` tutorials online][tutorials] for other
templates and usages of `wasm-pack`.
[tutorials]: https://rustwasm.github.io/docs/wasm-pack/tutorials/index.html
[template-docs]: https://rustwasm.github.io/docs/wasm-pack/tutorials/npm-browser-packages/index.html
## 🚴 Usage
### 🐑 Use `cargo generate` to Clone this Template
[Learn more about `cargo generate` here.](https://github.com/ashleygwilliams/cargo-generate)
```
cargo generate --git https://github.com/rustwasm/wasm-pack-template.git --name my-project
cd my-project
```
### 🛠️ Build with `wasm-pack build`
```
wasm-pack build
```
### 🔬 Test in Headless Browsers with `wasm-pack test`
```
wasm-pack test --headless --firefox
```
### 🎁 Publish to NPM with `wasm-pack publish`
```
wasm-pack publish
```
## 🔋 Batteries Included
* [`wasm-bindgen`](https://github.com/rustwasm/wasm-bindgen) for communicating
between WebAssembly and JavaScript.
* [`console_error_panic_hook`](https://github.com/rustwasm/console_error_panic_hook)
for logging panic messages to the developer console.
* [`wee_alloc`](https://github.com/rustwasm/wee_alloc), an allocator optimized
for small code size.

View File

@@ -0,0 +1,13 @@
{
"name": "crate",
"private": true,
"version": "0.0.1",
"author": "Jake Runzer <jakerunzer@gmail.com>",
"license": "MIT",
"main": "pkg/crate.js",
"scripts": {
"dev": "cargo watch -i .gitignore -i 'pkg/*' -s 'wasm-pack build'",
"build": "wasm-pack build",
"clean": "cargo clean && rm -rf pkg"
}
}

View File

@@ -0,0 +1,168 @@
mod utils;
use std::fmt;
use js_sys::Math::random;
use wasm_bindgen::prelude::*;
// When the `wee_alloc` feature is enabled, use `wee_alloc` as the global
// allocator.
#[cfg(feature = "wee_alloc")]
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
#[wasm_bindgen]
extern "C" {
fn alert(s: &str);
}
#[wasm_bindgen]
pub fn value() -> String {
"Hello from Rust!".into()
}
#[wasm_bindgen]
#[repr(u8)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Cell {
Dead = 0,
Alive = 1,
}
#[wasm_bindgen]
pub struct Universe {
width: u32,
height: u32,
cells: Vec<Cell>,
}
impl Cell {
fn toggle(&mut self) {
*self = match *self {
Cell::Dead => Cell::Alive,
Cell::Alive => Cell::Dead,
}
}
}
#[wasm_bindgen]
impl Universe {
pub fn new() -> Universe {
let width = 64;
let height = 64;
let cells = (0..width * height).map(|_i| new_cell()).collect();
Universe {
width,
height,
cells,
}
}
pub fn toggle_cell(&mut self, row: u32, column: u32) {
let idx = self.get_index(row, column);
self.cells[idx].toggle()
}
pub fn restart(&mut self) {
self.cells = (0..self.width * self.height).map(|_i| new_cell()).collect();
}
pub fn render(&self) -> String {
self.to_string()
}
pub fn width(&self) -> u32 {
self.width
}
pub fn height(&self) -> u32 {
self.height
}
pub fn cells(&self) -> *const Cell {
self.cells.as_ptr()
}
fn get_index(&self, row: u32, column: u32) -> usize {
(row * self.width + column) as usize
}
fn live_neighbor_count(&self, row: u32, column: u32) -> u8 {
let mut count = 0;
for delta_row in [self.height - 1, 0, 1].iter().cloned() {
for delta_col in [self.width - 1, 0, 1].iter().cloned() {
if delta_row == 0 && delta_col == 0 {
continue;
}
let neighbor_row = (row + delta_row) % self.height;
let neighbor_col = (column + delta_col) % self.height;
let idx = self.get_index(neighbor_row, neighbor_col);
count += self.cells[idx] as u8;
}
}
count
}
pub fn tick(&mut self) {
let mut next = self.cells.clone();
for row in 0..self.height {
for col in 0..self.width {
let idx = self.get_index(row, col);
let cell = self.cells[idx];
let live_neighbors = self.live_neighbor_count(row, col);
let next_cell = match (cell, live_neighbors) {
// Rule 1: Any live cell with fewer than two live neighbours
// dies, as if caused by underpopulation.
(Cell::Alive, x) if x < 2 => Cell::Dead,
// Rule 2: Any live cell with two or three live neighbours
// lives on to the next generation.
(Cell::Alive, 2) | (Cell::Alive, 3) => Cell::Alive,
// Rule 3: Any live cell with more than three live
// neighbours dies, as if by overpopulation.
(Cell::Alive, x) if x > 3 => Cell::Dead,
// Rule 4: Any dead cell with exactly three live neighbours
// becomes a live cell, as if by reproduction.
(Cell::Dead, 3) => Cell::Alive,
// All other cells remain in the same state.
(otherwise, _) => otherwise,
};
next[idx] = next_cell;
}
}
self.cells = next;
}
}
fn new_cell() -> Cell {
if random() < 0.5 {
Cell::Alive
} else {
Cell::Dead
}
}
impl fmt::Display for Universe {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
for line in self.cells.as_slice().chunks(self.width as usize) {
for &cell in line {
let symbol = if cell == Cell::Dead { '◻' } else { '◼' };
write!(f, "{}", symbol)?;
}
write!(f, "\n")?;
}
Ok(())
}
}

View File

@@ -0,0 +1,10 @@
pub fn set_panic_hook() {
// When the `console_error_panic_hook` feature is enabled, we can call the
// `set_panic_hook` function at least once during initialization, and then
// we will get better error messages if our code ever panics.
//
// For more details see
// https://github.com/rustwasm/console_error_panic_hook#readme
#[cfg(feature = "console_error_panic_hook")]
console_error_panic_hook::set_once();
}

View File

@@ -0,0 +1,13 @@
//! Test suite for the Web and headless browsers.
#![cfg(target_arch = "wasm32")]
extern crate wasm_bindgen_test;
use wasm_bindgen_test::*;
wasm_bindgen_test_configure!(run_in_browser);
#[wasm_bindgen_test]
fn pass() {
assert_eq!(1 + 1, 2);
}

View File

@@ -0,0 +1,4 @@
module.exports = {
presets: [["next/babel", { "preset-react": { runtime: "automatic" } }]],
plugins: ["babel-plugin-macros", ["styled-components", { ssr: true }]],
};

View File

@@ -0,0 +1,37 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env.local
.env.development.local
.env.test.local
.env.production.local
# vercel
.vercel
.blog_index_data
.blog_index_data_previews

View File

@@ -0,0 +1,22 @@
MIT License
Copyright (c) 2021 Jake Runzer
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,21 @@
# NextJS Starter
This is my goto starter for new NextJS projects. It has
- [Typescript](https://www.typescriptlang.org/)
- [Tailwind](https://tailwindcss.com/) + [Twin](https://github.com/ben-rogerson/twin.macro) for styling
- Light/dark modes
- [Fathom](https://usefathom.com/) for analytics
- Simple page/layout structure
## Development
This is a [Next.js](https://nextjs.org/) site
```bash
# Install deps
yarn
# Start development server
yarn dev
```

View File

@@ -0,0 +1,2 @@
/// <reference types="next" />
/// <reference types="next/types/global" />

View File

@@ -0,0 +1,11 @@
module.exports = {
webpack: (config, { isServer }) => {
if (!isServer) {
// Unset client-side javascript that only works server-side
// https://github.com/vercel/next.js/issues/7755#issuecomment-508633125
config.node = { fs: "empty", module: "empty" };
}
return config;
},
};

View File

@@ -0,0 +1,42 @@
{
"name": "www",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start --port ${PORT-3000}",
"tsc": "tsc -p .",
"clean": "rm -rf .next"
},
"dependencies": {
"crate": "0.0.1",
"next": "10.0.9",
"next-seo": "^4.23.0",
"next-themes": "^0.0.14",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-feather": "^2.0.9",
"styled-components": "^5.2.1",
"theme-custom-properties": "^1.0.0"
},
"devDependencies": {
"@types/node": "^14.14.37",
"@types/react": "^17.0.3",
"@types/styled-components": "^5.1.9",
"autoprefixer": "^10.2.5",
"babel-plugin-macros": "^3.0.1",
"babel-plugin-styled-components": "^1.12.0",
"postcss": "^8.2.8",
"react-is": "^17.0.2",
"tailwindcss": "^2.0.4",
"ts-node": "^9.1.1",
"twin.macro": "^2.3.1",
"typescript": "^4.2.3"
},
"babelMacros": {
"twin": {
"preset": "styled-components"
}
}
}

View File

@@ -0,0 +1,3 @@
module.exports = {
plugins: ["tailwindcss", "autoprefixer"],
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 432 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 840 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,19 @@
{
"name": "Jake",
"short_name": "",
"icons": [
{
"src": "/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#e41b61",
"background_color": "#f8f9fa",
"display": "standalone"
}

View File

@@ -0,0 +1,177 @@
import { Cell, Universe } from "crate";
import { memory } from "crate/pkg/crate_bg.wasm";
import React, { useEffect, useRef, useState } from "react";
import { Play, Pause, RefreshCw } from "react-feather";
import tw from "twin.macro";
import { Link } from "./Link";
const CELL_SIZE = 10; // px
const GRID_COLOR = "#CCCCCC";
const DEAD_COLOR = "#FFFFFF";
const ALIVE_COLOR = "#000000";
const Life: React.FC = () => {
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const universeRef = useRef(Universe.new());
const [isPaused, setIsPaused] = useState(false);
const drawGrid = (
ctx: CanvasRenderingContext2D,
width: number,
height: number,
) => {
ctx.beginPath();
ctx.strokeStyle = GRID_COLOR;
// Vertical lines.
for (let i = 0; i <= width; i++) {
ctx.moveTo(i * (CELL_SIZE + 1) + 1, 0);
ctx.lineTo(i * (CELL_SIZE + 1) + 1, (CELL_SIZE + 1) * height + 1);
}
// Horizontal lines.
for (let j = 0; j <= height; j++) {
ctx.moveTo(0, j * (CELL_SIZE + 1) + 1);
ctx.lineTo((CELL_SIZE + 1) * width + 1, j * (CELL_SIZE + 1) + 1);
}
ctx.stroke();
};
const getIndex = (row: number, column: number, width: number) => {
return row * width + column;
};
const drawCells = (
ctx: CanvasRenderingContext2D,
universe: Universe,
width: number,
height: number,
) => {
const cellsPtr = universe.cells();
const cells = new Uint8Array(memory.buffer, cellsPtr, width * height);
ctx.beginPath();
for (let row = 0; row < height; row++) {
for (let col = 0; col < width; col++) {
const idx = getIndex(row, col, width);
ctx.fillStyle = cells[idx] === Cell.Dead ? DEAD_COLOR : ALIVE_COLOR;
ctx.fillRect(
col * (CELL_SIZE + 1) + 1,
row * (CELL_SIZE + 1) + 1,
CELL_SIZE,
CELL_SIZE,
);
}
}
ctx.stroke();
};
useEffect(() => {
const canvas = canvasRef.current;
const ctx = canvas?.getContext("2d");
if (canvas == null || ctx == null) return;
const universe = universeRef.current;
const width = universe.width();
const height = universe.height();
canvas.height = (CELL_SIZE + 1) * height + 1;
canvas.width = (CELL_SIZE + 1) * width + 1;
let animationId: number | null = null;
const renderLoop = () => {
if (!isPaused) {
universe.tick();
}
drawGrid(ctx, width, height);
drawCells(ctx, universe, width, height);
animationId = requestAnimationFrame(renderLoop);
};
renderLoop();
return () => {
if (animationId != null) {
cancelAnimationFrame(animationId);
}
};
}, [isPaused]);
const onClickCell = (x: number, y: number) => {
const canvas = canvasRef.current;
const ctx = canvas?.getContext("2d");
if (canvas == null || ctx == null) return;
const universe = universeRef.current;
const width = universe.width();
const height = universe.height();
const boundingRect = canvas.getBoundingClientRect();
const scaleX = canvas.width / boundingRect.width;
const scaleY = canvas.height / boundingRect.height;
const canvasLeft = (x - boundingRect.left) * scaleX;
const canvasTop = (y - boundingRect.top) * scaleY;
const row = Math.min(Math.floor(canvasTop / (CELL_SIZE + 1)), height - 1);
const col = Math.min(Math.floor(canvasLeft / (CELL_SIZE + 1)), width - 1);
universe.toggle_cell(row, col);
drawGrid(ctx, width, height);
drawCells(ctx, universe, width, height);
};
return (
<div tw="leading-none">
<div tw="mb-4">
<div tw="space-y-4 mb-4">
<h1 tw="font-bold text-4xl">Game of Life</h1>
<p>
Implemented in Rust based off the{" "}
<Link
href="https://rustwasm.github.io/book/game-of-life/introduction.html"
tw="text-pink-600 underline hover:text-pink-500"
>
Wasm Game of Life tutorial
</Link>
</p>
</div>
<div tw="flex space-x-4 mt-4">
<button
onClick={() => setIsPaused(!isPaused)}
css={[tw`focus:outline-none focus:ring ring-pink-500 rounded-sm`]}
>
{isPaused ? <Play /> : <Pause />}
</button>
<button
onClick={() => {
universeRef?.current.restart();
}}
css={[tw`focus:outline-none focus:ring ring-pink-500 rounded-sm`]}
>
<RefreshCw />
</button>
</div>
</div>
<canvas
ref={canvasRef}
onClick={e => onClickCell(e.clientX, e.clientY)}
/>
</div>
);
};
export default Life;

View File

@@ -0,0 +1,38 @@
import NLink from "next/link";
import React, { useMemo } from "react";
import "twin.macro";
export interface Props {
href: string;
external?: boolean;
className?: string;
}
const isExternalLink = (href: string) =>
href == null || href.startsWith("http://") || href.startsWith("https://");
const useIsExternalLink = (href: string) =>
useMemo(() => isExternalLink(href), [href]);
export const Link: React.FC<Props> = ({
href,
external,
children,
...props
}) => {
const isExternal = (useIsExternalLink(href) || external) ?? false;
if (isExternal) {
return (
<a href={href} target="_blank" rel="noopener" {...props}>
{children}
</a>
);
}
return (
<NLink href={href} passHref>
<a {...props}>{children}</a>
</NLink>
);
};

View File

@@ -0,0 +1,54 @@
import * as React from "react";
import { DefaultSeo, NextSeo, NextSeoProps } from "next-seo";
import Head from "next/head";
import { DefaultSeoProps } from "next-seo";
export interface Props extends NextSeoProps {
title?: string;
description?: string;
image?: string;
}
const title = "WASM Starter";
export const url = "";
const description = "Game of Life implemented in Rust";
const image = "";
const config: DefaultSeoProps = {
title,
description,
openGraph: {
type: "website",
url,
site_name: title,
images: [{ url: image }],
},
};
export const SEO: React.FC<Props> = ({ image, ...props }) => {
const title = props.title ?? config.title;
const description = props.description || config.description;
return (
<>
<DefaultSeo {...config} />
<NextSeo
{...props}
{...(image == null
? {}
: {
openGraph: {
images: [{ url: image }],
},
})}
/>
<Head>
<title>{title}</title>
<meta name="description" content={description} />
</Head>
</>
);
};

View File

@@ -0,0 +1,26 @@
import { Link } from "./Link";
import tw from "twin.macro";
export const Header = tw.h1`
text-4xl font-extrabold mb-8
`;
export const SubHeader = tw.h2`
text-2xl font-bold mb-4 mt-8
`;
export const Paragraph = tw.p`
mb-12 max-w-sm text-article
`;
export const PillLink = tw(Link)`
block text-fg rounded-sm px-1 bg-accentDim hover:bg-accent hover:text-bg dark:hover:text-fg
`;
export const HighlightLink = tw(Link)`
font-bold hover:text-accent
`;
export const UnderlineLink = tw(HighlightLink)`
underline
`;

View File

@@ -0,0 +1,21 @@
import React from "react";
import { Props as SEOProps, SEO } from "../components/SEO";
import "twin.macro";
export interface Props {
seo?: SEOProps;
}
export const Page: React.FC<Props> = props => {
return (
<>
<SEO {...props.seo} />
<div tw="flex flex-col w-full px-6 mx-auto">
<main tw="flex-grow py-20 md:py-20 w-full mx-auto">
{props.children}
</main>
</div>
</>
);
};

View File

@@ -0,0 +1,16 @@
import { AppProps } from "next/app";
import React from "react";
import { GlobalStyles } from "../styles/GlobalStyles";
import { TwinGlobalStyles } from "../styles/TwinGlobalStyles";
const MyApp = ({ Component, pageProps }: AppProps) => {
return (
<>
<TwinGlobalStyles />
<GlobalStyles />
<Component {...pageProps} />
</>
);
};
export default MyApp;

View File

@@ -0,0 +1,28 @@
import Document from "next/document";
import { ServerStyleSheet } from "styled-components";
export default class MyDocument extends Document {
static async getInitialProps(ctx) {
const sheet = new ServerStyleSheet();
const originalRenderPage = ctx.renderPage;
try {
ctx.renderPage = () =>
originalRenderPage({
enhanceApp: App => props => sheet.collectStyles(<App {...props} />),
});
const initialProps = await Document.getInitialProps(ctx);
return {
...initialProps,
styles: (
<>
{initialProps.styles}
{sheet.getStyleElement()}
</>
),
};
} finally {
sheet.seal();
}
}
}

View File

@@ -0,0 +1,17 @@
import { NextPage } from "next";
import dynamic from "next/dynamic";
import React from "react";
import "twin.macro";
import { Page } from "../layouts/Page";
const DynamicLife = dynamic(() => import("../components/Life"));
const Home: NextPage = () => {
return (
<Page>
<DynamicLife />
</Page>
);
};
export default Home;

View File

@@ -0,0 +1,10 @@
import { createGlobalStyle } from "styled-components";
import tw from "twin.macro";
export const GlobalStyles = createGlobalStyle`
body {
${tw`antialiased`}
${tw`text-black bg-gray-50 leading-relaxed`}
${tw`font-sans`}
}
`;

View File

@@ -0,0 +1,6 @@
import React from "react";
import { GlobalStyles } from "twin.macro";
export const TwinGlobalStyles: React.FC = () => {
return <GlobalStyles />;
};

View File

@@ -0,0 +1,15 @@
const colors = require("tailwindcss/colors");
module.exports = {
purge: ["./src/**/*.{js,ts,jsx,tsx}"],
darkMode: "class", // 'media' or 'class'
theme: {
fontFamily: {
sans: `ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"`,
},
colors: colors,
},
variants: {
extend: {},
},
};

View File

@@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noImplicitAny": false,
"forceConsistentCasingInFileNames": true,
"downlevelIteration": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve"
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}

View File

@@ -0,0 +1,38 @@
// twin.d.ts
import "twin.macro";
import styledImport, { CSSProp, css as cssImport } from "styled-components";
import {} from "styled-components/cssprop";
declare module "twin.macro" {
// The styled and css imports
const styled: typeof styledImport;
const css: typeof cssImport;
}
declare module "react" {
// The css prop
interface HTMLAttributes<T> extends DOMAttributes<T> {
css?: CSSProp;
tw?: string;
}
// The inline svg css prop
interface SVGProps<T> extends SVGProps<SVGSVGElement> {
css?: CSSProp;
}
// <style jsx> and <style jsx global> support for styled-jsx
interface StyleHTMLAttributes<T> extends HTMLAttributes<T> {
jsx?: boolean;
global?: boolean;
}
}
// The 'as' prop on styled components
declare global {
namespace JSX {
interface IntrinsicAttributes<T> extends DOMAttributes<T> {
as?: string | React.ComponentType;
}
}
}

File diff suppressed because it is too large Load Diff

3109
examples/rust-wasm/yarn.lock Normal file

File diff suppressed because it is too large Load Diff