diff --git a/.github/workflows/neocities.yml b/.github/workflows/neocities.yml index 5f2ff19..c2db337 100644 --- a/.github/workflows/neocities.yml +++ b/.github/workflows/neocities.yml @@ -6,7 +6,6 @@ on: - master env: - node-version: lts/* FORCE_COLOR: 1 concurrency: # prevent concurrent deploys doing starnge things @@ -23,7 +22,7 @@ jobs: - name: Use Node.js uses: actions/setup-node@v4 with: - node-version: ${{env.node-version}} + node-version-file: package.json check-latest: true - run: npm i - run: npm run build diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7e61feb..9ec483a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,8 +8,7 @@ on: required: true env: - node_version: lts/* - FORCE_COLOR: 2 + FORCE_COLOR: 1 jobs: version_and_release: @@ -19,10 +18,10 @@ jobs: with: # fetch full history so things like auto-changelog work properly fetch-depth: 0 - - name: Use Node.js ${{ env.node_version }} + - name: Use Node.js uses: actions/setup-node@v4 with: - node-version: ${{ env.node_version }} + node-version-file: package.json check-latest: true # setting a registry enables the NODE_AUTH_TOKEN env variable where we can set an npm token. REQUIRED registry-url: 'https://registry.npmjs.org' @@ -37,4 +36,3 @@ jobs: newversion: ${{ github.event.inputs.newversion }} github_token: ${{ secrets.GITHUB_TOKEN }} # built in actions token. Passed tp gh-release if in use. npm_token: ${{ secrets.NPM_TOKEN }} # user set secret token generated at npm - diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ec6e8ad..9d3e1dd 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,4 +1,3 @@ ---- name: tests on: [pull_request, push] @@ -13,7 +12,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - node-version: [lts/*] + node-version: [lts/*, '23'] steps: - uses: actions/checkout@v4 @@ -23,12 +22,30 @@ jobs: node-version: ${{ matrix.node-version }} check-latest: true - run: npm i - - run: npm test + - name: Run tests + run: | + node_major=$(node -p "process.versions.node.split('.')[0]") + if [ "$node_major" -lt 23 ]; then + NODE_OPTIONS="--experimental-strip-types" npm test + else + npm test + fi - name: Coveralls uses: coverallsapp/github-action@v2 with: github-token: ${{ secrets.GITHUB_TOKEN }} files: .tap/report/lcov.info + parallel: true + + coverage: + needs: test + runs-on: ubuntu-latest + steps: + - name: Close parallel build + uses: coverallsapp/github-action@v2 + with: + parallel-finished: true + automerge: needs: test diff --git a/README.md b/README.md index 1bb0699..60ae90b 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ npm install top-bun - 🌎 [`top-bun` docs website](https://top-bun.org) - πŸ’¬ [Discord Chat](https://discord.gg/AVTsPRGeR9) - πŸ“’ [v7 Announcement](https://bret.io/blog/2023/reintroducing-top-bun/) +- πŸ“˜ [Full TypeScript Support](#typescript-support) ## Table of Contents @@ -73,6 +74,12 @@ src % tree β”‚ β”œβ”€β”€ page.html # Raw html pages are also supported. They support handlebars template blocks. β”‚ β”œβ”€β”€ page.vars.js # pages can define page variables in a page.vars.js. β”‚ └── style.css +β”œβ”€β”€ js-page +β”‚ └── page.js # A page can also just be a plain javascript function that returns content +β”œβ”€β”€ ts-page +β”‚ β”œβ”€β”€ client.ts # client bundles can be written in typescript via type stripping +β”‚ β”œβ”€β”€ page.vars.ts # pages can define page variables in a page.vars.js. +β”‚ └── page.ts # Anywhere you can use js in top-bun, you can also use typescript files. They compile via speedy type stripping. β”œβ”€β”€ feeds β”‚ └── feeds.template.js # Templates let you generate any file you want from variables and page data. β”œβ”€β”€ layouts # layouts can live anywhere. The inner content of your page is slotted into your layout. @@ -80,6 +87,7 @@ src % tree β”‚ β”œβ”€β”€ blog.layout.css # layouts can define an additional layout style. β”‚ β”œβ”€β”€ blog.layout.client.js # layouts can also define a layout client. β”‚ β”œβ”€β”€ article.layout.js # layouts can extend other layouts, since they are just functions. +β”‚ β”œβ”€β”€ typescript.layout.ts # layouts can also be written in typescript β”‚ └── root.layout.js # the default layout is called root. β”œβ”€β”€ globals # global assets can live anywhere. Here they are in a folder called globals. β”‚ β”œβ”€β”€ global.client.js # you can define a global js client that loads on every page. @@ -588,7 +596,7 @@ These imports will include the `root.layout.js` layout assets into the `blog.lay All static assets in the `src` directory are copied 1:1 to the `public` directory. Any file in the `src` directory that doesn't end in `.js`, `.css`, `.html`, or `.md` is copied to the `dest` directory. -### πŸ“ `--copy` directories +### `--copy` directories You can specify directories to copy into your `dest` directory using the `--copy` flag. Everything in those directories will be copied as-is into the destination, including js, css, html and markdown, preserving the internal directory structure. Conflicting files are not detected or reported and will cause undefined behavior. @@ -926,18 +934,6 @@ Template files receive a similar set of variables: - `pages`: An array of [`PageData`](https://github.com/bcomnes/top-bun/blob/master/lib/build-pages/page-data.js) instances for every page in the site build. Use this array to introspect pages to generate feeds and index pages. - `template`: An object of the template file data being rendered. -### Variable types - -The following types are exported from `top-bun`: - -```ts -LayoutFunction -PostVarsFunction -PageFunction -TemplateFunction -TemplateAsyncIterator -``` - Where `T` is your set of variables in the `vars` object. ### `postVars` post processing variables (Advanced) {#postVars} @@ -992,6 +988,115 @@ This `postVars` renders some html from page introspection of the last 5 blog pos \{{{ vars.blogPostsHtml }}} ``` + +## TypeScript Support + +`top-bun` now supports **TypeScript** via native type-stripping in Node.js. + +- **Requires Node.js β‰₯23** *(built-in)* or **Node.js 22** with the `NODE_OPTIONS="--experimental-strip-types" top-bun` env variable. +- Seamlessly mix `.ts`, `.mts`, `.cts` files alongside `.js`, `.mjs`, `.cjs`. +- No explicit compilation step neededβ€”Node.js handles type stripping at runtime. +- Fully compatible with existing `top-bun` file naming conventions. + +### Supported File Types + +Anywhere you can use a `.js`, `.mjs` or `.cjs` file in top-bun, you can now use `.ts`, `.mts`, `.cts`. +When running in a Node.js context, [type-stripping](https://nodejs.org/api/typescript.html#type-stripping) is used. +When running in a web client context, [esbuild](https://esbuild.github.io/content-types/#typescript) type stripping is used. +Type stripping provides 0 type checking, so be sure to set up `tsc` and `tsconfig.json` so you can catch type errors while editing or in CI. + +### Recommended `tsconfig.json` + +Install [@voxpelli/tsconfig](https://ghub.io/@voxpelli/tsconfig) which provides type checking in `.js` and `.ts` files and preconfigured for `--no-emit` and extend with type stripping friendly rules: + +```json +{ + "extends": "@voxpelli/tsconfig/node20.json", + "compilerOptions": { + "skipLibCheck": true, + "erasableSyntaxOnly": true, + "allowImportingTsExtensions": true, + "rewriteRelativeImportExtensions": true, + "verbatimModuleSyntax": true + }, + "include": [ + "**/*", + ], + "exclude": [ + "**/*.js", + "node_modules", + "coverage", + ".github" + ] +} +``` + +### Using TypeScript with top-bun Types + +You can use `top-bun`'s built-in types to strongly type your layout, page, and template functions. The following types are available: + +```ts +import type { + LayoutFunction, + PostVarsFunction, + PageFunction, + TemplateFunction, + TemplateAsyncIterator +} from 'top-bun' +``` + +They are all generic and accept a variable template that you can develop and share between files. + +### TypeScript Examples + +#### Page Function + +```typescript +// page.ts +import type { PageFunction } from 'top-bun' + +export const vars = { + message: 'TypeScript pages are easy!' +} + +const page: PageFunction = async ({ vars }) => { + return `

Hello from TypeScript!

${vars.message}

` +} + +export default page +``` + +#### Layout Function + +```typescript +// root.layout.ts +import type { LayoutFunction } from 'top-bun' +import { html, render } from 'uhtml-isomorphic' + +type Vars = { + siteName: string, + title?: string +} + +const layout: LayoutFunction = ({ vars, scripts, styles, children }) => { + return render(String, html` + + + + ${vars.title ? `${vars.title} | ` : ''}${vars.siteName} + ${styles?.map(style => html``)} + ${scripts?.map(script => html``)} + + + ${children} + + + `) +} + +export default layout +``` + ## Design Goals - Convention over configuration. All configuration should be optional, and at most it should be minimal. @@ -1013,8 +1118,8 @@ This `postVars` renders some html from page introspection of the last 5 blog pos - Garbage in, garbage out. Don't over-correct bad input. - Conventions + standards. Vanilla file types. No new file extensions. No weird syntax to learn. Language tools should just work because you aren't doing anything weird or out of band. - Encourage directly runnable source files. Direct run is an incredible, undervalued feature more people should learn to use. -- Support typescript, via ts-in-js and type stripping features when available. -- Embrace the now. Limit support to pretend you are working in the future (technology predictions nearly always are wrong!) +- Support typescript, via ts-in-js and type stripping features. Leave type checking to tsc. +- Embrace the now. Limit support on features that let one pretend they are working with future ecosystem features e.g. pseudo esm (technology predictions nearly always are wrong!) ## FAQ @@ -1077,7 +1182,9 @@ Some notable features are included below, see the [roadmap](https://github.com/u - [x] Built in browsersync dev server - [x] Real default layout style builds - [x] Esbuild settings escape hatch -- [x] Copy folders +- [x] Copy folders +- [x] Full Typescript support via native type stripping +- [ ] JSX support in client bundles - ...[See roadmap](https://github.com/users/bcomnes/projects/3/) ## History diff --git a/examples/type-stripping/package.json b/examples/type-stripping/package.json new file mode 100644 index 0000000..3da3e9e --- /dev/null +++ b/examples/type-stripping/package.json @@ -0,0 +1,27 @@ +{ + "name": "@top-bun/preact-example", + "version": "0.0.0", + "type": "module", + "scripts": { + "start": "npm run watch", + "build": "npm run clean && top-bun", + "clean": "rm -rf public && mkdir -p public", + "watch": "npm run clean && tb --watch" + }, + "author": "Bret Comnes (https://bret.io/)", + "license": "MIT", + "dependencies": { + "@preact/signals": "^2.0.0", + "highlight.js": "^11.9.0", + "htm": "^3.1.1", + "mine.css": "^9.0.1", + "preact": "^10.24.0", + "preact-render-to-string": "^6.5.11", + "top-bun": "../../." + }, + "devDependencies": { + "@voxpelli/tsconfig": "^15.0.0", + "npm-run-all2": "^6.0.0", + "typescript": "~5.8.2" + } +} diff --git a/examples/type-stripping/src/README.md b/examples/type-stripping/src/README.md new file mode 100644 index 0000000..d9c4051 --- /dev/null +++ b/examples/type-stripping/src/README.md @@ -0,0 +1,5 @@ +# Preact example + +This is a preact example. + +[Isomorphic Component Rendering](./isomorphic/) diff --git a/examples/type-stripping/src/globals/global.client.ts b/examples/type-stripping/src/globals/global.client.ts new file mode 100644 index 0000000..63290d7 --- /dev/null +++ b/examples/type-stripping/src/globals/global.client.ts @@ -0,0 +1,3 @@ +import { toggleTheme } from 'mine.css' +// @ts-ignore +window.toggleTheme = toggleTheme diff --git a/examples/type-stripping/src/globals/global.css b/examples/type-stripping/src/globals/global.css new file mode 100644 index 0000000..84d5e65 --- /dev/null +++ b/examples/type-stripping/src/globals/global.css @@ -0,0 +1,3 @@ +@import 'mine.css/dist/mine.css'; +@import 'mine.css/dist/layout.css'; +@import 'highlight.js/styles/github-dark-dimmed.css'; diff --git a/examples/type-stripping/src/isomorphic/client.ts b/examples/type-stripping/src/isomorphic/client.ts new file mode 100644 index 0000000..cbd8fed --- /dev/null +++ b/examples/type-stripping/src/isomorphic/client.ts @@ -0,0 +1,55 @@ +import { html, Component } from 'htm/preact' +import { render } from 'preact' +import { useCallback } from 'preact/hooks' +import { useSignal, useComputed } from '@preact/signals' + +const Header = ({ name }) => html`

${name} List

` + +const Footer = props => { + const count = useSignal(0) + const double = useComputed(() => count.value * 2) + + const handleClick = useCallback(() => { + count.value++ + }, [count]) + + return html`
+ ${count} + ${double} + ${props.children} + +
` +} + +class App extends Component { + addTodo () { + const { todos = [] } = this.state + this.setState({ todos: todos.concat(`Item ${todos.length}`) }) + } + + render ({ page }, { todos = [] }) { + return html` +
+ <${Header} name="ToDo's (${page})" /> +
    + ${todos.map(todo => html` +
  • ${todo}
  • + `)} +
+ + <${Footer}>footer content here +
+ ` + } +} + +export const page = () => html` + <${App} page="Isomorphic"/> + <${Footer}>footer content here + <${Footer}>footer content here + ` + +if (typeof window !== 'undefined') { + const renderTarget = document.querySelector('.app-main') + render(page(), renderTarget) +} diff --git a/examples/type-stripping/src/isomorphic/page.ts b/examples/type-stripping/src/isomorphic/page.ts new file mode 100644 index 0000000..a348ef6 --- /dev/null +++ b/examples/type-stripping/src/isomorphic/page.ts @@ -0,0 +1,5 @@ +import { page } from './client.ts' + +export default () => { + return page() +} diff --git a/examples/type-stripping/src/layouts/root.layout.ts b/examples/type-stripping/src/layouts/root.layout.ts new file mode 100644 index 0000000..474a685 --- /dev/null +++ b/examples/type-stripping/src/layouts/root.layout.ts @@ -0,0 +1,51 @@ +import { html } from 'htm/preact' +import { render } from 'preact-render-to-string' + +import type { LayoutFunction } from 'top-bun' + +interface Vars { + title?: string + siteName?: string + defaultStyle?: boolean + basePath?: string +} + +type DefaultRootLayout = LayoutFunction + +const defaultRootLayout: DefaultRootLayout = ({ + vars: { + title, + siteName = 'TopBun', + basePath, + }, + scripts, + styles, + children, +}) => /* html */` + + + ${render(html` + + + ${title ? `${title}` : ''}${title && siteName ? ' | ' : ''}${siteName} + + ${scripts + ? scripts.map(script => html`