├── README.md ├── .github ├── FUNDING.yml ├── assets │ ├── logomark.svg │ ├── microsite.svg │ └── built-with-microsite.svg └── workflows │ ├── benchmark.yml │ └── release.yml ├── .prettierignore ├── examples ├── with-fela │ ├── tsconfig.json │ ├── public │ │ ├── favicon.ico │ │ └── favicon.svg │ ├── snowpack.config.js │ ├── src │ │ ├── components │ │ │ ├── Box.tsx │ │ │ └── Providers.tsx │ │ ├── utils │ │ │ └── fela.ts │ │ └── pages │ │ │ ├── index.tsx │ │ │ └── _document.tsx │ └── package.json ├── hello-world │ ├── tsconfig.json │ ├── src │ │ ├── global │ │ │ ├── index.css │ │ │ └── index.ts │ │ ├── components │ │ │ └── Counter.tsx │ │ └── pages │ │ │ └── index.tsx │ ├── public │ │ ├── favicon.ico │ │ └── favicon.svg │ ├── snowpack.config.js │ └── package.json ├── with-goober │ ├── tsconfig.json │ ├── public │ │ ├── favicon.ico │ │ └── favicon.svg │ ├── snowpack.config.js │ ├── package.json │ └── src │ │ ├── pages │ │ ├── index.tsx │ │ └── _document.tsx │ │ └── components │ │ ├── GlobalStyle.tsx │ │ └── Logo.tsx ├── custom-document │ ├── tsconfig.json │ ├── public │ │ ├── favicon.ico │ │ └── favicon.svg │ ├── snowpack.config.js │ ├── package.json │ └── src │ │ └── pages │ │ ├── _document.tsx │ │ ├── page2.tsx │ │ └── index.tsx ├── custom-ssr-fallback │ ├── tsconfig.json │ ├── public │ │ ├── favicon.ico │ │ └── favicon.svg │ ├── snowpack.config.js │ ├── package.json │ └── src │ │ ├── components │ │ └── Counter.tsx │ │ ├── pages │ │ └── index.tsx │ │ └── global │ │ └── index.css ├── vercel.json ├── partial-hydration │ ├── src │ │ ├── components │ │ │ ├── Idle │ │ │ │ ├── index.module.css │ │ │ │ └── index.tsx │ │ │ ├── Visible │ │ │ │ ├── index.module.css │ │ │ │ └── index.tsx │ │ │ ├── index.tsx │ │ │ ├── LinkedCounters │ │ │ │ ├── index.module.css │ │ │ │ └── index.tsx │ │ │ ├── Counter │ │ │ │ ├── index.module.css │ │ │ │ └── index.tsx │ │ │ ├── LinkedCounter │ │ │ │ ├── index.module.css │ │ │ │ └── index.tsx │ │ │ ├── Clock.tsx │ │ │ └── Static │ │ │ │ └── index.tsx │ │ ├── utils │ │ │ └── linked-state.ts │ │ ├── global │ │ │ └── index.css │ │ └── pages │ │ │ └── index.tsx │ ├── tsconfig.json │ ├── snowpack.config.js │ └── package.json ├── root │ ├── public │ │ ├── favicon.ico │ │ └── favicon.svg │ ├── tsconfig.json │ ├── src │ │ ├── global │ │ │ └── index.css │ │ └── pages │ │ │ └── index.tsx │ ├── snowpack.config.js │ └── package.json ├── dynamic-routes │ ├── src │ │ ├── utils │ │ │ └── linked-state.ts │ │ └── pages │ │ │ ├── index.tsx │ │ │ └── posts │ │ │ └── [id].tsx │ ├── tsconfig.json │ ├── snowpack.config.js │ └── package.json └── named-exports │ ├── tsconfig.json │ ├── snowpack.config.js │ ├── src │ ├── components │ │ ├── default.tsx │ │ ├── named.tsx │ │ └── default-and-named.tsx │ └── pages │ │ └── index.tsx │ └── package.json ├── benchmark ├── next │ ├── counter │ │ ├── pages │ │ │ ├── index.module.css │ │ │ ├── _app.tsx │ │ │ └── index.tsx │ │ ├── next-env.d.ts │ │ ├── global │ │ │ └── index.css │ │ ├── package.json │ │ ├── components │ │ │ └── counter.tsx │ │ └── tsconfig.json │ └── static │ │ ├── pages │ │ ├── index.module.css │ │ ├── _app.tsx │ │ └── index.tsx │ │ ├── next-env.d.ts │ │ ├── global │ │ └── index.css │ │ ├── package.json │ │ └── tsconfig.json ├── gatsby │ ├── counter │ │ ├── src │ │ │ ├── pages │ │ │ │ ├── index.module.css │ │ │ │ └── index.tsx │ │ │ ├── global │ │ │ │ └── index.css │ │ │ └── components │ │ │ │ └── counter.tsx │ │ ├── gatsby-config.js │ │ └── package.json │ └── static │ │ ├── src │ │ ├── pages │ │ │ ├── index.module.css │ │ │ └── index.tsx │ │ └── global │ │ │ └── index.css │ │ ├── gatsby-config.js │ │ └── package.json ├── microsite │ ├── counter │ │ ├── tsconfig.json │ │ ├── src │ │ │ ├── global │ │ │ │ └── index.css │ │ │ ├── pages │ │ │ │ ├── index.tsx │ │ │ │ └── index.module.css │ │ │ └── components │ │ │ │ └── counter.tsx │ │ └── package.json │ └── static │ │ ├── tsconfig.json │ │ ├── src │ │ ├── global │ │ │ └── index.css │ │ └── pages │ │ │ ├── index.tsx │ │ │ └── index.module.css │ │ └── package.json └── README.md ├── packages ├── templates │ ├── next │ │ ├── src │ │ │ ├── global │ │ │ │ ├── index.css │ │ │ │ └── index.ts │ │ │ └── pages │ │ │ │ └── index.tsx │ │ ├── public │ │ │ ├── favicon.ico │ │ │ └── favicon.svg │ │ ├── .gitignore │ │ ├── tsconfig.json │ │ ├── .prettierrc.json │ │ ├── package.json │ │ ├── README.md │ │ └── LICENSE │ ├── default │ │ ├── src │ │ │ ├── global │ │ │ │ ├── index.css │ │ │ │ └── index.ts │ │ │ └── pages │ │ │ │ └── index.tsx │ │ ├── public │ │ │ ├── favicon.ico │ │ │ └── favicon.svg │ │ ├── .gitignore │ │ ├── tsconfig.json │ │ ├── .prettierrc.json │ │ ├── package.json │ │ ├── README.md │ │ └── LICENSE │ └── README.md ├── microsite │ ├── src │ │ ├── client │ │ │ ├── csr.tsx │ │ │ ├── hydrate.tsx │ │ │ └── hooks.ts │ │ ├── utils │ │ │ ├── hydration.ts │ │ │ ├── fs.ts │ │ │ ├── router.ts │ │ │ ├── command.ts │ │ │ └── open.ts │ │ ├── cli │ │ │ └── microsite-serve.ts │ │ ├── error.tsx │ │ ├── bin │ │ │ └── microsite.ts │ │ ├── server │ │ │ └── fetch.ts │ │ ├── page.tsx │ │ ├── hydrate.tsx │ │ ├── global.tsx │ │ ├── runtime │ │ │ └── index.tsx │ │ └── document.tsx │ ├── assets │ │ ├── types │ │ │ ├── index.d.ts │ │ │ └── env.d.ts │ │ ├── snowpack-plugin.cjs │ │ ├── snowpack.config.cjs │ │ └── openChrome.appleScript │ ├── base.json │ ├── tsconfig.json │ ├── scripts │ │ └── clean.js │ ├── LICENSE │ ├── package.json │ └── README.md └── create-microsite │ ├── tsconfig.json │ ├── CHANGELOG.md │ ├── package.json │ └── src │ └── index.ts ├── .changeset ├── smooth-eyes-count.md ├── cool-geckos-thank.md ├── config.json ├── README.md └── pre.json ├── lerna.json ├── site ├── src │ ├── pages │ │ ├── index.module.css │ │ └── index.tsx │ ├── global │ │ └── index.css │ └── components │ │ └── Logo.tsx ├── tsconfig.json ├── vercel.json ├── package.json └── CHANGELOG.md ├── .vscode └── settings.json ├── .gitignore ├── docs ├── advanced │ ├── debugging.md │ └── global-state.md ├── README.md ├── basic │ ├── relative-paths.md │ ├── styling.md │ ├── typescript.md │ ├── data-fetching.md │ ├── bundled-javascript.md │ └── pages.md ├── engines.md ├── resources.md └── getting-started.md ├── scripts ├── generate-examples.js ├── copy-examples.js └── vitals.js ├── LICENSE ├── package.json └── CODE_OF_CONDUCT.md /README.md: -------------------------------------------------------------------------------- 1 | ./packages/microsite/README.md -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [natemoo-re] 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | **/page.tsx 2 | **/fetch.ts 3 | -------------------------------------------------------------------------------- /examples/with-fela/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "microsite/base" 3 | } 4 | -------------------------------------------------------------------------------- /benchmark/next/counter/pages/index.module.css: -------------------------------------------------------------------------------- 1 | .main { 2 | color: red; 3 | } 4 | -------------------------------------------------------------------------------- /benchmark/next/static/pages/index.module.css: -------------------------------------------------------------------------------- 1 | .main { 2 | color: red; 3 | } 4 | -------------------------------------------------------------------------------- /examples/hello-world/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "microsite/base" 3 | } 4 | -------------------------------------------------------------------------------- /examples/with-goober/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "microsite/base" 3 | } 4 | -------------------------------------------------------------------------------- /benchmark/gatsby/counter/src/pages/index.module.css: -------------------------------------------------------------------------------- 1 | .main { 2 | color: red; 3 | } 4 | -------------------------------------------------------------------------------- /benchmark/gatsby/static/src/pages/index.module.css: -------------------------------------------------------------------------------- 1 | .main { 2 | color: red; 3 | } 4 | -------------------------------------------------------------------------------- /examples/custom-document/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "microsite/base" 3 | } 4 | -------------------------------------------------------------------------------- /examples/custom-ssr-fallback/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "microsite/base" 3 | } 4 | -------------------------------------------------------------------------------- /examples/hello-world/src/global/index.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | -------------------------------------------------------------------------------- /packages/templates/next/src/global/index.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | -------------------------------------------------------------------------------- /examples/vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "cleanUrls": true, 3 | "trailingSlash": false 4 | } 5 | -------------------------------------------------------------------------------- /packages/templates/default/src/global/index.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | -------------------------------------------------------------------------------- /examples/partial-hydration/src/components/Idle/index.module.css: -------------------------------------------------------------------------------- 1 | .idle { 2 | color: red; 3 | } 4 | -------------------------------------------------------------------------------- /examples/partial-hydration/src/components/Visible/index.module.css: -------------------------------------------------------------------------------- 1 | .visible { 2 | color: blue; 3 | } 4 | -------------------------------------------------------------------------------- /packages/templates/README.md: -------------------------------------------------------------------------------- 1 | # Microsite Templates 2 | 3 | ``` 4 | npm init microsite 5 | ``` 6 | -------------------------------------------------------------------------------- /benchmark/next/static/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /benchmark/next/counter/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /examples/root/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/natemoo-re/microsite/HEAD/examples/root/public/favicon.ico -------------------------------------------------------------------------------- /examples/hello-world/src/global/index.ts: -------------------------------------------------------------------------------- 1 | export default async function() { 2 | console.log('Hello microsite!'); 3 | } 4 | -------------------------------------------------------------------------------- /packages/templates/next/src/global/index.ts: -------------------------------------------------------------------------------- 1 | export default async function() { 2 | console.log('Hello microsite!'); 3 | } 4 | -------------------------------------------------------------------------------- /examples/with-fela/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/natemoo-re/microsite/HEAD/examples/with-fela/public/favicon.ico -------------------------------------------------------------------------------- /packages/templates/default/src/global/index.ts: -------------------------------------------------------------------------------- 1 | export default async function() { 2 | console.log('Hello microsite!'); 3 | } 4 | -------------------------------------------------------------------------------- /.changeset/smooth-eyes-count.md: -------------------------------------------------------------------------------- 1 | --- 2 | "microsite": patch 3 | --- 4 | 5 | Fix bug where useGlobalState would cause builds to fail 6 | -------------------------------------------------------------------------------- /examples/hello-world/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/natemoo-re/microsite/HEAD/examples/hello-world/public/favicon.ico -------------------------------------------------------------------------------- /examples/with-goober/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/natemoo-re/microsite/HEAD/examples/with-goober/public/favicon.ico -------------------------------------------------------------------------------- /.changeset/cool-geckos-thank.md: -------------------------------------------------------------------------------- 1 | --- 2 | "microsite": patch 3 | --- 4 | 5 | Fix an issue where named exports would not be properly hydrated 6 | -------------------------------------------------------------------------------- /examples/custom-document/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/natemoo-re/microsite/HEAD/examples/custom-document/public/favicon.ico -------------------------------------------------------------------------------- /packages/templates/next/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/natemoo-re/microsite/HEAD/packages/templates/next/public/favicon.ico -------------------------------------------------------------------------------- /packages/templates/default/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/natemoo-re/microsite/HEAD/packages/templates/default/public/favicon.ico -------------------------------------------------------------------------------- /examples/custom-ssr-fallback/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/natemoo-re/microsite/HEAD/examples/custom-ssr-fallback/public/favicon.ico -------------------------------------------------------------------------------- /packages/templates/next/.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | 3 | .microsite/ 4 | .vscode/ 5 | node_modules/ 6 | $RECYCLE.BIN/ 7 | 8 | .DS_Store 9 | Thumbs.db 10 | .env 11 | -------------------------------------------------------------------------------- /benchmark/gatsby/counter/gatsby-config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | siteMetadata: { 3 | title: "Gatsby", 4 | }, 5 | plugins: ['gatsby-plugin-typescript'] 6 | }; 7 | -------------------------------------------------------------------------------- /benchmark/gatsby/static/gatsby-config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | siteMetadata: { 3 | title: "Gatsby", 4 | }, 5 | plugins: ['gatsby-plugin-typescript'] 6 | }; 7 | -------------------------------------------------------------------------------- /packages/templates/default/.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | 3 | .microsite/ 4 | .vscode/ 5 | node_modules/ 6 | $RECYCLE.BIN/ 7 | 8 | .DS_Store 9 | Thumbs.db 10 | .env 11 | -------------------------------------------------------------------------------- /examples/dynamic-routes/src/utils/linked-state.ts: -------------------------------------------------------------------------------- 1 | import { createGlobalState } from "microsite/global"; 2 | 3 | export const state = createGlobalState({ 4 | count: 0, 5 | }); 6 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": ["packages/*", "examples/*", "benchmark/**", "site"], 3 | "version": "independent", 4 | "npmClient": "yarn", 5 | "useWorkspaces": true 6 | } 7 | -------------------------------------------------------------------------------- /examples/partial-hydration/src/utils/linked-state.ts: -------------------------------------------------------------------------------- 1 | import { createGlobalState } from "microsite/global"; 2 | 3 | export const state = createGlobalState({ 4 | count: 0, 5 | }); 6 | -------------------------------------------------------------------------------- /benchmark/next/static/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import "../global/index.css"; 2 | 3 | function App({ Component, pageProps }) { 4 | return ; 5 | } 6 | 7 | export default App; 8 | -------------------------------------------------------------------------------- /benchmark/next/counter/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import "../global/index.css"; 2 | 3 | function App({ Component, pageProps }) { 4 | return ; 5 | } 6 | 7 | export default App; 8 | -------------------------------------------------------------------------------- /examples/partial-hydration/src/components/index.tsx: -------------------------------------------------------------------------------- 1 | export { default as Idle } from "./Idle"; 2 | export { default as Static } from "./Static"; 3 | export { default as Visible } from "./Visible"; 4 | -------------------------------------------------------------------------------- /examples/root/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "microsite/base", 3 | "compilerOptions": { 4 | "baseUrl": "src", 5 | "paths": { 6 | "@/*": ["./*"] 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /benchmark/microsite/counter/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "microsite/base", 3 | "compilerOptions": { 4 | "baseUrl": "src", 5 | "paths": { 6 | "@/*": ["./*"] 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /benchmark/microsite/static/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "microsite/base", 3 | "compilerOptions": { 4 | "baseUrl": "src", 5 | "paths": { 6 | "@/*": ["./*"] 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/dynamic-routes/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "microsite/base", 3 | "compilerOptions": { 4 | "baseUrl": "./src", 5 | "paths": { 6 | "@/*": ["./*"] 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/named-exports/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "microsite/base", 3 | "compilerOptions": { 4 | "baseUrl": "./src", 5 | "paths": { 6 | "@/*": ["./*"] 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/templates/default/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "microsite/base", 3 | "compilerOptions": { 4 | "baseUrl": "src", 5 | "paths": { 6 | "@/*": ["./*"] 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/templates/next/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "microsite/base", 3 | "compilerOptions": { 4 | "baseUrl": "src", 5 | "paths": { 6 | "@/*": ["./*"] 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /site/src/pages/index.module.css: -------------------------------------------------------------------------------- 1 | .main { 2 | width: 100vw; 3 | height: 100vh; 4 | display: flex; 5 | flex-direction: column; 6 | align-items: center; 7 | justify-content: center; 8 | } 9 | -------------------------------------------------------------------------------- /examples/partial-hydration/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "microsite/base", 3 | "compilerOptions": { 4 | "baseUrl": "./src", 5 | "paths": { 6 | "@/*": ["./*"] 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /site/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "microsite/base", 3 | "compilerOptions": { 4 | "baseUrl": "./src", 5 | "paths": { 6 | "@/*": ["./*"] 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/partial-hydration/src/components/LinkedCounters/index.module.css: -------------------------------------------------------------------------------- 1 | .flex { 2 | margin-top: 4em; 3 | display: flex; 4 | flex-flow: row wrap; 5 | align-items: center; 6 | justify-content: center; 7 | } 8 | -------------------------------------------------------------------------------- /site/vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "cleanUrls": true, 3 | "trailingSlash": false, 4 | "redirects": [ 5 | { "source": "/(.*)", "destination": "https://github.com/natemoo-re/microsite", "permanent": false } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /benchmark/next/static/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import styles from "./index.module.css"; 2 | 3 | export default function Home() { 4 | return ( 5 |
6 |

Hello world!

7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /examples/root/src/global/index.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | :root { 6 | font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; 7 | } 8 | -------------------------------------------------------------------------------- /examples/root/snowpack.config.js: -------------------------------------------------------------------------------- 1 | import { resolve, join, dirname } from 'path'; 2 | import { fileURLToPath } from 'url'; 3 | 4 | const workspaceRoot = resolve(join(dirname(fileURLToPath(import.meta.url)), '..', '..')); 5 | 6 | export default { 7 | workspaceRoot 8 | } 9 | -------------------------------------------------------------------------------- /examples/with-fela/snowpack.config.js: -------------------------------------------------------------------------------- 1 | import { resolve, join, dirname } from 'path'; 2 | import { fileURLToPath } from 'url'; 3 | 4 | const workspaceRoot = resolve(join(dirname(fileURLToPath(import.meta.url)), '..', '..')); 5 | 6 | export default { 7 | workspaceRoot 8 | } 9 | -------------------------------------------------------------------------------- /.github/assets/logomark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /examples/custom-document/snowpack.config.js: -------------------------------------------------------------------------------- 1 | import { resolve, join, dirname } from 'path'; 2 | import { fileURLToPath } from 'url'; 3 | 4 | const workspaceRoot = resolve(join(dirname(fileURLToPath(import.meta.url)), '..', '..')); 5 | 6 | export default { 7 | workspaceRoot 8 | } 9 | -------------------------------------------------------------------------------- /examples/dynamic-routes/snowpack.config.js: -------------------------------------------------------------------------------- 1 | import { resolve, join, dirname } from 'path'; 2 | import { fileURLToPath } from 'url'; 3 | 4 | const workspaceRoot = resolve(join(dirname(fileURLToPath(import.meta.url)), '..', '..')); 5 | 6 | export default { 7 | workspaceRoot 8 | } 9 | -------------------------------------------------------------------------------- /examples/hello-world/snowpack.config.js: -------------------------------------------------------------------------------- 1 | import { resolve, join, dirname } from 'path'; 2 | import { fileURLToPath } from 'url'; 3 | 4 | const workspaceRoot = resolve(join(dirname(fileURLToPath(import.meta.url)), '..', '..')); 5 | 6 | export default { 7 | workspaceRoot 8 | } 9 | -------------------------------------------------------------------------------- /examples/named-exports/snowpack.config.js: -------------------------------------------------------------------------------- 1 | import { resolve, join, dirname } from 'path'; 2 | import { fileURLToPath } from 'url'; 3 | 4 | const workspaceRoot = resolve(join(dirname(fileURLToPath(import.meta.url)), '..', '..')); 5 | 6 | export default { 7 | workspaceRoot 8 | } 9 | -------------------------------------------------------------------------------- /examples/with-goober/snowpack.config.js: -------------------------------------------------------------------------------- 1 | import { resolve, join, dirname } from 'path'; 2 | import { fileURLToPath } from 'url'; 3 | 4 | const workspaceRoot = resolve(join(dirname(fileURLToPath(import.meta.url)), '..', '..')); 5 | 6 | export default { 7 | workspaceRoot 8 | } 9 | -------------------------------------------------------------------------------- /packages/microsite/src/client/csr.tsx: -------------------------------------------------------------------------------- 1 | import { h, render } from 'preact'; 2 | 3 | export default (Page: any, props: any) => { 4 | const Component = Page.Component ? Page.Component : Page; 5 | render(, document.getElementById('__microsite')); 6 | } 7 | -------------------------------------------------------------------------------- /benchmark/next/counter/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import styles from "./index.module.css"; 2 | import Counter from "../components/counter"; 3 | 4 | export default function Home() { 5 | return ( 6 |
7 | 8 |
9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /examples/custom-ssr-fallback/snowpack.config.js: -------------------------------------------------------------------------------- 1 | import { resolve, join, dirname } from 'path'; 2 | import { fileURLToPath } from 'url'; 3 | 4 | const workspaceRoot = resolve(join(dirname(fileURLToPath(import.meta.url)), '..', '..')); 5 | 6 | export default { 7 | workspaceRoot 8 | } 9 | -------------------------------------------------------------------------------- /examples/partial-hydration/snowpack.config.js: -------------------------------------------------------------------------------- 1 | import { resolve, join, dirname } from 'path'; 2 | import { fileURLToPath } from 'url'; 3 | 4 | const workspaceRoot = resolve(join(dirname(fileURLToPath(import.meta.url)), '..', '..')); 5 | 6 | export default { 7 | workspaceRoot 8 | } 9 | -------------------------------------------------------------------------------- /examples/named-exports/src/components/default.tsx: -------------------------------------------------------------------------------- 1 | import { withHydrate } from "microsite/hydrate"; 2 | 3 | const DefaultComponent = () => ( 4 |

5 | Hello default 6 |

7 | ); 8 | 9 | export default withHydrate(DefaultComponent, { method: "idle" }); 10 | -------------------------------------------------------------------------------- /packages/microsite/assets/types/index.d.ts: -------------------------------------------------------------------------------- 1 | import "./env"; 2 | import type preact from "preact"; 3 | 4 | /* GLOBAL */ 5 | declare global { 6 | // h and Fragment are automatically injected 7 | var h: typeof preact.h; 8 | var Fragment: typeof preact.Fragment; 9 | } 10 | 11 | export {}; 12 | -------------------------------------------------------------------------------- /examples/named-exports/src/components/named.tsx: -------------------------------------------------------------------------------- 1 | import { withHydrate } from "microsite/hydrate"; 2 | 3 | const NamedComponentInternal = () => ( 4 |

5 | Hello named 6 |

7 | ); 8 | 9 | export const NamedComponent = withHydrate(NamedComponentInternal, { 10 | method: "idle", 11 | }); 12 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@1.4.0/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "linked": [], 6 | "access": "public", 7 | "baseBranch": "main", 8 | "updateInternalDependencies": "patch", 9 | "ignore": ["@example/*"] 10 | } 11 | -------------------------------------------------------------------------------- /examples/root/public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /examples/with-fela/public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /examples/hello-world/public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /examples/with-goober/public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /packages/templates/next/public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "files.exclude": { 4 | "**/.git": true, 5 | "**/.svn": true, 6 | "**/.hg": true, 7 | "**/CVS": true, 8 | "**/.DS_Store": true, 9 | "**/microsite/{bin,cli,client,server,utils}": true, 10 | "**/microsite/*.d.ts": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /benchmark/gatsby/static/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import "../global/index.css"; 3 | import styles from "./index.module.css"; 4 | 5 | const IndexPage = () => { 6 | return ( 7 |
8 |

Hello world!

9 |
10 | ); 11 | }; 12 | 13 | export default IndexPage; 14 | -------------------------------------------------------------------------------- /examples/custom-document/public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /examples/custom-ssr-fallback/public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /packages/templates/default/public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /packages/templates/next/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "bracketSpacing": true, 4 | "jsxBracketSameLine": false, 5 | "jsxSingleQuote": false, 6 | "quoteProps": "consistent", 7 | "printWidth": 180, 8 | "semi": true, 9 | "singleQuote": true, 10 | "tabWidth": 2, 11 | "trailingComma": "all", 12 | "useTabs": false 13 | } 14 | -------------------------------------------------------------------------------- /packages/templates/default/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "bracketSpacing": true, 4 | "jsxBracketSameLine": false, 5 | "jsxSingleQuote": false, 6 | "quoteProps": "consistent", 7 | "printWidth": 180, 8 | "semi": true, 9 | "singleQuote": true, 10 | "tabWidth": 2, 11 | "trailingComma": "all", 12 | "useTabs": false 13 | } 14 | -------------------------------------------------------------------------------- /site/src/global/index.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | margin: 0; 4 | } 5 | 6 | :root { 7 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, 8 | Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif; 9 | font-size: clamp(14px, calc(0.5rem + 1vw), 20px); 10 | line-height: 1.25; 11 | 12 | padding: 1rem; 13 | } 14 | -------------------------------------------------------------------------------- /examples/hello-world/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@example/hello-world", 3 | "homepage": "/hello-world", 4 | "type": "module", 5 | "version": "0.0.0", 6 | "scripts": { 7 | "start": "microsite", 8 | "build": "microsite build", 9 | "serve": "microsite build --serve" 10 | }, 11 | "devDependencies": { 12 | "microsite": "1.2.2-next.0" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /benchmark/next/counter/global/index.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 7 | } 8 | 9 | a { 10 | color: inherit; 11 | text-decoration: none; 12 | } 13 | 14 | * { 15 | box-sizing: border-box; 16 | } 17 | -------------------------------------------------------------------------------- /benchmark/next/static/global/index.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 7 | } 8 | 9 | a { 10 | color: inherit; 11 | text-decoration: none; 12 | } 13 | 14 | * { 15 | box-sizing: border-box; 16 | } 17 | -------------------------------------------------------------------------------- /benchmark/gatsby/counter/src/global/index.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 7 | } 8 | 9 | a { 10 | color: inherit; 11 | text-decoration: none; 12 | } 13 | 14 | * { 15 | box-sizing: border-box; 16 | } 17 | -------------------------------------------------------------------------------- /benchmark/gatsby/static/src/global/index.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 7 | } 8 | 9 | a { 10 | color: inherit; 11 | text-decoration: none; 12 | } 13 | 14 | * { 15 | box-sizing: border-box; 16 | } 17 | -------------------------------------------------------------------------------- /benchmark/microsite/static/src/global/index.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 7 | } 8 | 9 | a { 10 | color: inherit; 11 | text-decoration: none; 12 | } 13 | 14 | * { 15 | box-sizing: border-box; 16 | } 17 | -------------------------------------------------------------------------------- /examples/custom-document/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@example/custom-document", 3 | "homepage": "/custom-document", 4 | "type": "module", 5 | "version": "0.0.0", 6 | "scripts": { 7 | "start": "microsite", 8 | "build": "microsite build", 9 | "serve": "microsite build --serve" 10 | }, 11 | "devDependencies": { 12 | "microsite": "1.2.2-next.0" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/root/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@example/root", 3 | "version": "0.0.0", 4 | "type": "module", 5 | "scripts": { 6 | "start": "microsite", 7 | "build": "microsite build", 8 | "serve": "microsite build --serve" 9 | }, 10 | "devDependencies": { 11 | "microsite": "1.2.2-next.0" 12 | }, 13 | "dependencies": { 14 | "title": "^3.4.2" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /benchmark/gatsby/counter/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import "../global/index.css"; 3 | import styles from "./index.module.css"; 4 | 5 | import Counter from "../components/counter"; 6 | 7 | const IndexPage = () => { 8 | return ( 9 |
10 | 11 |
12 | ); 13 | }; 14 | 15 | export default IndexPage; 16 | -------------------------------------------------------------------------------- /benchmark/microsite/counter/src/global/index.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 7 | } 8 | 9 | a { 10 | color: inherit; 11 | text-decoration: none; 12 | } 13 | 14 | * { 15 | box-sizing: border-box; 16 | } 17 | -------------------------------------------------------------------------------- /examples/partial-hydration/src/components/Counter/index.module.css: -------------------------------------------------------------------------------- 1 | .counter { 2 | display: flex; 3 | align-items: center; 4 | justify-content: center; 5 | } 6 | 7 | .counter > button { 8 | padding: 1rem; 9 | } 10 | 11 | .counter > p { 12 | font-size: 4rem; 13 | margin-inline: 3rem; 14 | font-variant-numeric: tabular-nums; 15 | min-width: 2ch; 16 | text-align: center; 17 | } 18 | -------------------------------------------------------------------------------- /examples/custom-ssr-fallback/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@example/custom-ssr-fallback", 3 | "homepage": "/custom-ssr-fallback", 4 | "type": "module", 5 | "version": "0.0.0", 6 | "scripts": { 7 | "start": "microsite", 8 | "build": "microsite build", 9 | "serve": "microsite build --serve" 10 | }, 11 | "devDependencies": { 12 | "microsite": "1.2.2-next.0" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/with-fela/src/components/Box.tsx: -------------------------------------------------------------------------------- 1 | import { useFela } from "preact-fela"; 2 | 3 | const Box = () => { 4 | const { css } = useFela(); 5 | 6 | return ( 7 |
17 | ); 18 | }; 19 | 20 | export default Box; 21 | -------------------------------------------------------------------------------- /examples/named-exports/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@example/named-exports", 3 | "homepage": "/named-exports", 4 | "version": "0.0.0", 5 | "private": true, 6 | "type": "module", 7 | "scripts": { 8 | "start": "microsite", 9 | "build": "microsite build -- --debug-hydration", 10 | "serve": "microsite build -- --debug-hydration --serve" 11 | }, 12 | "devDependencies": { 13 | "microsite": "1.2.2-next.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /benchmark/microsite/static/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionalComponent } from "preact"; 2 | import { definePage } from "microsite/page"; 3 | import styles from "./index.module.css"; 4 | 5 | interface IndexProps {} 6 | 7 | const Index: FunctionalComponent = () => { 8 | return ( 9 |
10 |

Hello world!

11 |
12 | ); 13 | }; 14 | 15 | export default definePage(Index); 16 | -------------------------------------------------------------------------------- /examples/dynamic-routes/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@example/dynamic-routes", 3 | "homepage": "/dynamic-routes", 4 | "version": "0.0.0", 5 | "private": true, 6 | "type": "module", 7 | "scripts": { 8 | "start": "microsite", 9 | "build": "microsite build -- --debug-hydration", 10 | "serve": "microsite build -- --debug-hydration --serve" 11 | }, 12 | "devDependencies": { 13 | "microsite": "1.2.2-next.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /site/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "site", 3 | "version": "0.0.17-next.0", 4 | "private": true, 5 | "volta": { 6 | "node": "12.20.1" 7 | }, 8 | "type": "module", 9 | "scripts": { 10 | "start": "microsite", 11 | "vercel-build": "echo 'build'", 12 | "build": "microsite build --debug-hydration", 13 | "serve": "microsite build --serve" 14 | }, 15 | "dependencies": { 16 | "microsite": "1.2.2-next.0" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /benchmark/next/counter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@benchmark/next-counter", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build && next export", 8 | "start": "next start" 9 | }, 10 | "dependencies": { 11 | "next": "10.0.7", 12 | "react": "17.0.1", 13 | "react-dom": "17.0.1" 14 | }, 15 | "devDependencies": { 16 | "@types/react": "^17.0.2" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /benchmark/next/static/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@benchmark/next-static", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build && next export", 8 | "start": "next start" 9 | }, 10 | "dependencies": { 11 | "next": "10.0.7", 12 | "react": "17.0.1", 13 | "react-dom": "17.0.1" 14 | }, 15 | "devDependencies": { 16 | "@types/react": "^17.0.2" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist/ 4 | out/ 5 | .microsite 6 | *.tgz 7 | demo/ 8 | .cache 9 | .next 10 | 11 | benchmark/gatsby-simple/public 12 | *.log 13 | 14 | packages/microsite/**/*.js 15 | packages/microsite/**/*.d.ts 16 | !packages/microsite/base.json 17 | !packages/microsite/scripts/**/* 18 | !packages/microsite/assets/**/* 19 | 20 | packages/create-microsite/**/*.js 21 | packages/create-microsite/**/*.d.ts 22 | 23 | .vercel 24 | examples/examples.json 25 | -------------------------------------------------------------------------------- /benchmark/next/counter/components/counter.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | const Counter: React.FC<{ initialCount?: number }> = ({ initialCount = 0 }) => { 4 | const [count, setCount] = React.useState(initialCount); 5 | 6 | return ( 7 |
8 | 9 |

{count}

10 | 11 |
12 | ); 13 | }; 14 | 15 | export default Counter; 16 | -------------------------------------------------------------------------------- /benchmark/microsite/counter/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionalComponent } from "preact"; 2 | import { definePage } from "microsite/page"; 3 | import styles from "./index.module.css"; 4 | 5 | import Counter from "../components/counter"; 6 | 7 | interface IndexProps {} 8 | 9 | const Index: FunctionalComponent = () => { 10 | return ( 11 |
12 | 13 |
14 | ); 15 | }; 16 | 17 | export default definePage(Index); 18 | -------------------------------------------------------------------------------- /examples/with-fela/src/utils/fela.ts: -------------------------------------------------------------------------------- 1 | import { createRenderer } from 'fela' 2 | import { rehydrate } from 'fela-dom' 3 | import webPreset from 'fela-preset-web'; 4 | 5 | let renderer = null; 6 | export default function getRenderer() { 7 | if (!renderer) { 8 | renderer = createRenderer({ 9 | plugins: [ 10 | ...webPreset 11 | ], 12 | }) 13 | if (typeof window !== 'undefined') { 14 | rehydrate(renderer); 15 | } 16 | } 17 | return renderer; 18 | } 19 | -------------------------------------------------------------------------------- /examples/with-goober/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@example/with-goober", 3 | "homepage": "/with-goober", 4 | "type": "module", 5 | "version": "0.0.0", 6 | "scripts": { 7 | "start": "microsite", 8 | "build": "microsite build", 9 | "serve": "microsite build --serve" 10 | }, 11 | "devDependencies": { 12 | "microsite": "1.2.2-next.0" 13 | }, 14 | "dependencies": { 15 | "goober": "^2.0.29" 16 | }, 17 | "volta": { 18 | "node": "12.20.1" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.github/workflows/benchmark.yml: -------------------------------------------------------------------------------- 1 | name: Benchmark 2 | 3 | on: [workflow_dispatch] 4 | 5 | jobs: 6 | benchmark: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v2 12 | 13 | - name: Setup Volta 14 | uses: volta-cli/action@v1 15 | 16 | - run: yarn 17 | 18 | - run: yarn bootstrap:benchmark 19 | 20 | - run: yarn benchmark 21 | 22 | - uses: EndBug/add-and-commit@v7 23 | with: 24 | add: 'benchmark/README.md' 25 | -------------------------------------------------------------------------------- /benchmark/gatsby/counter/src/components/counter.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | const Counter: React.FC<{ initialCount?: number }> = ({ initialCount = 0 }) => { 4 | const [count, setCount] = React.useState(initialCount); 5 | 6 | return ( 7 |
8 | 9 |

{count}

10 | 11 |
12 | ); 13 | }; 14 | 15 | export default Counter; 16 | -------------------------------------------------------------------------------- /examples/with-fela/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@example/with-fela", 3 | "homepage": "/with-fela", 4 | "type": "module", 5 | "version": "0.0.0", 6 | "scripts": { 7 | "start": "microsite", 8 | "build": "microsite build", 9 | "serve": "microsite build --serve" 10 | }, 11 | "devDependencies": { 12 | "fela": "11.5.2", 13 | "fela-dom": "11.5.2", 14 | "fela-preset-web": "11.5.2", 15 | "microsite": "1.2.2-next.0", 16 | "preact-fela": "11.5.2" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/named-exports/src/components/default-and-named.tsx: -------------------------------------------------------------------------------- 1 | import { withHydrate } from "microsite/hydrate"; 2 | 3 | const DefaultComponent = () => ( 4 |

5 | Hello default 6 |

7 | ); 8 | const NamedComponentInternal = () => ( 9 |

10 | Hello named 11 |

12 | ); 13 | 14 | export const NamedComponent = withHydrate(NamedComponentInternal, { 15 | method: "idle", 16 | }); 17 | 18 | export default withHydrate(DefaultComponent, { method: "idle" }); 19 | -------------------------------------------------------------------------------- /examples/hello-world/src/components/Counter.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "preact/hooks"; 2 | import { withHydrate } from "microsite/hydrate"; 3 | 4 | const Counter = () => { 5 | const [count, setCount] = useState(0); 6 | 7 | return ( 8 | <> 9 | 10 | {count} 11 | 12 | 13 | ); 14 | }; 15 | 16 | export default withHydrate(Counter, { method: "idle" }); 17 | -------------------------------------------------------------------------------- /docs/advanced/debugging.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Debugging Microsite 3 | --- 4 | 5 | As Microsite is still in the early stages, you may run into some unhandled issues. The following arguments might be helpful when running `microsite build`. 6 | 7 | - `--debug-hydration` adds `console.log` output for component hydration events in the browser 8 | - `--no-clean` prevents Microsite from removing the intermediate output at `./.microsite/build`. Since builds occur in two stages, there may be some clues for possible issues in the output of the first build stage. 9 | -------------------------------------------------------------------------------- /examples/custom-document/src/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | defineDocument, 3 | Html, 4 | Head, 5 | Main, 6 | MicrositeScript, 7 | } from "microsite/document"; 8 | 9 | const Document = () => ( 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | ); 18 | 19 | export default defineDocument(Document, { 20 | async prepare({ renderPage }) { 21 | const page = await renderPage(); 22 | return { ...page }; 23 | }, 24 | }); 25 | -------------------------------------------------------------------------------- /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/master/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /examples/partial-hydration/src/components/Idle/index.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionalComponent } from "preact"; 2 | import { withHydrate } from "microsite/hydrate"; 3 | import Counter from "../Counter"; 4 | 5 | import css from "./index.module.css"; 6 | 7 | const Idle: FunctionalComponent = (props: any) => { 8 | return ( 9 |
10 | 11 |

Hydrated as soon as possible (on idle)

12 |
13 | ); 14 | }; 15 | 16 | export default withHydrate(Idle, { method: "idle" }); 17 | -------------------------------------------------------------------------------- /packages/templates/next/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "todo", 3 | "type": "module", 4 | "scripts": { 5 | "start": "microsite", 6 | "build": "microsite build", 7 | "serve": "microsite build --serve" 8 | }, 9 | "devDependencies": { 10 | "microsite": "next", 11 | "husky": "^4.3.0", 12 | "lint-staged": "^10.4.2", 13 | "prettier": "^2.1.2" 14 | }, 15 | "husky": { 16 | "hooks": { 17 | "pre-commit": "lint-staged" 18 | } 19 | }, 20 | "lint-staged": { 21 | "*.{ts,tsx,css,md}": "prettier --write" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/templates/default/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "todo", 3 | "type": "module", 4 | "scripts": { 5 | "start": "microsite", 6 | "build": "microsite build", 7 | "serve": "microsite build --serve" 8 | }, 9 | "devDependencies": { 10 | "microsite": "^1.1.0-next.7", 11 | "husky": "^4.3.0", 12 | "lint-staged": "^10.4.2", 13 | "prettier": "^2.1.2" 14 | }, 15 | "husky": { 16 | "hooks": { 17 | "pre-commit": "lint-staged" 18 | } 19 | }, 20 | "lint-staged": { 21 | "*.{ts,tsx,css,md}": "prettier --write" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /benchmark/gatsby/static/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@benchmark/gatsby-static", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "develop": "gatsby develop", 7 | "start": "gatsby develop", 8 | "build": "gatsby build", 9 | "serve": "gatsby serve", 10 | "clean": "gatsby clean" 11 | }, 12 | "dependencies": { 13 | "gatsby": "^2.32.8", 14 | "react": "^16.13.1", 15 | "react-dom": "^16.13.1" 16 | }, 17 | "devDependencies": { 18 | "@types/node": "^14.14.31", 19 | "@types/react": "^17.0.2", 20 | "@types/react-dom": "^17.0.1" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/partial-hydration/src/components/LinkedCounter/index.module.css: -------------------------------------------------------------------------------- 1 | .counter { 2 | display: flex; 3 | align-items: center; 4 | justify-content: center; 5 | } 6 | 7 | .counter > button { 8 | padding: 1rem; 9 | } 10 | 11 | .counter > p { 12 | font-size: 4rem; 13 | margin-inline: 3rem; 14 | font-variant-numeric: tabular-nums; 15 | min-width: 2ch; 16 | text-align: center; 17 | } 18 | 19 | .root { 20 | display: flex; 21 | flex-flow: column nowrap; 22 | padding: 2em; 23 | } 24 | 25 | .root > span { 26 | display: block; 27 | margin-top: 1rem; 28 | text-align: center; 29 | } 30 | -------------------------------------------------------------------------------- /benchmark/gatsby/counter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@benchmark/gatsby-counter", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "develop": "gatsby develop", 7 | "start": "gatsby develop", 8 | "build": "gatsby build", 9 | "serve": "gatsby serve", 10 | "clean": "gatsby clean" 11 | }, 12 | "dependencies": { 13 | "gatsby": "^2.32.8", 14 | "react": "^16.13.1", 15 | "react-dom": "^16.13.1" 16 | }, 17 | "devDependencies": { 18 | "@types/node": "^14.14.31", 19 | "@types/react": "^17.0.2", 20 | "@types/react-dom": "^17.0.1" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/partial-hydration/src/components/Counter/index.tsx: -------------------------------------------------------------------------------- 1 | import { h, FunctionalComponent } from "preact"; 2 | import { useState } from "preact/hooks"; 3 | import css from "./index.module.css"; 4 | 5 | const Counter: FunctionalComponent = ({ initialCount = 0 }) => { 6 | const [count, setCount] = useState(initialCount); 7 | 8 | return ( 9 |
10 | 11 |

{count}

12 | 13 |
14 | ); 15 | }; 16 | 17 | export default Counter; 18 | -------------------------------------------------------------------------------- /benchmark/microsite/counter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@benchmark/microsite-counter", 3 | "version": "0.1.0", 4 | "type": "module", 5 | "scripts": { 6 | "start": "microsite", 7 | "build": "microsite build", 8 | "serve": "microsite build --serve" 9 | }, 10 | "devDependencies": { 11 | "husky": "^4.3.0", 12 | "lint-staged": "^10.4.2", 13 | "microsite": "1.2.1", 14 | "prettier": "^2.1.2" 15 | }, 16 | "husky": { 17 | "hooks": { 18 | "pre-commit": "lint-staged" 19 | } 20 | }, 21 | "lint-staged": { 22 | "*.{ts,tsx,css,md}": "prettier --write" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /benchmark/microsite/static/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@benchmark/microsite-static", 3 | "version": "0.1.0", 4 | "type": "module", 5 | "scripts": { 6 | "start": "microsite", 7 | "build": "microsite build", 8 | "serve": "microsite build --serve" 9 | }, 10 | "devDependencies": { 11 | "husky": "^4.3.0", 12 | "lint-staged": "^10.4.2", 13 | "microsite": "1.2.1", 14 | "prettier": "^2.1.2" 15 | }, 16 | "husky": { 17 | "hooks": { 18 | "pre-commit": "lint-staged" 19 | } 20 | }, 21 | "lint-staged": { 22 | "*.{ts,tsx,css,md}": "prettier --write" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/templates/default/README.md: -------------------------------------------------------------------------------- 1 | 2 | microsite 3 | 4 | 5 | # Microsite Starter 6 | 7 | Microsite is a next-generation static site generator. 8 | 9 | ## Getting Started 10 | 11 | To spin up a development server, run: 12 | 13 | ``` 14 | npm start 15 | ``` 16 | 17 | To build your production-ready site, run: 18 | 19 | ``` 20 | npm run build 21 | # or build + serve with 22 | npm run serve 23 | ``` 24 | -------------------------------------------------------------------------------- /packages/templates/next/README.md: -------------------------------------------------------------------------------- 1 | 2 | microsite 3 | 4 | 5 | # Microsite Starter 6 | 7 | Microsite is a next-generation static site generator. 8 | 9 | ## Getting Started 10 | 11 | To spin up a development server, run: 12 | 13 | ``` 14 | npm start 15 | ``` 16 | 17 | To build your production-ready site, run: 18 | 19 | ``` 20 | npm run build 21 | # or build + serve with 22 | npm run serve 23 | ``` 24 | -------------------------------------------------------------------------------- /examples/with-fela/src/components/Providers.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from 'preact' 2 | import { RendererProvider } from 'preact-fela' 3 | import getFelaRenderer from '../utils/fela' 4 | 5 | const renderer = getFelaRenderer() 6 | 7 | const FelaProvider: FunctionComponent = ({ children }) => { 8 | return ( 9 | 10 | {children} 11 | 12 | ) 13 | } 14 | 15 | const Providers: FunctionComponent = ({ children }) => ( 16 | 17 | { children } 18 | 19 | ); 20 | 21 | export default Providers; 22 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | The documentation for Microsite is still in progress. If you have any questions at all, please consider posting on [GitHub Discussions](https://github.com/natemoo-re/microsite/discussions). 2 | 3 | - [**Getting Started**](./getting-started.md) 4 | 5 | - **Basic** 6 | - [Pages](./basic/pages.md) 7 | - [Data Fetching](./basic/data-fetching.md) 8 | - [Bundled JavaScript](./basic/bundled-javascript.md) 9 | - [Styling](./basic/styling.md) 10 | - [TypeScript](./basic/typescript.md) 11 | - **Advanced** 12 | - [Global State](./advanced/global-state.md) 13 | - [Debugging](./advanced/debugging.md) 14 | -------------------------------------------------------------------------------- /benchmark/microsite/counter/src/components/counter.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from "preact"; 2 | import { useState } from "preact/hooks"; 3 | import { withHydrate } from "microsite/hydrate"; 4 | 5 | const Counter: FunctionComponent<{ initialCount?: number }> = ({ 6 | initialCount = 0, 7 | }) => { 8 | const [count, setCount] = useState(initialCount); 9 | 10 | return ( 11 |
12 | 13 |

{count}

14 | 15 |
16 | ); 17 | }; 18 | 19 | export default withHydrate(Counter); 20 | -------------------------------------------------------------------------------- /packages/microsite/base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "allowUnreachableCode": false, 5 | "declaration": false, 6 | "experimentalDecorators": true, 7 | "noUnusedLocals": true, 8 | "noUnusedParameters": true, 9 | "lib": ["dom", "ES2019"], 10 | "moduleResolution": "Node", 11 | "module": "ESNext", 12 | "target": "ES2020", 13 | "jsx": "react", 14 | "jsxFactory": "h", 15 | "jsxFragmentFactory": "Fragment", 16 | "skipLibCheck": true 17 | }, 18 | "include": ["../../src", "./assets/types/index.d.ts"], 19 | "exclude": ["node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /examples/partial-hydration/src/components/Visible/index.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionalComponent } from "preact"; 2 | import { withHydrate } from "microsite/hydrate"; 3 | import Counter from "../Counter"; 4 | 5 | import css from "./index.module.css"; 6 | 7 | const Visible: FunctionalComponent = (props: any) => { 8 | let message = "Hydrated when visible"; 9 | if (Object.keys(props).length > 0) message += " (with props)"; 10 | 11 | return ( 12 |
13 | 14 |

{message}

15 |
16 | ); 17 | }; 18 | 19 | export default withHydrate(Visible, { method: "visible" }); 20 | -------------------------------------------------------------------------------- /scripts/generate-examples.js: -------------------------------------------------------------------------------- 1 | import { promises as fsp } from 'fs'; 2 | import { join } from 'path' 3 | 4 | import { createRequire } from 'module'; 5 | const require = createRequire(import.meta.url); 6 | 7 | async function run() { 8 | const base = join(require.resolve('lerna').split('/node_modules')[0], 'examples'); 9 | const ents = await fsp.readdir('./examples', { withFileTypes: true }); 10 | let results = ents.map((ent) => ent.isDirectory() ? ent.name : null).filter(name => name && !['dist', '.microsite', 'root'].includes(name)) 11 | 12 | await fsp.writeFile(join(base, 'examples.json'), JSON.stringify(results)); 13 | } 14 | 15 | run(); 16 | -------------------------------------------------------------------------------- /packages/create-microsite/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "allowUnreachableCode": false, 5 | "declaration": true, 6 | "declarationDir": "./", 7 | "esModuleInterop": true, 8 | "experimentalDecorators": true, 9 | "lib": ["dom", "ES2019"], 10 | "outDir": "./", 11 | "moduleResolution": "Node", 12 | "module": "ESNext", 13 | "target": "ES2017", 14 | "noUnusedLocals": true, 15 | "noUnusedParameters": true, 16 | "baseUrl": "./src", 17 | "paths": { 18 | "@/*": ["./*"] 19 | } 20 | }, 21 | "include": ["src"], 22 | "exclude": ["node_modules"] 23 | } 24 | -------------------------------------------------------------------------------- /packages/microsite/src/client/hydrate.tsx: -------------------------------------------------------------------------------- 1 | import { h, createContext } from "preact"; 2 | 3 | export const HydrateContext = createContext(false); 4 | 5 | export const withHydrate = (Component: any) => { 6 | const name = Component.displayName || Component.name; 7 | 8 | const HydratedComponent = (props: any) => ( 9 | 10 | 11 | 12 | ); 13 | HydratedComponent.__withHydrate = true; 14 | 15 | Object.defineProperty(HydratedComponent, "name", { 16 | value: `withHydrate(${name})`, 17 | configurable: true, 18 | }); 19 | return HydratedComponent; 20 | }; 21 | -------------------------------------------------------------------------------- /.changeset/pre.json: -------------------------------------------------------------------------------- 1 | { 2 | "mode": "pre", 3 | "tag": "next", 4 | "initialVersions": { 5 | "@example/custom-document": "0.0.0", 6 | "@example/custom-ssr-fallback": "0.0.0", 7 | "@example/dynamic-routes": "0.0.0", 8 | "@example/hello-world": "0.0.0", 9 | "@example/partial-hydration": "0.0.0", 10 | "@example/root": "0.0.0", 11 | "@example/with-fela": "0.0.0", 12 | "@example/with-goober": "0.0.0", 13 | "create-microsite": "0.2.0", 14 | "microsite": "1.2.1", 15 | "site": "0.0.16", 16 | "@example/named-exports": "0.0.0" 17 | }, 18 | "changesets": [ 19 | "cool-geckos-thank", 20 | "smooth-eyes-count" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /benchmark/next/counter/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "src", 4 | "paths": { 5 | "@/*": ["./*"] 6 | }, 7 | "target": "es5", 8 | "lib": ["dom", "dom.iterable", "esnext"], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": false, 12 | "forceConsistentCasingInFileNames": true, 13 | "noEmit": true, 14 | "esModuleInterop": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "jsx": "preserve" 20 | }, 21 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 22 | "exclude": ["node_modules"] 23 | } 24 | -------------------------------------------------------------------------------- /benchmark/next/static/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "src", 4 | "paths": { 5 | "@/*": ["./*"] 6 | }, 7 | "target": "es5", 8 | "lib": ["dom", "dom.iterable", "esnext"], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": false, 12 | "forceConsistentCasingInFileNames": true, 13 | "noEmit": true, 14 | "esModuleInterop": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "jsx": "preserve" 20 | }, 21 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 22 | "exclude": ["node_modules"] 23 | } 24 | -------------------------------------------------------------------------------- /examples/with-fela/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionalComponent } from 'preact'; 2 | import { definePage } from 'microsite/page'; 3 | import { Head, seo } from 'microsite/head'; 4 | 5 | import Providers from '../components/Providers'; 6 | import Box from '../components/Box'; 7 | 8 | interface IndexProps {} 9 | 10 | const Index: FunctionalComponent = () => { 11 | return ( 12 | 13 | 14 | With Fela 15 | 16 | 17 |
18 |

Welcome to Microsite + Fela!

19 | 20 | 21 |
22 |
23 | ); 24 | }; 25 | 26 | export default definePage(Index); 27 | -------------------------------------------------------------------------------- /packages/microsite/src/utils/hydration.ts: -------------------------------------------------------------------------------- 1 | import type { ManifestEntry } from "./build"; 2 | 3 | export function generateHydrateScript( 4 | hydrateBindings: ManifestEntry["hydrateBindings"], 5 | opts: { basePath?: string } = {} 6 | ) { 7 | const { basePath = "/" } = opts; 8 | const entries = Object.fromEntries( 9 | Object.entries(hydrateBindings) 10 | .map(([file, exports]) => 11 | Object.entries(exports).map(([key, exportName]) => [ 12 | key, 13 | [exportName, `${basePath}${file}`], 14 | ]) 15 | ) 16 | .flat(1) 17 | ); 18 | 19 | return `import init from "${basePath}_static/vendor/microsite.js";\ninit(${JSON.stringify( 20 | entries 21 | )})`; 22 | } 23 | -------------------------------------------------------------------------------- /examples/custom-document/src/pages/page2.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionalComponent } from "preact"; 2 | import { definePage } from "microsite/page"; 3 | import { Head, seo } from "microsite/head"; 4 | 5 | interface IndexProps {} 6 | 7 | const Index: FunctionalComponent = () => { 8 | return ( 9 | <> 10 | 11 | Page 2 12 | 13 | 14 | 15 | 16 | 17 |
18 |

This is just validating that both pages get different titles!

19 |
20 | 21 | ); 22 | }; 23 | 24 | export default definePage(Index); 25 | -------------------------------------------------------------------------------- /examples/partial-hydration/src/components/Clock.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionalComponent } from "preact"; 2 | import { useEffect, useState } from "preact/hooks"; 3 | import { withHydrate } from "microsite/hydrate"; 4 | 5 | const Clock: FunctionalComponent<{ initialDate: string }> = ({ 6 | initialDate, 7 | }) => { 8 | const [date, setDate] = useState(initialDate); 9 | 10 | useEffect(() => { 11 | let id = setInterval(() => { 12 | setDate(new Date().toLocaleString().replace(", ", " at ")); 13 | }, 1000); 14 | 15 | return () => { 16 | clearInterval(id); 17 | }; 18 | }, []); 19 | 20 | return

The current date is {date}

; 21 | }; 22 | 23 | export default withHydrate(Clock, { method: "idle" }); 24 | -------------------------------------------------------------------------------- /examples/with-goober/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionalComponent } from 'preact'; 2 | import { definePage } from 'microsite/page'; 3 | import { Head, seo } from 'microsite/head'; 4 | 5 | import GlobalStyle from '../components/GlobalStyle'; 6 | import Logo from '../components/Logo'; 7 | 8 | interface IndexProps {} 9 | 10 | const Index: FunctionalComponent = () => { 11 | return ( 12 | <> 13 | 14 | With Goober 15 | 16 | 17 | 18 | 19 |
20 |

Microsite ✕ Goober

21 |

Name a more iconic duo

22 | 23 | 24 |
25 | 26 | ); 27 | }; 28 | 29 | export default definePage(Index); 30 | -------------------------------------------------------------------------------- /packages/microsite/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "allowUnreachableCode": false, 5 | "skipDefaultLibCheck": true, 6 | "declaration": true, 7 | "declarationDir": "./", 8 | "esModuleInterop": true, 9 | "experimentalDecorators": true, 10 | "lib": ["dom", "ES2019"], 11 | "outDir": "./dist", 12 | "moduleResolution": "Node", 13 | "module": "ESNext", 14 | "target": "ES2017", 15 | "noUnusedLocals": true, 16 | "noUnusedParameters": true, 17 | "jsx": "react", 18 | "jsxFactory": "h", 19 | "jsxFragmentFactory": "Fragment", 20 | "baseUrl": "./src", 21 | }, 22 | "include": ["src", "./internal.d.ts"], 23 | "exclude": ["node_modules"] 24 | } 25 | -------------------------------------------------------------------------------- /examples/dynamic-routes/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionalComponent } from "preact"; 2 | import { definePage } from "microsite/page"; 3 | import { Head, seo } from "microsite/head"; 4 | 5 | const Index: FunctionalComponent = ({ renderedAt }) => { 6 | return ( 7 | <> 8 | 9 | Dynamic Pages 10 | 11 | 12 |
13 |

Check out all these dynamic pages!

14 | 15 |
    16 | {Array.from({ length: 5 }, (_, i) => ( 17 |
  • 18 | Page {i + 1} 19 |
  • 20 | ))} 21 |
22 |
23 | 24 | ); 25 | }; 26 | 27 | export default definePage(Index); 28 | -------------------------------------------------------------------------------- /examples/hello-world/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionalComponent } from 'preact'; 2 | import { definePage } from 'microsite/page'; 3 | import { Head, seo } from 'microsite/head'; 4 | 5 | import Counter from '../components/Counter'; 6 | 7 | interface IndexProps {} 8 | 9 | const Index: FunctionalComponent = () => { 10 | return ( 11 | <> 12 | 13 | Microsite 14 | 15 | 16 | 17 | 18 | 19 |
20 |

Welcome to Microsite!

21 | 22 | 23 |
24 | 25 | ); 26 | }; 27 | 28 | export default definePage(Index); 29 | -------------------------------------------------------------------------------- /examples/partial-hydration/src/components/LinkedCounters/index.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionalComponent } from "preact"; 2 | import LinkedCounter from "../LinkedCounter"; 3 | import css from "./index.module.css"; 4 | 5 | const LinkedCounters: FunctionalComponent = () => { 6 | return ( 7 |
8 |

But wait! What about *global* state?

9 |

10 | Microsite renders each component in a separate tree, but{" "} 11 | microsite/global allows components to share state between 12 | trees. 13 |

14 | 15 |
16 | 17 | 18 |
19 |
20 | ); 21 | }; 22 | 23 | export default LinkedCounters; 24 | -------------------------------------------------------------------------------- /examples/partial-hydration/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@example/partial-hydration", 3 | "homepage": "/partial-hydration", 4 | "version": "0.0.0", 5 | "private": true, 6 | "volta": { 7 | "node": "12.20.1" 8 | }, 9 | "type": "module", 10 | "scripts": { 11 | "start": "microsite", 12 | "build": "microsite build -- --debug-hydration", 13 | "serve": "microsite build -- --debug-hydration --serve" 14 | }, 15 | "devDependencies": { 16 | "husky": "^4.3.0", 17 | "lint-staged": "^10.4.2", 18 | "microsite": "1.2.2-next.0", 19 | "prettier": "^2.1.2", 20 | "typescript": "^4.1.2" 21 | }, 22 | "husky": { 23 | "hooks": { 24 | "pre-commit": "lint-staged" 25 | } 26 | }, 27 | "lint-staged": { 28 | "*.{ts,tsx,css,md}": "prettier --write" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /examples/custom-ssr-fallback/src/components/Counter.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "preact/hooks"; 2 | import { withHydrate } from "microsite/hydrate"; 3 | 4 | const DummyCounter = () => ( 5 |
6 | 7 |

0

8 | 9 |
10 | ) 11 | 12 | const Counter = () => { 13 | const [count, setCount] = useState(0); 14 | 15 | return ( 16 |
17 | 18 |

{count}

19 | 20 |
21 | ); 22 | }; 23 | 24 | export default withHydrate(Counter, { method: "idle", fallback: }); 25 | -------------------------------------------------------------------------------- /examples/partial-hydration/src/global/index.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | :root { 8 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, 9 | Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif; 10 | } 11 | 12 | section { 13 | display: flex; 14 | flex-direction: column; 15 | align-items: center; 16 | justify-content: center; 17 | padding: 25vh 0; 18 | max-width: 1280px; 19 | margin-inline: auto; 20 | padding-inline: 1rem; 21 | } 22 | 23 | section > p { 24 | font-size: 1.25rem; 25 | margin-top: 1em; 26 | max-width: 60ch; 27 | margin-inline: auto; 28 | text-align: center; 29 | } 30 | 31 | code { 32 | padding: 0.125em; 33 | font-size: 100%; 34 | background: #eee; 35 | border-radius: 0.25em; 36 | } 37 | -------------------------------------------------------------------------------- /site/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionalComponent } from "preact"; 2 | import { definePage } from "microsite/page"; 3 | import { Head, seo } from "microsite/head"; 4 | 5 | import css from './index.module.css'; 6 | import Logo from '@/components/Logo'; 7 | 8 | const Index: FunctionalComponent = () => { 9 | return ( 10 | <> 11 | 12 | Microsite 13 | 14 | Microsite is a next-generation static site generator built on top of Snowpack, 15 | including features like Automatic Partial Hydration. 16 | 17 | 18 | 19 |
20 | 21 |

This site is under construction.

22 |
23 | 24 | ); 25 | }; 26 | 27 | export default definePage(Index); 28 | -------------------------------------------------------------------------------- /examples/custom-document/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionalComponent } from "preact"; 2 | import { definePage } from "microsite/page"; 3 | import { Head, seo } from "microsite/head"; 4 | 5 | interface IndexProps { 6 | a: string 7 | } 8 | 9 | const Index: FunctionalComponent = (props) => { 10 | return ( 11 | <> 12 | 13 | Microsite 14 | 15 | 16 | 17 | 18 | 19 |
20 |

Welcome to Microsite!

21 |
{JSON.stringify(props)}
22 |
23 | 24 | ); 25 | }; 26 | 27 | export default definePage(Index, { 28 | async getStaticProps() { 29 | return { props: { a: 'a' } } 30 | } 31 | }); 32 | -------------------------------------------------------------------------------- /examples/named-exports/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionalComponent } from "preact"; 2 | import { definePage } from "microsite/page"; 3 | import { Head, seo } from "microsite/head"; 4 | 5 | import DefaultComponentA from "../components/default"; 6 | import { NamedComponent as NamedComponentA } from "../components/named"; 7 | import DefaultComponentB, { 8 | NamedComponent as NamedComponentB, 9 | } from "../components/default-and-named"; 10 | 11 | const Index: FunctionalComponent = () => { 12 | return ( 13 | <> 14 | 15 | Named Exports 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | ); 27 | }; 28 | 29 | export default definePage(Index); 30 | -------------------------------------------------------------------------------- /scripts/copy-examples.js: -------------------------------------------------------------------------------- 1 | import { promises as fsp } from 'fs'; 2 | import fse from 'fs-extra'; 3 | import { join } from 'path' 4 | 5 | import { createRequire } from 'module'; 6 | const require = createRequire(import.meta.url); 7 | 8 | const { copy } = fse; 9 | 10 | async function run() { 11 | const base = join(require.resolve('lerna').split('/node_modules')[0], 'examples'); 12 | const ents = await fsp.readdir(base, { withFileTypes: true }); 13 | let results = ents.map((ent) => ent.isDirectory() ? ent.name : null).filter(name => name && !['dist', '.microsite', 'root'].includes(name)) 14 | 15 | await copy(join(base, 'root/dist'), join(base, 'dist'), { recursive: true }); 16 | await Promise.all(results.map((example) => copy(join(base, `${example}/dist/${example}`), join(base, `dist/${example}`), { recursive: true }))); 17 | } 18 | 19 | run(); 20 | -------------------------------------------------------------------------------- /packages/templates/default/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionalComponent } from 'preact'; 2 | import { definePage } from 'microsite/page'; 3 | import { Head, seo } from 'microsite/head'; 4 | 5 | interface IndexProps {} 6 | 7 | const Index: FunctionalComponent = () => { 8 | return ( 9 | <> 10 | 11 | Microsite 12 | 13 | 14 | 15 | 16 | 17 |
18 |

Welcome to Microsite!

19 |

20 | Ready to build something amazing? Read the docs to get started. 21 |

22 |
23 | 24 | ); 25 | }; 26 | 27 | export default definePage(Index); 28 | -------------------------------------------------------------------------------- /packages/templates/next/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionalComponent } from 'preact'; 2 | import { definePage } from 'microsite/page'; 3 | import { Head, seo } from 'microsite/head'; 4 | 5 | interface IndexProps {} 6 | 7 | const Index: FunctionalComponent = () => { 8 | return ( 9 | <> 10 | 11 | Microsite 12 | 13 | 14 | 15 | 16 | 17 |
18 |

Welcome to Microsite!

19 |

20 | Ready to build something amazing? Read the docs to get started. 21 |

22 |
23 | 24 | ); 25 | }; 26 | 27 | export default definePage(Index); 28 | -------------------------------------------------------------------------------- /examples/with-goober/src/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | defineDocument, 3 | Html, 4 | Head, 5 | Main, 6 | MicrositeScript, 7 | } from "microsite/document"; 8 | import { setup, extractCss } from 'goober'; 9 | setup(h); 10 | 11 | const Document = ({ css }) => ( 12 | 13 | 14 | 15 | 16 | 17 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /packages/microsite/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "microsite", 3 | "version": "1.2.2-next.0", 4 | "type": "module", 5 | "author": { 6 | "name": "Nate Moore", 7 | "email": "nate@natemoo.re", 8 | "url": "https://natemoo.re" 9 | }, 10 | "license": "MIT", 11 | "types": "./assets/microsite-env.d.ts", 12 | "bin": { 13 | "microsite": "./dist/bin/microsite.js" 14 | }, 15 | "scripts": { 16 | "prepare": "yarn build", 17 | "prebuild": "node ./scripts/clean.js", 18 | "build": "tsc -p tsconfig.json --skipLibCheck" 19 | }, 20 | "exports": { 21 | "./base": "./base.json", 22 | "./base.json": "./base.json", 23 | "./runtime": "./dist/runtime/index.js", 24 | "./client/csr": "./dist/client/csr.js", 25 | "./client/hooks": "./dist/client/hooks.js", 26 | "./client/hydrate": "./dist/client/hydrate.js", 27 | "./server/fetch": "./dist/server/fetch.js", 28 | "./document": "./dist/document.js", 29 | "./error": "./dist/error.js", 30 | "./global": "./dist/global.js", 31 | "./head": "./dist/head.js", 32 | "./hydrate": "./dist/hydrate.js", 33 | "./page": "./dist/page.js", 34 | "./assets/snowpack-plugin.cjs": "./assets/snowpack-plugin.cjs", 35 | "./assets/snowpack.config.cjs": "./assets/snowpack.config.cjs", 36 | "./assets/types/*": "./assets/types/*.d.ts", 37 | "./package.json": "./package.json" 38 | }, 39 | "files": [ 40 | "assets", 41 | "dist", 42 | "base.json", 43 | "**/*.d.ts" 44 | ], 45 | "dependencies": { 46 | "@prefresh/snowpack": "^3.1.1", 47 | "@snowpack/plugin-dotenv": "^2.0.5", 48 | "arg": "^5.0.0", 49 | "esbuild": "^0.9.5", 50 | "execa": "^4.0.3", 51 | "globby": "^11.0.1", 52 | "kleur": "^4.1.3", 53 | "node-fetch": "3.0.0-beta.9", 54 | "open": "^7.3.0", 55 | "path-browserify": "^1.0.1", 56 | "polka": "^0.5.2", 57 | "preact": "^10.5.13", 58 | "preact-render-to-string": "^5.1.16", 59 | "recast": "^0.20.4", 60 | "rollup": "^2.32.1", 61 | "rollup-plugin-styles": "^3.11.0", 62 | "sirv": "^1.0.10", 63 | "snowpack": "^3.1.1" 64 | }, 65 | "devDependencies": { 66 | "@types/node": "^12.19.0", 67 | "del": "^6.0.0" 68 | }, 69 | "repository": { 70 | "type": "git", 71 | "url": "https://github.com/natemoo-re/microsite.git", 72 | "directory": "packages/microsite" 73 | }, 74 | "bugs": { 75 | "url": "https://github.com/natemoo-re/microsite/issues" 76 | }, 77 | "homepage": "https://github.com/natemoo-re/microsite#readme", 78 | "volta": { 79 | "node": "12.20.1" 80 | }, 81 | "engines": { 82 | "node": ">=12.20.1" 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /site/src/components/Logo.tsx: -------------------------------------------------------------------------------- 1 | import { JSX } from "preact"; 2 | 3 | const Logo = (props: JSX.SVGAttributes) => ( 4 | 11 | 12 | 13 | 14 | 18 | 19 | ); 20 | 21 | export default Logo; 22 | -------------------------------------------------------------------------------- /docs/getting-started.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | The documentation for Microsite is still in progress. If you have any questions at all, please consider posting on [GitHub Discussions](https://github.com/natemoo-re/microsite/discussions). 4 | 5 | ## System Requirements 6 | - Latest [`node` v12 LTS (Erbium)](https://nodejs.org/download/release/latest-v12.x/) or [`node` v14 LTS (Fermium)](https://nodejs.org/download/release/latest-v14.x/) 7 | - MacOS, Windows (including WSL), or Linux 8 | 9 | ## Setup 10 | The easiest way to create a new Microsite project is to use `create-microsite`. It will set up everything automatically for you. To create a project, run: 11 | 12 | ``` 13 | npm init microsite 14 | ``` 15 | 16 | ## Manual Setup 17 | 18 | Install `microsite` and `preact` in your project: 19 | 20 | ``` 21 | npm install microsite preact 22 | ``` 23 | 24 | Open `package.json` and add the following `scripts`: 25 | 26 | ```json 27 | "scripts": { 28 | "start": "microsite", 29 | "build": "microsite build", 30 | "serve": "microsite serve" 31 | } 32 | ``` 33 | 34 | These scripts are based on common patterns in the Node ecosystem: 35 | 36 | - `start` - Runs `microsite dev` which starts Microsite in development mode 37 | - `build` - Runs `microsite build` which builds your project for production 38 | - `serve` - Runs `microsite serve` which serves the ouput of `microsite build`. As a shortcut, both steps can be called using `microsite build --serve`. 39 | 40 | Microsite is structured around the concept of pages. A page is a Preact component exported from a `.jsx` or `.tsx` file in the `src/pages` directory. 41 | 42 | Pages will generate a corresponding HTML file based on the file name. For example `src/pages/about.tsx` will be output as `/about.html`. 43 | 44 | Create a `src` directory inside your project and a `pages` directory inside of `./src/`. Let's create your first page by adding the following to `./src/pages/index.tsx`. 45 | 46 | ```tsx 47 | import { FunctionComponent } from 'preact'; 48 | 49 | const HomePage: FunctionComponent = () => { 50 | return
Welcome to Microsite!
51 | } 52 | 53 | export default HomePage; 54 | ``` 55 | 56 | Optionally, you may create a `public` directory in the project root (`./public`) to serve any static assets. 57 | 58 | To start developing your project, run `npm start`. This will automatically open the development server on `http://localhost:8888`. 59 | 60 | We already have some incredible features up and running: 61 | - Automatic compilation and bundling (powered by [Snowpack](https://snowpack.dev) and [esbuild](https://esbuild.github.io/)) 62 | - Stateful HMR (powered by [Prefresh](https://github.com/JoviDeCroock/prefresh)) 63 | - Static generation of files in `./src/pages/` 64 | - Static file serving. `./public/` is mapped to `/` 65 | -------------------------------------------------------------------------------- /docs/basic/data-fetching.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Data Fetching 3 | description: How to fetch content in Microsite 4 | --- 5 | 6 | Microsite exposes two functions to hook into **static generation** on a per-page basis. At build time, these hooks allow you to fetch dynamic data from the network or read local files. 7 | 8 | - `getStaticProps`: Fetch data at build time 9 | - `getStaticPaths`: Specify dynamic routes to pre-render based on data. 10 | 11 | ## `definePage` 12 | 13 | Microsite exposes a `definePage` utility from `microsite/page` which can bundle all of these functions together with the Preact component which defines your page. This allows TypeScript to automatically infer the correct types because `definePage` is aware of your component's props. In addition to `getStaticPaths` and `getStaticProps`, you can optionally include the current filename as `path` to have TypeScript infer the `params` which `getStaticPaths` expects to be returned. 14 | 15 | ```tsx 16 | import { definePage } from 'microsite/page'; 17 | 18 | const Component: FunctionalComponent = () => { /* ... */ } 19 | 20 | export default definePage(Component, { 21 | path: '/blog/[slug]', 22 | async getStaticProps() {} 23 | async getStaticPaths() {} 24 | }) 25 | ``` 26 | 27 | ## `getStaticProps` 28 | 29 | If included, Microsite pre-renders this page at build time using the props returned by `getStaticProps`. 30 | 31 | ```tsx 32 | export default definePage(Component, { 33 | async getStaticProps(context) { 34 | return { 35 | props: {}, // will be passed to the Component as props 36 | }; 37 | }, 38 | }); 39 | ``` 40 | 41 | The `context` parameter is an object containing the following keys: 42 | 43 | - `path` is the raw path of this route. 44 | - `params` contains the route parameters for pages using dynamic routes. 45 | 46 | `getStaticProps` should return an object with: 47 | 48 | - `props` A **required** object with the props that will be received by the page component. 49 | 50 | ## `getStaticPaths` 51 | 52 | If a page has dynamic routes and uses `getStaticProps` it needs to define a list of paths that have to be rendered to HTML at build time. 53 | 54 | ```tsx 55 | export default definePage(Component, { 56 | async getStaticProps(context) { 57 | paths: [ 58 | { params: { ... } } // See the "paths" section below 59 | ], 60 | } 61 | }) 62 | ``` 63 | 64 | ### The `paths` key (required) 65 | 66 | The `paths` key determines which pages will be server-side rendered. For example, a page using dynamic routes such as `/posts/[id].tsx` would use `getStaticPaths` and return the following: 67 | 68 | ```tsx 69 | return { 70 | paths: [{ params: { id: "1" } }, { params: { id: "2" } }], 71 | }; 72 | ``` 73 | 74 | From this, Microsite will generate `/posts/1` and `/posts/2` using the page component in `/posts/[id].tsx`. 75 | -------------------------------------------------------------------------------- /packages/microsite/src/utils/router.ts: -------------------------------------------------------------------------------- 1 | const DYNAMIC_ROUTE = /\[[^/]+?\](?=\/|$)/; 2 | function isDynamicRoute(route: string): boolean { 3 | return DYNAMIC_ROUTE.test(route); 4 | } 5 | 6 | const cleanPathSegment = (segment: string) => { 7 | return segment.replace(/[\[\]]/g, "").replace(/\.\.\./, ""); 8 | }; 9 | 10 | const pathToSegments = (path: string) => 11 | path.split("/").map((text) => { 12 | const isDynamic = isDynamicRoute(text); 13 | const isCatchAll = isDynamic && text.slice(1, -1).startsWith("..."); 14 | return { text, isDynamic, isCatchAll }; 15 | }); 16 | 17 | export interface Params { 18 | [param: string]: string | string[]; 19 | } 20 | 21 | export interface RouteInfo { 22 | segments: ReturnType; 23 | params: Params; 24 | } 25 | 26 | export type StaticPath

= 27 | | string 28 | | { params: P; meta?: any }; 29 | 30 | export interface StaticPropsContext

{ 31 | path: string; 32 | params: P; 33 | } 34 | 35 | // TODO: prefetch 36 | export interface StaticPathsContext {} 37 | 38 | function getParamsFromPath(fileName: string, path: string): Params { 39 | path = normalizePathName(path); 40 | const segments = pathToSegments(normalizePathName(fileName)); 41 | const parts = path.split("/"); 42 | return parts.reduce((acc, part, i) => { 43 | const segment = segments[i] ?? segments[segments.length - 1]; 44 | if (segment.isCatchAll) { 45 | const key = segment.text.slice(4, -1); 46 | return { ...acc, [key]: [...(acc[key] ?? []), part] }; 47 | } 48 | if (segment.isDynamic) { 49 | const key = segment.text.slice(1, -1); 50 | return { ...acc, [key]: part }; 51 | } 52 | return acc; 53 | }, {}); 54 | } 55 | 56 | const stripLeadingSlash = (str: string) => str.replace(/^\//, ""); 57 | const stripTrailingExt = (str: string) => str.replace(/\..*$/, ""); 58 | export const normalizePathName = (str: string) => 59 | stripTrailingExt(stripLeadingSlash(str)); 60 | 61 | export function getPathFromParams(fileName: string, params: Params): string { 62 | const segments = pathToSegments(normalizePathName(fileName)); 63 | 64 | return ( 65 | "/" + 66 | segments 67 | .reduce((path: string[], segment) => { 68 | const key = cleanPathSegment(segment.text); 69 | const value = params[key]; 70 | 71 | if (typeof value === "undefined") return path.concat(key); 72 | return path.concat(...(Array.isArray(value) ? value : [value])); 73 | }, []) 74 | .join("/") 75 | ); 76 | } 77 | 78 | export function generateStaticPropsContext( 79 | fileName: string, 80 | pathOrParams: string | { params: Params } 81 | ): StaticPropsContext { 82 | if (typeof pathOrParams === "string") { 83 | return { 84 | path: normalizePathName(pathOrParams), 85 | params: getParamsFromPath(fileName, pathOrParams), 86 | }; 87 | } else if (typeof pathOrParams === "object") { 88 | return { 89 | path: getPathFromParams(fileName, pathOrParams.params), 90 | params: pathOrParams.params, 91 | }; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /scripts/vitals.js: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import puppeteer from "puppeteer"; 3 | import lighthouse from "lighthouse"; 4 | import { createServer } from "http"; 5 | import sirv from "sirv"; 6 | import { URL } from "url"; 7 | 8 | // const Good3G = { 9 | // 'offline': false, 10 | // 'downloadThroughput': 1.5 * 1024 * 1024 / 8, 11 | // 'uploadThroughput': 750 * 1024 / 8, 12 | // 'latency': 40 13 | // }; 14 | 15 | const phone = puppeteer.devices["Nexus 5X"]; 16 | 17 | // async function calcVitals() { 18 | // var script = document.createElement('script'); 19 | // script.src = 'https://unpkg.com/web-vitals'; 20 | // function report(metric) { 21 | // if (!window.vitals) window.vitals = {}; 22 | // window.vitals[metric.name] = metric.value; 23 | // console.log(metric.name, metric.value); 24 | // } 25 | // script.onload = function() { 26 | // webVitals.getTTFB(report, true); 27 | // webVitals.getFCP(report, true); 28 | // webVitals.getCLS(report, true); 29 | // webVitals.getLCP(report, true); 30 | // } 31 | // await new Promise((resolve) => { 32 | // if (document.readyState != 'loading') { 33 | // resolve(); 34 | // } else { 35 | // document.addEventListener('DOMContentLoaded', resolve, { once: true }); 36 | // } 37 | // }) 38 | 39 | // document.head.appendChild(script); 40 | // } 41 | 42 | export default async function getVitals(assetPath) { 43 | const PORT = 3001; 44 | const handler = sirv(assetPath); 45 | 46 | const server = createServer((req, res) => { 47 | handler(req, res, () => { 48 | res.statusCode = 404; 49 | res.end(""); 50 | }); 51 | }).listen(PORT); 52 | 53 | const url = `http://localhost:${PORT}`; 54 | const vitals = await extractVitals(url); 55 | 56 | server.close(); 57 | return vitals; 58 | } 59 | 60 | export async function extractVitals(url) { 61 | const browser = await puppeteer.launch({ 62 | args: ["--no-sandbox"], 63 | timeout: 10000, 64 | headless: true, 65 | }); 66 | 67 | try { 68 | const page = await browser.newPage(); 69 | const client = await page.target().createCDPSession(); 70 | 71 | await client.send("Network.enable"); 72 | await client.send("ServiceWorker.enable"); 73 | 74 | await page.emulate(phone); 75 | await page.goto(url); 76 | 77 | const { lhr } = await lighthouse( 78 | url, 79 | { 80 | port: new URL(browser.wsEndpoint()).port, 81 | output: "json", 82 | }, 83 | { 84 | extends: "lighthouse:default", 85 | settings: { 86 | onlyAudits: [ 87 | "first-meaningful-paint", 88 | "speed-index", 89 | "first-cpu-idle", 90 | "interactive", 91 | ], 92 | }, 93 | } 94 | ); 95 | 96 | browser.close(); 97 | 98 | return Object.values(lhr.audits); 99 | } catch (error) { 100 | console.log(error); 101 | browser.close(); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /docs/basic/bundled-javascript.md: -------------------------------------------------------------------------------- 1 | # Bundled JavaScript 2 | 3 | Microsite enforces performance best practices by not emitting any JavaScript by default—every byte of JavaScript is an explicit opt-in choice left to the developer. 4 | 5 | JavaScript is, of course, essential for spec-compliant accessibility and other expected features. This is where Microsite's **Automatic Partial Hydration (APH)** comes into play. 6 | 7 | ## Automatic Partial Hydration 8 | 9 | Current JavaScript-based SSG solutions send the entire component tree down to the client for hydration, even if the content is entirely static. 10 | 11 | > Preact components offer the right primitive for defining the layout and structure of your site. The only components that should be sent to the client are ones that truly interactive. 12 | 13 | Microsite requires a hint from the author, the `withHydrate` HOC, to implement APH and ship highly optimized modules to the client. 14 | 15 | **Example** Consider a simple counter component which uses `preact/hooks` and attaches `onClick` handlers to rendered `button` elements. 16 | 17 | ```tsx 18 | import { useState } from "preact/hooks"; 19 | import { withHydrate } from "microsite/hydrate"; 20 | 21 | const Counter = () => { 22 | const [count, setCount] = useState(0); 23 | 24 | return ( 25 | <> 26 | 27 | {count} 28 | 29 | 30 | ); 31 | }; 32 | 33 | export default withHydrate(Counter, { method: "idle" }); 34 | ``` 35 | 36 | With this code, Microsite can determine that this component should be included in the final JavaScript bundle. Given a number of hydrated components, Microsite will also automatically implement route-based code-splitting and intelligent lazy loading. 37 | 38 | > Importantly, hydrated components are not executed until the browser is idle, the component is visible, or a user is about to interact with it. See `withHydrate.method` options below. 39 | 40 | ### Caveats 41 | 42 | There are a few things to keep in mind when leveraging APH: 43 | 44 | - Hydrated components cannot contain any other hydrated component, as hydration is controlled by the top-level component. 45 | 46 | - Hydrated components should be placed as deep as possible in your app's tree for the most efficient bundles. 47 | 48 | - Hydrated components can't accept _rich_ (component) `children` as a prop because it's non-trivial to serialize them. Strings and numbers _are_ accepted in the `children` prop. 49 | 50 | 51 | ### `withHydrate` Options 52 | 53 | **method** 54 | 55 | As a developer, you know exactly how your site is structured, so Microsite allows you to tweak how hydration occurs to optimize your specific use case. 56 | 57 | - `idle` (default) hydrates the component as soon as possible, when the browser executes [`requestIdleCallback`](https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback) code. 58 | 59 | - `visible` hydrates the component as soon as it enters the viewport, via [`IntersectionObserver`](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver). 60 | -------------------------------------------------------------------------------- /packages/create-microsite/src/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import degit from "degit"; 3 | import prompts from "prompts"; 4 | import fs from "fs"; 5 | import arg from "arg"; 6 | import { dim, bold, green, cyan, underline, red, white } from "kleur/colors"; 7 | import { resolve } from "path"; 8 | 9 | const REPO = `natemoo-re/microsite`; 10 | const TEMPLATES = ["default", "next"]; 11 | 12 | type Args = arg.Result<{ 13 | "--force": BooleanConstructor; 14 | "--next": BooleanConstructor; 15 | }>; 16 | 17 | async function clone( 18 | template: typeof TEMPLATES[number], 19 | dir: string, 20 | args: Args 21 | ) { 22 | return new Promise((resolve, reject) => { 23 | const emitter = degit(`${REPO}/packages/templates/${template}#main`, { 24 | cache: false, 25 | force: args["--force"] ?? false, 26 | verbose: true, 27 | }); 28 | 29 | emitter 30 | .clone(dir) 31 | .then(() => { 32 | resolve(); 33 | }) 34 | .catch((err) => { 35 | reject(err); 36 | }); 37 | }); 38 | } 39 | 40 | async function run() { 41 | console.log(); 42 | let [name, ...argv] = process.argv.slice(2); 43 | const args = arg( 44 | { 45 | "--force": Boolean, 46 | "--next": Boolean, 47 | }, 48 | { argv } 49 | ); 50 | let template = args["--next"] ? "next" : "default"; 51 | 52 | if (!name) { 53 | const response = await prompts({ 54 | type: "text", 55 | name: "name", 56 | message: `Project name:`, 57 | initial: "microsite-project", 58 | validate: (value) => (!value?.trim() ? "Please enter a value" : true), 59 | }); 60 | 61 | let normalizedName = response.name?.trim().replace(/\s+/g, "-"); 62 | if (!normalizedName) { 63 | console.log( 64 | `${bold(red("✖"))} Cancelled. Please enter a project name to continue.` 65 | ); 66 | return; 67 | } 68 | 69 | name = normalizedName; 70 | } 71 | 72 | try { 73 | let root = resolve(name); 74 | 75 | if (fs.existsSync(root) && !args["--force"]) { 76 | const existing = fs.readdirSync(root); 77 | if (existing.length) { 78 | const { yes } = await prompts({ 79 | type: "confirm", 80 | name: "yes", 81 | initial: "Y", 82 | message: 83 | `Target directory "./${name}" is not empty.\n ` + 84 | `Remove existing files and continue?`, 85 | }); 86 | if (yes) { 87 | args["--force"] = true; 88 | } else { 89 | return; 90 | } 91 | } 92 | } 93 | await clone(template, root, args); 94 | } catch (err) { 95 | if (err.code === "DEST_NOT_EMPTY") { 96 | console.log( 97 | `${bold(red("✖"))} ${cyan("./" + name)} is not empty. Use ${bold( 98 | white("--force") 99 | )} to override.` 100 | ); 101 | } else { 102 | console.error(err); 103 | } 104 | return; 105 | } 106 | 107 | console.log( 108 | `${bold(green("✓"))} Created ${underline( 109 | green("./" + name) 110 | )} from template ${underline(cyan(template))}` 111 | ); 112 | 113 | const pkgManager = /yarn/.test(process.env.npm_execpath) ? 'yarn' : 'npm'; 114 | 115 | console.log(); 116 | console.log(dim(` Next steps:`)); 117 | console.log() 118 | console.log(` cd ./${name}`); 119 | console.log(` ${pkgManager === 'yarn' ? `yarn` : `npm install`}`) 120 | console.log(` ${pkgManager === 'yarn' ? `yarn start` : `npm start`}`) 121 | } 122 | 123 | run().then(() => console.log()); 124 | -------------------------------------------------------------------------------- /packages/microsite/src/global.tsx: -------------------------------------------------------------------------------- 1 | // Adapted from https://github.com/pmndrs/valtio 2 | import { useEffect, useMemo, useState } from "preact/hooks"; 3 | 4 | const LISTENERS = Symbol(); 5 | const SNAPSHOT = Symbol(); 6 | 7 | const isObject = (x: unknown): x is object => 8 | typeof x === "object" && x !== null; 9 | 10 | const createProxy = (initialObject: T = {} as T): T => { 11 | let version = 0; 12 | const listeners = new Set<() => void>(); 13 | const incrementVersion = () => { 14 | version = version + 1; 15 | listeners.forEach((listener) => listener()); 16 | }; 17 | 18 | const proxy = new Proxy(Object.create(initialObject.constructor.prototype), { 19 | get(target, prop) { 20 | if (prop === LISTENERS) { 21 | return listeners; 22 | } 23 | if (prop === SNAPSHOT) { 24 | const snapshot = Object.create(target.constructor.prototype); 25 | Reflect.ownKeys(target).forEach((key) => { 26 | const value = target[key]; 27 | if (isObject(value)) { 28 | snapshot[key] = (value as any)[SNAPSHOT]; 29 | } else { 30 | snapshot[key] = value; 31 | } 32 | }); 33 | return snapshot; 34 | } 35 | return target[prop]; 36 | }, 37 | deleteProperty(target, prop) { 38 | const value = target[prop]; 39 | if (isObject(value)) { 40 | (value as any)[LISTENERS].delete(incrementVersion); 41 | } 42 | delete target[prop]; 43 | incrementVersion(); 44 | return true; 45 | }, 46 | set(target, prop, value) { 47 | if (isObject(value)) { 48 | if (value[LISTENERS]) { 49 | target[prop] = value; 50 | } else { 51 | target[prop] = createProxy(value); 52 | } 53 | target[prop][LISTENERS].add(incrementVersion); 54 | } else { 55 | target[prop] = value; 56 | } 57 | incrementVersion(); 58 | return true; 59 | }, 60 | }); 61 | Reflect.ownKeys(initialObject).forEach((key) => { 62 | proxy[key] = (initialObject as any)[key]; 63 | }); 64 | 65 | return proxy; 66 | }; 67 | 68 | const subscribe = (proxy: any, callback: () => void) => { 69 | proxy[LISTENERS].add(callback); 70 | return () => { 71 | proxy[LISTENERS].delete(callback); 72 | }; 73 | }; 74 | 75 | const getSnapshot = (proxy: any) => proxy[SNAPSHOT]; 76 | 77 | export const createGlobalState = createProxy; 78 | 79 | export const useGlobalState = (source: T = {} as T): T => { 80 | const subscription = useMemo( 81 | () => ({ 82 | getCurrentValue: () => getSnapshot(source), 83 | subscribe: (callback: () => void) => subscribe(source, callback), 84 | }), 85 | [source] 86 | ); 87 | 88 | const [state, setState] = useState(() => ({ 89 | value: subscription.getCurrentValue(), 90 | })); 91 | 92 | let valueToReturn = state.value; 93 | 94 | useEffect(() => { 95 | let didUnsubscribe = false; 96 | 97 | const checkForUpdates = () => { 98 | if (didUnsubscribe) return; 99 | const value = subscription.getCurrentValue(); 100 | 101 | setState((prevState) => { 102 | if (prevState.value === value) { 103 | return prevState; 104 | } 105 | 106 | return { value }; 107 | }); 108 | }; 109 | 110 | let unsubscribe = subscription.subscribe(checkForUpdates); 111 | checkForUpdates(); 112 | 113 | return () => { 114 | didUnsubscribe = true; 115 | unsubscribe(); 116 | }; 117 | }, [subscription]); 118 | 119 | return valueToReturn; 120 | }; 121 | -------------------------------------------------------------------------------- /packages/microsite/README.md: -------------------------------------------------------------------------------- 1 | > [!CAUTION] 2 | > Microsite is no longer maintained. The partial hydration techniques pioneered in this project served as a precursor to [Astro](https://astro.build/), which [@natemoo-re](https://github.com/natemoo-re) co-created, and a number of other islands architecture-based frameworks like [Fresh](https://fresh.deno.dev/). 3 | > 4 | > Consider this archive a reference implementation for islands architecture—may it inspire you to _do more with less JavaScript_. 5 | 6 |

7 | 8 |
9 |
10 | 11 |
12 | microsite 13 |
14 | 15 |

16 | Read the docs 17 | | 18 | See the live examples 19 | | 20 | Join our Discord 21 |

22 | 23 |
24 |
25 | 26 | `microsite` is a fast, opinionated static-site generator (SSG) built on top of [Snowpack](https://snowpack.dev). It outputs extremely minimal clientside code using [**automatic partial hydration**](https://github.com/natemoo-re/microsite/blob/main/docs/basic/bundled-javascript.md#automatic-partial-hydration). 27 | 28 | ```bash 29 | npm init microsite 30 | ``` 31 | 32 | --- 33 | 34 | Microsite is an **ESM node package**, so it needs to run in a Node environment which supports ESM. We support the latest version of [`node` v12.x LTS (Erbium)](https://nodejs.org/download/release/latest-v12.x/) — see [Engines](https://github.com/natemoo-re/microsite/blob/main/docs/engines.md) for more details. 35 | 36 | Ensure that your project includes `"type": "module"` in `package.json`, which will allow you to use ESM in your project's `node` scripts. 37 | 38 | ## Pages 39 | 40 | Microsite uses the file-system to generate your static site, meaning each component in `src/pages` outputs a corresponding HTML file. 41 | 42 | Page templates are `.js`, `.jsx`, or `.tsx` files which export a `default` a [Preact](https://preactjs.com/) component. 43 | 44 | ## Styles 45 | 46 | Styles are written using CSS Modules. `src/global.css` is, as you guessed, a global CSS file injected on every page. 47 | Per-page/per-component styles are also inject on the correct pages. They are modules and must be named `*.module.css`. 48 | 49 | ## Project structure 50 | 51 | ``` 52 | project/ 53 | ├── public/ // copied to dist/ 54 | ├── src/ 55 | │ ├── global/ 56 | │ │ └── index.css // included in every generated page 57 | │ │ └── index.ts // shipped entirely to client, if present 58 | │ ├── pages/ // fs-based routing like Next.js 59 | │ │ └── index.tsx 60 | └── tsconfig.json 61 | ``` 62 | 63 | ## Acknowledgments 64 | 65 | - [Markus Oberlehner](https://twitter.com/maoberlehner), [`vue-lazy-hydration`](https://github.com/maoberlehner/vue-lazy-hydration) 66 | - [Markus Oberlehner](https://twitter.com/maoberlehner), [Building Partially Hydrated, Progressively Enhanced Static Websites with Isomorphic Preact and Eleventy](https://markus.oberlehner.net/blog/building-partially-hydrated-progressively-enhanced-static-websites-with-isomorphic-preact-and-eleventy/) 67 | - [Lukas Bombach](https://twitter.com/luke_schmuke), [The case of partial hydration (with Next and Preact)](https://medium.com/@luke_schmuke/how-we-achieved-the-best-web-performance-with-partial-hydration-20fab9c808d5) 68 | - [Jason Miller](https://twitter.com/_developit) and [Addy Osmani](https://twitter.com/addyosmani), [Rendering on the Web](https://developers.google.com/web/updates/2019/02/rendering-on-the-web) 69 | - [Poimandres](https://github.com/pmndrs), [`valtio`](https://github.com/pmndrs/valtio) for inspiring `microsite/global` 70 | -------------------------------------------------------------------------------- /benchmark/README.md: -------------------------------------------------------------------------------- 1 | # Benchmarks 2 | 3 | These benchmarks are designed to measure the speed and output of various P/React-based static site generators. Please see [About](#about) for a detailed breakdown of the different benchmarks. 4 | 5 | > Would you like to see the numbers for another tool? Feel free to [open an issue](https://github.com/natemoo-re/microsite/issues/new)! 6 | 7 | ### Wait, how is Microsite _that_ small? 8 | 9 | Great question! Microsite treats every byte of JavaScript as an opt-in, which means the default output does not include Preact or any JavaScript. For interactive components, [automatic partial hydration](https://github.com/natemoo-re/microsite/blob/main/docs/basic/bundled-javascript.md#automatic-partial-hydration) strips any static components from your bundle and includes **only** your hydrated components. In those cases, Preact is loaded from the [Skypack](https://www.skypack.dev/) CDN and is not reflected in these benchmarks (for now.) 10 | 11 | 12 | 13 | ## Microsite 14 | 15 | | Benchmark | Build duration | JS files | JS size (raw) | JS size (gzip) | JS size (brotli) | First Meaningful Paint | Speed Index | First CPU Idle | Time to Interactive | 16 | | :-------- | -------------: | -------: | ------------: | -------------: | ---------------: | ---------------------: | ----------: | -------------: | ------------------: | 17 | | static | 1.8s | 0 | 0B | 0B | 0B | 0.9 s | 0.9 s | 0.9 s | 0.9 s | 18 | | counter | 1.9s | 2 | 2.83kB | 1.54kB | 1.38kB | 1.0 s | 1.0 s | 1.0 s | 1.0 s | 19 | 20 | ## NextJS 21 | 22 | | Benchmark | Build duration | JS files | JS size (raw) | JS size (gzip) | JS size (brotli) | First Meaningful Paint | Speed Index | First CPU Idle | Time to Interactive | 23 | | :-------- | -------------: | -------: | ------------: | -------------: | ---------------: | ---------------------: | ----------: | -------------: | ------------------: | 24 | | static | 9.4s | 10 | 294.56kB | 98.42kB | 87.47kB | 2.5 s | 2.5 s | 2.5 s | 2.5 s | 25 | | counter | 9.5s | 10 | 295.85kB | 99.03kB | 87.99kB | 2.5 s | 2.5 s | 2.5 s | 2.5 s | 26 | 27 | ## Gatsby 28 | 29 | | Benchmark | Build duration | JS files | JS size (raw) | JS size (gzip) | JS size (brotli) | First Meaningful Paint | Speed Index | First CPU Idle | Time to Interactive | 30 | | :-------- | -------------: | -------: | ------------: | -------------: | ---------------: | ---------------------: | ----------: | -------------: | ------------------: | 31 | | static | 10.1s | 6 | 392.92kB | 123.66kB | 91.69kB | 2.8 s | 2.8 s | 2.8 s | 2.8 s | 32 | | counter | 9.5s | 6 | 393.23kB | 123.75kB | 91.79kB | 2.7 s | 2.7 s | 2.7 s | 2.7 s | 33 | 34 | 35 | 36 | --- 37 | 38 | ## About 39 | 40 | In order to normalize the results across tools with different feature sets, all benchmarks are configured to be as close to Microsite's defaults as possible. For example, NextJS benchmarks measure the output of `next build && next export` to generate fully static client assets. 41 | 42 | ### Static 43 | 44 | The static benchmark consists of a simple `hello-world` project. It generates a single page that renders "Hello world!" and no interactive components. It is authored in TypeScript, contains one global stylesheet, and one CSS Module. 45 | 46 | ### Counter 47 | 48 | The counter benchmark is exactly the same as **Static**, except it renders an interactive "Counter" component rather than static text. This should give you a sense of Microsite's base output size when using `withHydrate`. 49 | -------------------------------------------------------------------------------- /packages/microsite/src/utils/command.ts: -------------------------------------------------------------------------------- 1 | import { resolve, relative } from "path"; 2 | import module from "module"; 3 | const { createRequire } = module; 4 | const require = createRequire(import.meta.url); 5 | 6 | import { fileExists } from "./fs.js"; 7 | import { loadConfiguration as loadUserConfiguration } from "snowpack"; 8 | 9 | const pkg = require(resolve(process.cwd(), "package.json")); 10 | const DEFAULT_BASE_PATH = pkg.homepage || "/"; 11 | const _config = require("microsite/assets/snowpack.config.cjs"); 12 | 13 | export function resolveNormalizedBasePath(args: { 14 | ["--base-path"]?: string; 15 | [key: string]: any; 16 | }) { 17 | let basePath = args["--base-path"] ?? DEFAULT_BASE_PATH; 18 | return basePath === "/" 19 | ? basePath 20 | : `/${basePath.replace(/^\//, "").replace(/\/$/, "")}/`; 21 | } 22 | 23 | export async function loadConfiguration(mode: "dev" | "build") { 24 | const overrides = await getOverrides(mode); 25 | return loadUserConfiguration(overrides); 26 | } 27 | 28 | export async function getOverrides(mode: "dev" | "build") { 29 | const [tsconfigPath] = await Promise.all([findTsOrJsConfig()]); 30 | const aliases = tsconfigPath 31 | ? resolveTsconfigPathsToAlias({ tsconfigPath }) 32 | : {}; 33 | 34 | switch (mode) { 35 | case "dev": 36 | return { 37 | ..._config, 38 | buildOptions: { 39 | ..._config.buildOptions, 40 | ssr: true, 41 | }, 42 | packageOptions: { 43 | ..._config.packageOptions, 44 | external: [..._config.packageOptions.external].filter( 45 | (v) => !v.startsWith("microsite") 46 | ), 47 | }, 48 | plugins: [..._config.plugins], 49 | alias: { 50 | ...aliases, 51 | ...(_config.alias ?? {}), 52 | "microsite/hydrate": "microsite/client/hydrate", 53 | }, 54 | }; 55 | case "build": 56 | return { 57 | ..._config, 58 | devOptions: { 59 | ..._config.devOptions, 60 | hmr: false, 61 | port: 0, 62 | hmrPort: 0, 63 | }, 64 | buildOptions: { 65 | ..._config.buildOptions, 66 | ssr: true, 67 | }, 68 | plugins: [..._config.plugins], 69 | alias: { 70 | ...aliases, 71 | ...(_config.alias ?? {}), 72 | }, 73 | packageOptions: { 74 | ..._config.packageOptions, 75 | external: [..._config.packageOptions.external].filter( 76 | (v) => v !== "preact" 77 | ), 78 | rollup: { 79 | ...(_config.installOptions?.rollup ?? {}), 80 | plugins: [ 81 | { 82 | name: "@microsite/auto-external", 83 | options(opts) { 84 | return Object.assign({}, opts, { 85 | external: (source: string) => source.startsWith("preact"), 86 | }); 87 | }, 88 | }, 89 | ], 90 | }, 91 | }, 92 | }; 93 | } 94 | } 95 | 96 | const findTsOrJsConfig = async () => { 97 | const cwd = process.cwd(); 98 | const tsconfig = resolve(cwd, "./tsconfig.json"); 99 | if (await fileExists(tsconfig)) return tsconfig; 100 | const jsconfig = resolve(cwd, "./jsconfig.json"); 101 | if (await fileExists(jsconfig)) return jsconfig; 102 | return null; 103 | }; 104 | 105 | function resolveTsconfigPathsToAlias({ 106 | tsconfigPath = "./tsconfig.json", 107 | } = {}) { 108 | let { baseUrl, paths } = require(tsconfigPath)?.compilerOptions ?? {}; 109 | if (!(baseUrl && paths)) return {}; 110 | 111 | baseUrl = resolve(process.cwd(), baseUrl); 112 | 113 | const aliases = {}; 114 | 115 | Object.keys(paths).forEach((item) => { 116 | const key = item.replace("/*", ""); 117 | const value = 118 | "./" + 119 | relative( 120 | process.cwd(), 121 | resolve(baseUrl, paths[item][0].replace("/*", "").replace("*", "")) 122 | ); 123 | 124 | aliases[key] = value; 125 | }); 126 | 127 | return aliases; 128 | } 129 | -------------------------------------------------------------------------------- /docs/basic/pages.md: -------------------------------------------------------------------------------- 1 | # Pages 2 | 3 | In Microsite, a page is a Preact component exported from a `.jsx` or `.tsx` file in the `src/pages` directory. Pages will generate a corresponding HTML file based on the file name. 4 | 5 | **Example** If you create the following file at `src/pages/about.tsx`, then it will be accessible at `/about`. 6 | 7 | ```tsx 8 | import { FunctionComponent } from 'preact'; 9 | 10 | const About: FunctionComponent = () => { 11 | return
About
12 | } 13 | 14 | export default About; 15 | ``` 16 | 17 | > **Note** Currently, [Automatic Partial Hydration](https://github.com/natemoo-re/microsite/blob/main/docs/basic/bundled-javascript.md) uses a code-splitting technique which extracts **components** but not top-level files within `src/pages/**`. This ensures that your entire `Page` component and all its children are not included in the final client bundle. See [**Caveats**](https://github.com/natemoo-re/microsite/blob/main/docs/basic/bundled-javascript.md#caveats) for more detail. 18 | 19 | ## Pages with Dynamic Routing 20 | 21 | Microsite supports pages with dynamic routes, which may be familiar if you've used something like [Next.js](https://nextjs.org/). 22 | 23 | **Example** If you create a file called `src/pages/posts/[id].tsx`, then it will be accessible at `posts/1`, `posts/2`, etc. 24 | 25 | ## Static Generation 26 | 27 | Microsite is a **Static Site Generator**, meaning it generates HTML files at **build** time. This strategy works phenomenally well for for content-based sites. For highly dynamic _applications_, you may consider [Next.js](https://nextjs.org) as an alternative. 28 | 29 | Microsite allows you to statically generate any page with or without data. 30 | 31 | ### Static Generation without data 32 | 33 | By default, Microsite pre-renders pages using Static Generation without fetching data. Here's an example: 34 | 35 | ```tsx 36 | import { FunctionComponent } from 'preact'; 37 | 38 | const About: FunctionComponent = () => { 39 | return
About
40 | } 41 | 42 | export default About; 43 | ``` 44 | 45 | Note that this page does not need to fetch any external data to be pre-rendered. A single HTML file will be generated per page during build time. 46 | 47 | ### Static Generation with data 48 | 49 | Some pages require fetching external data for pre-rendering. In these cases, Microsite exposes a utility function called `definePage` which allows you to hook into Static Generation at build-time. 50 | 51 | If your page **content** depends on external data: Use `definePage` with `getStaticProps`. 52 | 53 | ```tsx 54 | import { FunctionComponent } from 'preact'; 55 | import { definePage } from 'microsite/page'; 56 | 57 | const BlogPost: FunctionComponent<{ posts: any[] }> = ({ posts }) => { 58 | // render 59 | } 60 | 61 | export default definePage(BlogPost, { 62 | async getStaticProps() { 63 | const res = await fetch('https://.../posts'); 64 | const posts = await res.json(); 65 | return { 66 | props: { posts } 67 | } 68 | } 69 | }); 70 | ``` 71 | 72 | If your page **paths** depend on external data: Use `definePage` with `getStaticPaths` (usually in addition to `getStaticProps`). 73 | 74 | ```tsx 75 | import { FunctionComponent } from 'preact'; 76 | import { definePage } from 'microsite/page'; 77 | 78 | const BlogPost: FunctionComponent<{ post: any }> = ({ post }) => { 79 | // render 80 | } 81 | 82 | export default definePage(BlogPost, { 83 | path: '/posts/[id]', 84 | async getStaticPaths() { 85 | const res = await fetch('https://.../posts'); 86 | const posts = await res.json(); 87 | const paths = posts.map((post) => `/posts/${post.id}`); 88 | 89 | return { paths }; 90 | }, 91 | async getStaticProps({ params }) { 92 | const res = await fetch(`https://.../posts/${params.id}`); 93 | const post = await res.json(); 94 | return { 95 | props: { post } 96 | } 97 | } 98 | }); 99 | ``` 100 | 101 | To learn more about how `getStaticPaths` works, check out the [Data Fetching documentation](./data-fetching.md). 102 | 103 | 104 | -------------------------------------------------------------------------------- /packages/microsite/src/utils/open.ts: -------------------------------------------------------------------------------- 1 | // Sourced from Snowpack 2 | // https://github.com/snowpackjs/snowpack/blob/2cbbdbbad1c4f842f86ff56d19f86afedf07d2e2/snowpack/src/util.ts#L156:L227 3 | 4 | /* 5 | * MIT License 6 | * 7 | * Copyright (c) 2019 Fred K. Schott 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the "Software"), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in all 17 | * copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | * SOFTWARE. 26 | */ 27 | import open from "open"; 28 | import execa from "execa"; 29 | import { join } from "path"; 30 | 31 | const cwd = process.cwd(); 32 | 33 | const appNames = { 34 | win32: { 35 | brave: "brave", 36 | chrome: "chrome", 37 | }, 38 | darwin: { 39 | brave: "Brave Browser", 40 | chrome: "Google Chrome", 41 | }, 42 | linux: { 43 | brave: "brave", 44 | chrome: "google-chrome", 45 | }, 46 | }; 47 | 48 | async function openInExistingChromeBrowser(url: string) { 49 | // see if Chrome process is open; fail if not 50 | await execa.command('ps cax | grep "Google Chrome"', { 51 | shell: true, 52 | }); 53 | // use open Chrome tab if exists; create new Chrome tab if not 54 | const openChrome = execa( 55 | `osascript ${join( 56 | "node_modules", 57 | "microsite", 58 | "assets", 59 | "openChrome.appleScript" 60 | )} "${encodeURI(url)}"`, 61 | { 62 | cwd, 63 | stdio: "ignore", 64 | shell: true, 65 | } 66 | ); 67 | // if Chrome doesn’t respond within 3s, fall back to opening new tab in default browser 68 | let isChromeStalled = setTimeout(() => { 69 | openChrome.cancel(); 70 | }, 3000); 71 | try { 72 | await openChrome; 73 | } catch (err) { 74 | console.error(err); 75 | if (err.isCanceled) { 76 | console.warn( 77 | `Chrome not responding to Snowpack after 3s. Opening in new tab.` 78 | ); 79 | } else { 80 | console.error(err.toString() || err); 81 | } 82 | throw err; 83 | } finally { 84 | clearTimeout(isChromeStalled); 85 | } 86 | } 87 | export async function openInBrowser( 88 | protocol: string, 89 | hostname: string, 90 | port: number, 91 | basePath: string, 92 | browser: string 93 | ): Promise { 94 | const url = `${protocol}//${hostname}:${port}${basePath.replace(/\/$/, '')}`; 95 | browser = /chrome/i.test(browser) 96 | ? appNames[process.platform]["chrome"] 97 | : /brave/i.test(browser) 98 | ? appNames[process.platform]["brave"] 99 | : browser; 100 | const isMac = process.platform === "darwin"; 101 | const isBrowserChrome = /chrome|default/i.test(browser); 102 | if (!isMac || !isBrowserChrome) { 103 | await (browser === "default" ? open(url) : open(url, { app: browser })); 104 | return; 105 | } 106 | 107 | try { 108 | // If we're on macOS, and we haven't requested a specific browser, 109 | // we can try opening Chrome with AppleScript. This lets us reuse an 110 | // existing tab when possible instead of creating a new one. 111 | await openInExistingChromeBrowser(url); 112 | } catch (err) { 113 | // if no open Chrome process, just go ahead and open default browser. 114 | await open(url); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /packages/microsite/src/runtime/index.tsx: -------------------------------------------------------------------------------- 1 | import { h, hydrate as rehydrate, render } from "preact"; 2 | 3 | const win = window as any; 4 | if (!("requestIdleCallback" in window)) { 5 | win.requestIdleCallback = function (cb) { 6 | return setTimeout(function () { 7 | var start = Date.now(); 8 | cb({ 9 | didTimeout: false, 10 | timeRemaining: function () { 11 | return Math.max(0, 50 - (Date.now() - start)); 12 | }, 13 | }); 14 | }, 1); 15 | }; 16 | 17 | win.cancelIdleCallback = function (id) { 18 | clearTimeout(id); 19 | }; 20 | } 21 | 22 | const createObserver = (hydrate) => { 23 | if (!("IntersectionObserver" in window)) return null; 24 | 25 | const io = new IntersectionObserver((entries) => { 26 | entries.forEach((entry) => { 27 | const isIntersecting = 28 | entry.isIntersecting || entry.intersectionRatio > 0; 29 | if (!isIntersecting) return; 30 | hydrate(); 31 | io.disconnect(); 32 | }); 33 | }); 34 | 35 | return io; 36 | }; 37 | 38 | function attach(fragment, data, { name, source }, cb) { 39 | const { 40 | p: { children = null, ...props } = {}, 41 | m: method = "idle", 42 | f: flush, 43 | } = data; 44 | 45 | const hydrate = async () => { 46 | if (win.__MICROSITE_DEBUG) 47 | console.log(`[Hydrate] <${name} /> hydrated via "${method}"`); 48 | const { [name]: Component } = await import(source); 49 | 50 | if (flush) { 51 | render(h(Component, props, children), fragment); 52 | } else { 53 | rehydrate(h(Component, props, children), fragment); 54 | } 55 | if (cb) cb(); 56 | }; 57 | 58 | switch (method) { 59 | case "idle": { 60 | if (!("requestAnimationFrame" in window)) return setTimeout(hydrate, 0); 61 | 62 | win.requestIdleCallback( 63 | () => { 64 | requestAnimationFrame(hydrate); 65 | }, 66 | { timeout: 500 } 67 | ); 68 | break; 69 | } 70 | case "visible": { 71 | if (!("IntersectionObserver" in window)) return hydrate(); 72 | 73 | const observer = createObserver(hydrate); 74 | const childElements = fragment.childNodes.filter( 75 | (node) => node.nodeType === node.ELEMENT_NODE 76 | ); 77 | for (const child of childElements) { 78 | observer.observe(child); 79 | } 80 | break; 81 | } 82 | } 83 | } 84 | 85 | function createPersistentFragment(parentNode, childNodes) { 86 | const last = childNodes && childNodes[childNodes.length - 1].nextSibling; 87 | function insert(child, before) { 88 | try { 89 | parentNode.insertBefore(child, before || last); 90 | } catch (e) {} 91 | } 92 | return { 93 | parentNode, 94 | firstChild: childNodes[0], 95 | childNodes, 96 | appendChild: insert, 97 | insertBefore: insert, 98 | removeChild(child) { 99 | parentNode.removeChild(child); 100 | }, 101 | }; 102 | } 103 | 104 | const ATTR_REGEX = /(:?\w+)=([^\s]*)/g; 105 | function parseHydrateBoundary(node) { 106 | if (!node.textContent) return {}; 107 | const text = node.textContent.slice("?h ".length, -1); 108 | 109 | let props = {}; 110 | let result = ATTR_REGEX.exec(text); 111 | while (result) { 112 | let [, attr, val] = result; 113 | if (attr === "p") { 114 | props[attr] = JSON.parse(atob(val)); 115 | } else { 116 | props[attr] = val; 117 | } 118 | result = ATTR_REGEX.exec(text); 119 | } 120 | return props; 121 | } 122 | 123 | function findHydrationPoints() { 124 | const nodeIterator = document.createNodeIterator( 125 | document.documentElement, 126 | NodeFilter.SHOW_COMMENT, 127 | { 128 | acceptNode(node) { 129 | if (node.textContent && node.textContent.startsWith("?h c")) 130 | return NodeFilter.FILTER_ACCEPT; 131 | return NodeFilter.FILTER_REJECT; 132 | }, 133 | } 134 | ); 135 | 136 | const toHydrate = []; 137 | 138 | while (nodeIterator.nextNode()) { 139 | const start = nodeIterator.referenceNode; 140 | const data = parseHydrateBoundary(start); 141 | const childNodes = []; 142 | 143 | let end = start.nextSibling; 144 | while (end) { 145 | if ( 146 | end.nodeType === end.COMMENT_NODE && 147 | end.textContent && 148 | end.textContent.startsWith("?h p") 149 | ) { 150 | Object.assign(data, parseHydrateBoundary(end)); 151 | break; 152 | } 153 | childNodes.push(end); 154 | end = end.nextSibling; 155 | } 156 | 157 | toHydrate.push([ 158 | createPersistentFragment(start.parentNode, childNodes), 159 | data, 160 | [start, end], 161 | ]); 162 | } 163 | return toHydrate; 164 | } 165 | 166 | export default (manifest) => { 167 | const init = () => { 168 | const $cmps = findHydrationPoints(); 169 | 170 | for (const [fragment, data, markers] of $cmps) { 171 | const { c: key } = data; 172 | const [name, source] = manifest[key]; 173 | if (name && source) { 174 | attach(fragment, data, { name, source }, () => { 175 | fragment.childNodes.forEach((child) => 176 | child.tagName === "HYDRATE-PLACEHOLDER" ? child.remove() : null 177 | ); 178 | markers.forEach((marker) => marker.remove()); 179 | }); 180 | } 181 | } 182 | }; 183 | 184 | win.requestIdleCallback(init, { timeout: 1000 }); 185 | }; 186 | -------------------------------------------------------------------------------- /packages/microsite/src/document.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | h, 3 | createContext, 4 | Fragment, 5 | FunctionalComponent, 6 | JSX, 7 | ComponentType, 8 | ComponentProps, 9 | } from "preact"; 10 | import { useContext } from "preact/hooks"; 11 | import { generateHydrateScript } from "./utils/hydration.js"; 12 | 13 | export const __HeadContext = createContext({ 14 | head: { current: [] }, 15 | }); 16 | 17 | /** @internal */ 18 | export const __InternalDocContext = createContext({}); 19 | 20 | const _Document = () => { 21 | return ( 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | ); 30 | }; 31 | 32 | interface RenderPageResult { 33 | __renderPageResult: any; 34 | [key: string]: any; 35 | } 36 | 37 | export const defineDocument = >( 38 | Document: T, 39 | ctx: { 40 | prepare: (ctx: { 41 | renderPage: () => Promise; 42 | }) => Promise, "children"> & RenderPageResult>; 43 | } 44 | ) => { 45 | return Object.assign(Document, ctx); 46 | }; 47 | 48 | export const Document = defineDocument(_Document, { 49 | async prepare({ renderPage }) { 50 | const page = await renderPage(); 51 | return { ...page }; 52 | }, 53 | }); 54 | 55 | export const Html: FunctionalComponent> = ({ 56 | lang = "en", 57 | dir = "ltr", 58 | ...props 59 | }) => ; 60 | 61 | export const Main: FunctionalComponent, 63 | "id" | "dangerouslySetInnerHTML" | "children" 64 | >> = (props) => { 65 | const { __renderPageResult } = useContext(__InternalDocContext); 66 | return ( 67 |
72 | ); 73 | }; 74 | 75 | export const Head: FunctionalComponent> = ({ 76 | children, 77 | ...props 78 | }) => { 79 | const { 80 | dev = false, 81 | preconnect = [], 82 | basePath = "/", 83 | hasGlobalScript = false, 84 | preload = [], 85 | styles = [], 86 | __renderPageHead, 87 | } = useContext(__InternalDocContext); 88 | const shouldIncludeBasePath = basePath !== "/"; 89 | const prefix = shouldIncludeBasePath ? "./" : "/"; 90 | 91 | return ( 92 | 93 | 94 | 98 | 99 | {shouldIncludeBasePath && } 100 | 101 | {preconnect.map((href) => ( 102 | 103 | ))} 104 | {hasGlobalScript && ( 105 | 106 | )} 107 | {preload.map((href) => ( 108 | 109 | ))} 110 | {styles && 111 | styles.map((href) => ( 112 | 113 | ))} 114 | {styles && 115 | styles.map((href) => ( 116 | 117 | ))} 118 | 119 | {children} 120 | 121 | {dev && } 122 | {__renderPageHead} 123 | {dev && } 124 | 125 | ); 126 | }; 127 | 128 | export const MicrositeScript: FunctionalComponent = () => { 129 | const { 130 | __csrUrl, 131 | debug, 132 | hasGlobalScript, 133 | basePath, 134 | scripts, 135 | dev, 136 | devProps, 137 | } = useContext(__InternalDocContext); 138 | 139 | return ( 140 | 141 | {dev && ( 142 | 143 |