diff --git a/.dependency-cruiser.json b/.dependency-cruiser.json deleted file mode 100644 index 2651faa..0000000 --- a/.dependency-cruiser.json +++ /dev/null @@ -1,129 +0,0 @@ -{ - "forbidden": [ - { - "name": "not-to-test", - "comment": "Don't allow dependencies from outside the test folder to test", - "severity": "error", - "from": { - "pathNot": "^(test|spec)" - }, - "to": { - "path": "^(test|spec)" - } - }, - { - "name": "not-to-spec", - "comment": "Don't allow dependencies to (typescript/ javascript/ coffeescript) spec files", - "severity": "error", - "from": {}, - "to": { - "path": "\\.spec\\.(js|ts|ls|coffee|litcoffee|coffee\\.md)$" - } - }, - { - "name": "no-circular", - "severity": "warn", - "comment": "Warn in case there's circular dependencies", - "from": {}, - "to": { - "circular": true - } - }, - { - "name": "no-deprecated-core", - "comment": "Warn about dependencies on deprecated core modules.", - "severity": "warn", - "from": {}, - "to": { - "dependencyTypes": [ - "core" - ], - "path": "^(punycode|domain|constants|sys|_linklist)$" - } - }, - { - "name": "no-deprecated-npm", - "comment": "These npm modules are deprecated - find an alternative.", - "severity": "warn", - "from": {}, - "to": { - "dependencyTypes": [ - "deprecated" - ] - } - }, - { - "name": "not-to-unresolvable", - "comment": "Don't allow dependencies on modules dependency-cruiser can't resolve to files on disk (which probably means they don't exist)", - "severity": "error", - "from": {}, - "to": { - "couldNotResolve": true - } - }, - { - "name": "not-to-dev-dep", - "severity": "error", - "comment": "Don't allow dependencies from src/app/lib to a development only package", - "from": { - "path": "^(src|app|lib)", - "pathNot": "\\.spec\\.(js|ts|ls|coffee|litcoffee|coffee\\.md)$" - }, - "to": { - "dependencyTypes": [ - "npm-dev" - ] - } - }, - { - "name": "no-non-package-json", - "severity": "error", - "comment": "Don't allow dependencies to packages not in package.json (except from within node_modules)", - "from": { - "pathNot": "^node_modules" - }, - "to": { - "dependencyTypes": [ - "unknown", - "undetermined", - "npm-no-pkg", - "npm-unknown" - ] - } - }, - { - "name": "optional-deps-used", - "severity": "info", - "comment": "nothing serious - but just check you have some serious try/ catches around the import/ requires of these", - "from": {}, - "to": { - "dependencyTypes": [ - "npm-optional" - ] - } - }, - { - "name": "peer-deps-used", - "comment": "Warn about the use of a peer dependency (peer dependencies are deprecated with the advent of npm 3 - and probably gone with version 4).", - "severity": "warn", - "from": {}, - "to": { - "dependencyTypes": [ - "npm-peer" - ] - } - }, - { - "name": "no-duplicate-dep-types", - "comment": "Warn if a dependency you're actually using occurs in your package.json more than once (technically: has more than one dependency type)", - "severity": "warn", - "from": {}, - "to": { - "moreThanOneDependencyType": true - } - } - ], - "options": { - "doNotFollow": "^node_modules" - } -} diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 31ae44d..152df60 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,31 +5,157 @@ version: 2 updates: # Enable version updates for npm - package-ecosystem: "npm" - # Look for `package.json` and `lock` files in the `root` directory + groups: + typescript: + patterns: + - "@voxpelli/tsconfig" + - "@types/node" + - "typescript" + preact: + patterns: + - "preact" + - "preact-render-to-string" + - "htm" directory: "/" - # Check the npm registry for updates every day (weekdays) schedule: interval: "daily" - package-ecosystem: "npm" directory: "/examples/basic" + groups: + typescript: + patterns: + - "@voxpelli/tsconfig" + - "@types/node" + - "typescript" + schedule: + interval: "daily" + - package-ecosystem: "npm" + directory: "/examples/css-modules/" + groups: + typescript: + patterns: + - "@voxpelli/tsconfig" + - "@types/node" + - "typescript" schedule: interval: "daily" - package-ecosystem: "npm" directory: "/examples/default-layout/" + groups: + typescript: + patterns: + - "@voxpelli/tsconfig" + - "@types/node" + - "typescript" + schedule: + interval: "daily" + - package-ecosystem: "npm" + directory: "/examples/esbuild-settings" + groups: + typescript: + patterns: + - "@voxpelli/tsconfig" + - "@types/node" + - "typescript" + schedule: + interval: "daily" + - package-ecosystem: "npm" + directory: "/examples/markdown-settings/" + groups: + typescript: + patterns: + - "@voxpelli/tsconfig" + - "@types/node" + - "typescript" schedule: interval: "daily" - package-ecosystem: "npm" directory: "/examples/nested-dest/" + groups: + typescript: + patterns: + - "@voxpelli/tsconfig" + - "@types/node" + - "typescript" + schedule: + interval: "daily" + - package-ecosystem: "npm" + directory: "/examples/preact-isomorphic/" + groups: + typescript: + patterns: + - "@voxpelli/tsconfig" + - "@types/node" + - "typescript" schedule: interval: "daily" - package-ecosystem: "npm" - directory: "/examples/preact/" + directory: "/examples/react/" + groups: + typescript: + patterns: + - "@voxpelli/tsconfig" + - "@types/node" + - "typescript" + react: + patterns: + - "react" + - "react-dom" + - "@types/react" + - "@types/react-dom" schedule: interval: "daily" - package-ecosystem: "npm" directory: "/examples/string-layouts/" + groups: + typescript: + patterns: + - "@voxpelli/tsconfig" + - "@types/node" + - "typescript" + schedule: + interval: "daily" + - package-ecosystem: "npm" + directory: "/examples/tailwind/" + groups: + typescript: + patterns: + - "@voxpelli/tsconfig" + - "@types/node" + - "typescript" + schedule: + interval: "daily" + - package-ecosystem: "npm" + directory: "/examples/type-stripping/" + groups: + typescript: + patterns: + - "@voxpelli/tsconfig" + - "@types/node" + - "typescript" + schedule: + interval: "daily" + - package-ecosystem: "npm" + directory: "/examples/uhtml-isomorphic/" + groups: + typescript: + patterns: + - "@voxpelli/tsconfig" + - "@types/node" + - "typescript" + schedule: + interval: "daily" + - package-ecosystem: "npm" + directory: "/examples/worker-examples/" + groups: + typescript: + patterns: + - "@voxpelli/tsconfig" + - "@types/node" + - "typescript" schedule: interval: "daily" + # Enable version updates for pnpm # Enable updates to github actions - package-ecosystem: "github-actions" directory: "/" diff --git a/.github/workflows/neocities-old.yml b/.github/workflows/neocities-old.yml new file mode 100644 index 0000000..9e067a5 --- /dev/null +++ b/.github/workflows/neocities-old.yml @@ -0,0 +1,36 @@ +name: Deploy old website to neociteis + +on: + push: + branches: + - master + +env: + FORCE_COLOR: 1 + +concurrency: # prevent concurrent deploys doing starnge things + group: deploy-to-neocities-old + cancel-in-progress: true + +jobs: + deploy: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + + - name: Use Node.js + uses: actions/setup-node@v6 + with: + node-version-file: package.json + check-latest: true + - run: npm i + - run: npm run build + + - name: Deploy to neocities + uses: bcomnes/deploy-to-neocities@v3 + with: + api_key: ${{ secrets.NEOCITIES_API_TOKEN }} + cleanup: true + neocities_supporter: true + preview_before_deploy: true diff --git a/.github/workflows/neocities.yml b/.github/workflows/neocities.yml index c2db337..592f0e6 100644 --- a/.github/workflows/neocities.yml +++ b/.github/workflows/neocities.yml @@ -1,9 +1,9 @@ -name: Deploy to neociteis +name: Deploy domstack website to neociteis on: push: branches: - - master + - next env: FORCE_COLOR: 1 @@ -17,10 +17,10 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Use Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version-file: package.json check-latest: true @@ -30,7 +30,7 @@ jobs: - name: Deploy to neocities uses: bcomnes/deploy-to-neocities@v3 with: - api_token: ${{ secrets.NEOCITIES_API_TOKEN }} + api_key: ${{ secrets.NEOCITIES_DOMSTACK_API_TOKEN }} cleanup: true - neocoties_supporter: true + neocities_supporter: true preview_before_deploy: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9ec483a..5e776f2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,12 +14,12 @@ jobs: version_and_release: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: # fetch full history so things like auto-changelog work properly fetch-depth: 0 - name: Use Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version-file: package.json check-latest: true diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9d3e1dd..bde822b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -15,9 +15,9 @@ jobs: node-version: [lts/*, '23'] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: ${{ matrix.node-version }} check-latest: true @@ -34,7 +34,7 @@ jobs: uses: coverallsapp/github-action@v2 with: github-token: ${{ secrets.GITHUB_TOKEN }} - files: .tap/report/lcov.info + files: coverage/lcov.info parallel: true coverage: diff --git a/.gitignore b/.gitignore index dae330e..f30cc4f 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,5 @@ coverage *.d.ts *.d.ts.map +!types/**/*.d.ts +!types/**/*.d.ts.map diff --git a/CHANGELOG.md b/CHANGELOG.md index eb7d515..75c008f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,72 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). -## [v10.5.3](https://github.com/bcomnes/top-bun/compare/v10.5.2...v10.5.3) +## [v11.0.0-beta.7](https://github.com/bcomnes/top-bun/compare/v11.0.0-beta.6...v11.0.0-beta.7) + +### Commits + +- Add Async type variants for the various layout and page functions [`4bd6bdb`](https://github.com/bcomnes/top-bun/commit/4bd6bdbc10f595a80205e5928a63120292230835) + +## [v11.0.0-beta.6](https://github.com/bcomnes/top-bun/compare/v11.0.0-beta.5...v11.0.0-beta.6) - 2025-08-17 + +### Merged + +- Bump the typescript group with 2 updates [`#200`](https://github.com/bcomnes/top-bun/pull/200) + +### Commits + +- Dependabot groups [`9c68781`](https://github.com/bcomnes/top-bun/commit/9c68781c2e371dd8f628f51a8c3abc3eed422668) +- Remove @async tag [`518fe7e`](https://github.com/bcomnes/top-bun/commit/518fe7e7fef398ede48b65250c255447c775c115) +- More update groups [`14062fb`](https://github.com/bcomnes/top-bun/commit/14062fb8b297f1a0cdc82b1d5e559a4cc106d822) + +## [v11.0.0-beta.5](https://github.com/bcomnes/top-bun/compare/v11.0.0-beta.4...v11.0.0-beta.5) - 2025-08-16 + +### Commits + +- Fix children types on LayoutFunction [`33b93a3`](https://github.com/bcomnes/top-bun/commit/33b93a3382a4f20ff198268d80e0bb535890245a) + +## [v11.0.0-beta.4](https://github.com/bcomnes/top-bun/compare/v11.0.0-beta.3...v11.0.0-beta.4) - 2025-08-16 + +### Commits + +- Rename template variables [`9a9f3d3`](https://github.com/bcomnes/top-bun/commit/9a9f3d34f6f66e975aef640265259cf475cd492d) +- Remove logging [`e6717c5`](https://github.com/bcomnes/top-bun/commit/e6717c561a0e5dc6262c9d74fd7ba465550cb73d) + +## [v11.0.0-beta.3](https://github.com/bcomnes/top-bun/compare/v11.0.0-beta.2...v11.0.0-beta.3) - 2025-08-16 + +### Commits + +- Update docs [`5aa5778`](https://github.com/bcomnes/top-bun/commit/5aa5778621190e4147b39e3084222325f0ea0c77) + +## [v11.0.0-beta.2](https://github.com/bcomnes/top-bun/compare/v11.0.0-beta.1...v11.0.0-beta.2) - 2025-08-16 + +### Commits + +- Allow customization of the PageFunction return type [`eae282a`](https://github.com/bcomnes/top-bun/commit/eae282a5f547289a30bbc1b08b1ceb1af15f5aea) +- Enable customization of LayoutFunction types [`0e1939a`](https://github.com/bcomnes/top-bun/commit/0e1939a78f37620fcd93705a9df0b848e93ddb6d) + +## [v11.0.0-beta.1](https://github.com/bcomnes/top-bun/compare/v11.0.0-beta.0...v11.0.0-beta.1) - 2025-08-16 + +### Merged + +- Bump @types/react from 18.3.23 to 19.1.10 in /examples/react [`#199`](https://github.com/bcomnes/top-bun/pull/199) +- Bump actions/checkout from 4 to 5 [`#196`](https://github.com/bcomnes/top-bun/pull/196) + +### Commits + +- Fix warning [`e60b67e`](https://github.com/bcomnes/top-bun/commit/e60b67e139490495ad30acf3550f473865cfc7ba) +- Set up new website action [`a8b0563`](https://github.com/bcomnes/top-bun/commit/a8b0563b805ed9528f99e6faa19b11a37bb8d9c8) +- Allow for sync PageFunctions [`701c42e`](https://github.com/bcomnes/top-bun/commit/701c42e163050f8d9213368fba378095cf97ab1c) + +## [v11.0.0-beta.0](https://github.com/bcomnes/top-bun/compare/v10.5.3...v11.0.0-beta.0) - 2025-07-20 + +### Commits + +- Switch examples over to use preact and general improvements [`289719d`](https://github.com/bcomnes/top-bun/commit/289719d53a69b3e3d7798794eca541628fdd59cd) +- More docs improvements [`0f16d6a`](https://github.com/bcomnes/top-bun/commit/0f16d6ae23930092f5a3fc82601152fb127c328d) +- Move to @import syntax [`debc2ae`](https://github.com/bcomnes/top-bun/commit/debc2aec4b81d19bac9aa91d689ec7fe6a6c2627) + +## [v10.5.3](https://github.com/bcomnes/top-bun/compare/v10.5.2...v10.5.3) - 2025-07-20 ### Merged diff --git a/README.md b/README.md index e4621ae..bfa6062 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,26 @@ -# πŸ₯ top-bun -[![npm version](https://img.shields.io/npm/v/top-bun.svg)](https://npmjs.org/package/top-bun) -[![Actions Status](https://github.com/bcomnes/top-bun/workflows/tests/badge.svg)](https://github.com/bcomnes/top-bun/actions) -[![Coverage Status](https://coveralls.io/repos/github/bcomnes/top-bun/badge.svg?branch=master)](https://coveralls.io/github/bcomnes/top-bun?branch=master) +# domstack +[![npm version](https://img.shields.io/npm/v/@domstack/static.svg)](https://npmjs.org/package/@domstack/static) +[![Actions Status](https://github.com/bcomnes/domstack/workflows/tests/badge.svg)](https://github.com/bcomnes/domstack/actions) +[![Coverage Status](https://coveralls.io/repos/github/bcomnes/domstack/badge.svg?branch=master)](https://coveralls.io/github/bcomnes/domstack?branch=master) [![Types in JS](https://img.shields.io/badge/types_in_js-yes-brightgreen)](https://github.com/voxpelli/types-in-js) -[![Neocities][neocities-img]](https://top-bun.org) +[![Neocities][neocities-img]](https://domstack.net) -`top-bun`: a traditional web bakery made with html, md, css and js. +`domstack`: Cut the [gordian knot](https://en.wikipedia.org/wiki/Gordian_Knot) of modern web development and build websites with a stack of html, md, css, ts, tsx, (and/or js/jsx). -(A bakery themed static site generator that's as fun as making bread.) +DOMStack provides a few project conventions around esbuild ande Node.js that lets you quickly, cleanly and easily build websites and web apps using all of your favorite technolgies without any framework specific impurities, unlocking the web platform as a freeform canvas. + +It's fast to learn, quick to build with, and performs better than you are used to. + +`domstack` currently ships a static site generator tool which is great for building static wesbites, and static PWA/MPAs. +There is an experimental fastify plugin in the works that will unlock dynamic hypermedia webapps using the same project structure. ```console -npm install top-bun +npm install @domstack/static ``` -- 🌎 [`top-bun` docs website](https://top-bun.org) +- 🌎 [domstack docs website](https://domstack.net) - πŸ’¬ [Discord Chat](https://discord.gg/AVTsPRGeR9) +- πŸ“’ [v11 - top-bun is now domstack](https://bret.io/blog/2023/top-bun-is-now-domstack/) - πŸ“’ [v7 Announcement](https://bret.io/blog/2023/reintroducing-top-bun/) - πŸ“˜ [Full TypeScript Support](#typescript-support) @@ -25,39 +31,39 @@ npm install top-bun ## Usage ```console -$ top-bun --help -Usage: top-bun [options] +$ domstack --help +Usage: domstack [options] - Example: top-bun --src website --dest public + Example: domstack --src website --dest public --src, -s path to source directory (default: "src") --dest, -d path to build destination directory (default: "public") --ignore, -i comma separated gitignore style ignore string - --drafts Build draft pages with the `.draft.{md,js,html}` page suffix. - --target, -t comma separated target strings for esbuild - --noEsbuildMeta skip writing the esbuild metafile to disk - --eject, -e eject the top bun default layout, style and client into the src flag directory + --drafts Build draft pages with the `.draft.{md,js,ts,html}` page suffix. + --eject, -e eject the DOMStack default layout, style and client into the src flag directory --watch, -w build, watch and serve the site build --watch-only watch and build the src folder without serving --copy path to directories to copy into dist; can be used multiple times --help, -h show help --version, -v show version information -top-bun (v10.5.1) +domstack (v11.0.0) ``` -`top-bun` builds a `src` directory into a `dest` directory (default: `public`). -`top-bun` is also aliased to a `tb` bin. +`domstack` builds a `src` directory into a `dest` directory (default: `public`). +`domstack` is also aliased to a `dom` bin. -- Running `top-bun` will result in a `build` by default. -- Running `top-bun --watch` or `top-bun -w` will build the site and start an auto-reloading development web-server that watches for changes. -- Running `top-bun --eject` or `top-bun -e` will extract the default layout, global styles, and client-side JavaScript into your source directory and add the necessary dependencies to your package.json. +- Running `domstack` will result in a `build` by default. +- Running `domstack --watch` or `domstack -w` will build the site and start an auto-reloading development web-server that watches for changes (provided by [Browsersync](https://browsersync.io)). +- Running `domstack --eject` or `domstack -e` will extract the default layout, global styles, and client-side JavaScript into your source directory and add the necessary dependencies to your package.json. -`top-bun` is primarily a unix `bin` written for the [Node.js](https://nodejs.org) runtime that is intended to be installed from `npm` as a `devDependency` inside a `package.json` committed to a `git` repository. +`domstack` is primarily a unix `bin` written for the [Node.js](https://nodejs.org) runtime that is intended to be installed from `npm` as a `devDependency` inside a `package.json` committed to a `git` repository. It can be used outside of this context, but it works best within it. ## Core Concepts -`top-bun` builds a website from "pages" in a `src` directory, nearly 1:1 into a `dest` directory. +`domstack` is a static site generator that builds a website from "pages" in a `src` directory, nearly 1:1 into a `dest` directory. +By building "pages" from their `src` location to the `dest` destination, the directory structure inside of `src` becomes a "filesystem router" naturally, without any additional moving systems or structures. + A `src` directory tree might look something like this: ```bash @@ -65,47 +71,53 @@ src % tree . β”œβ”€β”€ md-page β”‚ β”œβ”€β”€ README.md # directories with README.md in them turn into /md-page/index.html. -β”‚ β”œβ”€β”€ client.js # Every page can define its own client.js script that loads only with it. +β”‚ β”œβ”€β”€ client.ts # Every page can define its own client.ts script that loads only with it. β”‚ β”œβ”€β”€ style.css # Every page can define its own style.css style that loads only with it. β”‚ β”œβ”€β”€ loose-md-page.md # loose markdown get built in place, but lacks some page features. β”‚ └── nested-page # pages are built in place and can nest. β”‚ β”œβ”€β”€ README.md # This page is accessed at /md-page/nested-page/. -β”‚ β”œβ”€β”€ client.js # nested pages are just pages, so they also can have a page scoped client and style. -β”‚ └── style.css +β”‚ β”œβ”€β”€ style.css # nested pages are just pages, so they also can have a page scoped client and style. +β”‚ └── client.js # Anywhere JS loads, you can use .js or .ts β”œβ”€β”€ html-page -β”‚ β”œβ”€β”€ client.jsx # client bundles can also be written in .jsx/.tsx +β”‚ β”œβ”€β”€ client.tsx # client bundles can also be written in .jsx/.tsx β”‚ β”œβ”€β”€ 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. +β”‚ β”œβ”€β”€ page.vars.ts # pages can define page variables in a page.vars.ts β”‚ └── style.css β”œβ”€β”€ js-page -β”‚ └── page.js # A page can also just be a plain javascript function that returns content +β”‚ └── page.js # A page can also just be a plain javascript function that returns content. They can also be type checked. β”œβ”€β”€ 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. +β”‚ β”œβ”€β”€ client.ts # domstack provides type-stripping via Node.JS and esbuild +β”‚ β”œβ”€β”€ page.vars.ts # use tsc to run typechecking +β”‚ └── page.ts β”œβ”€β”€ feeds -β”‚ └── feeds.template.js # Templates let you generate any file you want from variables and page data. +β”‚ └── feeds.template.ts # Templates let you generate any file you want from variables and page data. +β”œβ”€β”€ page-with-workers +β”‚ β”œβ”€β”€ client.ts +β”‚ └── page.ts +β”‚ β”œβ”€β”€ counter.worker.ts # Web workers use a .worker.{ts,js} naming convention and are auto-bundled +β”‚ └── analytics.worker.js β”œβ”€β”€ layouts # layouts can live anywhere. The inner content of your page is slotted into your layout. -β”‚ β”œβ”€β”€ blog.layout.js # pages specify which layout they want by setting a `layout` page variable. +β”‚ β”œβ”€β”€ blog.layout.ts # pages specify which layout they want by setting a `layout` page variable. β”‚ β”œβ”€β”€ 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. +β”‚ β”œβ”€β”€ blog.layout.client.ts # layouts can also define a layout client. +β”‚ β”œβ”€β”€ article.layout.ts # layouts can extend other layouts, since they are just functions. +β”‚ β”œβ”€β”€ javascript.layout.js # layouts can also be written in javascript +β”‚ └── root.layout.ts # 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. +β”‚ β”œβ”€β”€ global.client.ts # you can define a global client that loads on every page. β”‚ β”œβ”€β”€ global.css # you can define a global css file that loads on every page. -β”‚ β”œβ”€β”€ global.vars.js # site wide variables get defined in global.vars.js. -β”‚ └── esbuild.settings.js # You can even customize the build settings passed to esbuild! +β”‚ β”œβ”€β”€ global.vars.ts # site wide variables get defined in global.vars.ts +β”‚ β”œβ”€β”€ markdown-it.settings.ts # You can customize the markdown-it instance used to render markdown +β”‚ └── esbuild.settings.ts # You can even customize the build settings passed to esbuild β”œβ”€β”€ README.md # This is just a top level page built from a README.md file. -β”œβ”€β”€ client.js # the top level page can define a page scoped js client. -β”œβ”€β”€ style.js # the top level page can define a page scoped Css style. +β”œβ”€β”€ client.ts # the top level page can define a page scoped js client. +β”œβ”€β”€ style.css # the top level page can define a page scoped css style. └── favicon-16x16.png # static assets can live anywhere. Anything other than JS, CSS and HTML get copied over automatically. ``` -The core idea of `top-bun` is that a `src` directory of markdown, html and js "inner" documents will be transformed into layout wrapped html documents in the `dest` directory, along with page scoped js and css bundles, as well as a global stylesheet and global js bundle. +The core idea of `domstack` is that a `src` directory of markdown, html and ts/js "inner" documents will be transformed into layout wrapped html documents in the `dest` directory, along with page scoped js and css bundles, as well as a global stylesheet and global js bundle. -It ships with sane defaults so that you can point `top-bun` at a standard [markdown documented repository](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github) and have it build a website with near-zero preparation. +It ships with sane defaults so that you can point `domstack` at a standard [markdown documented repository](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github) and have it build a website with near-zero preparation. ## Examples @@ -113,23 +125,26 @@ A collection of examples can be found in the [`./examples`](./examples) folder. To run examples: -```console -$ git clone git@github.com:bcomnes/top-bun.git -$ cd top-bun +```bash +$ git clone git@github.com:bcomnes/domstack.git +$ cd domstack +# install the top level deps $ npm i -$ npm run example:{example-name} +$ cd example:{example-name} +# install the example deps $ npm i +# start the example $ npm start ``` ### Additional examples -Here are some additional external examples of larger top-bun projects. -If you have a project that uses top-bun and could act as a nice example, please PR it to the list! +Here are some additional external examples of larger domstack projects. +If you have a project that uses domstack and could act as a nice example, please PR it to the list! -- [Blog Example](https://github.com/bcomnes/bret.io/) -- [Isomorphic Static/Client App](https://github.com/hifiwi-fi/breadcrum.net/tree/master/packages/web/client) -- [Zero-Conf Markdown Docs](https://github.com/bcomnes/deploy-to-neocities/blob/70b264bcb37fca5b21e45d6cba9265f97f6bfa6f/package.json#L38) +- [Blog Example](https://github.com/bcomnes/bret.io/) - A personal blog written with DOMStack +- [Isomorphic Static/Client App](https://github.com/hifiwi-fi/breadcrum.net/tree/master/packages/web/client) - Pages build from client templates and hydrate on load. +- [Zero-Conf Markdown Docs](https://github.com/bcomnes/deploy-to-neocities/blob/70b264bcb37fca5b21e45d6cba9265f97f6bfa6f/package.json#L38) - A npm package with markdown docs, transformed into a website without any any configuration ## Pages @@ -137,17 +152,17 @@ Pages are a named directories inside of `src`, with **one of** the following pag - `md` pages are [CommonMark](https://commonmark.org) markdown pages, with an optional [YAML](https://yaml.org) front-matter block. - `html` pages are an inner [html](https://developer.mozilla.org/en-US/docs/Web/HTML) fragment that get inserted into the page layout. -- `js` pages are a [js](https://developer.mozilla.org/en-US/docs/Web/JavaScript) file that exports a default function that resolves into an inner-html fragment that is inserted into the page layout. +- `ts`/`js` pages are a [ts](https://developer.mozilla.org/en-US/docs/Glossary/TypeScript)/[js](https://developer.mozilla.org/en-US/docs/Web/JavaScript) file that exports a default function that resolves into an inner-html fragment that is inserted into the page layout. -Variables are available in all pages. `md` and `html` pages support variable access via [handlebars][hb] template blocks. `js` pages receive variables as part of the argument passed to them. See the [Variables](#variables) section for more info. +Variables are available in all pages. `md` and `html` pages support variable access via [handlebars][hb] template blocks. `ts`/`js` pages receive variables as part of the argument passed to them. See the [Variables](#variables) section for more info. -A special variable called `layout` determines which layout the page is rendered into. +Pages can define a special variable called `layout` determines which layout the page is rendered into. -Because pages are just directories, they nest and structure naturally. Directories in the `src` folder that lack one of these special page files can exist along side page directories and can be used to store co-located code or static assets without conflict. +Because pages are just directories, they nest and structure naturally as a filesystem router. Directories in the `src` folder that lack one of these special page files can exist along side page directories and can be used to store co-located code or static assets without conflict. ### `md` pages -A `md` page looks like this: +A `md` page looks like this on the filesystem: ```bash src/page-name/README.md @@ -167,15 +182,15 @@ An example of a `md` page: ```md --- -title: A title for my markdown -favoriteBread: 'Baguette' +title: A title for a markdown page +favoriteColor: 'Blue' --- -Just writing about baking. +Just writing about web development. -## Favorite breads +## Favorite colors -My favorite bread is \{{ vars.favoriteBread }}. +My favorite color is {{ vars.favoriteColor }}. ``` ### `html` pages @@ -187,7 +202,7 @@ src/page-name/page.html ``` - `html` pages are named `page.html` inside an associated page folder. -- `html` pages are the simplest page type in `top-bun`. They let you build with raw html for when you don't want that page to have access to markdown features. Some pages are better off with just raw `html`. +- `html` pages are the simplest page type in `domstack`. They let you build with raw html for when you don't want that page to have access to markdown features. Some pages are better off with just raw `html`, and the rules with building `html` in a real `html` file are much more flexible than inside of a `md` file. - `html` page variables can only be set in a `page.vars.js` file inside the page directory. - `html` pages support [handlebars][hb] template placeholders. - You can disable `html` page [handlebars][hb] processing by setting the `handlebars` variable to `false`. @@ -195,35 +210,44 @@ src/page-name/page.html An example `html` page: ```html -

Favorite breads

+

Favorite frameworks

``` -### `js` pages +### `ts`/`js` pages -A `js` page looks like this: +A `ts`/`js` page looks like this: ```bash +src/page-name/page.ts +# or src/page-name/page.js ``` -- `js` pages consist of a named directory with a `page.js` inside of it, that exports a default function that returns the contents of the inner page. -- a `js` page needs to `export default` a function (async or sync) that accepts a variables argument and returns a string of the inner html of the page, or any other type that your layout can accept. -- A `js` page can export a `vars` object or function (async or sync) that takes highest variable precedence when rendering the page. `export vars` is similar to a `md` page's front matter. -- A `js` page receives the standard `top-bun` [Variables](#variables) set. -- There is no built in handlebars support in `js` pages, however you are free to use any template library that you can import. -- `js` pages are run in a Node.js context only. +- `js`/`ts` pages consist of a named directory with a `page.js` or `page.ts` inside of it, that exports a default function that returns the contents of the inner page. +- a `js`/`ts` page needs to `export default` a function (async or sync) that accepts a variables argument and returns a string of the inner html of the page, or any other type that your layout can accept. +- You can specify the return type using `PageFunction` where `T` is the variables type and `U` is the return type (defaults to `any`). +- A `js`/`ts` page can export a `vars` object or function (async or sync) that takes highest variable precedence when rendering the page. `export vars` is similar to a `md` page's front matter. +- A `js`/`ts` page receives the standard `domstack` [Variables](#variables) set. +- There is no built in handlebars support in `js`/`ts` pages, however you are free to use any template library that you can import. +- `js`/`ts` pages are run in a Node.js context only. -An example `js` page: +An example TypeScript page: -```js -export default async ({ +```typescript +import type { PageFunction } from '@domstack/static' + +export const vars = { + favoriteCookie: 'Chocolate Chip with Sea Salt' +} + +export default const page: PageFunction = async ({ vars }) => { return /* html */`
@@ -232,35 +256,28 @@ export default async ({
` } -export const vars = { - favoriteCookie: 'Chocolate Chip with Sea Salt' -} ``` -It is it's recommended to use some level of template processing over raw string templates so that html is well formed and you default escape variable values. Here is a more realistic `js` example that uses [`uhtml`](https://github.com/WebReflection/uhtml) and [types-in-js](https://github.com/voxpelli/types-in-js) and `top-bun` page introspection. +It is recommended to use some level of template processing over raw string templates so that HTML is well-formed and variable values are properly escaped. Here is a more realistic TypeScript example that uses [`preact`](https://preactjs.com/) with [`htm`](https://github.com/developit/htm) and `domstack` page introspection. -```js -// @ts-ignore -import { html } from 'uhtml-isomorphic' +```typescript +import { html } from 'htm/preact' import { dirname, basename } from 'node:path' +import type { PageFunction } from '@domstack/static' -/** - * @template T - * @typedef {import('top-bun').LayoutFunction} LayoutFunction - */ - -/** - * @type {LayoutFunction<{ - * favoriteCake: string - * }>} - */ -export default async function blogIndex ({ - vars: { - favoriteCake - }, +type BlogVars = { + favoriteCake: string +} + +export const vars = { + favoriteCake: 'Chocolate Cloud Cake' +} + +export default const blogIndex: PageFunction = async ({ + vars: { favoriteCake }, pages -}) { +}) => { const yearPages = pages.filter(page => dirname(page.pageInfo.path) === 'blog') return html`

I love ${favoriteCake}!!

@@ -269,10 +286,6 @@ export default async function blogIndex ({
` } - -export const vars = { - favoriteCake: 'Chocolate Cloud Cake' -} ``` ### Page Styles @@ -301,17 +314,17 @@ An example of a page `style.css` file: ### Page JS Bundles -You can create a `client.js` file in any page folder. +You can create a `client.ts` or `client.js` file in any page folder. Page bundles are client side JS bundles that are loaded on that one page only. -You can import common code and modules from relative paths, or `npm` modules. -The `client.js` page bundles are bundle-split with every other client-side js entry-point, so importing common chunks of code are loaded in a maximally efficient way. +You can import common code and modules from relative paths, or `npm` modules out of `node_modules`. +The `client.js` page bundles are bundle-split with every other client-side js/ts entry-point, so importing common chunks of code are loaded in a maximally efficient way. Page bundles are run in a browser context only, however they can share carefully crafted code that also runs in a Node.js or layout context. -`js` page bundles are bundled using [`esbuild`][esbuild]. +`ts`/`js` page bundles are bundled using [`esbuild`][esbuild]. An example of a page `client.js` file: -```js -/* /some-page/client.js */ +```typescript +/* /some-page/client.ts */ import { funnyLibrary } from 'funny-library' import { someHelper } from '../helpers/foo.js' @@ -321,13 +334,14 @@ await funnyLibrary() #### .tsx/.jsx -Client bundles support .jsx and .tsx. They default to preact, so if you want mainlain recat, customize your esbuild settings to load that instead. +Client bundles support .jsx and .tsx. They default to preact, so if you want mainlain react, customize your esbuild settings to load that instead. +See the [react](./examples/react/) example for more details. ### Page variable files -Each page can also have a `page.vars.js` file that exports a `default` function or object that contains page specific variables. +Each page can also have a `page.vars.ts` or `page.vars.js` file that exports a `default` sync/async function or object that contains page specific variables. -```js +```typescript // export an object export default { my: 'vars' @@ -344,11 +358,11 @@ export default async () => { } ``` -Page variable files have higher precedent than `global.vars.js` variables, but lower precedent than frontmatter or `vars` page exports. +Page variable files have higher precedent than `global.vars.ts` variables, but lower precedent than frontmatter or `vars` ts/js page exports. ### Draft pages -If you add a `.draft.{md,html,js}` to any of the page types, the page is considered a draft page. +If you add a `.draft.{md,html,ts,js}` to any of the page types, the page is considered a draft page. Draft pages are not built by default. If you pass the `--drafts` flag when building or watching, the draft pages will be built. When draft pages are omitted, they are completely ignored. @@ -359,6 +373,50 @@ It is a good idea to display something indicating the page is a draft in your te Any static assets near draft pages will still be copied because static assets are processed in parallel from page generation (to keep things fast). If you have an idea on how to relate static assets to a draft page for omission, please open a discussion issue. +Draft pages let you work on pages before they are ready and easily omit them from a build when deploying pages that are ready. + +## Web Workers + +You can easily write [web workers](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers) for a page by adding a file called `${name}.worker.ts` or `${name}.worker.js` where `name` becomes the name of the worker filename in the `workers.json` file. +DOMStack will build these similarly to page `client.ts` bundles, and will even bundle split their contents with the rest of your site. + +``` +page-directory/ + β”œβ”€β”€ page.js + β”œβ”€β”€ client.js + β”œβ”€β”€ counter.worker.js # Worker with counter functionality + └── data.worker.js # Worker for data processing +``` + +To use a woker, load in a `./workers.json` file that is generated along with the worker bundle to get the final name of the worker entrypoint and then create a worker with that filename. + +```typescript +// First, fetch the workers.json to get worker paths in your client.ts +async function initializeWorkers() { + const response = await fetch('./workers.json'); + const workersData = await response.json(); + + // Initialize workers with the correct hashed filenames + const counterWorker = new Worker( + new URL(`./${workersData.counter}`, import.meta.url), + { type: 'module' } + ); + + // Use the worker + counterWorker.postMessage({ action: 'increment' }); + + counterWorker.onmessage = (e) => { + console.log(e.data); + }; + + return counterWorker; +} + +const worker = await initializeWorkers(); +``` + +See the [Web Workers Example](https://github.com/domstack/domstack/tree/master/examples/worker-example) for a complete implementation. + ## Layouts Layouts are "outer page templates" that pages get rendered into. @@ -367,6 +425,11 @@ You can define as many as you want, and they can live anywhere in the `src` dire Layouts are named `${layout-name}.layout.js` where `${layout-name}` becomes the name of the layout. Layouts should have a unique name, and layouts with duplicate name will result in a build error. +Layouts can be typed using `LayoutFunction` where: +- `T` is the variables type +- `U` is the type of content received from pages (defaults to `any`) +- `V` is the layout's return type (defaults to `string` for HTML output) + Example layout file names: ```bash @@ -374,7 +437,7 @@ src/layouts/root.layout.js # this layout is references as 'root' src/other-layouts/article.layout.js # this layout is references as 'article' ``` -At a minimum, your site requires a `root` layout (a file named `root.layout.js`), though `top-bun` ships a default `root` layout so defining one in your `src` directory is optional, though recommended. +At a minimum, your site requires a `root` layout (a file named `root.layout.js`), though `domstack` ships a default `root` layout so defining one in your `src` directory is optional, though recommended. All pages have a `layout` variable that defaults to `root`. If you set the `layout` variable to a different name, pages will build with a layout matching the name you set to that variable. @@ -393,7 +456,7 @@ A page referencing a layout name that doesn't have a matching layout file will r ### The default `root.layout.js` -A layout is a js file that `export default`'s an async or sync function that implements an outer-wrapper html template that will house the inner content from the page (`children`) being rendered. Think of the bread in a sandwich. That's a layout. πŸ₯ͺ +A layout is a `ts`/`js` file that `export default`'s an async or sync function that implements an outer-wrapper html template that will house the inner content from the page (`children`) being rendered. Think of the frame around a picture. That's a layout. πŸ–ΌοΈ It is always passed a single object argument with the following entries: @@ -404,104 +467,108 @@ It is always passed a single object argument with the following entries: - `pages`: An array of page data that you can use to generate index pages with, or any other page-introspection based content that you desire. - `page`: An object with metadata and other facts about the current page being rendered into the template. This will also be found somewhere in the `pages` array. -The default `root.layout.js` is featured below, and is implemented with [`uhtml`][uhtml], though it could just be done with a template literal or any other template system. - -`root.layout.js` can live anywhere in the `src` directory. - -```js -// @ts-ignore -import { html, render } from 'uhtml-isomorphic' - -/** - * @template {Record} T - * @typedef {import('top-bun').LayoutFunction} LayoutFunction - */ - -/** - * Build all of the bundles using esbuild. - * - * @type {LayoutFunction<{ - * title: string, - * siteName: string, - * defaultStyle: boolean - * }>} - */ -export default function defaultRootLayout ({ +The default `root.layout.ts` is featured below, and is implemented with [`preact`](https://preactjs.com/) and [`htm`](https://github.com/developit/htm), though it could just be done with a template literal or any other template system that runs in Node.js. + +`root.layout.ts` can live anywhere in the `src` directory. + +```typescript +import { html } from 'htm/preact' +import { render } from 'preact-render-to-string' +import type { LayoutFunction } from '@domstack/static' + +type RootLayoutVars = { + title: string, + siteName: string, + defaultStyle: boolean, + basePath?: string +} + +export default const defaultRootLayout: LayoutFunction = ({ vars: { title, - siteName = 'TopBun' + siteName = 'Domstack', + basePath, /* defaultStyle = true Set this to false in global or page vars to disable the default style in the default layout */ }, scripts, styles, - children - /* pages */ - /* page */ -}) { - return render(String, html` + children, + pages, + page, +}) => { + return /* html */` - - - ${title ? `${title}` : ''}${title && siteName ? ' | ' : ''}${siteName} - - ${scripts - ? scripts.map(script => html``) - : null} - ${styles - ? styles.map(style => html``) - : null} - - -
- ${typeof children === 'string' ? html([children]) : children /* Support both uhtml and string children. Optional. */} -
- + ${render(html` + + + ${title ? `${title}` : ''}${title && siteName ? ' | ' : ''}${siteName} + + ${scripts + ? scripts.map(script => html``)} - - - ${children} - - - `) -} +// Page returns VDOM +const page: PageFunction<{title: string}, VDOMNode> = ({ vars }) => ({ + type: 'h1', + props: {}, + children: [vars.title] +}) -export default layout +// Layout accepts VDOM, returns HTML string +const layout: LayoutFunction<{site: string}, VDOMNode, string> = ({ children }) => { + const html = renderVDOM(children) // Convert VDOM to HTML + return `${html}` +} ``` ## Design Goals @@ -1132,8 +1246,8 @@ export default layout - Library agnostic. Strings are the interchange format. - Pages are shallow apps. New page, new blank canvas. - Just a program. `js` pages and layouts are just JavaScript programs. This provides an escape hatch to do anything. Use any template language want, but probably just use tagged template literals. -- Steps remain orthogonal. Static file copying, css and js bundling, are mere optimizations on top of the `src` folder. The `src` folder should essentially run in the browser. Each step in a `top-bun` build should work independent of the others. This allows for maximal parallelism when building. -- Standardized entrypoints. Every page in a `top-bun` site has a natural and obvious entrypoint. There is no magic redirection to learn about. +- Steps remain orthogonal. Static file copying, css and js bundling, are mere optimizations on top of the `src` folder. The `src` folder should essentially run in the browser. Each step in a `domstack` build should work independent of the others. This allows for maximal parallelism when building. +- Standardized entrypoints. Every page in a `domstack` site has a natural and obvious entrypoint. There is no magic redirection to learn about. - Pages build into `index.html` files inside of named directories. This allows for naturally colocated assets next to the page, pretty URLs and full support for relative URLs. - No parallel directory structures. You should never be forced to have two directories with identical layouts to put files next to each other. Everything should be colocatable. - Markdown entrypoints are named README.md. This allows for the `src` folder to be fully navigable in GitHub and other git repo hosting providing a natural hosted CMS UI. @@ -1146,31 +1260,191 @@ export default layout ## FAQ -Top-**Bun**? Like the JS runtime? +Why DOMStack? -: No, like the bakery from Wallace and Gromit in ["A Matter of Loaf and Death"](https://www.youtube.com/watch?v=zXBmZLmfQZ4s) +: DOMStack is named after the DOM (Document Object Model) and the concept of stacking technologies together to build websites. It represents the layering of HTML, CSS, and JavaScript in a cohesive build system. -How does `top-bun` relate to [`sitedown`](https://ghub.io/sitedown) +How does `domstack` relate to [`sitedown`](https://ghub.io/sitedown) -: `top-bun` used to be called `siteup` which is sort of like "markup", which is related to "markdown", which inspired the project `sitedown` to which `top-bun` is a spiritual off-shot of. Put a folder of web documents in your `top-bun` oven, and bake a website. +: `domstack` used to be called `siteup` which is sort of like "markup", which is related to "markdown", which inspired the project `sitedown` to which `domstack` is a spiritual off-shoot of. Put a folder of web documents in your `domstack` build system, and generate a website. ## Examples -Look at [examples](./examples/) and `top-bun` [dependents](https://github.com/bcomnes/top-bun/network/dependents) for some examples how `top-bun` can work. +Look at [examples](./examples/) and `domstack` [dependents](https://github.com/bcomnes/domstack/network/dependents) for some examples how `domstack` can work. ## Implementation -`top-bun` bundles the best tools for every technology in the stack: +`domstack` bundles the best tools for every technology in the stack: - `js` and `css` is bundled with [`esbuild`](https://github.com/evanw/esbuild). - `md` is processed with [markdown-it](https://github.com/markdown-it/markdown-it). - static files are processed with [cpx2](https://github.com/bcomnes/cpx2). +- `ts` support via native typestripping in Node.js and esbuild. +- `jsx/tsx` support via esbuild. These tools are treated as implementation details, but they may be exposed more in the future. The idea is that they can be swapped out for better tools in the future if they don't make it. +### Build Process Flow + +The following diagram illustrates the DomStack build process: + +``` + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ START β”‚ + β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β–Ό + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ identifyPages() β”‚ + β”‚ β”‚ + β”‚ β€’ Find pages β”‚ + β”‚ β€’ Find layouts β”‚ + β”‚ β€’ Find templates β”‚ + β”‚ β€’ Find globals β”‚ + β”‚ β€’ Find settings β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β”‚ + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ β”‚ β”‚ + β–Ό β–Ό β–Ό +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ buildEsbuild() β”‚ β”‚ buildStatic() β”‚ β”‚ buildCopy() β”‚ +β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ +β”‚ β€’ Bundle JS/CSS β”‚ β”‚ β€’ Copy static β”‚ β”‚ β€’ Copy extra β”‚ +β”‚ β€’ Generate β”‚ β”‚ files β”‚ β”‚ directories β”‚ +β”‚ metafile β”‚ β”‚ (if enabled) β”‚ β”‚ from opts β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ β”‚ β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β–Ό + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ buildPages() β”‚ + β”‚ β”‚ + β”‚ β€’ Process HTML β”‚ + β”‚ β€’ Process MD β”‚ + β”‚ β€’ Process JS β”‚ + β”‚ β€’ Apply layouts β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β–Ό + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ Return Results β”‚ + β”‚ β”‚ + β”‚ β€’ siteData β”‚ + β”‚ β€’ esbuildResults β”‚ + β”‚ β€’ staticResults β”‚ + β”‚ β€’ copyResults β”‚ + β”‚ β€’ pageResults β”‚ + β”‚ β€’ warnings β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +The build process follows these key steps: + +1. **Page identification** - Scans the source directory to identify all pages, layouts, templates, and global assets +2. **Destination preparation** - Ensures the destination directory is ready for the build output +3. **Parallel asset processing** - Three operations run concurrently: + - JavaScript and CSS bundling via esbuild + - Static file copying (when enabled) + - Additional directory copying (from `--copy` options) +4. **Page building** - Processes all pages, applying layouts and generating final HTML + +This architecture allows for efficient parallel processing of independent tasks while maintaining the correct build order dependencies. + +#### buildPages() Detail + +The `buildPages()` step processes pages in parallel with a concurrency limit: + +``` + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ buildPages() β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ Resolve Once: β”‚ + β”‚ β€’ Global vars β”‚ + β”‚ β€’ All layouts β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ Parallel Page Queue β”‚ + β”‚(Concurrency: min(CPUs, 24))β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ β”‚ β”‚ + β–Ό β–Ό β–Ό +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ MD Page Task β”‚ β”‚ HTML Page Task β”‚ β”‚ JS Page Task β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚1. Read .md β”‚ β”‚ β”‚ β”‚1. Read .htmlβ”‚ β”‚ β”‚ β”‚1. Import .jsβ”‚ β”‚ +β”‚ β”‚ file β”‚ β”‚ β”‚ β”‚ file β”‚ β”‚ β”‚ β”‚ module β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β–Ό β”‚ β”‚ β–Ό β”‚ β”‚ β–Ό β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚2. Extract β”‚ β”‚ β”‚ β”‚2. Variable β”‚ β”‚ β”‚ β”‚2. Variable β”‚ β”‚ +β”‚ β”‚ frontmatter β”‚ β”‚ β”‚ β”‚ Resolution β”‚ β”‚ β”‚ β”‚ Resolution β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β–Ό β”‚ β”‚ β–Ό β”‚ β”‚ β–Ό β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Frontmatter β”‚ β”‚ β”‚ β”‚page.vars.js β”‚ β”‚ β”‚ β”‚ Exported β”‚ β”‚ +β”‚ β”‚ vars β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ vars β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β–Ό β”‚ β”‚ β–Ό β”‚ β”‚ β–Ό β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚page.vars.js β”‚ β”‚ β”‚ β”‚ postVars β”‚ β”‚ β”‚ β”‚page.vars.js β”‚ β”‚ +β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β–Ό β”‚ β”‚ β–Ό β”‚ β”‚ β–Ό β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ postVars β”‚ β”‚ β”‚ β”‚3. Handlebarsβ”‚ β”‚ β”‚ β”‚ postVars β”‚ β”‚ +β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ (if enabled)β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β–Ό β”‚ β”‚ β–Ό β”‚ β”‚ β–Ό β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚3. Render MD β”‚ β”‚ β”‚ β”‚4. Render β”‚ β”‚ β”‚ β”‚3. Execute β”‚ β”‚ +β”‚ β”‚ to HTML β”‚ β”‚ β”‚ β”‚ with layoutβ”‚ β”‚ β”‚ β”‚ page func β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β–Ό β”‚ β”‚ β–Ό β”‚ β”‚ β–Ό β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚4. Extract β”‚ β”‚ β”‚ β”‚5. Write HTMLβ”‚ β”‚ β”‚ β”‚4. Render β”‚ β”‚ +β”‚ β”‚ title (h1) β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ with layoutβ”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β–Ό β”‚ β”‚ β”‚ β”‚ β–Ό β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚5. Render β”‚ β”‚ β”‚ β”‚ β”‚ β”‚5. Write HTMLβ”‚ β”‚ +β”‚ β”‚ with layoutβ”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β–Ό β”‚ β”‚ β”‚ β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚6. Write HTMLβ”‚ β”‚ β”‚ β”‚ β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β”‚ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ β”‚ β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β–Ό + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ Complete when β”‚ + β”‚ all pages done β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +Variable Resolution Layers: +- **Global vars** - Site-wide variables from `global.vars.js` (resolved once) +- **Layout vars** - Layout-specific variables from layout functions (resolved once) +- **Page-specific vars** vary by type: + - **MD pages**: frontmatter β†’ page.vars.js β†’ postVars + - **HTML pages**: page.vars.js β†’ postVars + - **JS pages**: exported vars β†’ page.vars.js β†’ postVars +- **postVars** - Post-processing function that can modify variables based on all resolved data + ## Roadmap -`top-bun` works and has a rudimentary watch command, but hasn't been battle tested yet. +`domstack` works and has a rudimentary watch command, but hasn't been battle tested yet. If you end up trying it out, please open any issues or ideas that you have, and feel free to share what you build. Some notable features are included below, see the [roadmap](https://github.com/users/bcomnes/projects/3/) for a more in depth view of whats planned. @@ -1199,7 +1473,7 @@ Some notable features are included below, see the [roadmap](https://github.com/u - [x] Handlebars template support in `md` and `html` - [x] `mjs` and `cjs` file extension support - [x] Improved watch log output -- [x] Docs website built with `top-bin`: https://top-bun.org +- [x] Docs website built with `domstack`: https://domstack.net - [x] `--eject` cli flag - [x] Global assets can live anywhere - [x] Built in browsersync dev server @@ -1208,10 +1482,17 @@ Some notable features are included below, see the [roadmap](https://github.com/u - [x] Copy folders - [x] Full Typescript support via native type stripping - [x] JSX+TSX support in client bundles +- [x] Rename to domstack +- [x] markdown-it.settings.ts support +- [x] page-worker.worker.ts page worker support - ...[See roadmap](https://github.com/users/bcomnes/projects/3/) ## History +DOMStack started its life as `top-bun` in 2023, named after the bakery from Wallace and Gromit. The project was created to provide a simple, fast, and flexible static site generator that could handle modern web development needs while staying true to web standards. + +The project was renamed to DOMStack in version 11 to better reflect its purpose and avoid confusion with the Bun JavaScript runtime. The name DOMStack represents the layering of web technologies (HTML, CSS, JavaScript). +It is also an homage to [substack](https://substack.net) as well as a play on the productname that stole his name. ## Links @@ -1223,7 +1504,8 @@ Some notable features are included below, see the [roadmap](https://github.com/u [MIT](LICENSE) -[uhtml]: https://github.com/WebReflection/uhtml +[preact]: https://preactjs.com/ +[htm]: https://github.com/developit/htm [hb]: https://handlebarsjs.com [esbuild]: http://esbuild.github.io -[neocities-img]: https://img.shields.io/website/https/top-bun.neocities.org?label=neocities&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAGhlWElmTU0AKgAAAAgABAEGAAMAAAABAAIAAAESAAMAAAABAAEAAAEoAAMAAAABAAIAAIdpAAQAAAABAAAAPgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAIKADAAQAAAABAAAAIAAAAAAueefIAAACC2lUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNS40LjAiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyI+CiAgICAgICAgIDx0aWZmOk9yaWVudGF0aW9uPjE8L3RpZmY6T3JpZW50YXRpb24+CiAgICAgICAgIDx0aWZmOlBob3RvbWV0cmljSW50ZXJwcmV0YXRpb24+MjwvdGlmZjpQaG90b21ldHJpY0ludGVycHJldGF0aW9uPgogICAgICAgICA8dGlmZjpSZXNvbHV0aW9uVW5pdD4yPC90aWZmOlJlc29sdXRpb25Vbml0PgogICAgICAgICA8dGlmZjpDb21wcmVzc2lvbj4xPC90aWZmOkNvbXByZXNzaW9uPgogICAgICA8L3JkZjpEZXNjcmlwdGlvbj4KICAgPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4Kpl32MAAABzBJREFUWAnFVwtwnFUV/v5//31ks5tsE9I8moS0iWETSNKUVpBKDKFQxtrCUIpacHQEGYk16FQHaZ3ajjqjOGWqOKUyMCl2xFoKhQJDBQftpOnAmDZoOyRNjCS1SdO8H5vXPv7rd/7NZvIipQjjmfn23Me555x77rnnv6sppTT8H0n/tG1rmlZIVBG+eW1JBD4t0GA8cYZQcS7ncXL7bFuYPfBJ9mlwtxg3bJoSTvx0tn7LAU48IJNE3GyBj9unrlJC2XRt4vGvLFGGrkXYDxEl03WyDyfRRoiHrxOfiBPU85bovPezi5pHnlmhHq5IsaLAXHhltgPXi+A0VE8X+Dht6lov+uw2rf/8nmIlDjQ+fp1yO/SYnaKYXoOC5QSu8trgddnND7rHv0EvOymwTcbnI867OZ5PLCOKiUIijQgS54nPE3hsfXog2WNY2Z+V5MDXVifjd3/ths/jquL0QyIj9EdC3V6UoLr25KurU73D0ieOEIniKbkc063EduLPRDcR2828/DOpzrbBp0ut3UsEBMe3X2PJuhw2sWHplgjkEViyyBGM93gcf3kkxVP2hNZ1sWfoLg7/jbttJC8jMgiLHHYj4EuIb81I9gQLM92O0iyH+9pUlZSdGDHCJjA0biI/zZ3NxIstsfjKpfFYmROHutYxDwduIo6JAxI6LIq3cSmtpCSg9jF3UsXuix2tHb3L7YZevHRx/FBZvrNzTaEnLTfFQHaSna6CSrghjbVMJzRbtC1KFqC1xT5xAFdnZdxPMcsBS1wpDLHhEoWpiXbj3R8mZ1zoT0Caz677PE4fdDunJYIzd2UtvoKfWwq9+PnRiwgMDd5RX/PGVRIBixLjbNNKpQaP1wO/NzYb47ON0yEzAhUJQjOYJhKFy9DybDcyk+y40DeSdOz5J+5h7CBAxDQdl1k7d5rGHWW74Cz/GdM0gQGSWrMwxTl0VBRSlnSmoblMjIel0zkgN+gKSDFl7G7YMm+C4d8Ix4pvQ4XGPpKC8snQ/vPfvYXiwPuy6tylK3RAFokTpuU/NF8u08dAzbkA/nCylyVeBOanJawJQpcGxjMkB04QdzS0j5ujQVNntZK5BSkwYaIvEEZmQgjm4AeweTOguRah4ZKJdbubeZwKaYl23HptNNQxZeMhE0fqBrDthXZraHTCtKydlF73cFhv67l8FGRnm55sQcGjZ/GTI50IN75kKdMTsywnzMmtj4XmhuDRP13Ag8+2YnA0GrVgWDFmwFld10dN03TXNg2jIMNlKfywn//0BXGyKWBNv904isj5GqjhdmjeJSjMzUDttmUYChpYnS+1ZiY9+IUUrCvxIS/Nic/tbAiOBBkBltoeGn9PRA+c6Jm5Yp5edrIDlWsWw09Ht23IgBrvQ+i9Zy1JcaKE1+zmZTp0c240i7LiwJIPXdPACMnmw9ZriOV2Czu/ES3v7izAdZlx0rw8SQLy/jtu/AEmstfhTP3fcUPRUkS6ziB0eh/M/hZovCkx6ugP4ccvtuO1+gGMMI9IfbGM289j6JSRY/8YEIbmSxM4enoA+2t60MuEm0NyA2xOuL5UDaPgXjQ0NODmW27DgVeOw5a3Dq6Nh2DLWcMnyOjU0v6RME63jloJOjnYZ0VAOozCb8kq4506fG4bOgZCU1fphe/m4osliZNrokwFA3Cs/A7sq6qsgU0bN+LwS9GE9Pv9cLvd8Ofn4Zl7wlC9zXRWSnmUnqvpDVY+1yZ38WgsAjKzX34kNF1DYeQtduLOFT4ceSRvjnFEQrClFMK2/FsIBALYu3evZfw2mxe/Yj1obGzExY4OfPmr98Hu38QCOSGqp+j3tT3RLAZek0SwiMlYxyjIFu6WgX3fzMGNufKonYd49kNGOspLrkdTUxMikQhS4r34tZGDZObEHkccdu3chQ0bNiDc/OoMBQdqe/HOv0aSONhBHJ5yYFLqR+QVoYjyPcT7+mJVLsZ5n988O4gTvHrfX5uKMimjzOJEewhbt25FZ2cnWlpaUF1djdcTR1A6NoH24BiC/E4IKSaiyMuX9OVT/Xh4f5tkn0R+Czc9MOdZzokHLGmuiLPr8qqViqKchqYObcmNvnCeLlajz9+uzGCAOpTiNVabN2+25ETWMAxVV1enzPEBS254X5GqWpsmHwqRkfP4OpdF8y/WmM4psJ3HIVuYMr7n/qwZz6uRp/xq4uQvuSxK4sTBgwfVjh07VH19veInWnW9+j11uDJdlebEj0zqaiC/gSum/gxN3QJOzCA6sIIDv2D0KlhdrWS9Jt2F9aU+FKQ7eeYKi3kaSaur4C29j98lE4P9XWg59z5OnXgDb7/1pvlOY7c5EbYKjug+RFTSeJ90pmi6N/O1KbiKeIqOtJFPhXl6m87OGae8hPoU8SSxaj7dMvahEeCiGUQjcm/LiHLCT8hbUsaGCKk2wqWWNxHykD1LA13kC9JHdmBBLf/D5H8By9d+IkwR5NMAAAAASUVORK5CYII= +[neocities-img]: https://img.shields.io/website/https/domstack.neocities.org?label=neocities&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAGhlWElmTU0AKgAAAAgABAEGAAMAAAABAAIAAAESAAMAAAABAAEAAAEoAAMAAAABAAIAAIdpAAQAAAABAAAAPgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAIKADAAQAAAABAAAAIAAAAAAueefIAAACC2lUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNS40LjAiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyI+CiAgICAgICAgIDx0aWZmOk9yaWVudGF0aW9uPjE8L3RpZmY6T3JpZW50YXRpb24+CiAgICAgICAgIDx0aWZmOlBob3RvbWV0cmljSW50ZXJwcmV0YXRpb24+MjwvdGlmZjpQaG90b21ldHJpY0ludGVycHJldGF0aW9uPgogICAgICAgICA8dGlmZjpSZXNvbHV0aW9uVW5pdD4yPC90aWZmOlJlc29sdXRpb25Vbml0PgogICAgICAgICA8dGlmZjpDb21wcmVzc2lvbj4xPC90aWZmOkNvbXByZXNzaW9uPgogICAgICA8L3JkZjpEZXNjcmlwdGlvbj4KICAgPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4Kpl32MAAABzBJREFUWAnFVwtwnFUV/v5//31ks5tsE9I8moS0iWETSNKUVpBKDKFQxtrCUIpacHQEGYk16FQHaZ3ajjqjOGWqOKUyMCl2xFoKhQJDBQftpOnAmDZoOyRNjCS1SdO8H5vXPv7rd/7NZvIipQjjmfn23Me555x77rnnv6sppTT8H0n/tG1rmlZIVBG+eW1JBD4t0GA8cYZQcS7ncXL7bFuYPfBJ9mlwtxg3bJoSTvx0tn7LAU48IJNE3GyBj9unrlJC2XRt4vGvLFGGrkXYDxEl03WyDyfRRoiHrxOfiBPU85bovPezi5pHnlmhHq5IsaLAXHhltgPXi+A0VE8X+Dht6lov+uw2rf/8nmIlDjQ+fp1yO/SYnaKYXoOC5QSu8trgddnND7rHv0EvOymwTcbnI867OZ5PLCOKiUIijQgS54nPE3hsfXog2WNY2Z+V5MDXVifjd3/ths/jquL0QyIj9EdC3V6UoLr25KurU73D0ieOEIniKbkc063EduLPRDcR2828/DOpzrbBp0ut3UsEBMe3X2PJuhw2sWHplgjkEViyyBGM93gcf3kkxVP2hNZ1sWfoLg7/jbttJC8jMgiLHHYj4EuIb81I9gQLM92O0iyH+9pUlZSdGDHCJjA0biI/zZ3NxIstsfjKpfFYmROHutYxDwduIo6JAxI6LIq3cSmtpCSg9jF3UsXuix2tHb3L7YZevHRx/FBZvrNzTaEnLTfFQHaSna6CSrghjbVMJzRbtC1KFqC1xT5xAFdnZdxPMcsBS1wpDLHhEoWpiXbj3R8mZ1zoT0Caz677PE4fdDunJYIzd2UtvoKfWwq9+PnRiwgMDd5RX/PGVRIBixLjbNNKpQaP1wO/NzYb47ON0yEzAhUJQjOYJhKFy9DybDcyk+y40DeSdOz5J+5h7CBAxDQdl1k7d5rGHWW74Cz/GdM0gQGSWrMwxTl0VBRSlnSmoblMjIel0zkgN+gKSDFl7G7YMm+C4d8Ix4pvQ4XGPpKC8snQ/vPfvYXiwPuy6tylK3RAFokTpuU/NF8u08dAzbkA/nCylyVeBOanJawJQpcGxjMkB04QdzS0j5ujQVNntZK5BSkwYaIvEEZmQgjm4AeweTOguRah4ZKJdbubeZwKaYl23HptNNQxZeMhE0fqBrDthXZraHTCtKydlF73cFhv67l8FGRnm55sQcGjZ/GTI50IN75kKdMTsywnzMmtj4XmhuDRP13Ag8+2YnA0GrVgWDFmwFld10dN03TXNg2jIMNlKfywn//0BXGyKWBNv904isj5GqjhdmjeJSjMzUDttmUYChpYnS+1ZiY9+IUUrCvxIS/Nic/tbAiOBBkBltoeGn9PRA+c6Jm5Yp5edrIDlWsWw09Ht23IgBrvQ+i9Zy1JcaKE1+zmZTp0c240i7LiwJIPXdPACMnmw9ZriOV2Czu/ES3v7izAdZlx0rw8SQLy/jtu/AEmstfhTP3fcUPRUkS6ziB0eh/M/hZovCkx6ugP4ccvtuO1+gGMMI9IfbGM289j6JSRY/8YEIbmSxM4enoA+2t60MuEm0NyA2xOuL5UDaPgXjQ0NODmW27DgVeOw5a3Dq6Nh2DLWcMnyOjU0v6RME63jloJOjnYZ0VAOozCb8kq4506fG4bOgZCU1fphe/m4osliZNrokwFA3Cs/A7sq6qsgU0bN+LwS9GE9Pv9cLvd8Ofn4Zl7wlC9zXRWSnmUnqvpDVY+1yZ38WgsAjKzX34kNF1DYeQtduLOFT4ceSRvjnFEQrClFMK2/FsIBALYu3evZfw2mxe/Yj1obGzExY4OfPmr98Hu38QCOSGqp+j3tT3RLAZek0SwiMlYxyjIFu6WgX3fzMGNufKonYd49kNGOspLrkdTUxMikQhS4r34tZGDZObEHkccdu3chQ0bNiDc/OoMBQdqe/HOv0aSONhBHJ5yYFLqR+QVoYjyPcT7+mJVLsZ5n988O4gTvHrfX5uKMimjzOJEewhbt25FZ2cnWlpaUF1djdcTR1A6NoH24BiC/E4IKSaiyMuX9OVT/Xh4f5tkn0R+Czc9MOdZzokHLGmuiLPr8qqViqKchqYObcmNvnCeLlajz9+uzGCAOpTiNVabN2+25ETWMAxVV1enzPEBS254X5GqWpsmHwqRkfP4OpdF8y/WmM4psJ3HIVuYMr7n/qwZz6uRp/xq4uQvuSxK4sTBgwfVjh07VH19veInWnW9+j11uDJdlebEj0zqaiC/gSum/gxN3QJOzCA6sIIDv2D0KlhdrWS9Jt2F9aU+FKQ7eeYKi3kaSaur4C29j98lE4P9XWg59z5OnXgDb7/1pvlOY7c5EbYKjug+RFTSeJ90pmi6N/O1KbiKeIqOtJFPhXl6m87OGae8hPoU8SSxaj7dMvahEeCiGUQjcm/LiHLCT8hbUsaGCKk2wqWWNxHykD1LA13kC9JHdmBBLf/D5H8By9d+IkwR5NMAAAAASUVORK5CYII= diff --git a/bin.js b/bin.js index c92d18f..9d8cbf6 100755 --- a/bin.js +++ b/bin.js @@ -1,13 +1,17 @@ #!/usr/bin/env node -// @ts-ignore +/** + * @import {DomStackOpts as DomStackOpts} from './lib/builder.js' + * @import { ArgscloptsParseArgsOptionsConfig } from 'argsclopts' + */ + import { readFile } from 'node:fs/promises' import { resolve, join, relative } from 'node:path' import { parseArgs } from 'node:util' import { printHelpText } from 'argsclopts' import readline from 'node:readline' import process from 'process' -// @ts-ignore +// @ts-expect-error import tree from 'pretty-tree' import { inspect } from 'util' import { packageDirectory } from 'package-directory' @@ -15,16 +19,11 @@ import { readPackage } from 'read-pkg' import { addPackageDependencies } from 'write-package' import { copyFile } from './lib/helpers/copy-file.js' -import { TopBun } from './index.js' -import { TopBunAggregateError } from './lib/helpers/top-bun-aggregate-error.js' +import { DomStack } from './index.js' +import { DomStackAggregateError } from './lib/helpers/dom-stack-aggregate-error.js' import { generateTreeData } from './lib/helpers/generate-tree-data.js' import { askYesNo } from './lib/helpers/cli-prompt.js' -/** - * @import {TopBunOpts} from './lib/builder.js' - * @import { ArgscloptsParseArgsOptionsConfig } from 'argsclopts' - */ - const __dirname = import.meta.dirname async function getPkg () { @@ -69,7 +68,7 @@ const options = { eject: { type: 'boolean', short: 'e', - help: 'eject the top bun default layout, style and client into the src flag directory', + help: 'eject the DOMStack default layout, style and client into the src flag directory', }, watch: { type: 'boolean', @@ -162,7 +161,7 @@ async function run () { } console.log(` -top-bun eject actions: +domstack eject actions: - Write ${join(relativeSrc, targetLayoutPath)} - Write ${join(relativeSrc, targetGlobalStylePath)} - Write ${join(relativeSrc, targetGlobalClientPath)} @@ -200,7 +199,7 @@ top-bun eject actions: process.exit(0) } - /** @type {TopBunOpts} */ + /** @type {DomStackOpts} */ const opts = {} if (argv['ignore']) opts.ignore = String(argv['ignore']).split(',') @@ -213,14 +212,14 @@ top-bun eject actions: opts.copy = copyPaths.map(p => resolve(cwd, p)) } - const topBun = new TopBun(src, dest, opts) + const domStack = new DomStack(src, dest, opts) process.once('SIGINT', quit) process.once('SIGTERM', quit) async function quit () { - if (topBun.watching) { - const results = await topBun.stopWatching() + if (domStack.watching) { + const results = await domStack.stopWatching() console.log(results) console.log('watching stopped') } @@ -230,7 +229,7 @@ top-bun eject actions: if (!argv['watch'] && !argv['watch-only']) { try { - const results = await topBun.build() + const results = await domStack.build() console.log(tree(generateTreeData(cwd, src, dest, results))) if (results?.warnings?.length > 0) { console.log( @@ -247,7 +246,7 @@ top-bun eject actions: console.log('\nBuild Success!\n\n') } catch (err) { if (!(err instanceof Error || err instanceof AggregateError)) throw new Error('Non-error thrown', { cause: err }) - if (err instanceof TopBunAggregateError) { + if (err instanceof DomStackAggregateError) { if (err?.results?.siteData?.pages) { console.log(tree(generateTreeData(cwd, src, dest, err.results))) } @@ -259,7 +258,7 @@ top-bun eject actions: process.exit(1) } } else { - const initialResults = await topBun.watch({ + const initialResults = await domStack.watch({ serve: !argv['watch-only'], }) console.log(tree(generateTreeData(cwd, src, dest, initialResults))) @@ -279,6 +278,6 @@ top-bun eject actions: } run().catch(err => { - console.error(new Error('Unhandled top-bun error', { cause: err })) + console.error(new Error('Unhandled domstack error', { cause: err })) process.exit(1) }) diff --git a/dependencygraph.svg b/dependencygraph.svg index cf4f222..7db9b83 100644 --- a/dependencygraph.svg +++ b/dependencygraph.svg @@ -1,1306 +1,1685 @@ - - - + + dependency-cruiser output - + -cluster_fs - -fs +cluster_.tap + +.tap -cluster_lib - -lib +cluster_.tap/report + +report -cluster_lib/build-esbuild - -build-esbuild +cluster_.tap/report/lcov-report + +lcov-report -cluster_lib/build-pages - -build-pages +cluster_fs + +fs -cluster_lib/build-pages/page-builders - -page-builders +cluster_lib + +lib -cluster_lib/build-pages/page-builders/html - -html +cluster_lib/build-copy + +build-copy -cluster_lib/build-pages/page-builders/js - -js +cluster_lib/build-esbuild + +build-esbuild -cluster_lib/build-pages/page-builders/md - -md +cluster_lib/build-pages + +build-pages -cluster_lib/build-static - -build-static +cluster_lib/build-pages/page-builders + +page-builders -cluster_lib/helpers - -helpers +cluster_lib/build-pages/page-builders/html + +html -cluster_test-cases - -test-cases +cluster_lib/build-pages/page-builders/js + +js -cluster_test-cases/build-errors - -build-errors +cluster_lib/build-pages/page-builders/md + +md -cluster_test-cases/build-errors/src - -src +cluster_lib/build-static + +build-static -cluster_test-cases/build-errors/src/another-broken-page - -another-broken-page +cluster_lib/helpers + +helpers -cluster_test-cases/conflict-pages - -conflict-pages +cluster_test-cases + +test-cases -cluster_test-cases/conflict-pages/src - -src +cluster_test-cases/build-errors + +build-errors -cluster_test-cases/default-layout - -default-layout +cluster_test-cases/build-errors/src + +src -cluster_test-cases/default-layout/public - -public +cluster_test-cases/build-errors/src/another-broken-page + +another-broken-page -cluster_test-cases/default-layout/public/top-bun-defaults - -top-bun-defaults +cluster_test-cases/conflict-pages + +conflict-pages -cluster_test-cases/default-layout/src - -src +cluster_test-cases/conflict-pages/src + +src -cluster_test-cases/general-features - -general-features +cluster_test-cases/default-layout + +default-layout -cluster_test-cases/general-features/public - -public +cluster_test-cases/default-layout/public + +public -cluster_test-cases/general-features/public/globals - -globals +cluster_test-cases/default-layout/public/dom-stack-defaults + +dom-stack-defaults -cluster_test-cases/general-features/public/html-page - -html-page +cluster_test-cases/default-layout/src + +src -cluster_test-cases/general-features/public/js-page - -js-page +cluster_test-cases/drafts + +drafts -cluster_test-cases/general-features/public/layouts - -layouts +cluster_test-cases/drafts/public + +public -cluster_test-cases/general-features/public/md-page - -md-page +cluster_test-cases/drafts/public/dom-stack-defaults + +dom-stack-defaults -cluster_test-cases/general-features/src - -src +cluster_test-cases/drafts/src + +src -cluster_test-cases/general-features/src/blog - -blog +cluster_test-cases/drafts/src/a-draft-js + +a-draft-js -cluster_test-cases/general-features/src/blog/2023 - -2023 +cluster_test-cases/general-features + +general-features -cluster_test-cases/general-features/src/blog/2023/the-first-blog-post-from-2023 - -the-first-blog-post-from-2023 +cluster_test-cases/general-features/copyfolder + +copyfolder -cluster_test-cases/general-features/src/globals - -globals +cluster_test-cases/general-features/copyfolder/oldsite + +oldsite -cluster_test-cases/general-features/src/handlebars-html - -handlebars-html +cluster_test-cases/general-features/public + +public -cluster_test-cases/general-features/src/handlebars-html/disabled-handlebars-html - -disabled-handlebars-html +cluster_test-cases/general-features/public/blog + +blog -cluster_test-cases/general-features/src/html-page - -html-page +cluster_test-cases/general-features/public/blog/2025 + +2025 -cluster_test-cases/general-features/src/js-page - -js-page +cluster_test-cases/general-features/public/blog/2025/a-blog-post-from-2025 + +a-blog-post-from-2025 -cluster_test-cases/general-features/src/js-page/js-no-async-export - -js-no-async-export +cluster_test-cases/general-features/public/globals + +globals -cluster_test-cases/general-features/src/js-page/js-no-style-client - -js-no-style-client +cluster_test-cases/general-features/public/html-page + +html-page -cluster_test-cases/general-features/src/layouts - -layouts +cluster_test-cases/general-features/public/js-page + +js-page -cluster_test-cases/general-features/src/libs - -libs +cluster_test-cases/general-features/public/layouts + +layouts -cluster_test-cases/general-features/src/md-page - -md-page +cluster_test-cases/general-features/public/md-page + +md-page -cluster_test-cases/general-features/src/templates - -templates +cluster_test-cases/general-features/public/oldsite + +oldsite -cluster_test-cases/nested-dest - -nested-dest +cluster_test-cases/general-features/public/worker-page + +worker-page -cluster_test-cases/nested-dest/md-page - -md-page +cluster_test-cases/general-features/src + +src -cluster_test-cases/nested-dest/public - -public +cluster_test-cases/general-features/src/blog + +blog -cluster_test-cases/nested-dest/public/md-page - -md-page +cluster_test-cases/general-features/src/blog/2023 + +2023 -cluster_test-cases/nested-dest/public/top-bun-defaults - -top-bun-defaults +cluster_test-cases/general-features/src/blog/2023/a-draft-from-2023 + +a-draft-from-2023 -cluster_test-cases/page-build-errors - -page-build-errors +cluster_test-cases/general-features/src/blog/2023/the-first-blog-post-from-2023 + +the-first-blog-post-from-2023 -cluster_test-cases/page-build-errors/public - -public +cluster_test-cases/general-features/src/blog/2025 + +2025 -cluster_test-cases/page-build-errors/public/top-bun-defaults - -top-bun-defaults +cluster_test-cases/general-features/src/blog/2025/a-blog-post-from-2025 + +a-blog-post-from-2025 -cluster_test-cases/page-build-errors/src - -src +cluster_test-cases/general-features/src/globals + +globals +cluster_test-cases/general-features/src/handlebars-html + +handlebars-html + + +cluster_test-cases/general-features/src/handlebars-html/disabled-handlebars-html + +disabled-handlebars-html + + +cluster_test-cases/general-features/src/html-page + +html-page + + +cluster_test-cases/general-features/src/js-page + +js-page + + +cluster_test-cases/general-features/src/js-page/js-no-async-export + +js-no-async-export + + +cluster_test-cases/general-features/src/js-page/js-no-style-client + +js-no-style-client + + +cluster_test-cases/general-features/src/layouts + +layouts + + +cluster_test-cases/general-features/src/libs + +libs + + +cluster_test-cases/general-features/src/md-page + +md-page + + +cluster_test-cases/general-features/src/templates + +templates + + +cluster_test-cases/general-features/src/worker-page + +worker-page + + +cluster_test-cases/nested-dest + +nested-dest + + +cluster_test-cases/nested-dest/md-page + +md-page + + +cluster_test-cases/nested-dest/public + +public + + +cluster_test-cases/nested-dest/public/dom-stack-defaults + +dom-stack-defaults + + +cluster_test-cases/nested-dest/public/md-page + +md-page + + +cluster_test-cases/page-build-errors + +page-build-errors + + +cluster_test-cases/page-build-errors/public + +public + + +cluster_test-cases/page-build-errors/public/dom-stack-defaults + +dom-stack-defaults + + +cluster_test-cases/page-build-errors/src + +src + + cluster_test-cases/page-build-errors/src/another-broken-page - -another-broken-page + +another-broken-page - + +.tap/report/lcov-report/block-navigation.js + + +block-navigation.js + + + + + +.tap/report/lcov-report/prettify.js + + +prettify.js + + + + + +.tap/report/lcov-report/sorter.js + + +sorter.js + + + + + bin.d.ts - - -bin.d.ts + + +bin.d.ts - + bin.js - - -bin.js + + +bin.js - + index.js - - -index.js + + +index.js bin.js->index.js - - + + - + lib/helpers/cli-prompt.js - - -cli-prompt.js + + +cli-prompt.js bin.js->lib/helpers/cli-prompt.js - - + + - + lib/helpers/copy-file.js - - -copy-file.js + + +copy-file.js bin.js->lib/helpers/copy-file.js - - + + - - -lib/helpers/generate-tree-data.js - - -generate-tree-data.js + + +lib/helpers/dom-stack-aggregate-error.js + + +dom-stack-aggregate-error.js - + -bin.js->lib/helpers/generate-tree-data.js - - +bin.js->lib/helpers/dom-stack-aggregate-error.js + + - - -lib/helpers/top-bun-aggregate-error.js - - -top-bun-aggregate-error.js + + +lib/helpers/generate-tree-data.js + + +generate-tree-data.js - + -bin.js->lib/helpers/top-bun-aggregate-error.js - - +bin.js->lib/helpers/generate-tree-data.js + + - + fs/promises - - -promises + + +promises bin.js->fs/promises - - + + - - -index.js->lib/helpers/top-bun-aggregate-error.js - - + + +index.js->lib/helpers/dom-stack-aggregate-error.js + + + + + +lib/build-copy/index.js + + +index.js + + + + + +index.js->lib/build-copy/index.js + + - + lib/build-static/index.js - - -index.js + + +index.js - + index.js->lib/build-static/index.js - - + + - + lib/builder.js - - -builder.js + + +builder.js - + index.js->lib/builder.js - - + + + + + +eslint.config.js + + +eslint.config.js + + - + index.d.ts - - -index.d.ts + + +index.d.ts - + -lib/builder.js->lib/helpers/top-bun-aggregate-error.js - - +lib/builder.js->lib/helpers/dom-stack-aggregate-error.js + + + + + +lib/builder.js->lib/build-copy/index.js + + - + lib/builder.js->lib/build-static/index.js - - + + - + lib/build-esbuild/index.js - - -index.js + + +index.js - + lib/builder.js->lib/build-esbuild/index.js - - + + - + lib/build-pages/index.js - - -index.js + + +index.js - + lib/builder.js->lib/build-pages/index.js - - + + - + lib/helpers/ensure-dest.js - - -ensure-dest.js + + +ensure-dest.js - + lib/builder.js->lib/helpers/ensure-dest.js - - + + - + lib/identify-pages.js - - -identify-pages.js + + +identify-pages.js - + lib/builder.js->lib/identify-pages.js - - + + - + lib/build-esbuild/index.js->fs/promises - - + + - + lib/build-pages/resolve-vars.js - - -resolve-vars.js + + +resolve-vars.js - + lib/build-esbuild/index.js->lib/build-pages/resolve-vars.js - - + + lib/build-pages/index.js->lib/build-pages/resolve-vars.js - - + + - + lib/helpers/key-by.js - - -key-by.js + + +key-by.js - + lib/build-pages/index.js->lib/helpers/key-by.js - - + + - + lib/build-pages/page-builders/index.js - - -index.js + + +index.js - + lib/build-pages/index.js->lib/build-pages/page-builders/index.js - - + + - + lib/build-pages/page-builders/page-writer.js - - -page-writer.js + + +page-writer.js - + lib/build-pages/index.js->lib/build-pages/page-builders/page-writer.js - - + + - + lib/build-pages/page-data.js - - -page-data.js + + +page-data.js - -lib/build-pages/index.js->lib/build-pages/page-data.js - - - - - -lib/build-pages/resolve-layout.js - - -resolve-layout.js - - - - -lib/build-pages/index.js->lib/build-pages/resolve-layout.js - - +lib/build-pages/index.js->lib/build-pages/page-data.js + + - + lib/build-pages/page-builders/html/index.js - - -index.js + + +index.js lib/build-pages/page-builders/index.js->lib/build-pages/page-builders/html/index.js - - + + - + lib/build-pages/page-builders/js/index.js - - -index.js + + +index.js lib/build-pages/page-builders/index.js->lib/build-pages/page-builders/js/index.js - - + + - + lib/build-pages/page-builders/md/index.js - - -index.js + + +index.js lib/build-pages/page-builders/index.js->lib/build-pages/page-builders/md/index.js - - + + - + lib/build-pages/page-builders/template-builder.js - - -template-builder.js + + +template-builder.js lib/build-pages/page-builders/index.js->lib/build-pages/page-builders/template-builder.js - - + + lib/build-pages/page-builders/page-writer.js->fs/promises - - + + lib/build-pages/page-data.js->lib/build-pages/resolve-vars.js - - + + lib/build-pages/page-data.js->lib/build-pages/page-builders/index.js - - + + lib/build-pages/page-builders/html/index.js->fs/promises - - + + lib/build-pages/page-builders/md/index.js->fs/promises - - + + - + lib/build-pages/page-builders/md/get-md.js - - -get-md.js + + +get-md.js lib/build-pages/page-builders/md/index.js->lib/build-pages/page-builders/md/get-md.js - - + + lib/build-pages/page-builders/template-builder.js->fs/promises - - + + - + lib/helpers/ensure-dest.js->fs/promises - - + + - + lib/identify-pages.js->lib/build-pages/index.js - - + + - - -lib/helpers/top-bun-error.js - - -top-bun-error.js + + +lib/helpers/dom-stack-error.js + + +dom-stack-error.js - - -lib/identify-pages.js->lib/helpers/top-bun-error.js - - + + +lib/identify-pages.js->lib/helpers/dom-stack-error.js + + + + + +lib/helpers/has-ts.js + + +has-ts.js + + + + + +lib/identify-pages.js->lib/helpers/has-ts.js + + + + + +node:test + + +node:test + + + + + +page.vars.ts + + +page.vars.ts + + - + test-cases/build-errors/index.test.js - - -index.test.js + + +index.test.js - + test-cases/build-errors/index.test.js->index.js - - + + - + test-cases/build-errors/index.test.js->fs/promises - - + + + + + +test-cases/build-errors/index.test.js->node:test + + - + test-cases/build-errors/src/another-broken-page/page.js - - -page.js + + +page.js - + test-cases/build-errors/src/client.js - - -client.js + + +client.js - + test-cases/conflict-pages/index.test.js - - -index.test.js + + +index.test.js - + test-cases/conflict-pages/index.test.js->index.js - - + + - + test-cases/conflict-pages/index.test.js->fs/promises - - + + + + + +test-cases/conflict-pages/index.test.js->node:test + + - + test-cases/conflict-pages/src/page.js - - -page.js + + +page.js - + test-cases/conflict-pages/src/page.vars.js - - -page.vars.js + + +page.vars.js - + test-cases/default-layout/index.test.js - - -index.test.js + + +index.test.js - + test-cases/default-layout/index.test.js->index.js - - + + - + test-cases/default-layout/index.test.js->fs/promises - - + + - - -test-cases/default-layout/public/top-bun-defaults/default.client.js-GFXWYOYE.js - - -default.client.js-GFXWYOYE.js + + +test-cases/default-layout/index.test.js->node:test + + + + + +test-cases/default-layout/public/dom-stack-defaults/default.client.js-QOQNIH5Z.js + + +default.client.js-QOQNIH5Z.js - + test-cases/default-layout/src/global.vars.js - - -global.vars.js + + +global.vars.js + + + + + +test-cases/drafts/index.test.js + + +index.test.js + + + + + +test-cases/drafts/index.test.js->index.js + + + + + +test-cases/drafts/index.test.js->fs/promises + + + + + +test-cases/drafts/index.test.js->node:test + + + + + +test-cases/drafts/public/dom-stack-defaults/default.client.js-QOQNIH5Z.js + + +default.client.js-QOQNIH5Z.js + + + + + +test-cases/drafts/src/a-draft-js/page.draft.js + + +page.draft.js + + + + + +test-cases/general-features/copyfolder/oldsite/client.js + + +client.js - + test-cases/general-features/index.test.js - - -index.test.js + + +index.test.js - + test-cases/general-features/index.test.js->index.js - - + + - + test-cases/general-features/index.test.js->fs/promises - - + + + + + +test-cases/general-features/index.test.js->node:test + + + + + +test-cases/general-features/public/blog/2025/a-blog-post-from-2025/client-DLTRJAAZ.js + + +client-DLTRJAAZ.js + + - + test-cases/general-features/public/client-XA44T36G.js - - -client-XA44T36G.js + + +client-XA44T36G.js - + test-cases/general-features/public/globals/global.client-PLTCHKAX.js - - -global.client-PLTCHKAX.js + + +global.client-PLTCHKAX.js - + test-cases/general-features/public/html-page/client-P33YF4QG.js - - -client-P33YF4QG.js + + +client-P33YF4QG.js - + test-cases/general-features/public/js-page/client-GDCZFMOL.js - - -client-GDCZFMOL.js + + +client-GDCZFMOL.js - + test-cases/general-features/public/layouts/blog.layout.client-GHS6BWW7.js - - -blog.layout.client-GHS6BWW7.js + + +blog.layout.client-GHS6BWW7.js - + test-cases/general-features/public/md-page/client-R4ZZXMTO.js - - -client-R4ZZXMTO.js + + +client-R4ZZXMTO.js + + + + + +test-cases/general-features/public/oldsite/client.js + + +client.js + + + + + +test-cases/general-features/public/worker-page/client-GMP3DHQT.js + + +client-GMP3DHQT.js + + + + + +test-cases/general-features/public/worker-page/counter.worker-OKYOGZ4K.js + + +counter.worker-OKYOGZ4K.js + + + + + +test-cases/general-features/src/blog/2023/a-draft-from-2023/page.vars.js + + +page.vars.js - + test-cases/general-features/src/blog/2023/the-first-blog-post-from-2023/page.vars.js - - -page.vars.js + + +page.vars.js + + + + + +test-cases/general-features/src/blog/2025/a-blog-post-from-2025/client.tsx + + +client.tsx + + + + + +test-cases/general-features/src/blog/2025/a-blog-post-from-2025/page.ts + + +page.ts - + test-cases/general-features/src/blog/page.js - - -page.js + + +page.js - + test-cases/general-features/src/client.js - - -client.js + + +client.js - + test-cases/general-features/src/libs/a-lib.js - - -a-lib.js + + +a-lib.js - + test-cases/general-features/src/client.js->test-cases/general-features/src/libs/a-lib.js - - + + - + test-cases/general-features/src/feeds.template.js - - -feeds.template.js + + +feeds.template.js - + test-cases/general-features/src/globals/global.client.js - - -global.client.js + + +global.client.js - + test-cases/general-features/src/globals/global.vars.js - - -global.vars.js + + +global.vars.js - + test-cases/general-features/src/handlebars-html/disabled-handlebars-html/page.vars.js - - -page.vars.js + + +page.vars.js - + test-cases/general-features/src/handlebars-html/page.vars.js - - -page.vars.js + + +page.vars.js - + test-cases/general-features/src/html-page/client.js - - -client.js + + +client.js - + test-cases/general-features/src/html-page/page.vars.js - - -page.vars.js + + +page.vars.js - + test-cases/general-features/src/js-page/client.js - - -client.js + + +client.js - + test-cases/general-features/src/js-page/js-no-async-export/page.js - - -page.js + + +page.js - + test-cases/general-features/src/js-page/js-no-style-client/page.js - - -page.js + + +page.js - + test-cases/general-features/src/js-page/page.js - - -page.js + + +page.js - + test-cases/general-features/src/js-page/page.vars.js - - -page.vars.js + + +page.vars.js - + test-cases/general-features/src/layouts/blog.layout.client.js - - -blog.layout.client.js + + +blog.layout.client.js - + test-cases/general-features/src/layouts/blog.layout.js - - -blog.layout.js + + +blog.layout.js - + test-cases/general-features/src/layouts/root.layout.js - - -root.layout.js + + +root.layout.js - + test-cases/general-features/src/layouts/blog.layout.js->test-cases/general-features/src/layouts/root.layout.js - - + + + + + +test-cases/general-features/src/layouts/ts.layout.ts + + +ts.layout.ts + + + + + +test-cases/general-features/src/layouts/ts.layout.ts->test-cases/general-features/src/layouts/root.layout.js + + + + + +test-cases/general-features/src/markdown-it.settings.js + + +markdown-it.settings.js + + - + test-cases/general-features/src/md-page/client.js - - -client.js + + +client.js - + test-cases/general-features/src/md-page/page.vars.js - - -page.vars.js + + +page.vars.js - + test-cases/general-features/src/page.vars.js - - -page.vars.js + + +page.vars.js - + test-cases/general-features/src/templates/async-iterator.template.js - - -async-iterator.template.js + + +async-iterator.template.js - + test-cases/general-features/src/templates/object-array.template.js - - -object-array.template.js + + +object-array.template.js - + test-cases/general-features/src/templates/simple.txt.template.js - - -simple.txt.template.js + + +simple.txt.template.js - + test-cases/general-features/src/templates/single-object.template.js - - -single-object.template.js + + +single-object.template.js + + + + + +test-cases/general-features/src/worker-page/client.js + + +client.js + + + + + +test-cases/general-features/src/worker-page/counter.worker.js + + +counter.worker.js + + + + + +test-cases/general-features/src/worker-page/page.js + + +page.js - + test-cases/nested-dest/global.vars.js - - -global.vars.js + + +global.vars.js - + test-cases/nested-dest/index.test.js - - -index.test.js + + +index.test.js - + test-cases/nested-dest/index.test.js->index.js - - + + - + test-cases/nested-dest/index.test.js->fs/promises - - + + + + + +test-cases/nested-dest/index.test.js->node:test + + - + test-cases/nested-dest/md-page/client.js - - -client.js + + +client.js - + test-cases/nested-dest/md-page/page.vars.js - - -page.vars.js + + +page.vars.js - - -test-cases/nested-dest/public/md-page/client-BI3TWBFA.js - - -client-BI3TWBFA.js + + +test-cases/nested-dest/public/dom-stack-defaults/default.client.js-QOQNIH5Z.js + + +default.client.js-QOQNIH5Z.js - - -test-cases/nested-dest/public/top-bun-defaults/default.client.js-GFXWYOYE.js - - -default.client.js-GFXWYOYE.js + + +test-cases/nested-dest/public/md-page/client-BI3TWBFA.js + + +client-BI3TWBFA.js - + test-cases/page-build-errors/index.test.js - - -index.test.js + + +index.test.js - + test-cases/page-build-errors/index.test.js->index.js - - + + - + test-cases/page-build-errors/index.test.js->fs/promises - - + + + + + +test-cases/page-build-errors/index.test.js->node:test + + - + test-cases/page-build-errors/public/client-MPACHZ4G.js - - -client-MPACHZ4G.js + + +client-MPACHZ4G.js - - -test-cases/page-build-errors/public/top-bun-defaults/default.client.js-GFXWYOYE.js - - -default.client.js-GFXWYOYE.js + + +test-cases/page-build-errors/public/dom-stack-defaults/default.client.js-QOQNIH5Z.js + + +default.client.js-QOQNIH5Z.js - + test-cases/page-build-errors/src/another-broken-page/page.js - - -page.js + + +page.js - + test-cases/page-build-errors/src/client.js - - -client.js + + +client.js - + test-cases/page-build-errors/src/page.js - - -page.js + + +page.js diff --git a/esbuild.settings.js b/esbuild.settings.js new file mode 100644 index 0000000..e09d04c --- /dev/null +++ b/esbuild.settings.js @@ -0,0 +1,18 @@ +/** + * @import { BuildOptions } from '.' + */ + +/** + * + * @param {BuildOptions} esbuildSettings + * @returns Promise + */ +export default async function esbuildSettingsOverride (esbuildSettings) { + // Configure loader for TTF font files + esbuildSettings.loader = { + ...esbuildSettings.loader, + '.ttf': 'copy' + } + + return esbuildSettings +} diff --git a/eslint.config.js b/eslint.config.js index 2a3cb56..caf35f0 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,6 +1,7 @@ import neostandard, { resolveIgnoresFromGitignore } from 'neostandard' export default neostandard({ + env: ['browser'], ignores: [ ...resolveIgnoresFromGitignore(), 'test-cases/build-errors/src/**/*.js', diff --git a/examples/basic/README.md b/examples/basic/README.md new file mode 100644 index 0000000..49cca25 --- /dev/null +++ b/examples/basic/README.md @@ -0,0 +1,93 @@ +# Basic DOMStack Example with TypeScript + +This example demonstrates a fundamental website built with DOMStack using TypeScript, showcasing core features without advanced customization. + +## Overview + +The basic example illustrates: +- Multiple page types (Markdown, HTML, JavaScript/TypeScript) +- Layout system and page nesting +- Asset handling +- Client-side TypeScript integration +- CSS styling (global and page-specific) +- Variables and metadata +- TypeScript integration with proper type definitions + +## Getting Started + +### Prerequisites + +- Node.js 22.x or higher + +### Installation + +```bash +# Install dependencies +npm install +``` + +### Building the Example + +```bash +# Build the site +npm run build + +# Watch for changes during development +npm run watch +``` + +The built site will be in the `public` directory. + +## Project Structure + +``` +src/ +β”œβ”€β”€ layouts/ # Layout templates +β”‚ β”œβ”€β”€ root.layout.ts # Main layout (TypeScript) +β”‚ └── child.layout.ts # Nested layout (TypeScript) +β”œβ”€β”€ md-page/ # Markdown page examples +β”‚ β”œβ”€β”€ page.vars.ts # Page variables (TypeScript) +β”‚ └── client.ts # Page-specific client script (TypeScript) +β”œβ”€β”€ js-page/ # JavaScript page examples (kept as JS for demonstration) +β”‚ β”œβ”€β”€ loose-assets/ # Contains TypeScript files +β”‚ β”‚ β”œβ”€β”€ page.ts # TypeScript page +β”‚ β”‚ β”œβ”€β”€ client.ts # TypeScript client +β”‚ β”‚ └── shared-lib.ts # TypeScript shared library +β”œβ”€β”€ html-page/ # HTML page examples +β”‚ └── client.ts # Page-specific client script (TypeScript) +β”œβ”€β”€ global.css # Global styles +β”œβ”€β”€ global.client.ts # Global client-side TypeScript +β”œβ”€β”€ global.vars.ts # Global variables (TypeScript) +└── README.md # Main content (becomes index.html) +``` + +### Key Features Demonstrated + +### Page Types +- **Markdown pages** - Simple content authoring with frontmatter +- **JavaScript/TypeScript pages** - Dynamic content generation with full JS/TS capabilities +- **HTML pages** - Direct HTML control for complex layouts + +### Layouts +The example demonstrates DOMStack's layout system with nested layouts that wrap page content, fully typed with TypeScript interfaces. + +### Assets +Static assets like images are co-located with content and automatically copied to the output directory. + +### Styling +Both global and page-specific CSS is demonstrated, showing how to scope styles appropriately. + +### TypeScript Integration +- **Strong typing** - Full TypeScript support with interfaces for layouts, pages, and components +- **JSDoc example** - The js-page directory is kept as JavaScript with JSDoc comments to demonstrate compatibility +- **Type definitions** - Proper type definitions for page variables, layout functions, and client scripts + +## Learn More + +This is one of several examples in the DOMStack repository. For more advanced features, check out the other examples like: +- css-modules +- preact +- tailwind +- and more... + +For complete documentation, visit the [DOMStack GitHub repository](https://github.com/bcomnes/domstack). diff --git a/examples/basic/package.json b/examples/basic/package.json index 7fa7593..e028d85 100644 --- a/examples/basic/package.json +++ b/examples/basic/package.json @@ -5,22 +5,25 @@ "type": "module", "scripts": { "start": "npm run watch", - "build": "npm run clean && top-bun", + "build": "npm run clean && domstack", "clean": "rm -rf public && mkdir -p public", - "watch": "npm run clean && run-p watch:*", - "watch:serve": "browser-sync start --server 'public' --files 'public'", - "watch:top-bun": "npm run build -- --watch" + "watch": "npm run clean && dom --watch", + "test": "tsc" }, "keywords": [], "author": "Bret Comnes (https://bret.io/)", "license": "MIT", "devDependencies": { - "browser-sync": "^2.26.7", - "npm-run-all2": "^6.0.0" + "npm-run-all2": "^6.0.0", + "typescript": "~5.8.2", + "@voxpelli/tsconfig": "^15.0.0" }, "dependencies": { - "top-bun": "../../.", - "uhtml-isomorphic": "^2.1.0", - "mine.css": "^9.0.1" + "@domstack/static": "file:../../.", + "htm": "^3.1.1", + "preact": "^10.26.6", + "preact-render-to-string": "^6.5.13", + "mine.css": "^9.0.1", + "highlight.js": "^11.9.0" } } diff --git a/examples/basic/src/README.md b/examples/basic/src/README.md index 7688bf4..b4e966d 100644 --- a/examples/basic/src/README.md +++ b/examples/basic/src/README.md @@ -1,16 +1,65 @@ --- +title: Basic DOMStack Example md-files: support yaml frontmatter! --- -# Minimal top-bun example -This example demonstrates a example of a minimal website, no customization. +# Basic DOMStack Example -It's just a `src` folder with a few markdown files that link to each other. Markdown files can link directly to their markdown counterparts so navigation works inside GitHub's built in markdown source navigator. +This example demonstrates a complete basic website built with DOMStack, showcasing the core features without advanced customization. +Please inspect the website, the web inspector (source and network tab), the src code in the example folder as well as the ouput files in the public folder to get an idea of how everything works. -- [loose-file.md](./loose-file.md) -- [nested-md](./md-page/README.md) -- [sub-page](./md-page/sub-page/README.md) +## Features Demonstrated -Also notice how the title of this document set the `title` variable for the page, and renders in the title `` properly. -Page builders can implement variable extraction based on assumptions like this for a given document type. -More automatic variable extraction is planned, like `git` metadata (first commit date, last commit date that touched the file. etc). +- Multiple page types (Markdown, HTML, JavaScript) +- Page layouts and nesting +- Asset handling +- Client-side JavaScript +- CSS styling (global and page-specific) +- Frontmatter variables + +## Project Structure + +``` +src/ +β”œβ”€β”€ layouts/ # Layout templates +β”œβ”€β”€ md-page/ # Markdown page examples +β”œβ”€β”€ js-page/ # JavaScript page examples +β”œβ”€β”€ html-page/ # HTML page examples +β”œβ”€β”€ global.css # Global styles +β”œβ”€β”€ global.client.js # Global client-side JavaScript +β”œβ”€β”€ global.vars.js # Global variables +└── README.md # This file (becomes index.html) +``` + +## Page Examples + +Navigate through different page types: + +- [Loose Markdown File](./loose-file.md) +- [Markdown Page Example](./md-page/README.md) +- [Nested Markdown Page](./md-page/sub-page/README.md) +- [JavaScript Page Example](./js-page/) +- [HTML Page Example](./html-page/page.html) + +## How It Works + +- **Markdown Pages**: The title of this document (`h1`) becomes the `title` variable for the page and renders in the `` tag. +- **Layouts**: Pages use layouts defined in the `layouts` directory. +- **Assets**: Static assets are copied to the output directory. +- **Styling**: Both global and page-specific CSS is processed and included. +- **Client JS**: JavaScript bundles are created for enhanced functionality. + +## Building the Example + +Run the following commands: + +```bash +npm install +npm run build +``` + +To watch for changes during development: + +```bash +npm run watch +``` diff --git a/examples/basic/src/client.js b/examples/basic/src/client.ts similarity index 56% rename from examples/basic/src/client.js rename to examples/basic/src/client.ts index 760509e..e7a7000 100644 --- a/examples/basic/src/client.js +++ b/examples/basic/src/client.ts @@ -1,6 +1,6 @@ console.log('This client bundle only loads in the root page.') -// Each page folder can have a client.js file in it. -// The client.js file is treated as an entry point script for just the +// Each page folder can have a client.ts file in it. +// The client.ts file is treated as an entry point script for just the // page it is associated with, but all dependencies are de-duped via // esbuild. diff --git a/examples/basic/src/global.client.js b/examples/basic/src/global.client.ts similarity index 71% rename from examples/basic/src/global.client.js rename to examples/basic/src/global.client.ts index ee39271..b840c4d 100644 --- a/examples/basic/src/global.client.js +++ b/examples/basic/src/global.client.ts @@ -1,5 +1,12 @@ +// @ts-expect-error import { toggleTheme } from 'mine.css' +declare global { + interface Window { + toggleTheme: typeof toggleTheme; + } +} + window.toggleTheme = toggleTheme console.log('The global client is loaded on every page.') diff --git a/examples/basic/src/global.css b/examples/basic/src/global.css index 7f28c70..15bc369 100644 --- a/examples/basic/src/global.css +++ b/examples/basic/src/global.css @@ -1,5 +1,6 @@ @import 'mine.css/dist/mine.css'; @import 'mine.css/dist/layout.css'; +@import 'highlight.js/styles/github-dark-dimmed.css'; /* The global.css in the root directory of the site is loaded on every page. */ diff --git a/examples/basic/src/global.vars.js b/examples/basic/src/global.vars.js deleted file mode 100644 index 8376754..0000000 --- a/examples/basic/src/global.vars.js +++ /dev/null @@ -1,10 +0,0 @@ -// The global.vars.js file should export default either an object, function that -// returns an object or an async function that returns an object. -// -// These variables are available to every page, and have the lowest precedence. - -export default async function () { - return { - siteName: 'top-bun basic', - } -} diff --git a/examples/basic/src/global.vars.ts b/examples/basic/src/global.vars.ts new file mode 100644 index 0000000..92bdeed --- /dev/null +++ b/examples/basic/src/global.vars.ts @@ -0,0 +1,15 @@ +// The global.vars.ts file should export default either an object, function that +// returns an object or an async function that returns an object. +// +// These variables are available to every page, and have the lowest precedence. + +interface GlobalVars { + siteName: string; + [key: string]: unknown; +} + +export default async function (): Promise<GlobalVars> { + return { + siteName: 'domstack basic', + } +} diff --git a/examples/basic/src/html-page/client.js b/examples/basic/src/html-page/client.ts similarity index 100% rename from examples/basic/src/html-page/client.js rename to examples/basic/src/html-page/client.ts diff --git a/examples/basic/src/js-page/loose-assets/client.js b/examples/basic/src/js-page/loose-assets/client.js deleted file mode 100644 index 8318a45..0000000 --- a/examples/basic/src/js-page/loose-assets/client.js +++ /dev/null @@ -1,3 +0,0 @@ -import sharedData from './shared-lib.js' - -console.log(sharedData.shared) diff --git a/examples/basic/src/js-page/loose-assets/client.ts b/examples/basic/src/js-page/loose-assets/client.ts new file mode 100644 index 0000000..4f2d131 --- /dev/null +++ b/examples/basic/src/js-page/loose-assets/client.ts @@ -0,0 +1,3 @@ +import sharedData from './shared-lib.ts' + +console.log(sharedData.shared) diff --git a/examples/basic/src/js-page/loose-assets/page.js b/examples/basic/src/js-page/loose-assets/page.js deleted file mode 100644 index f737df3..0000000 --- a/examples/basic/src/js-page/loose-assets/page.js +++ /dev/null @@ -1,22 +0,0 @@ -import { html } from 'uhtml-isomorphic' - -import sharedData from './shared-lib.js' - -export default async function JSPage () { - return html` - <p> - You can keep loose assets basically anywhere in the <pre>src</pre> directory. - If they are css or js files, they get included into the built website into any of the - client bundle they are imported into. - </p> - <p> - This page demonstrates that with the shared-lib.js and local-import.css files - that get imported into the page.js, client.js and style.css files for this page. - </p> - <p>${sharedData.shared}</p> - ` -} - -export const vars = { - title: 'JS Page with loose assets', -} diff --git a/examples/basic/src/js-page/loose-assets/page.ts b/examples/basic/src/js-page/loose-assets/page.ts new file mode 100644 index 0000000..84fe842 --- /dev/null +++ b/examples/basic/src/js-page/loose-assets/page.ts @@ -0,0 +1,32 @@ +import { html } from 'htm/preact' +import type { PageFunction } from '@domstack/static' + +import sharedData from './shared-lib.js' +import type { PageVars } from '../../layouts/root.layout.ts' + +const JSPage: PageFunction<PageVars> = async () => { + return html` + <div> + <p> + You can keep loose assets basically anywhere in the <pre>src</pre> directory. + If they are css or js files, they get included into the built website into any of the + client bundle they are imported into. + </p> + <p> + This page demonstrates that with the shared-lib.js and local-import.css files + that get imported into the page.js, client.js and style.css files for this page. + </p> + <p>${sharedData.shared}</p> + </div> + ` +} + +export default JSPage + +interface PageVariables extends PageVars { + title: string; +} + +export const vars: Partial<PageVariables> = { + title: 'JS Page with loose assets', +} diff --git a/examples/basic/src/js-page/loose-assets/shared-lib.js b/examples/basic/src/js-page/loose-assets/shared-lib.js deleted file mode 100644 index 9660ec1..0000000 --- a/examples/basic/src/js-page/loose-assets/shared-lib.js +++ /dev/null @@ -1,6 +0,0 @@ -// js library files can live anywhere in the src directory, and they are only included -// when a js page or client bundle imports them. - -export default { - shared: 'data', -} diff --git a/examples/basic/src/js-page/loose-assets/shared-lib.ts b/examples/basic/src/js-page/loose-assets/shared-lib.ts new file mode 100644 index 0000000..f8cf09d --- /dev/null +++ b/examples/basic/src/js-page/loose-assets/shared-lib.ts @@ -0,0 +1,12 @@ +// ts library files can live anywhere in the src directory, and they are only included +// when a ts page or client bundle imports them. + +interface SharedData { + shared: string; +} + +const sharedLib: SharedData = { + shared: 'data', +} + +export default sharedLib; diff --git a/examples/basic/src/js-page/page.js b/examples/basic/src/js-page/page.js index f7b49f3..b9fd736 100644 --- a/examples/basic/src/js-page/page.js +++ b/examples/basic/src/js-page/page.js @@ -1,5 +1,12 @@ -import { html } from 'uhtml-isomorphic' +/** + * @import { PageFunction } from '@domstack/static' + * @import { PageVars } from '../layouts/root.layout.js + */ +import { html } from 'htm/preact' +/** +* @type { PageFunction <PageVars> } +*/ export default async function JSPage ({ vars: { siteName, @@ -7,25 +14,64 @@ export default async function JSPage ({ } }) { return html` - <p>The js page is the only page type that can render the body with the set variables.</p> + <div class="js-page-example"> + <h1>JavaScript Page Example</h1> - <p> - All you have to do is export a default function (async or sync) that returns a string, or any - type that your layout can handle. - In this case, we are using <a href="https://ghub.io/uhtml-isomorphic"><pre>uhtml-isomorphic</pre></a>. - </p> + <section class="explanation"> + <h2>What is a JavaScript Page?</h2> + <p> + The JavaScript page type is the most powerful and flexible option in DOMStack. + It allows you to: + </p> + <ul> + <li>Access and use variables directly in your rendering logic</li> + <li>Generate dynamic content based on data or conditions</li> + <li>Use component-based architecture with Preact or other libraries</li> + <li>Return either HTML strings or component objects</li> + </ul> + </section> - <p>Here we access the <pre>siteName</pre> and <pre>title</pre> variables inside the page</p> + <section class="implementation"> + <h2>How to Implement</h2> + <p> + Export a default function (async or sync) that returns a string or any + type that your layout can handle. In this example, we're using + <a href="https://github.com/developit/htm"><code>htm/preact</code></a> for JSX-like syntax. + </p> + <div class="code-example"> + <pre><code>export default async function MyPage({ vars }) { + return html\`<div>Content here</div>\` +}</code></pre> + </div> + </section> - <p>${siteName}</p> - <p>${title}</p> + <section class="variables-demo"> + <h2>Using Variables</h2> + <p>Here we access the <code>siteName</code> and <code>title</code> variables inside the page:</p> + <div class="variable-display"> + <div><strong>Site Name:</strong> ${siteName}</div> + <div><strong>Page Title:</strong> ${title}</div> + </div> + </section> - <p>JS pages can also have a page scoped <pre>client.js</pre> and <pre>style.css</pre>. It - is an incredibly flexible page type. - </p> + <section class="additional-features"> + <h2>Additional Features</h2> + <p>JavaScript pages support:</p> + <ul> + <li>Page-scoped <code>client.js</code> for browser interactions</li> + <li>Page-scoped <code>style.css</code> for component styling</li> + <li>Page-specific variables via <code>export const vars</code></li> + <li>Async data fetching before rendering</li> + </ul> + </section> + + <a href="../" class="back-link">← Back to Home</a> + </div> ` } +// Define page-specific variables export const vars = { - title: 'JS Page', + title: 'JavaScript Page Example', + description: 'Learn how to use JavaScript pages in DOMStack for dynamic content generation' } diff --git a/examples/basic/src/js-page/style.css b/examples/basic/src/js-page/style.css index dc48aa3..c167396 100644 --- a/examples/basic/src/js-page/style.css +++ b/examples/basic/src/js-page/style.css @@ -1,3 +1,74 @@ -.js-page-class { - background: purple; +.js-page-example { + max-width: 800px; + margin: 0 auto; + padding: 1rem; + font-family: system-ui, -apple-system, sans-serif; +} + +.js-page-example h1 { + color: #333; + border-bottom: 2px solid #6200ee; + padding-bottom: 0.5rem; +} + +.js-page-example section { + margin-bottom: 2rem; + padding: 1.5rem; + border-radius: 8px; + background-color: #f9f9f9; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.js-page-example h2 { + color: #6200ee; + margin-top: 0; + margin-bottom: 1rem; +} + +.js-page-example .code-example { + background-color: #272822; + color: #f8f8f2; + padding: 1rem; + border-radius: 4px; + overflow-x: auto; +} + +.js-page-example .code-example pre { + margin: 0; +} + +.js-page-example code { + background-color: #eee; + padding: 0.2rem 0.4rem; + border-radius: 3px; + font-family: monospace; + font-size: 0.9em; +} + +.js-page-example .code-example code { + background-color: transparent; + padding: 0; +} + +.js-page-example .variable-display { + background-color: #fff; + border: 1px solid #ddd; + padding: 1rem; + border-radius: 4px; + margin-top: 1rem; +} + +.js-page-example .back-link { + display: inline-block; + margin-top: 1rem; + padding: 0.5rem 1rem; + background-color: #6200ee; + color: white; + text-decoration: none; + border-radius: 4px; + transition: background-color 0.2s; +} + +.js-page-example .back-link:hover { + background-color: #3700b3; } diff --git a/examples/basic/src/layouts/child.layout.js b/examples/basic/src/layouts/child.layout.js deleted file mode 100644 index 4f9f622..0000000 --- a/examples/basic/src/layouts/child.layout.js +++ /dev/null @@ -1,22 +0,0 @@ -import { html } from 'uhtml-isomorphic' - -import defaultRootLayout from './root.layout.js' - -export default function articleLayout (args) { - const { children, ...rest } = args - const wrappedChildren = html` - <article class="bc-article h-entry" itemscope itemtype="http://schema.org/NewsArticle"> - - <h1>${rest.vars.title}</h1> - - <section class="e-content" itemprop="articleBody"> - ${typeof children === 'string' - ? html([children]) - : children /* Support both uhtml and string children. Optional. */ - } - </section> - </article> - ` - - return defaultRootLayout({ children: wrappedChildren, ...rest }) -} diff --git a/examples/basic/src/layouts/child.layout.ts b/examples/basic/src/layouts/child.layout.ts new file mode 100644 index 0000000..6df057f --- /dev/null +++ b/examples/basic/src/layouts/child.layout.ts @@ -0,0 +1,27 @@ +import type { LayoutFunction } from '@domstack/static' +import { html } from 'htm/preact' +import { render } from 'preact-render-to-string' + +import defaultRootLayout from './root.layout.js' +import type { PageVars } from './root.layout.js' + +const articleLayout: LayoutFunction<PageVars> = (args) => { + const { children, ...rest } = args + const wrappedChildren = render(html` + <article class="bc-article h-entry" itemscope itemtype="http://schema.org/NewsArticle"> + + <h1>${rest.vars.title}</h1> + + <section class="e-content" itemprop="articleBody"> + ${typeof children === 'string' + ? html`<div dangerouslySetInnerHTML=${{ __html: children }}></div>` + : children /* Support both preact and string children */ + } + </section> + </article> + `) + + return defaultRootLayout({ children: wrappedChildren, ...rest }) +} + +export default articleLayout diff --git a/examples/basic/src/layouts/root.layout.js b/examples/basic/src/layouts/root.layout.js deleted file mode 100644 index 4a832d4..0000000 --- a/examples/basic/src/layouts/root.layout.js +++ /dev/null @@ -1,42 +0,0 @@ -// The root.layout.js file must return the rendered page. -// It must implement the following variables: -// -// - children: the string or type that the page returns that represents the inner-content of the page -// - scripts: an array of urls that should be injected into the page as script tags, type module -// - styles: an array of urls that should be injected into the page as link rel="stylesheet" tags. -// -// All other variables are set on a page level basis, either by hand or by data extraction from the page type. - -import { html, render } from 'uhtml-isomorphic' - -export default async function RootLayout ({ - vars: { - title, - siteName, - }, - scripts, - styles, - children, -}) { - return render(String, html` - <!DOCTYPE html> - <html> - <head> - <meta charset="utf-8"> - <title>${siteName}${title ? ` | ${title}` : ''} - - ${scripts - ? scripts.map(script => html``) - : null} - ${styles - ? styles.map(style => html``) - : null} - - -
- ${typeof children === 'string' ? html([children]) : children /* Support both uhtml and string children. Optional. */} -
- - -`) -} diff --git a/examples/basic/src/layouts/root.layout.ts b/examples/basic/src/layouts/root.layout.ts new file mode 100644 index 0000000..1ef49db --- /dev/null +++ b/examples/basic/src/layouts/root.layout.ts @@ -0,0 +1,58 @@ +// The root.layout.ts file must return the rendered page. +// It must implement the following variables: +// +// - children: the string or type that the page returns that represents the inner-content of the page +// - scripts: an array of urls that should be injected into the page as script tags, type module +// - styles: an array of urls that should be injected into the page as link rel="stylesheet" tags. +// +// All other variables are set on a page level basis, either by hand or by data extraction from the page type. + +import { html } from 'htm/preact' +import { render } from 'preact-render-to-string' +import type { LayoutFunction } from '@domstack/static' + +export interface PageVars { + title: string; + siteName: string; + basePath?: string; +} + +const RootLayout: LayoutFunction = async ({ + vars: { + title, + siteName, + basePath + }, + scripts, + styles, + children, +}) => { + return /* html */` + + + ${render(html` + + + ${siteName}${title ? ` | ${title}` : ''} + + ${scripts + ? scripts.map(script => html``).join('\n ')} + +` +} diff --git a/examples/nested-dest/README.md b/examples/nested-dest/README.md index 9d50829..ce23b59 100644 --- a/examples/nested-dest/README.md +++ b/examples/nested-dest/README.md @@ -1,13 +1,53 @@ -# Nested dest +# Nested Destination Example -One of the design goals of `top-bun` was to allow you to point `top-bun` at a generic -library repository, and render out all of the markdown inside of it, into a `dest` folder. +## Overview -The issue with this is that `dest` folder essentially lives inside of the `src` folder, -so it was important to support sane ignore patterns by default so you don't fall into a recursive render -loop, or try to render out `node_modules` etc. +This example demonstrates one of DOMStack's key features: the ability to build a documentation site from an existing repository structure without reorganizing it. -This example site points `top-bun` at the root of the example `/`, and builds into `/public`, so its a good -example how hoe flexible top-bun can be. +## How It Works -boop beep +DOMStack can use any directory as its source, including the root of your project. This allows you to: + +1. Generate documentation directly from your project's existing markdown files +2. Keep source files in their original locations +3. Build the site into a separate destination directory + +## Key Concepts + +### Source and Destination Paths + +In this example: +- Source: The root directory (`/`) +- Destination: A subdirectory (`/public`) + +### Avoiding Recursive Processing + +When your destination folder is inside your source tree, DOMStack needs to avoid: +- Processing files in the destination folder (recursive loop) +- Processing unwanted directories like `node_modules` + +This is handled through intelligent ignore patterns: +- Default ignore patterns for common directories +- Custom ignore patterns through the `--ignore` flag + +## Example Configuration + +The build command in this example uses: + +```bash +domstack --src . --ignore ignore +``` + +This tells DOMStack to: +- Use the current directory (`.`) as the source +- Explicitly ignore the `ignore` directory +- Apply default ignore patterns for `node_modules`, etc. + +## Try It Yourself + +1. Examine this project's structure - notice how files are in the root +2. Look at the built output in `/public` to see how files are processed +3. Check `package.json` to see how the build command is configured + +- [CHANGELOG.md](./CHANGELOG.md) +- [CONTRIBUTING.md](./CONTRIBUTING.md) diff --git a/examples/nested-dest/global.vars.js b/examples/nested-dest/global.vars.js index 467289d..2f20898 100644 --- a/examples/nested-dest/global.vars.js +++ b/examples/nested-dest/global.vars.js @@ -1,4 +1,4 @@ export default { defaultStyle: false, - siteName: 'nested top-bun example', + siteName: 'nested depscan example', } diff --git a/examples/nested-dest/package.json b/examples/nested-dest/package.json index 97e954a..5b42a6a 100644 --- a/examples/nested-dest/package.json +++ b/examples/nested-dest/package.json @@ -5,22 +5,19 @@ "type": "module", "scripts": { "start": "npm run watch", - "build": "npm run clean && top-bun --src . --ignore ignore", + "build": "npm run clean && domstack --src . --ignore ignore", "clean": "rm -rf public && mkdir -p public", "watch": "npm run clean && run-p watch:*", - "watch:serve": "browser-sync start --server 'public' --files 'public'", - "watch:top-bun": "npm run build -- --watch" + "watch:domstack": "npm run build -- --watch" }, "keywords": [], "author": "Bret Comnes (https://bret.io/)", "license": "MIT", "dependencies": { - "top-bun": "../../.", - "mine.css": "^9.0.1", - "uhtml-isomorphic": "^2.1.0" + "@domstack/static": "file:../../.", + "mine.css": "^9.0.1" }, "devDependencies": { - "browser-sync": "^2.26.7", "npm-run-all2": "^6.0.0" } } diff --git a/examples/preact-isomorphic/package.json b/examples/preact-isomorphic/package.json new file mode 100644 index 0000000..f5ce358 --- /dev/null +++ b/examples/preact-isomorphic/package.json @@ -0,0 +1,27 @@ +{ + "name": "@domstack/preact-example", + "version": "0.0.0", + "type": "module", + "scripts": { + "start": "npm run watch", + "build": "npm run clean && domstack", + "clean": "rm -rf public && mkdir -p public", + "watch": "npm run clean && dom --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", + "@domstack/static": "file:../../." + }, + "devDependencies": { + "@voxpelli/tsconfig": "^15.0.0", + "npm-run-all2": "^6.0.0", + "typescript": "~5.8.2" + } +} diff --git a/examples/preact-isomorphic/src/README.md b/examples/preact-isomorphic/src/README.md new file mode 100644 index 0000000..634d084 --- /dev/null +++ b/examples/preact-isomorphic/src/README.md @@ -0,0 +1,41 @@ +# Preact Isomorphic Rendering Example + +This example demonstrates how to implement isomorphic rendering with Preact in DOMStack. Isomorphic rendering means the same components can be rendered on both the server and client, providing benefits of server-side rendering (SSR) with client-side interactivity. + +## What is Isomorphic Rendering? + +Isomorphic (or universal) rendering combines: + +1. **Server-side rendering** - Components are rendered to HTML on the server first +2. **Client-side hydration** - JavaScript takes over in the browser to add interactivity +3. **Shared component code** - The same components work in both environments + +## Benefits + +- **Performance**: Faster initial page loads and time-to-content +- **SEO**: Search engines see fully rendered content +- **Accessibility**: Content is available without JavaScript +- **User Experience**: No flash of unstyled content or layout shifts + +## Examples in This Project + +- [Isomorphic Component Rendering](./isomorphic/) - Complete todo app rendered both server and client-side +- [JSX Client Mounting](./jsx-page/) - Example of client-side JSX rendering into static HTML + +## Implementation Approach + +This example uses: + +- **Preact** - A lightweight alternative to React +- **HTM** - JSX alternative using tagged template literals +- **Signals** - For reactive state management +- **preact-render-to-string** - For server-side rendering + +## How It Works + +1. The server renders components to HTML using `preact-render-to-string` +2. The HTML is sent to the browser with linked JavaScript +3. In the browser, the same components hydrate the existing HTML +4. Interactivity is enabled without replacing the DOM structure + +Learn more about these techniques in the examples! diff --git a/examples/preact-isomorphic/src/globals/global.client.ts b/examples/preact-isomorphic/src/globals/global.client.ts new file mode 100644 index 0000000..20121a7 --- /dev/null +++ b/examples/preact-isomorphic/src/globals/global.client.ts @@ -0,0 +1,2 @@ +import { toggleTheme } from 'mine.css' +window.toggleTheme = toggleTheme diff --git a/examples/preact/src/globals/global.css b/examples/preact-isomorphic/src/globals/global.css similarity index 100% rename from examples/preact/src/globals/global.css rename to examples/preact-isomorphic/src/globals/global.css diff --git a/examples/preact-isomorphic/src/isomorphic/client.ts b/examples/preact-isomorphic/src/isomorphic/client.ts new file mode 100644 index 0000000..026ab90 --- /dev/null +++ b/examples/preact-isomorphic/src/isomorphic/client.ts @@ -0,0 +1,260 @@ +/** + * Preact Isomorphic Example + * + * This file demonstrates a todo application that works with both: + * 1. Server-side rendering (when imported by page.js) + * 2. Client-side hydration (when loaded in the browser) + * + * It uses the same component code for both environments. + */ +import { html, Component } from 'htm/preact'; +import { render } from 'preact'; +import { useCallback } from 'preact/hooks'; +import { useSignal, useComputed } from '@preact/signals'; +import type { ComponentChildren, JSX } from 'preact'; + +/** + * Header component props + */ +interface HeaderProps { + name: string; + subtitle?: string; +} + +/** + * App Header Component + * Displays the title of the application + */ +const Header = ({ name, subtitle }: HeaderProps): JSX.Element => html` +
+

${name}

+ ${subtitle && html`

${subtitle}

`} +
+`; + +/** + * Todo item props + */ +interface TodoItemProps { + text: string; + completed: boolean; + onToggle: () => void; + onDelete: () => void; +} + +/** + * Todo Item Component + * Renders a single todo item with completion toggle + */ +const TodoItem = ({ text, completed, onToggle, onDelete }: TodoItemProps): JSX.Element => html` +
  • + + +
  • +`; + +/** + * Counter Component using Signals + * Demonstrates Preact Signals for reactive state management + */ +const Counter = (): JSX.Element => { + // Create a signal for the count value + const count = useSignal(0); + + // Derived state that automatically updates when count changes + const doubled = useComputed(() => count.value * 2); + const isEven = useComputed(() => count.value % 2 === 0); + + // Event handlers + const increment = useCallback(() => { count.value++; }, []); + const decrement = useCallback(() => { count.value > 0 && count.value--; }, []); + const reset = useCallback(() => { count.value = 0; }, []); + + return html` +
    +

    Signal-based Counter

    +
    + Count: ${count} + Doubled: ${doubled} +
    +
    + + + +
    +
    + `; +}; + +/** + * Todo item structure + */ +interface Todo { + id: number; + text: string; + completed: boolean; +} + +/** + * Todo app props + */ +interface TodoAppProps { + title?: string; +} + +/** + * Todo app state + */ +interface TodoAppState { + todos: Todo[]; + newTodo: string; +} + +/** + * Todo Application Component + * Manages a list of todos with add/toggle/delete functionality + */ +class TodoApp extends Component { + constructor(props: TodoAppProps) { + super(props); + // Initialize with example todos + this.state = { + todos: [ + { id: 1, text: 'Learn about SSR', completed: true }, + { id: 2, text: 'Build isomorphic apps', completed: false }, + { id: 3, text: 'Deploy to production', completed: false } + ], + newTodo: '' + }; + } + + // Update the new todo input value + updateNewTodo = (e: JSX.TargetedEvent): void => { + this.setState({ newTodo: e.currentTarget.value }); + }; + + // Add a new todo item + addTodo = (e: JSX.TargetedEvent): void => { + e.preventDefault(); + const { todos, newTodo } = this.state; + + if (newTodo.trim()) { + this.setState({ + todos: [ + ...todos, + { + id: Date.now(), + text: newTodo, + completed: false + } + ], + newTodo: '' + }); + } + }; + + // Toggle a todo's completion status + toggleTodo = (id: number): void => { + const { todos } = this.state; + this.setState({ + todos: todos.map(todo => + todo.id === id + ? { ...todo, completed: !todo.completed } + : todo + ) + }); + }; + + // Delete a todo item + deleteTodo = (id: number): void => { + const { todos } = this.state; + this.setState({ + todos: todos.filter(todo => todo.id !== id) + }); + }; + + render({ title }: TodoAppProps, { todos, newTodo }: TodoAppState): JSX.Element { + const remaining = todos.filter(todo => !todo.completed).length; + + return html` +
    + <${Header} + name=${title || 'Todo App'} + subtitle="Server + Client Rendering Example" + /> + +
    + + +
    + +
      + ${todos.map(todo => html` + <${TodoItem} + key=${todo.id} + text=${todo.text} + completed=${todo.completed} + onToggle=${() => this.toggleTodo(todo.id)} + onDelete=${() => this.deleteTodo(todo.id)} + /> + `)} +
    + +
    + ${remaining} item${remaining !== 1 ? 's' : ''} remaining +
    + + <${Counter} /> +
    + `; + } +} + +/** + * Main page export for both server and client rendering + * This is what gets rendered in both environments + */ +export const page = (): JSX.Element => html` +
    + <${TodoApp} title="Isomorphic Todo App" /> + +
    +

    How This Works

    +

    + This page is rendered on the server first, then hydrated on the client. + The same component code runs in both environments. +

    +

    + Try adding todos and toggling them. These interactions are handled + by client-side JavaScript, but the initial HTML comes from the server. +

    +
    +
    +`; + +/** + * Client-side only code + * This code only runs in the browser, not during server rendering + */ +if (typeof window !== 'undefined') { + // Find the container that was server-rendered + const renderTarget = document.querySelector('.app-main'); + + // Hydrate the existing HTML with interactive components + if (renderTarget) { + render(page(), renderTarget); + console.log('βœ… Preact isomorphic app successfully hydrated'); + } +} diff --git a/examples/preact-isomorphic/src/isomorphic/page.ts b/examples/preact-isomorphic/src/isomorphic/page.ts new file mode 100644 index 0000000..6ce0773 --- /dev/null +++ b/examples/preact-isomorphic/src/isomorphic/page.ts @@ -0,0 +1,6 @@ +import { page } from './client.ts'; +import type { JSX } from 'preact'; + +export default function(): JSX.Element { + return page(); +} diff --git a/examples/preact-isomorphic/src/isomorphic/style.css b/examples/preact-isomorphic/src/isomorphic/style.css new file mode 100644 index 0000000..f3c7d43 --- /dev/null +++ b/examples/preact-isomorphic/src/isomorphic/style.css @@ -0,0 +1,167 @@ +/* ===== Isomorphic Example Styles ===== */ + +.isomorphic-container { + max-width: 800px; + margin: 0 auto; + padding: 1rem; +} + +.app-header { + margin-bottom: 1.5rem; + padding-bottom: 0.5rem; + border-bottom: 2px solid var(--accent-midground); +} + +.app-header h1 { + margin-bottom: 0.5rem; +} + +.app-header .subtitle { + font-style: italic; +} + +.todo-app { + background-color: var(--layer-background); + border-radius: 8px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + padding: 1.5rem; + margin-bottom: 2rem; +} + +.todo-form { + display: flex; + margin-bottom: 1rem; +} + +.todo-form input { + flex: 1; + padding: 0.5rem; + border: 1px solid var(--accent-midground); + border-radius: 4px 0 0 4px; + background-color: var(--background); + color: var(--text); +} + +.todo-form button { + padding: 0.5rem 1rem; + background-color: #4a8fe7; + color: white; + border: none; + border-radius: 0 4px 4px 0; + cursor: pointer; +} + +.todo-form button:hover { + background-color: #3a7fd7; +} + +.todo-list { + list-style: none; + padding: 0; + margin: 0 0 1rem 0; +} + +.todo-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.75rem; + border-bottom: 1px solid var(--accent-midground); +} + +.todo-item:last-child { + border-bottom: none; +} + +.todo-item.completed .todo-text { + text-decoration: line-through; + opacity: 0.6; +} + +.todo-label { + display: flex; + align-items: center; + gap: 0.5rem; + flex: 1; +} + +.delete-btn { + background-color: #ff5252; + color: white; + border: none; + border-radius: 4px; + padding: 0.25rem 0.5rem; + font-size: 0.8rem; + cursor: pointer; +} + +.delete-btn:hover { + background-color: #ff0000; +} + +.todo-stats { + font-size: 0.9rem; + margin-bottom: 1rem; +} + +.counter-widget { + background-color: var(--accent-background); + border-radius: 6px; + padding: 1rem; + margin-top: 1rem; +} + +.counter-widget h3 { + margin-top: 0; + margin-bottom: 0.5rem; + font-size: 1.1rem; +} + +.counter-display { + display: flex; + justify-content: space-between; + padding: 0.5rem; + margin-bottom: 0.5rem; + background-color: var(--background); + border-radius: 4px; + transition: background-color 0.2s; +} + +.counter-display.even { + background-color: rgba(173, 216, 230, 0.2); +} + +.counter-display.odd { + background-color: rgba(255, 228, 181, 0.2); +} + +.counter-controls { + display: flex; + gap: 0.5rem; +} + +.counter-controls button { + flex: 1; + padding: 0.5rem; + border: none; + border-radius: 4px; + background-color: #4a8fe7; + color: white; + cursor: pointer; +} + +.counter-controls button:hover { + background-color: #3a7fd7; +} + +.info-panel { + background-color: var(--accent-background); + border-left: 4px solid #4a8fe7; + padding: 1rem; + border-radius: 0 4px 4px 0; +} + +.info-panel h2 { + margin-top: 0; + font-size: 1.2rem; +} diff --git a/examples/preact-isomorphic/src/jsx-page/client.jsx b/examples/preact-isomorphic/src/jsx-page/client.jsx new file mode 100644 index 0000000..2d4707f --- /dev/null +++ b/examples/preact-isomorphic/src/jsx-page/client.jsx @@ -0,0 +1,121 @@ +import { render } from 'preact' +import { useState, useEffect } from 'preact/hooks' + +/** + * Simple JSX Client-Side Component Example + * + * This demonstrates a Preact component using JSX syntax that runs + * exclusively on the client-side (browser). Unlike the isomorphic example, + * this component is not pre-rendered on the server. + */ + +// User profile card component +const ProfileCard = ({ name, role, avatar, isActive }) => ( +
    +
    + {`${name}'s + +
    +
    +

    {name}

    +

    {role}

    +
    +
    +) + +// Main application component +export const Page = () => { + // State for the counter + const [count, setCount] = useState(0) + + // State for theme toggling + const [darkMode, setDarkMode] = useState(false) + + // State for user profiles + const [users, setUsers] = useState([ + { id: 1, name: 'Alex Johnson', role: 'Developer', isActive: true }, + { id: 2, name: 'Sam Taylor', role: 'Designer', isActive: false }, + { id: 3, name: 'Jordan Casey', role: 'Product Manager', isActive: true } + ]) + + // Effect to demonstrate client-side lifecycle + useEffect(() => { + console.log('Component mounted in the browser') + + // Update document title when count changes + document.title = `Count: ${count}` + + return () => { + console.log('Component will unmount') + } + }, [count]) + + // Toggle a user's active status + const toggleUserStatus = (userId) => { + setUsers(users.map(user => + user.id === userId + ? { ...user, isActive: !user.isActive } + : user + )) + } + + return ( +
    +

    Client-Side JSX Rendering

    + +
    + +
    + +
    +

    Interactive Counter: {count}

    +
    + + + +
    +
    + +
    +

    User Profiles

    +

    Click on a profile to toggle active status:

    +
    + {users.map(user => ( +
    toggleUserStatus(user.id)}> + +
    + ))} +
    +
    + +
    +

    How This Works

    +

    Unlike the isomorphic example, this component:

    +
      +
    • Renders entirely on the client-side
    • +
    • Uses native JSX syntax instead of HTM
    • +
    • Mounts to an empty container in the HTML
    • +
    +
    +
    + ) +} + +// Mount the component to the DOM +const renderTarget = document.querySelector('.jsx-app') +if (renderTarget) { + console.log({ renderTarget }) + render(, renderTarget) +} diff --git a/examples/preact-isomorphic/src/jsx-page/page.html b/examples/preact-isomorphic/src/jsx-page/page.html new file mode 100644 index 0000000..28c1a88 --- /dev/null +++ b/examples/preact-isomorphic/src/jsx-page/page.html @@ -0,0 +1,38 @@ +
    +

    Client-Side JSX Rendering Example

    + +
    +

    + This example demonstrates how Preact JSX components can be mounted to static HTML pages. + Unlike isomorphic rendering, the component below is rendered entirely on the client-side. +

    + +
    +

    Key Concepts:

    +
      +
    • Static HTML page with an empty container
    • +
    • JSX syntax for component definition
    • +
    • Client-side only rendering lifecycle
    • +
    • No server-side pre-rendering
    • +
    +
    +
    + +
    +

    Live Demo:

    + +
    +
    + +
    +

    How It Works:

    +

    + The empty div.jsx-app above serves as a mount point. When the page loads, + the Preact component defined in client.jsx is rendered into this container. +

    +

    + This approach is useful when adding interactive components to otherwise static pages, + or when you want to isolate complex UI logic to the client-side only. +

    +
    +
    diff --git a/examples/preact-isomorphic/src/jsx-page/style.css b/examples/preact-isomorphic/src/jsx-page/style.css new file mode 100644 index 0000000..08e2a6b --- /dev/null +++ b/examples/preact-isomorphic/src/jsx-page/style.css @@ -0,0 +1,234 @@ +/* ===== JSX Page Styles ===== */ + +.jsx-page-container { + max-width: 800px; + margin: 0 auto; + padding: 1rem; +} + +.explanation-section, +.demo-section, +.code-reference { + margin-bottom: 2rem; +} + +.key-points { + background-color: var(--accent-background); + padding: 1rem; + border-radius: 6px; + margin: 1rem 0; +} + +.key-points h3 { + margin-top: 0; +} + +.key-points ul { + margin-bottom: 0; +} + +.jsx-demo { + background-color: var(--layer-background); + border-radius: 8px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + padding: 1.5rem; + transition: background-color 0.3s, color 0.3s; +} + +/* Custom dark theme for the demo component - separate from the system dark mode */ +.jsx-demo.dark-theme { + background-color: #222; + color: #eee; +} + +.theme-toggle { + margin-bottom: 1.5rem; + text-align: right; +} + +.theme-toggle button { + background-color: #4a8fe7; + color: white; + border: none; + border-radius: 4px; + padding: 0.5rem 1rem; + cursor: pointer; + font-weight: 500; + transition: all 0.2s ease; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.theme-toggle button:hover { + background-color: #3a7fd7; + transform: translateY(-1px); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15); +} + +.theme-toggle button:active { + transform: translateY(1px); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); +} + +.jsx-demo.dark-theme .theme-toggle button { + background-color: #f1c40f; + color: #222; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); +} + +.jsx-demo.dark-theme .theme-toggle button:hover { + background-color: #f4d03f; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.4); +} + +/* Counter section button styles */ +.counter-section .counter-controls { + display: flex; + gap: 0.75rem; + margin-top: 1rem; +} + +.counter-section .counter-controls button { + padding: 0.5rem 1rem; + background-color: #4a8fe7; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + font-weight: 500; + transition: all 0.2s ease; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + flex: 1; +} + +.counter-section .counter-controls button:hover:not([disabled]) { + background-color: #3a7fd7; + transform: translateY(-1px); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15); +} + +.counter-section .counter-controls button:active:not([disabled]) { + transform: translateY(1px); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); +} + +.counter-section .counter-controls button:disabled { + background-color: var(--accent-midground); + cursor: not-allowed; + opacity: 0.7; +} + +.jsx-demo.dark-theme .counter-section .counter-controls button { + background-color: #4a8fe7; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); + color: white; +} + +.jsx-demo.dark-theme .counter-section .counter-controls button:hover:not([disabled]) { + background-color: #3a7fd7; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.4); +} + +.jsx-demo.dark-theme .counter-section .counter-controls button:disabled { + background-color: #555; +} + +.counter-section, +.profiles-section, +.explanation { + margin-bottom: 2rem; + padding-bottom: 1.5rem; + border-bottom: 1px solid var(--accent-midground); +} + +.jsx-demo.dark-theme .counter-section, +.jsx-demo.dark-theme .profiles-section, +.jsx-demo.dark-theme .explanation { + border-bottom-color: #444; +} + +.counter-section h3, +.profiles-section h3, +.explanation h3 { + margin-top: 0; + margin-bottom: 1rem; + font-weight: 600; +} + +.profiles-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); + gap: 1rem; + margin-top: 1rem; +} + +.profile-card { + background-color: var(--accent-background); + border-radius: 8px; + overflow: hidden; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1); + cursor: pointer; + transition: transform 0.2s, box-shadow 0.2s; + margin-bottom: 0.5rem; + height: 100%; +} + +.jsx-demo.dark-theme .profile-card { + background-color: #333; +} + +.profile-card:hover { + transform: translateY(-3px); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); +} + +.profile-header { + padding: 1rem; + position: relative; + display: flex; + justify-content: center; + background-color: var(--accent-midground); +} + +.jsx-demo.dark-theme .profile-header { + background-color: #444; +} + +.avatar { + width: 64px; + height: 64px; + border-radius: 50%; + object-fit: cover; +} + +.status-indicator { + position: absolute; + bottom: 1rem; + right: calc(50% - 40px); + width: 12px; + height: 12px; + border-radius: 50%; + border: 2px solid var(--background); +} + +.status-indicator.active { + background-color: #4caf50; +} + +.status-indicator.inactive { + background-color: #ccc; +} + +.profile-info { + padding: 1rem; + text-align: center; +} + +.profile-info h3 { + margin: 0 0 0.25rem 0; + font-size: 1.1rem; +} + +.profile-info .role { + margin: 0; + font-size: 0.9rem; +} diff --git a/examples/preact/src/layouts/root.layout.js b/examples/preact-isomorphic/src/layouts/root.layout.ts similarity index 62% rename from examples/preact/src/layouts/root.layout.js rename to examples/preact-isomorphic/src/layouts/root.layout.ts index 7b1e3d7..e4a03d6 100644 --- a/examples/preact/src/layouts/root.layout.js +++ b/examples/preact-isomorphic/src/layouts/root.layout.ts @@ -1,26 +1,41 @@ -// @ts-ignore -import { html } from 'htm/preact' -import { render } from 'preact-render-to-string' +import { html } from 'htm/preact'; +import { render } from 'preact-render-to-string'; +import type { VNode } from 'preact'; /** - * @template {Record} T - * @typedef {import('../build-pages/resolve-layout.js').LayoutFunction} LayoutFunction + * Page variables that can be passed to the layout */ +export interface PageVars { + title?: string; + siteName?: string; + defaultStyle?: boolean; + basePath?: string; +} + +/** + * Props for layout functions + */ +export interface LayoutProps { + vars: T; + scripts?: string[]; + styles?: string[]; + children: string | VNode; + pages?: unknown[]; + page?: unknown; +} + +/** + * Type definition for layout functions + */ +export type LayoutFunction = (props: LayoutProps) => string; /** * Build all of the bundles using esbuild. - * - * @type {LayoutFunction<{ - * title: string, - * siteName: string, - * defaultStyle: boolean, - * basePath: string - * }>} */ -export default function defaultRootLayout ({ +export default function defaultRootLayout({ vars: { title, - siteName = 'TopBun', + siteName = 'Domstack', basePath, /* defaultStyle = true Set this to false in global or page to disable the default style in the default layout */ }, @@ -29,7 +44,7 @@ export default function defaultRootLayout ({ children, /* pages */ /* page */ -}) { +}: LayoutProps): string { return /* html */` @@ -49,11 +64,11 @@ export default function defaultRootLayout ({ ${render(html` ${typeof children === 'string' - ? html`
    ` + ? html`
    ` : html`
    ${children}
    ` } `)} - ` + `; } diff --git a/examples/preact-isomorphic/tsconfig.json b/examples/preact-isomorphic/tsconfig.json new file mode 100644 index 0000000..c0a6886 --- /dev/null +++ b/examples/preact-isomorphic/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "@voxpelli/tsconfig/node20.json", + "compilerOptions": { + "skipLibCheck": true, + "erasableSyntaxOnly": true, + "allowImportingTsExtensions": true, + "rewriteRelativeImportExtensions": true, + "verbatimModuleSyntax": true, + "jsx": "react-jsx", + "jsxImportSource": "preact" + }, + "include": [ + "**/*", + ], + "exclude": [ + "**/*.js", + "node_modules", + "coverage", + ".github" + ] +} diff --git a/examples/preact/src/README.md b/examples/preact/src/README.md deleted file mode 100644 index 93af148..0000000 --- a/examples/preact/src/README.md +++ /dev/null @@ -1,6 +0,0 @@ -# Preact example - -This is a preact example. - -- [Isomorphic Component Rendering](./isomorphic/) -- [JSX-page](./jsx-page/) diff --git a/examples/preact/src/isomorphic/client.js b/examples/preact/src/isomorphic/client.js deleted file mode 100644 index cbd8fed..0000000 --- a/examples/preact/src/isomorphic/client.js +++ /dev/null @@ -1,55 +0,0 @@ -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/preact/src/isomorphic/page.js b/examples/preact/src/isomorphic/page.js deleted file mode 100644 index d1d1ea9..0000000 --- a/examples/preact/src/isomorphic/page.js +++ /dev/null @@ -1,5 +0,0 @@ -import { page } from './client.js' - -export default () => { - return page() -} diff --git a/examples/preact/src/jsx-page/client.jsx b/examples/preact/src/jsx-page/client.jsx deleted file mode 100644 index fa47236..0000000 --- a/examples/preact/src/jsx-page/client.jsx +++ /dev/null @@ -1,12 +0,0 @@ -import { render } from 'preact' - -export const page = () => { - return ( -
    - look ma, client side jsx! -
    - ) -} - -const renderTarget = document.querySelector('.jsx-app') -render(page(), renderTarget) diff --git a/examples/preact/src/jsx-page/page.html b/examples/preact/src/jsx-page/page.html deleted file mode 100644 index bf0074d..0000000 --- a/examples/preact/src/jsx-page/page.html +++ /dev/null @@ -1,4 +0,0 @@ -
    -

    This is an html page, with a client.jsx that mounts onto it

    -
    -
    diff --git a/examples/react/package.json b/examples/react/package.json new file mode 100644 index 0000000..a4d0666 --- /dev/null +++ b/examples/react/package.json @@ -0,0 +1,29 @@ +{ + "name": "@domstack/react-typescript-example", + "version": "0.0.0", + "type": "module", + "scripts": { + "start": "npm run watch", + "build": "npm run clean && domstack", + "clean": "rm -rf public && mkdir -p public", + "watch": "npm run clean && domstack --watch", + "typecheck": "tsc --noEmit" + }, + "author": "Bret Comnes (https://bret.io/)", + "license": "MIT", + "dependencies": { + "@domstack/static": "file:../../.", + "htm": "^3.1.1", + "mine.css": "^9.0.1", + "react": "^19.1.1" + }, + "devDependencies": { + "@types/react": "^19.1.10", + "@types/react-dom": "^19.1.7", + "@voxpelli/tsconfig": "^15.0.0", + "npm-run-all2": "^6.0.0", + "react-dom": "^19.1.1", + "typescript": "~5.8.2", + "highlight.js": "^11.9.0" + } +} diff --git a/examples/react/src/README.md b/examples/react/src/README.md new file mode 100644 index 0000000..ad7a608 --- /dev/null +++ b/examples/react/src/README.md @@ -0,0 +1,53 @@ +# React with TypeScript in DOMStack + +This example demonstrates how to use React with TypeScript in DOMStack for client-side rendering. Unlike the Preact examples that come with DOMStack by default, this example shows how to override the default JSX configuration to use React with TypeScript instead. + +## What This Example Shows + +- How to configure ESBuild to use React instead of Preact for TSX +- Client-side rendering with React components written in TypeScript +- Type-safe React hooks for state management +- TypeScript interfaces and type definitions +- Integration with DOMStack's build system + +## Key Components + +1. **ESBuild Configuration**: Custom `esbuild.settings.ts` that overrides the default Preact JSX settings +2. **React Components**: Client-side components with typed hooks and state +3. **Static HTML Mount Points**: HTML pages with mount points for React components +4. **TypeScript Interfaces**: Type definitions for props, state, and functions + +## Example Structure + +- `globals/esbuild.settings.ts` - Configuration to use React instead of Preact +- [`react-page/`](./react-page/) - Client-side React component example with TypeScript +- `layouts/` - Basic layout structure + +## How It Works + +Unlike isomorphic examples with Preact, this example focuses on client-side rendering only. The workflow is: + +1. The HTML is served with empty containers +2. React components are loaded and mounted to these containers +3. All rendering happens in the browser + +## Getting Started + +Run the following commands: + +```bash +npm install +npm run build +``` + +To watch for changes during development: + +```bash +npm run watch +``` + +## React with TypeScript vs. Preact in DOMStack + +DOMStack uses Preact by default because it's smaller and has a compatible API with React. This example shows how to use React with TypeScript instead if you prefer or need specific React features with type safety. + +The key difference is in the `esbuild.settings.ts` file, which configures ESBuild to use React's TSX transformer and runtime, along with TypeScript support. diff --git a/examples/react/src/globals/esbuild.settings.ts b/examples/react/src/globals/esbuild.settings.ts new file mode 100644 index 0000000..da74b2c --- /dev/null +++ b/examples/react/src/globals/esbuild.settings.ts @@ -0,0 +1,21 @@ +/** + * Custom ESBuild Settings for React with TypeScript + * + * This file overrides the default DOMStack ESBuild configuration + * to replace Preact with React for TSX transformation and runtime. + */ +import type { BuildOptions } from 'esbuild' + +/** + * Configure ESBuild settings for React with TypeScript support + * + * @param esbuildSettings - The default ESBuild configuration + * @returns The modified ESBuild configuration + */ +export default async function esbuildSettingsOverride(esbuildSettings: BuildOptions): Promise { + // Override the JSX settings to use React instead of Preact + esbuildSettings.jsx = 'automatic' + esbuildSettings.jsxImportSource = 'react' + + return esbuildSettings +} diff --git a/examples/react/src/globals/global.client.ts b/examples/react/src/globals/global.client.ts new file mode 100644 index 0000000..782a44d --- /dev/null +++ b/examples/react/src/globals/global.client.ts @@ -0,0 +1,76 @@ +/** + * Global client-side TypeScript for React example + * + * This file is loaded on all pages before any page-specific TypeScript. + * It's a good place to add global event listeners, polyfills, or + * other initialization code that should run on every page. + */ + +// Define interfaces for our global utilities +interface DomstackUtils { + formatDate(date: Date): string; + getRandomItem(array: T[]): T; +} + +// Extend the Window interface to include our global utilities +interface Window { + domstackUtils: DomstackUtils; +} + +console.log('React example global client TypeScript loaded'); + +// Add a class to indicate JavaScript is enabled +document.documentElement.classList.add('js-enabled'); + +// Basic example of a global utility function +window.domstackUtils = { + /** + * Format a date in a human-readable format + * @param date - The date to format + * @returns Formatted date string + */ + formatDate: (date: Date): string => { + return new Intl.DateTimeFormat('en-US', { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric' + }).format(date); + }, + + /** + * Get a random item from an array + * @param array - The array to get a random item from + * @returns A random item from the array + */ + getRandomItem: (array: T[]): T => { + if (array.length === 0) { + throw new Error("Cannot get random item from empty array"); + } + const index = Math.floor(Math.random() * array.length); + // This assertion is safe because we've checked that array is not empty + return array[index] as T; + } +}; + +// Add dark mode detection +const prefersDarkMode: MediaQueryList = window.matchMedia('(prefers-color-scheme: dark)'); +if (prefersDarkMode.matches) { + document.body.classList.add('dark-mode-preferred'); +} + +// Listen for dark mode changes +prefersDarkMode.addEventListener('change', (e: MediaQueryListEvent): void => { + if (e.matches) { + document.body.classList.add('dark-mode-preferred'); + } else { + document.body.classList.remove('dark-mode-preferred'); + } +}); + +// Example of measuring and logging performance +const pageLoadTime: number = performance.now(); +window.addEventListener('load', (): void => { + const totalLoadTime: number = performance.now() - pageLoadTime; + console.log(`Page fully loaded in ${totalLoadTime.toFixed(2)}ms`); +}); \ No newline at end of file diff --git a/examples/react/src/globals/global.css b/examples/react/src/globals/global.css new file mode 100644 index 0000000..06985a0 --- /dev/null +++ b/examples/react/src/globals/global.css @@ -0,0 +1,120 @@ +@import 'mine.css/dist/mine.css'; +@import 'mine.css/dist/layout.css'; +@import 'highlight.js/styles/github-dark-dimmed.css'; + +/* Custom React Example Styles */ +:root { + --primary-color: #61dafb; + --secondary-color: #282c34; + --text-color: #333; + --background-color: #f5f5f5; + --card-background: #fff; + --success-color: #4caf50; + --warning-color: #ff9800; + --error-color: #f44336; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, + Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; + color: var(--text-color); + background-color: var(--background-color); + line-height: 1.6; +} + +.app-container { + max-width: 800px; + margin: 0 auto; + padding: 2rem; +} + +.react-demo { + background-color: var(--card-background); + border-radius: 8px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + padding: 1.5rem; + margin-bottom: 2rem; +} + +.react-header { + display: flex; + align-items: center; + margin-bottom: 1.5rem; +} + +.react-logo { + animation: spin 10s linear infinite; + height: 40px; + margin-right: 1rem; +} + +@keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +.button { + background-color: var(--primary-color); + color: var(--secondary-color); + border: none; + border-radius: 4px; + padding: 0.5rem 1rem; + font-size: 1rem; + cursor: pointer; + transition: background-color 0.2s, transform 0.1s; +} + +.button:hover { + background-color: #4ac0e0; + transform: translateY(-1px); +} + +.button:active { + transform: translateY(1px); +} + +.button-group { + display: flex; + gap: 0.5rem; + margin: 1rem 0; +} + +.card { + border-radius: 6px; + border: 1px solid #eee; + padding: 1rem; + margin-bottom: 1rem; + transition: box-shadow 0.2s; +} + +.card:hover { + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); +} + +.info-panel { + background-color: rgba(97, 218, 251, 0.1); + border-left: 4px solid var(--primary-color); + padding: 1rem; + margin: 1rem 0; + border-radius: 0 4px 4px 0; +} + +/* Form elements */ +input, select, textarea { + padding: 0.5rem; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 1rem; + width: 100%; + margin-bottom: 1rem; +} + +input:focus, select:focus, textarea:focus { + outline: none; + border-color: var(--primary-color); + box-shadow: 0 0 0 2px rgba(97, 218, 251, 0.2); +} diff --git a/examples/react/src/layouts/root.layout.ts b/examples/react/src/layouts/root.layout.ts new file mode 100644 index 0000000..a06208e --- /dev/null +++ b/examples/react/src/layouts/root.layout.ts @@ -0,0 +1,62 @@ +import { html } from 'htm/react' +import { renderToStaticMarkup } from 'react-dom/server' + +// Define TypeScript interfaces for layout props +interface LayoutVars { + title?: string; + siteName?: string; + basePath?: string; +} + +interface LayoutProps { + vars: LayoutVars; + scripts?: string[]; + styles?: string[]; + children: string | any; +} + +/** + * Basic layout for React with TypeScript example + * + * This layout is only used for the initial HTML page structure. + * React components will be mounted client-side after the page loads. + * Uses React DOM Server for server-side rendering. + */ +export default function rootLayout({ + vars: { + title, + siteName = 'React TypeScript Example', + basePath, + }, + scripts, + styles, + children, +}: LayoutProps): string { + return /* html */` + + + ${renderToStaticMarkup(html` + + + ${`${title ? `${title}` : ''}${title && siteName ? ' | ' : ''}${siteName}`} + + + ${scripts + ? scripts.map(script => html``) - : ''} - ${styles - ? styles.map(style => /* html */``) - : ''} - - - ${children} - + + + ${siteName || ''}${title ? ` | ${title}` : ''} + + ${scripts + ? scripts.map(script => /* html */``).join('\n ') + : ''} + ${styles + ? styles.map(style => /* html */``).join('\n ') + : ''} + + +
    + ${children} +
    + -` + ` } diff --git a/examples/tailwind/package.json b/examples/tailwind/package.json index 70657e9..56d6590 100644 --- a/examples/tailwind/package.json +++ b/examples/tailwind/package.json @@ -1,23 +1,25 @@ { - "name": "@top-bun/preact-example", + "name": "@domstack/preact-example", "version": "0.0.0", "type": "module", "scripts": { "start": "npm run watch", - "build": "npm run clean && top-bun", + "build": "npm run clean && domstack", "clean": "rm -rf public && mkdir -p public", - "watch": "npm run clean && tb --watch" + "watch": "npm run clean && dom --watch" }, "author": "Bret Comnes (https://bret.io/)", "license": "MIT", - "dependencies": {}, + "dependencies": { + "@tailwindcss/typography": "^0.5.16" + }, "devDependencies": { - "npm-run-all2": "^6.0.0", + "@domstack/static": "file:../../.", "@preact/signals": "^2.0.0", + "esbuild-plugin-tailwindcss": "2.1.0", "htm": "^3.1.1", + "npm-run-all2": "^6.0.0", "preact": "^10.24.0", - "preact-render-to-string": "^6.5.11", - "top-bun": "../../.", - "esbuild-plugin-tailwindcss": "2.0.1" + "preact-render-to-string": "^6.5.11" } } diff --git a/examples/tailwind/src/README.md b/examples/tailwind/src/README.md index 7febf99..069373d 100644 --- a/examples/tailwind/src/README.md +++ b/examples/tailwind/src/README.md @@ -1,5 +1,40 @@ -# Preact example +# Tailwind CSS in DOMStack -This is a preact example WITH TAILWIND! +This example demonstrates how to integrate Tailwind CSS with DOMStack, providing a powerful utility-first CSS framework for your websites and applications. -[Isomorphic Component Rendering](./isomorphic/) +## What is Tailwind CSS? + +Tailwind CSS is a utility-first CSS framework that allows you to build custom designs without leaving your HTML. Instead of pre-designed components, Tailwind provides low-level utility classes that let you build completely custom designs. + +## How This Example Works + +This example shows how to: + +1. Configure ESBuild to process Tailwind CSS +2. Import Tailwind into your global CSS +3. Use Tailwind classes in Preact components +4. Create responsive, utility-based designs + +## Key Files + +- `globals/esbuild.settings.js` - Configures the Tailwind plugin +- `globals/global.css` - Imports the Tailwind framework +- `isomorphic/client.js` - Demonstrates Tailwind classes in components + +## Example Component + +The example includes a Todo application with Tailwind styling for: +- Responsive layouts +- Spacing utilities +- Flexbox components +- Colors and shadows +- Interactive states (hover effects) + +## Getting Started + +Explore the [Isomorphic Component Rendering](./isomorphic/) example to see Tailwind in action. + +## Learn More + +- [Tailwind CSS Documentation](https://tailwindcss.com/docs) +- [DOMStack Documentation](https://github.com/bcomnes/domstack) diff --git a/examples/tailwind/src/globals/esbuild.settings.js b/examples/tailwind/src/globals/esbuild.settings.js index 7c0ba4e..171da22 100644 --- a/examples/tailwind/src/globals/esbuild.settings.js +++ b/examples/tailwind/src/globals/esbuild.settings.js @@ -1,8 +1,26 @@ +/** + * Tailwind CSS Integration for DOMStack + * + * This file configures ESBuild to process Tailwind CSS in your project. + * It enables utility-first CSS classes that can be used directly in your HTML and components. + */ import tailwindPlugin from 'esbuild-plugin-tailwindcss' +/** + * Configure ESBuild settings to include Tailwind CSS processing + * + * @param {import('esbuild').BuildOptions} esbuildSettings - The default ESBuild configuration + * @return {Promise} - The modified ESBuild configuration + */ export default async function esbuildSettingsOverride (esbuildSettings) { + // Add the Tailwind plugin to the ESBuild configuration esbuildSettings.plugins = [ tailwindPlugin(), ] + + // You can also add other ESBuild settings as needed + // esbuildSettings.minify = true; + // esbuildSettings.sourcemap = true; + return esbuildSettings } diff --git a/examples/tailwind/src/globals/global.css b/examples/tailwind/src/globals/global.css index bdb109c..3fbea23 100644 --- a/examples/tailwind/src/globals/global.css +++ b/examples/tailwind/src/globals/global.css @@ -1,2 +1,3 @@ @import "tailwindcss"; +@plugin "@tailwindcss/typography"; diff --git a/examples/tailwind/src/layouts/root.layout.js b/examples/tailwind/src/layouts/root.layout.js index 3a6b6dc..7616783 100644 --- a/examples/tailwind/src/layouts/root.layout.js +++ b/examples/tailwind/src/layouts/root.layout.js @@ -20,7 +20,7 @@ import { render } from 'preact-render-to-string' export default function defaultRootLayout ({ vars: { title, - siteName = 'TopBun', + siteName = 'Domstack', basePath, }, scripts, @@ -44,7 +44,7 @@ export default function defaultRootLayout ({ `)} ${render(html` - + ${typeof children === 'string' ? html`
    ` : html`
    ${children}
    ` diff --git a/examples/type-stripping/package.json b/examples/type-stripping/package.json index 3da3e9e..f5ce358 100644 --- a/examples/type-stripping/package.json +++ b/examples/type-stripping/package.json @@ -1,12 +1,12 @@ { - "name": "@top-bun/preact-example", + "name": "@domstack/preact-example", "version": "0.0.0", "type": "module", "scripts": { "start": "npm run watch", - "build": "npm run clean && top-bun", + "build": "npm run clean && domstack", "clean": "rm -rf public && mkdir -p public", - "watch": "npm run clean && tb --watch" + "watch": "npm run clean && dom --watch" }, "author": "Bret Comnes (https://bret.io/)", "license": "MIT", @@ -17,7 +17,7 @@ "mine.css": "^9.0.1", "preact": "^10.24.0", "preact-render-to-string": "^6.5.11", - "top-bun": "../../." + "@domstack/static": "file:../../." }, "devDependencies": { "@voxpelli/tsconfig": "^15.0.0", diff --git a/examples/type-stripping/src/README.md b/examples/type-stripping/src/README.md index f922c8c..52bd0b3 100644 --- a/examples/type-stripping/src/README.md +++ b/examples/type-stripping/src/README.md @@ -1,6 +1,50 @@ -# Preact example +# TypeScript Support in DOMStack -This is a preact example. +This example demonstrates how DOMStack handles TypeScript files by automatically stripping types during the build process, allowing you to use TypeScript without additional configuration. -- [Isomorphic Component Rendering](./isomorphic/) -- [tsx-client]('./isomorphic/') +## What is Type Stripping? + +Type stripping is the process of removing TypeScript type annotations during compilation to produce standard JavaScript. DOMStack performs this automatically using ESBuild, giving you: + +- Full TypeScript type checking during development +- Clean JavaScript output without runtime type overhead +- No need for separate TypeScript build steps + +## Features Demonstrated + +This example showcases: + +- `.ts` files for standard TypeScript +- `.tsx` files for JSX with TypeScript +- Type imports and exports +- Interface definitions +- Automatic handling of type annotations + +## Examples in This Project + +- [Isomorphic Component Rendering](./isomorphic/) - TypeScript with Preact +- [TSX Client Example](./tsx-page/) - TypeScript JSX components + +## How It Works + +1. Write your code using full TypeScript syntax +2. DOMStack detects `.ts` and `.tsx` file extensions +3. ESBuild automatically strips type annotations during bundling +4. Your pages and components work exactly like JavaScript versions + +## Benefits of TypeScript in DOMStack + +- **Developer Experience**: Get IDE autocompletion and type checking +- **Error Prevention**: Catch type errors before runtime +- **Documentation**: Types serve as self-documenting code +- **Zero Runtime Cost**: All types are removed in the final output + +## Getting Started with TypeScript + +To use TypeScript in your DOMStack project: + +1. Create files with `.ts` or `.tsx` extensions +2. Write TypeScript code normally +3. DOMStack will handle the rest automatically + +No additional setup required! diff --git a/examples/type-stripping/src/isomorphic/client.ts b/examples/type-stripping/src/isomorphic/client.ts index cbd8fed..78a66bb 100644 --- a/examples/type-stripping/src/isomorphic/client.ts +++ b/examples/type-stripping/src/isomorphic/client.ts @@ -1,55 +1,427 @@ -import { html, Component } from 'htm/preact' +import { html } from 'htm/preact' import { render } from 'preact' -import { useCallback } from 'preact/hooks' -import { useSignal, useComputed } from '@preact/signals' +import { useState, useCallback, useEffect } from 'preact/hooks' -const Header = ({ name }) => html`

    ${name} List

    ` -const Footer = props => { - const count = useSignal(0) - const double = useComputed(() => count.value * 2) +// ===== Type Definitions ===== - const handleClick = useCallback(() => { - count.value++ - }, [count]) +interface Task { + id: string; + title: string; + completed: boolean; + priority: 'low' | 'medium' | 'high'; + createdAt: Date; +} + +interface TaskFilterState { + status: 'all' | 'active' | 'completed'; + priority: 'all' | 'low' | 'medium' | 'high'; +} + +interface TaskStats { + total: number; + active: number; + completed: number; + byPriority: { + low: number; + medium: number; + high: number; + }; +} + +// Component Props +interface TaskItemProps { + task: Task; + onToggle: (id: string) => void; + onDelete: (id: string) => void; +} - return html`
    - ${count} - ${double} - ${props.children} - -
    ` +interface TaskFilterProps { + filters: TaskFilterState; + onFilterChange: (filters: Partial) => void; + stats: TaskStats; } -class App extends Component { - addTodo () { - const { todos = [] } = this.state - this.setState({ todos: todos.concat(`Item ${todos.length}`) }) +interface TaskFormProps { + onAddTask: (task: Omit) => void; +} + +interface TaskListProps { + tasks: Task[]; + onToggleTask: (id: string) => void; + onDeleteTask: (id: string) => void; +} + +// ===== Helper Functions ===== + +// Generate a unique ID (simplified version) +const generateId = (): string => { + return Date.now().toString(36) + Math.random().toString(36).substr(2, 5); +}; + +// Calculate task statistics +const calculateTaskStats = (tasks: Task[]): TaskStats => { + const completed = tasks.filter(t => t.completed).length; + + return { + total: tasks.length, + active: tasks.length - completed, + completed, + byPriority: { + low: tasks.filter(t => t.priority === 'low').length, + medium: tasks.filter(t => t.priority === 'medium').length, + high: tasks.filter(t => t.priority === 'high').length + } + }; +}; + +// Filter tasks based on current filters +const filterTasks = (tasks: Task[], filters: TaskFilterState): Task[] => { + return tasks.filter(task => { + // Filter by status + if (filters.status === 'active' && task.completed) return false; + if (filters.status === 'completed' && !task.completed) return false; + + // Filter by priority + if (filters.priority !== 'all' && task.priority !== filters.priority) return false; + + return true; + }); +}; + +// ===== Components ===== + +// Individual Task Item Component +const TaskItem = ({ task, onToggle, onDelete }: TaskItemProps) => { + const getPriorityClass = (priority: Task['priority']): string => { + switch (priority) { + case 'high': return 'task-priority-high'; + case 'medium': return 'task-priority-medium'; + case 'low': return 'task-priority-low'; + default: return ''; + } + }; + + return html` +
  • +
    + onToggle(task.id)} + /> + ${task.title} + ${task.priority} +
    + +
  • + `; +}; + +// Task Filter Component +const TaskFilter = ({ filters, onFilterChange, stats }: TaskFilterProps) => { + return html` +
    +
    +

    Filter by Status

    +
    + + + +
    +
    + +
    +

    Filter by Priority

    +
    + + + + +
    +
    +
    + `; +}; + +// Task Form Component +const TaskForm = ({ onAddTask }: TaskFormProps) => { + const [error, setError] = useState(null); + + // Simple approach using form submit + const handleSubmit = (e: Event): void => { + e.preventDefault(); + + // Get form inputs directly + const form = e.target as HTMLFormElement; + const titleInput = form.querySelector('input[name="title"]') as HTMLInputElement; + const prioritySelect = form.querySelector('select[name="priority"]') as HTMLSelectElement; + + // Validate + if (!titleInput || !titleInput.value.trim()) { + setError('Task title cannot be empty'); + return; + } + + // Add task + onAddTask({ + title: titleInput.value.trim(), + completed: false, + priority: prioritySelect.value as Task['priority'] + }); + + // Reset form + titleInput.value = ''; + prioritySelect.value = 'medium'; + setError(null); + }; + + return html` +
    +
    + + + + + +
    + + ${error ? html`

    ${error}

    ` : null} +
    + `; +}; + +// Task List Component +const TaskList = ({ tasks, onToggleTask, onDeleteTask }: TaskListProps) => { + if (tasks.length === 0) { + return html`

    No tasks to display

    `; } - render ({ page }, { todos = [] }) { - return html` -
    - <${Header} name="ToDo's (${page})" /> -
      - ${todos.map(todo => html` -
    • ${todo}
    • - `)} -
    - - <${Footer}>footer content here -
    - ` + return html` +
      + ${tasks.map(task => html` + <${TaskItem} + key=${task.id} + task=${task} + onToggle=${onToggleTask} + onDelete=${onDeleteTask} + /> + `)} +
    + `; +}; + +// ===== Main Application ===== + +// Sample initial tasks +const initialTasks: Task[] = [ + { + id: 'task-1', + title: 'Learn TypeScript', + completed: true, + priority: 'high', + createdAt: new Date() + }, + { + id: 'task-2', + title: 'Build isomorphic app with DOMStack', + completed: false, + priority: 'medium', + createdAt: new Date() + }, + { + id: 'task-3', + title: 'Deploy application', + completed: false, + priority: 'low', + createdAt: new Date() } -} +]; + +// Main TaskManager component +const TaskManager = () => { + // TypeScript typed state + const [tasks, setTasks] = useState(initialTasks); + const [filters, setFilters] = useState({ + status: 'all', + priority: 'all' + }); + + // Memoized task stats + const [stats, setStats] = useState(calculateTaskStats(tasks)); + + // Update stats when tasks change + useEffect(() => { + setStats(calculateTaskStats(tasks)); + }, [tasks]); + + // Filter tasks based on current filters + const filteredTasks = filterTasks(tasks, filters); + + // Event handlers with TypeScript types + const handleAddTask = useCallback((newTask: Omit): void => { + const task: Task = { + ...newTask, + id: generateId(), + createdAt: new Date() + }; + + setTasks(prevTasks => [...prevTasks, task]); + }, []); -export const page = () => html` - <${App} page="Isomorphic"/> - <${Footer}>footer content here - <${Footer}>footer content here - ` + const handleToggleTask = useCallback((id: string): void => { + setTasks(prevTasks => + prevTasks.map(task => + task.id === id ? { ...task, completed: !task.completed } : task + ) + ); + }, []); + const handleDeleteTask = useCallback((id: string): void => { + setTasks(prevTasks => prevTasks.filter(task => task.id !== id)); + }, []); + + const handleFilterChange = useCallback((newFilters: Partial): void => { + setFilters(prevFilters => ({ ...prevFilters, ...newFilters })); + }, []); + + const handleClearCompleted = useCallback((): void => { + setTasks(prevTasks => prevTasks.filter(task => !task.completed)); + }, []); + + // Data persistence - example of useEffect with TypeScript + useEffect((): void => { + // Only run in browser context + if (typeof window !== 'undefined') { + const savedTasks = localStorage.getItem('isomorphic-tasks'); + if (savedTasks) { + try { + // Parse and restore dates properly + const parsedTasks = JSON.parse(savedTasks, (key, value) => { + return key === 'createdAt' ? new Date(value) : value; + }); + setTasks(parsedTasks); + } catch (err) { + console.error('Failed to parse saved tasks:', err); + } + } + } + }, []); + + useEffect((): (() => void) | void => { + // Only run in browser context + if (typeof window !== 'undefined') { + const handleSave = (): void => { + localStorage.setItem('isomorphic-tasks', JSON.stringify(tasks)); + }; + + // Save on page unload + window.addEventListener('beforeunload', handleSave); + + // Clean up event listener + return (): void => { + window.removeEventListener('beforeunload', handleSave); + }; + } + }, [tasks]); + + return html` +
    +
    +

    Isomorphic Task Manager

    +

    Demonstrating TypeScript with DOMStack

    +
    + + <${TaskForm} onAddTask=${handleAddTask} /> + + <${TaskFilter} + filters=${filters} + onFilterChange=${handleFilterChange} + stats=${stats} + /> + + <${TaskList} + tasks=${filteredTasks} + onToggleTask=${handleToggleTask} + onDeleteTask=${handleDeleteTask} + /> + +
    + +
    +
    + `; +}; + +// Export for isomorphic rendering +export const page = (): any => html`<${TaskManager} />`; + +// Client-side rendering if (typeof window !== 'undefined') { - const renderTarget = document.querySelector('.app-main') - render(page(), renderTarget) + // Wait for DOM to be ready + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', () => { + const renderTarget = document.querySelector('.app-main'); + if (renderTarget) { + render(page(), renderTarget); + } + }); + } else { + const renderTarget = document.querySelector('.app-main'); + if (renderTarget) { + render(page(), renderTarget); + } + } } diff --git a/examples/type-stripping/src/isomorphic/page.ts b/examples/type-stripping/src/isomorphic/page.ts index a348ef6..150913e 100644 --- a/examples/type-stripping/src/isomorphic/page.ts +++ b/examples/type-stripping/src/isomorphic/page.ts @@ -1,5 +1,8 @@ import { page } from './client.ts' +// This demonstrates the isomorphic nature of the application: +// - In server context: renders the initial HTML +// - In browser context: hydrates with interactive functionality export default () => { return page() } diff --git a/examples/type-stripping/src/isomorphic/style.css b/examples/type-stripping/src/isomorphic/style.css new file mode 100644 index 0000000..8d1715c --- /dev/null +++ b/examples/type-stripping/src/isomorphic/style.css @@ -0,0 +1,275 @@ +/* Isomorphic Task Manager Styles */ + +.task-manager { + max-width: 800px; + margin: 0 auto; + padding: 2rem; + font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; +} + +.app-header { + text-align: center; + margin-bottom: 2rem; + padding-bottom: 1rem; + border-bottom: 1px solid #eaeaea; +} + +.app-header h1 { + margin: 0 0 0.5rem; + color: #333; +} + +.app-header p { + margin: 0; + color: #666; + font-size: 1rem; +} + +/* Task Form */ +.task-form { + margin-bottom: 2rem; +} + +.form-group { + display: flex; + gap: 0.5rem; +} + +.task-form input { + flex: 1; + padding: 0.75rem; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 1rem; +} + +.task-form input.error { + border-color: #e74c3c; +} + +.task-form select { + width: 150px; + padding: 0.75rem; + border: 1px solid #ddd; + border-radius: 4px; + background-color: white; +} + +.task-form button { + background-color: #3498db; + color: white; + border: none; + border-radius: 4px; + padding: 0.75rem 1.5rem; + cursor: pointer; + font-weight: 600; +} + +.task-form button:hover { + background-color: #2980b9; +} + +.task-form button:disabled { + background-color: #95a5a6; + cursor: not-allowed; +} + +.error-message { + color: #e74c3c; + margin-top: 0.5rem; + font-size: 0.875rem; +} + +/* Task Filters */ +.task-filters { + margin-bottom: 2rem; + display: flex; + gap: 2rem; + flex-wrap: wrap; +} + +.filter-group h3 { + font-size: 1rem; + margin-top: 0; + margin-bottom: 0.5rem; + color: #555; +} + +.btn-group { + display: flex; + gap: 0.5rem; +} + +.btn-group button { + background-color: #f1f1f1; + border: 1px solid #ddd; + border-radius: 4px; + padding: 0.5rem 1rem; + cursor: pointer; + font-size: 0.875rem; +} + +.btn-group button:hover { + background-color: #e7e7e7; +} + +.btn-group button.active { + background-color: #2c3e50; + color: white; + border-color: #2c3e50; +} + +/* Task List */ +.task-list { + list-style: none; + padding: 0; + margin: 0 0 2rem; +} + +.task-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 1rem; + border: 1px solid #eee; + border-radius: 4px; + margin-bottom: 0.5rem; + transition: all 0.2s ease; +} + +.task-item:hover { + box-shadow: 0 2px 5px rgba(0,0,0,0.1); +} + +.task-item.completed { + background-color: #f9f9f9; + opacity: 0.8; +} + +.task-item.completed .task-title { + text-decoration: line-through; + color: #7f8c8d; +} + +.task-content { + display: flex; + align-items: center; + gap: 0.75rem; + flex: 1; +} + +.task-title { + font-size: 1rem; + margin-right: 1rem; +} + +.task-priority { + font-size: 0.75rem; + padding: 0.25rem 0.5rem; + border-radius: 3px; + font-weight: 600; + text-transform: uppercase; +} + +.task-priority-high { + border-left: 4px solid #e74c3c; +} + +.task-priority-high .task-priority { + background-color: #fdedec; + color: #e74c3c; +} + +.task-priority-medium { + border-left: 4px solid #f39c12; +} + +.task-priority-medium .task-priority { + background-color: #fef5e7; + color: #f39c12; +} + +.task-priority-low { + border-left: 4px solid #3498db; +} + +.task-priority-low .task-priority { + background-color: #ebf5fb; + color: #3498db; +} + +.delete-btn { + background-color: transparent; + color: #7f8c8d; + border: 1px solid #ddd; + border-radius: 4px; + padding: 0.375rem 0.75rem; + cursor: pointer; + font-size: 0.875rem; +} + +.delete-btn:hover { + background-color: #e74c3c; + color: white; + border-color: #e74c3c; +} + +.no-tasks { + text-align: center; + color: #7f8c8d; + font-style: italic; + padding: 2rem; + border: 1px dashed #ddd; + border-radius: 4px; +} + +/* Actions */ +.actions { + display: flex; + justify-content: center; +} + +.clear-completed { + background-color: #ecf0f1; + color: #34495e; + border: none; + border-radius: 4px; + padding: 0.75rem 1.5rem; + cursor: pointer; + font-weight: 600; +} + +.clear-completed:hover:not(:disabled) { + background-color: #bdc3c7; +} + +.clear-completed:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* Responsive adjustments */ +@media (max-width: 600px) { + .form-group { + flex-direction: column; + } + + .task-filters { + flex-direction: column; + gap: 1rem; + } + + .task-item { + flex-direction: column; + align-items: flex-start; + } + + .task-content { + margin-bottom: 0.5rem; + width: 100%; + } + + .delete-btn { + align-self: flex-end; + } +} diff --git a/examples/type-stripping/src/layouts/root.layout.ts b/examples/type-stripping/src/layouts/root.layout.ts index 474a685..33c6571 100644 --- a/examples/type-stripping/src/layouts/root.layout.ts +++ b/examples/type-stripping/src/layouts/root.layout.ts @@ -1,7 +1,7 @@ import { html } from 'htm/preact' import { render } from 'preact-render-to-string' -import type { LayoutFunction } from 'top-bun' +import type { LayoutFunction } from '@domstack/static' interface Vars { title?: string @@ -15,7 +15,7 @@ type DefaultRootLayout = LayoutFunction const defaultRootLayout: DefaultRootLayout = ({ vars: { title, - siteName = 'TopBun', + siteName = 'Domstack', basePath, }, scripts, diff --git a/examples/type-stripping/src/tsx-page/client.tsx b/examples/type-stripping/src/tsx-page/client.tsx index 497ef69..73a7573 100644 --- a/examples/type-stripping/src/tsx-page/client.tsx +++ b/examples/type-stripping/src/tsx-page/client.tsx @@ -1,14 +1,133 @@ import { render } from 'preact' +import { useState, useEffect } from 'preact/hooks' + +// TypeScript interfaces for our component props +interface ButtonProps { + onClick: () => void; + variant: 'primary' | 'secondary' | 'danger'; + children: any; + disabled?: boolean; +} + +interface UserCardProps { + id: number; + name: string; + email: string; + role?: string; +} + +// Styled button component with TypeScript props +const Button = ({ onClick, variant, children, disabled = false }: ButtonProps) => { + // Compute classes based on variant + const getButtonClass = (): string => { + switch (variant) { + case 'primary': + return 'bg-blue-500 hover:bg-blue-700 text-white' + case 'secondary': + return 'bg-gray-500 hover:bg-gray-700 text-white' + case 'danger': + return 'bg-red-500 hover:bg-red-700 text-white' + default: + return 'bg-blue-500 hover:bg-blue-700 text-white' + } + } -export const page = () => { return ( -
    - look ma, client side jsx! + + ) +} + +// User card component with TypeScript props +const UserCard = ({ id, name, email, role = 'User' }: UserCardProps) => ( +
    +

    {name}

    +

    ID: {id}

    +

    Email: {email}

    +

    Role: {role}

    +
    +) + +// Main application component with state management +export const Page = () => { + // TypeScript typed state + const [users, setUsers] = useState([ + { id: 1, name: 'John Doe', email: 'john@example.com', role: 'Admin' }, + { id: 2, name: 'Jane Smith', email: 'jane@example.com' } + ]) + + const [count, setCount] = useState(0) + const [isLoading, setIsLoading] = useState(false) + + // TypeScript void return type + const incrementCounter = (): void => { + setCount(prev => prev + 1) + } + + // Add a new user with TypeScript type safety + const addUser = (): void => { + setIsLoading(true) + + // Simulate API call + setTimeout(() => { + const newUser: UserCardProps = { + id: users.length + 1, + name: `User ${users.length + 1}`, + email: `user${users.length + 1}@example.com` + } + + setUsers([...users, newUser]) + setIsLoading(false) + }, 500) + } + + // TypeScript with useEffect + useEffect((): void => { + document.title = `${count} clicks` + }, [count]) + + return ( +
    +

    TypeScript JSX Example

    + +
    +

    Counter: {count}

    + +
    + +
    +

    Users

    + {users.map(user => ( + + ))} + +
    + +
    +
    + +
    ) } +// TypeScript DOM null check const renderTarget = document.querySelector('.jsx-app') if (renderTarget) { - render(page(), renderTarget) + render(, renderTarget) } diff --git a/examples/type-stripping/src/tsx-page/page.html b/examples/type-stripping/src/tsx-page/page.html index bf0074d..4557f50 100644 --- a/examples/type-stripping/src/tsx-page/page.html +++ b/examples/type-stripping/src/tsx-page/page.html @@ -1,4 +1,30 @@
    -

    This is an html page, with a client.jsx that mounts onto it

    -
    +

    TypeScript JSX Integration Example

    +

    This example demonstrates how DOMStack can use TypeScript JSX (.tsx) files for client-side rendering with automatic type stripping.

    + +
    +

    Features Demonstrated:

    +
      +
    • TypeScript interfaces for component props
    • +
    • Strongly typed state management
    • +
    • Type-safe event handlers
    • +
    • Component composition with TypeScript
    • +
    +
    + +
    +

    Live Demo:

    +
    +
    + +
    +

    How It Works:

    +

    The client.tsx file contains TypeScript JSX code that:

    +
      +
    1. Defines interfaces for component props
    2. +
    3. Creates typed state variables
    4. +
    5. Uses type-safe functions
    6. +
    7. Gets compiled to JavaScript automatically by DOMStack
    8. +
    +
    diff --git a/examples/uhtml-isomorphic/README.md b/examples/uhtml-isomorphic/README.md new file mode 100644 index 0000000..a089390 --- /dev/null +++ b/examples/uhtml-isomorphic/README.md @@ -0,0 +1,110 @@ +# uhtml-isomorphic Example + +This example demonstrates how to use [uhtml-isomorphic](https://github.com/WebReflection/uhtml-isomorphic) with DOMStack for isomorphic component rendering. + +## Overview + +uhtml-isomorphic is a lightweight library that provides the same API for both server and client rendering, making it easy to build components that work in both environments. This approach offers several benefits: + +- Write components once, use them everywhere +- Server-side rendering for fast initial page loads +- Client-side hydration for interactivity +- No JSX compilation required +- Efficient DOM updates + +## Getting Started + +### Prerequisites + +- Node.js 22.x or higher + +### Installation + +```bash +# Install dependencies +npm install +``` + +### Building the Example + +```bash +# Build the site +npm run build + +# Watch for changes during development +npm run watch +``` + +The built site will be in the `public` directory. + +## Project Structure + +``` +src/ +β”œβ”€β”€ isomorphic/ # Isomorphic component example +β”‚ β”œβ”€β”€ client.js # Client-side hydration code +β”‚ └── page.js # Server-side rendering code +β”œβ”€β”€ html-mount/ # HTML mount example +β”‚ β”œβ”€β”€ client.js # Client mounting code +β”‚ └── page.html # Static HTML template +β”œβ”€β”€ layouts/ # Layout templates +β”‚ └── root.layout.js # Root layout using uhtml-isomorphic +└── README.md # Main content (becomes index.html) +``` + +## Key Features Demonstrated + +### 1. Isomorphic Components + +The isomorphic example shows how to: +- Create components that render the same way on server and client +- Share code between environments +- Add client-side interactivity via hydration + +### 2. HTML Mounting + +The HTML mount example demonstrates: +- Starting with static HTML content +- Using uhtml-isomorphic to enhance it with dynamic client-side features +- Mounting components to specific DOM elements + +### 3. uhtml-isomorphic Layout + +The root layout shows how to: +- Build a complete HTML document structure +- Insert dynamic content +- Handle scripts and styles + +## How uhtml-isomorphic Works + +uhtml-isomorphic uses tagged template literals to define components: + +```js +import { html, render } from 'uhtml-isomorphic' + +// Create a component +const myComponent = (name) => html` +
    +

    Hello, ${name}!

    +

    Welcome to uhtml-isomorphic

    +
    +` + +// Server-side rendering +const output = render(String, myComponent('World')) + +// Client-side rendering +const container = document.querySelector('.app') +render(container, myComponent('World')) +``` + +## Learn More + +- [uhtml-isomorphic Documentation](https://github.com/WebReflection/uhtml-isomorphic) +- [DOMStack Documentation](https://github.com/bcomnes/domstack) + +## Related Examples + +Check out these other DOMStack examples: +- basic - Core features demonstration +- string-layouts - Simple template string layouts \ No newline at end of file diff --git a/examples/preact/package.json b/examples/uhtml-isomorphic/package.json similarity index 64% rename from examples/preact/package.json rename to examples/uhtml-isomorphic/package.json index a8a55d7..51826e5 100644 --- a/examples/preact/package.json +++ b/examples/uhtml-isomorphic/package.json @@ -1,23 +1,20 @@ { - "name": "@top-bun/preact-example", + "name": "@domstack/uhtml-isomorphic-example", "version": "0.0.0", "type": "module", "scripts": { "start": "npm run watch", - "build": "npm run clean && top-bun", + "build": "npm run clean && domstack", "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": "../../." + "uhtml-isomorphic": "^2.1.0", + "@domstack/static": "../../." }, "devDependencies": { "npm-run-all2": "^6.0.0" diff --git a/examples/uhtml-isomorphic/src/README.md b/examples/uhtml-isomorphic/src/README.md new file mode 100644 index 0000000..1cc6663 --- /dev/null +++ b/examples/uhtml-isomorphic/src/README.md @@ -0,0 +1,24 @@ +# uhtml-isomorphic Example + +This example demonstrates using [uhtml-isomorphic](https://github.com/WebReflection/uhtml-isomorphic) for building isomorphic components with DOMStack. + +## Features + +- Server-side rendering with the same syntax as client-side code +- Hydration of server-rendered components +- Pure JavaScript approach (no JSX required) +- Lightweight and efficient DOM updates + +## Examples + +- [Isomorphic Component Rendering](./isomorphic/) - Full isomorphic rendering with hydration +- [HTML Mount Example](./html-mount/) - Client-side mounting to HTML pages + +## How It Works + +uhtml-isomorphic provides a unified API for both server and client rendering, allowing you to write components once and use them everywhere. This example shows how to: + +1. Create components using tagged template literals +2. Render on the server with DOMStack +3. Hydrate in the browser for interactivity +4. Use the same component code in both environments diff --git a/examples/preact/src/globals/global.client.js b/examples/uhtml-isomorphic/src/globals/global.client.js similarity index 100% rename from examples/preact/src/globals/global.client.js rename to examples/uhtml-isomorphic/src/globals/global.client.js diff --git a/examples/uhtml-isomorphic/src/globals/global.css b/examples/uhtml-isomorphic/src/globals/global.css new file mode 100644 index 0000000..84d5e65 --- /dev/null +++ b/examples/uhtml-isomorphic/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/uhtml-isomorphic/src/html-mount/client.js b/examples/uhtml-isomorphic/src/html-mount/client.js new file mode 100644 index 0000000..2d02d43 --- /dev/null +++ b/examples/uhtml-isomorphic/src/html-mount/client.js @@ -0,0 +1,12 @@ +import { html, render } from 'uhtml-isomorphic' + +export const page = () => { + return html` +
    + look ma, client side uhtml-isomorphic! +
    + ` +} + +const renderTarget = document.querySelector('.uhtml-app') +render(renderTarget, page()) diff --git a/examples/uhtml-isomorphic/src/html-mount/page.html b/examples/uhtml-isomorphic/src/html-mount/page.html new file mode 100644 index 0000000..f984c64 --- /dev/null +++ b/examples/uhtml-isomorphic/src/html-mount/page.html @@ -0,0 +1,4 @@ +
    +

    This is an html page, with a client.js that mounts onto it

    +
    +
    diff --git a/examples/uhtml-isomorphic/src/isomorphic/client.js b/examples/uhtml-isomorphic/src/isomorphic/client.js new file mode 100644 index 0000000..8438b7a --- /dev/null +++ b/examples/uhtml-isomorphic/src/isomorphic/client.js @@ -0,0 +1,41 @@ +// Simple counter state +let counter = 0 + +// Function to update counter display +function updateCounter () { + const counterElement = document.querySelector('.counter-value') + if (counterElement) { + counterElement.textContent = counter + } +} + +// Initialize client-side interactivity +function initializeCounter () { + const incrementButton = document.querySelector('.increment-button') + const decrementButton = document.querySelector('.decrement-button') + + if (incrementButton) { + incrementButton.addEventListener('click', () => { + counter++ + updateCounter() + }) + } + + if (decrementButton) { + decrementButton.addEventListener('click', () => { + counter-- + updateCounter() + }) + } +} + +// Hydrate the component when in browser +if (typeof window !== 'undefined') { + // Wait for DOM to be ready + window.addEventListener('DOMContentLoaded', () => { + // Initialize counter interactivity + initializeCounter() + + console.log('uhtml-isomorphic component hydrated!') + }) +} diff --git a/examples/uhtml-isomorphic/src/isomorphic/page.js b/examples/uhtml-isomorphic/src/isomorphic/page.js new file mode 100644 index 0000000..dab2183 --- /dev/null +++ b/examples/uhtml-isomorphic/src/isomorphic/page.js @@ -0,0 +1,17 @@ +import { html } from 'uhtml-isomorphic' + +export default () => { + return html` +
    +

    uhtml-isomorphic Example

    +

    This page is rendered using uhtml-isomorphic, which provides isomorphic rendering capabilities.

    +

    The client-side JavaScript will hydrate this component.

    +
    +

    Interactive Counter

    +

    Counter value: 0

    + + +
    +
    + ` +} diff --git a/examples/uhtml-isomorphic/src/layouts/root.layout.js b/examples/uhtml-isomorphic/src/layouts/root.layout.js new file mode 100644 index 0000000..72cf92a --- /dev/null +++ b/examples/uhtml-isomorphic/src/layouts/root.layout.js @@ -0,0 +1,53 @@ +import { html, render } from 'uhtml-isomorphic' + +/** + * @template {Record} T + * @typedef {import('../build-pages/resolve-layout.js').LayoutFunction} LayoutFunction + */ + +/** + * Build all of the bundles using esbuild. + * + * @type {LayoutFunction<{ + * title: string, + * siteName: string, + * defaultStyle: boolean, + * basePath: string + * }>} + */ +export default function defaultRootLayout ({ + vars: { + title, + siteName = 'Domstack', + basePath, + /* defaultStyle = true Set this to false in global or page to disable the default style in the default layout */ + }, + scripts, + styles, + children, + /* pages */ + /* page */ +}) { + return render(String, html` + + + + + ${title ? `${title}` : ''}${title && siteName ? ' | ' : ''}${siteName} + + ${scripts + ? scripts.map(script => html``) + : null} + ${styles + ? styles.map(style => html``) + : null} + + + ${typeof children === 'string' + ? html`
    ${html([children])}
    ` + : html`
    ${children}
    ` + } + + + `) +} diff --git a/examples/worker-example/README.md b/examples/worker-example/README.md new file mode 100644 index 0000000..0617b51 --- /dev/null +++ b/examples/worker-example/README.md @@ -0,0 +1,121 @@ +# DOMStack Web Workers Example + +This example demonstrates how to use web workers in a DOMStack project. + +## Overview + +Web workers provide a way to run JavaScript in background threads, allowing for resource-intensive operations without blocking the main thread. In this example, we demonstrate: + +1. A simple counter worker that maintains state +2. A Fibonacci calculator worker that performs computationally intensive operations + +## Installation & Running + +```bash +# Navigate to the example directory +cd examples/worker-example + +# Install dependencies +npm install + +# Build and serve +npm start +``` + +This will start a development server and open the example in your browser. + +## How It Works + +DOMStack supports web workers through a simple naming convention: + +- Create files with the pattern `{name}.worker.js` in your page directories +- During build, DOMStack generates a `meta.json` file with worker filename mappings +- Access the workers in your client code using this metadata + +## Implementation Details + +### 1. Worker Files + +The example includes two web worker files: + +- `counter.worker.js` - A worker that maintains a counter state and supports multiple operations +- `fibonacci.worker.js` - A worker that performs CPU-intensive Fibonacci calculations + +### 2. Using Workers in Pages + +The worker code is separated into dedicated client-side files (`client.js`). DOMStack generates a `meta.json` file with hashed worker paths: + +```js +// First, fetch the meta.json to get worker paths +async function initializeWorkers() { + const response = await fetch('./meta.json'); + const meta = await response.json(); + + // Initialize workers with the correct hashed filenames + const counterWorker = new Worker( + new URL(`./${meta.workers.counter}`, import.meta.url), + { type: 'module' } + ); + + // Use the workers + counterWorker.postMessage({ action: 'increment' }); + + counterWorker.onmessage = (e) => { + counterElement.textContent = e.data.count; + }; +} +``` + +## What You'll Learn + +- How to create web worker files in DOMStack +- How web workers are automatically bundled by the build system +- How to use the `meta.json` file to access worker paths +- Practical patterns for worker communication +- Keeping the UI responsive during heavy computations + +## Project Structure + +``` +worker-example/ +β”œβ”€β”€ package.json # Project dependencies and scripts +β”œβ”€β”€ src/ +β”‚ β”œβ”€β”€ globals/ # Global styles and variables +β”‚ β”œβ”€β”€ layouts/ # Page layouts +β”‚ β”œβ”€β”€ README.md # Home page content +β”‚ └── worker-page/ # Web worker example page +β”‚ β”œβ”€β”€ page.js # Main page template +β”‚ β”œβ”€β”€ client.js # Client-side code for worker interaction +β”‚ β”œβ”€β”€ counter.worker.js # Counter worker implementation +β”‚ β”œβ”€β”€ fibonacci.worker.js # Fibonacci calculator worker +β”‚ └── style.css # Page-specific styles +``` + +## Build Output + +When you build the project, DOMStack: + +1. Bundles each worker file with a unique hash in the filename +2. Creates a `meta.json` file in each page directory that contains workers +3. Maps the original worker names to their hashed filenames + +``` +public/worker-page/ +β”œβ”€β”€ index.html +β”œβ”€β”€ client-XXXX.js +β”œβ”€β”€ counter.worker-XXXX.js # Hashed worker filename +β”œβ”€β”€ fibonacci.worker-XXXX.js # Hashed worker filename +β”œβ”€β”€ meta.json # Contains worker path mappings +└── style-XXXX.css +``` + +## Benefits of Web Workers + +- **Performance** - Run CPU-intensive tasks without blocking the UI +- **Responsiveness** - Keep your app responsive during heavy computations +- **Isolation** - Workers run in a separate context with their own memory + +## Learn More + +- [MDN Web Workers API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API) +- [Using Web Workers](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers) \ No newline at end of file diff --git a/examples/worker-example/package.json b/examples/worker-example/package.json new file mode 100644 index 0000000..44d8b27 --- /dev/null +++ b/examples/worker-example/package.json @@ -0,0 +1,22 @@ +{ + "name": "@domstack/worker-example", + "version": "0.0.0", + "description": "DOMStack Web Workers Example", + "type": "module", + "scripts": { + "start": "npm run watch", + "build": "npm run clean && domstack", + "clean": "rm -rf public && mkdir -p public", + "watch": "npm run clean && dom --watch" + }, + "keywords": ["domstack", "web-workers", "static-site-generator"], + "author": "", + "license": "MIT", + "dependencies": { + "mine.css": "^9.0.1", + "@domstack/static": "file:../../." + }, + "devDependencies": { + "npm-run-all2": "^6.0.0" + } +} diff --git a/examples/worker-example/src/README.md b/examples/worker-example/src/README.md new file mode 100644 index 0000000..a5563e7 --- /dev/null +++ b/examples/worker-example/src/README.md @@ -0,0 +1,83 @@ +# DOMStack Web Workers Example + +This example demonstrates how to use web workers in a DOMStack project. + +## Features + +- **Counter Worker**: A simple worker that maintains state and responds to messages +- **Fibonacci Worker**: A worker that performs computationally intensive operations +- **Integration with DOMStack**: Shows how workers are bundled and made available to pages + +## [Worker Page Example](/worker-page/) + +Visit the [Worker Example page](/worker-page/) to see the web workers in action. The page demonstrates: + +1. How to create and structure web workers in DOMStack +2. How to communicate with workers using messages +3. How to handle responses from workers + +## How It Works + +DOMStack supports web workers through a special naming convention: + +- Create files with the pattern `{name}.worker.js` in your page directories +- DOMStack generates a `workers.json` file with worker filename mappings during build +- Use this metadata to initialize workers in your client.js files + +For example, with a file structure like: + +``` +page-directory/ + β”œβ”€β”€ page.js + β”œβ”€β”€ client.js + β”œβ”€β”€ counter.worker.js + └── fibonacci.worker.js +``` + +After building, DOMStack generates: + +``` +page-directory/ + β”œβ”€β”€ index.html + β”œβ”€β”€ client-XXXX.js + β”œβ”€β”€ counter.worker-XXXX.js + β”œβ”€β”€ fibonacci.worker-XXXX.js + └── workers.json # Contains worker path mappings +``` + +You can initialize the workers in your client.js: + +```js +// In client.js +async function initializeWorkers() { + // Fetch the workers.json to get the hashed worker filenames + const response = await fetch('./workers.json'); + const workersData = await response.json(); + + // Initialize workers with the correct hashed filenames + const counterWorker = new Worker( + new URL(`./${workersData.counter}`, import.meta.url), + { type: 'module' } + ); + + // Send messages to the worker + counterWorker.postMessage({ action: 'increment' }); + + // Receive messages from the worker + counterWorker.onmessage = (e) => { + console.log(e.data.count); + }; + + return counterWorker; +} + +// Initialize workers when the page loads +const counterWorker = await initializeWorkers(); +``` + +## Technical Details + +- Workers are bundled using esbuild during the build process +- Each worker gets its own bundle with proper hashing for cache busting +- Workers are loaded as ES modules by default +- The `workers.json` file helps client code find the correct hashed worker files \ No newline at end of file diff --git a/examples/worker-example/src/client.js b/examples/worker-example/src/client.js new file mode 100644 index 0000000..c4eee47 --- /dev/null +++ b/examples/worker-example/src/client.js @@ -0,0 +1,110 @@ +// Client-side script for the home page +// This script adds some dynamic elements to the home page + +document.addEventListener('DOMContentLoaded', () => { + // Add a status indicator to show if the browser supports web workers + const supportsWorkers = typeof Worker !== 'undefined' + + // Create the status element + const statusContainer = document.createElement('div') + statusContainer.className = 'worker-status' + statusContainer.innerHTML = ` +

    Web Worker Support

    +
    + ${supportsWorkers ? 'βœ“' : 'βœ—'} + + ${supportsWorkers + ? 'Your browser supports Web Workers!' + : 'Your browser does not support Web Workers'} + +
    + ${supportsWorkers + ? '

    You can run all the examples in this demo.

    ' + : '

    You need a modern browser to run these examples.

    '} + ` + + // Insert the status after the first section + const firstSection = document.querySelector('h2') + if (firstSection && firstSection.parentNode) { + firstSection.parentNode.insertBefore(statusContainer, firstSection.nextSibling) + } + + // Add a little animation for page links + document.querySelectorAll('a[href="/worker-page/"]').forEach(link => { + link.addEventListener('mouseenter', () => { + link.innerHTML = 'Web Worker Example β†’' + }) + + link.addEventListener('mouseleave', () => { + link.innerHTML = 'Web Worker Example' + }) + }) + + // Add some styles for our dynamic elements + const style = document.createElement('style') + style.textContent = ` + .worker-status { + margin: 2rem 0; + padding: 1.5rem; + border-radius: 8px; + background-color: #f8f9fa; + border: 1px solid #e9ecef; + } + + .worker-status h3 { + margin-top: 0; + margin-bottom: 1rem; + } + + .status { + display: flex; + align-items: center; + margin-bottom: 1rem; + } + + .status-icon { + display: inline-block; + width: 24px; + height: 24px; + line-height: 24px; + text-align: center; + border-radius: 50%; + margin-right: 10px; + font-weight: bold; + } + + .supported .status-icon { + background-color: #28a745; + color: white; + } + + .unsupported .status-icon { + background-color: #dc3545; + color: white; + } + + .status-text { + font-weight: bold; + } + + .supported .status-text { + color: #28a745; + } + + .unsupported .status-text { + color: #dc3545; + } + + .link-icon { + display: inline-block; + transition: transform 0.2s; + margin-left: 5px; + } + + a:hover .link-icon { + transform: translateX(3px); + } + ` + + document.head.appendChild(style) +}) diff --git a/examples/worker-example/src/globals/global.css b/examples/worker-example/src/globals/global.css new file mode 100644 index 0000000..ddd247b --- /dev/null +++ b/examples/worker-example/src/globals/global.css @@ -0,0 +1,68 @@ +@import 'mine.css/dist/mine.css'; +@import 'mine.css/dist/layout.css'; + +/* Global styles for the worker example */ +body { + font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, + Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; + line-height: 1.6; + color: #333; + background-color: #f8f9fa; +} + +h1, h2, h3 { + color: #0d6efd; + margin-top: 1rem; + margin-bottom: 1rem; +} + +button { + background-color: #0d6efd; + color: white; + border: none; + padding: 0.5rem 1rem; + border-radius: 0.25rem; + cursor: pointer; + font-size: 1rem; + transition: background-color 0.2s; +} + +button:hover { + background-color: #0b5ed7; +} + +button:disabled { + background-color: #6c757d; + cursor: not-allowed; +} + +.mine-layout { + padding: 2rem; + max-width: 1000px; + margin: 0 auto; +} + +a { + color: #0d6efd; + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +.navigation { + margin-bottom: 2rem; +} + +.navigation a { + margin-right: 1rem; +} + +.footer { + margin-top: 3rem; + padding-top: 1rem; + border-top: 1px solid #dee2e6; + text-align: center; + color: #6c757d; +} \ No newline at end of file diff --git a/examples/worker-example/src/globals/global.vars.js b/examples/worker-example/src/globals/global.vars.js new file mode 100644 index 0000000..45d5596 --- /dev/null +++ b/examples/worker-example/src/globals/global.vars.js @@ -0,0 +1,27 @@ +// global.vars.js file exports default variables that are available to all pages +// These have the lowest precedence in the variable resolution order + +export default { + siteName: 'DOMStack Web Workers', + description: 'Examples of using Web Workers with DOMStack', + author: 'DOMStack Team', + defaultLayout: 'root' +} + +// Alternatively, you can use an async function that returns an object: +// export default async function() { +// return { +// siteName: 'DOMStack Web Workers', +// description: 'Examples of using Web Workers with DOMStack', +// author: 'DOMStack Team', +// defaultLayout: 'root' +// } +// } + +// The browser variable is special and is exposed to client JS +export const browser = { + environment: 'browser', + features: { + webWorkers: 'available' + } +} diff --git a/examples/worker-example/src/layouts/root.layout.js b/examples/worker-example/src/layouts/root.layout.js new file mode 100644 index 0000000..afe650e --- /dev/null +++ b/examples/worker-example/src/layouts/root.layout.js @@ -0,0 +1,47 @@ +/** + * Root layout for the worker example + * + * This layout file determines the outer HTML structure of all pages. + * It receives variables, children content, scripts, and styles from the pages. + */ +export default function rootLayout ({ + vars: { + siteName, + title, + description, + }, + children, + scripts = [], + styles = [], +}) { + return ` + + + + + + ${title ? `${title} | ${siteName}` : siteName} + ${styles.map(style => ``).join('\n ')} + + +
    +
    + +
    + +
    + ${children} +
    + +
    +

    DOMStack Web Workers Example © ${new Date().getFullYear()}

    +
    +
    + + ${scripts.map(script => ``).join('\n ')} + +` +} diff --git a/examples/worker-example/src/style.css b/examples/worker-example/src/style.css new file mode 100644 index 0000000..7b1af51 --- /dev/null +++ b/examples/worker-example/src/style.css @@ -0,0 +1,100 @@ +/* Styles for the home page */ +h1 { + color: #0d6efd; + border-bottom: 2px solid #0d6efd; + padding-bottom: 0.5rem; + margin-bottom: 1.5rem; +} + +h2 { + margin-top: 2rem; + color: #495057; +} + +ul { + margin-left: 1.5rem; + line-height: 1.6; +} + +code { + background-color: #f8f9fa; + padding: 0.2rem 0.4rem; + border-radius: 3px; + font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + border: 1px solid #e9ecef; + color: #dc3545; +} + +pre { + background-color: #f8f9fa; + padding: 1rem; + border-radius: 4px; + border: 1px solid #dee2e6; + overflow-x: auto; + margin: 1.5rem 0; +} + +pre code { + background-color: transparent; + padding: 0; + border: none; + color: #212529; +} + +.features { + margin: 2rem 0; +} + +.example-link { + display: inline-block; + margin: 1rem 0; + background-color: #0d6efd; + color: white; + padding: 0.75rem 1.5rem; + border-radius: 4px; + font-weight: bold; + text-decoration: none; + transition: background-color 0.2s; +} + +.example-link:hover { + background-color: #0b5ed7; + text-decoration: none; +} + +.code-block { + position: relative; + margin: 2rem 0; +} + +.code-block::before { + content: attr(data-title); + position: absolute; + top: -12px; + left: 10px; + background-color: #fff; + padding: 0 0.5rem; + font-size: 0.85rem; + color: #6c757d; + border: 1px solid #dee2e6; + border-radius: 3px; +} + +.file-structure { + font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + font-size: 0.9rem; + line-height: 1.5; + margin: 1.5rem 0; +} + +.technical-details { + background-color: #f8f9fa; + padding: 1.5rem; + border-radius: 8px; + border: 1px solid #e9ecef; + margin: 2rem 0; +} + +.technical-details h2 { + margin-top: 0; +} \ No newline at end of file diff --git a/examples/worker-example/src/worker-page/client.js b/examples/worker-example/src/worker-page/client.js new file mode 100644 index 0000000..f596713 --- /dev/null +++ b/examples/worker-example/src/worker-page/client.js @@ -0,0 +1,152 @@ +// Web worker client-side script + +// Helper function to load workers using workers.json for path resolution +async function initializeWorkers () { + try { + // Fetch the workers.json file to get worker paths + const response = await fetch('./workers.json') + + if (!response.ok) { + console.error('Failed to load workers.json:', response.status) + return { error: true } + } + + const workersData = await response.json() + + if (!workersData.counter || !workersData.fibonacci) { + console.error('Invalid workers.json format:', workersData) + return { error: true } + } + + // Initialize workers with the correct hashed filenames + const counterWorker = new Worker( + new URL(`./${workersData.counter}`, import.meta.url), + { type: 'module' } + ) + + const fibWorker = new Worker( + new URL(`./${workersData.fibonacci}`, import.meta.url), + { type: 'module' } + ) + + return { counterWorker, fibWorker } + } catch (err) { + console.error('Error initializing workers:', err) + return { error: true } + } +} + +// Initialize UI elements when the DOM is ready +document.addEventListener('DOMContentLoaded', async () => { + // Counter example elements + const counterElement = document.getElementById('counter') + const lastOperationElement = document.getElementById('lastOperation') + const incrementButton = document.getElementById('increment') + const decrementButton = document.getElementById('decrement') + const resetButton = document.getElementById('reset') + const multiplyButton = document.getElementById('multiply') + + // Fibonacci example elements + const fibInput = document.getElementById('fibInput') + const calculateButton = document.getElementById('calculate') + const resultElement = document.getElementById('fibResult') + const computationTimeElement = document.getElementById('computationTime') + + // Initialize workers + const { counterWorker, fibWorker, error } = await initializeWorkers() + + if (error) { + // Show error message if workers couldn't be initialized + document.querySelectorAll('.demo-container').forEach(container => { + container.innerHTML = ` +
    +

    Failed to initialize workers. Please check the console for details.

    +

    This can happen if the workers.json file is missing or malformed.

    +
    + ` + }) + return + } + + // Set up counter worker + counterWorker.onmessage = (e) => { + counterElement.textContent = e.data.count + + // Display last operation if available + if (e.data.lastOperation) { + const op = e.data.lastOperation + lastOperationElement.textContent = `Last action: ${op.type} (${op.oldValue} β†’ ${op.newValue})` + } + } + + // Button event listeners for counter + incrementButton.addEventListener('click', () => { + counterWorker.postMessage({ action: 'increment' }) + }) + + decrementButton.addEventListener('click', () => { + counterWorker.postMessage({ action: 'decrement' }) + }) + + resetButton.addEventListener('click', () => { + counterWorker.postMessage({ action: 'reset' }) + }) + + multiplyButton.addEventListener('click', () => { + counterWorker.postMessage({ action: 'multiply', value: 2 }) + }) + + // Set up Fibonacci worker + let startTime + + fibWorker.onmessage = (e) => { + const endTime = performance.now() + const computationTime = endTime - startTime + + if (e.data.error) { + resultElement.textContent = `Error: ${e.data.error}` + } else { + resultElement.textContent = e.data.result + } + + computationTimeElement.textContent = + `Computation time: ${computationTime.toFixed(2)}ms` + + calculateButton.disabled = false + fibInput.disabled = false + } + + // Calculate button event listener + calculateButton.addEventListener('click', () => { + const n = parseInt(fibInput.value, 10) + + if (isNaN(n) || n < 0) { + resultElement.textContent = 'Please enter a valid positive number' + return + } + + startTime = performance.now() + resultElement.textContent = 'Calculating...' + computationTimeElement.textContent = '' + calculateButton.disabled = true + fibInput.disabled = true + + // Show the UI is still responsive during calculation + const dots = ['', '.', '..', '...'] + let dotIndex = 0 + + const dotAnimation = setInterval(() => { + resultElement.textContent = 'Calculating' + dots[dotIndex] + dotIndex = (dotIndex + 1) % dots.length + }, 300) + + // Send message to worker + fibWorker.postMessage({ n }) + + // Cleanup animation when worker responds + fibWorker.addEventListener('message', function clearAnimation () { + clearInterval(dotAnimation) + fibWorker.removeEventListener('message', clearAnimation) + }, { once: true }) + }) +}) diff --git a/examples/worker-example/src/worker-page/counter.worker.ts b/examples/worker-example/src/worker-page/counter.worker.ts new file mode 100644 index 0000000..4f2b1ef --- /dev/null +++ b/examples/worker-example/src/worker-page/counter.worker.ts @@ -0,0 +1,100 @@ +/** + * Counter Web Worker Example + * + * This worker maintains a counter state and responds to various actions: + * - increment: Adds 1 to the counter + * - decrement: Subtracts 1 from the counter + * - reset: Sets the counter back to zero + * - set: Sets the counter to a specific value + * - multiply: Multiplies the counter by a value + */ + +// Initialize counter state +let count = 0 +const operationHistory = [] +const MAX_HISTORY = 10 + +// Listen for messages from the main thread +self.onmessage = (event) => { + const { action, value } = event.data + const oldCount = count + const timestamp = new Date().toISOString() + + switch (action) { + case 'increment': + // Increment the counter + count++ + recordOperation('increment', oldCount, count, timestamp) + break + + case 'decrement': + // Decrement the counter + count = Math.max(0, count - 1) // Prevent negative values + recordOperation('decrement', oldCount, count, timestamp) + break + + case 'reset': + // Reset the counter to zero + count = 0 + recordOperation('reset', oldCount, count, timestamp) + break + + case 'set': + // Set the counter to a specific value + if (typeof value === 'number' && !isNaN(value)) { + count = Math.max(0, value) // Ensure non-negative + recordOperation('set', oldCount, count, timestamp) + } + break + + case 'multiply': + // Multiply the counter by a value + if (typeof value === 'number' && !isNaN(value)) { + count = Math.floor(count * value) + recordOperation('multiply', oldCount, count, timestamp, value) + } + break + + case 'getHistory': + // Return operation history + self.postMessage({ count, history: operationHistory }) + return + + default: + // Unknown action + console.warn(`Unknown action: ${action}`) + } + + // Send the current count back to the main thread + self.postMessage({ count, lastOperation: operationHistory[0] }) +} + +/** + * Records an operation in the history + * + * @param {string} type - The type of operation + * @param {number} oldValue - The value before the operation + * @param {number} newValue - The value after the operation + * @param {string} timestamp - When the operation occurred + * @param {number} [param] - Optional parameter for the operation + */ +function recordOperation (type, oldValue, newValue, timestamp, param) { + const operation = { + type, + oldValue, + newValue, + timestamp, + param + } + + // Add to the beginning of the array + operationHistory.unshift(operation) + + // Trim history to maximum size + if (operationHistory.length > MAX_HISTORY) { + operationHistory.pop() + } +} + +// Send initial count on startup +self.postMessage({ count, history: operationHistory }) diff --git a/examples/worker-example/src/worker-page/fibonacci.worker.js b/examples/worker-example/src/worker-page/fibonacci.worker.js new file mode 100644 index 0000000..c813ac5 --- /dev/null +++ b/examples/worker-example/src/worker-page/fibonacci.worker.js @@ -0,0 +1,89 @@ +/** + * Fibonacci Web Worker Example + * + * This worker calculates Fibonacci numbers using different methods + * based on the input size to demonstrate various computation strategies. + */ + +// Listen for messages from the main thread +self.onmessage = (event) => { + const { n } = event.data + + if (typeof n !== 'number' || n < 0) { + self.postMessage({ + error: 'Invalid input. Please provide a positive number.' + }) + return + } + + // Simulate intense computation by adding a small delay for demonstration + // This helps show the benefits of using web workers + const startTime = performance.now() + + // Choose algorithm based on input size + let result + if (n <= 40) { + // For smaller numbers, use the iterative approach + result = fibonacciIterative(n) + } else { + // For larger numbers, we'd use a more optimized approach + // but still simulate the longer computation time + simulateHeavyComputation() + result = fibonacciIterative(n) + } + + const computationTime = performance.now() - startTime + console.log(`Fibonacci(${n}) took ${computationTime.toFixed(2)}ms to calculate`) + + // Send the result back to the main thread + self.postMessage({ result }) +} + +/** + * Calculate the nth Fibonacci number using iteration + * This is more efficient for larger numbers + * + * @param {number} n - The position in the Fibonacci sequence (0-based) + * @return {number} The nth Fibonacci number + */ +function fibonacciIterative (n) { + // Handle edge cases + if (n === 0) return 0 + if (n === 1) return 1 + + let a = 0 + let b = 1 + let temp + + // Iterative calculation + for (let i = 2; i <= n; i++) { + temp = a + b + a = b + b = temp + } + + return b +} + +/** + * Recursive implementation (not used for large numbers due to performance) + * Included for educational purposes only + */ +// function fibonacciRecursive (n) { +// if (n <= 1) return n +// return fibonacciRecursive(n - 1) + fibonacciRecursive(n - 2) +// } + +/** + * Simulate a heavy computation by performing unnecessary work + * This is just to demonstrate the benefit of web workers for UI responsiveness + */ +function simulateHeavyComputation () { + // Simulate intensive computation with a deliberate delay + const start = performance.now() + while (performance.now() - start < 1000) { + // Busy wait to simulate CPU-intensive work + // eslint-disable-next-line no-unused-expressions + Math.random() * Math.random() + } +} diff --git a/examples/worker-example/src/worker-page/page.html b/examples/worker-example/src/worker-page/page.html new file mode 100644 index 0000000..a139a35 --- /dev/null +++ b/examples/worker-example/src/worker-page/page.html @@ -0,0 +1,90 @@ +
    +

    {{ vars.title }}

    +

    + This page demonstrates how to use web workers in DOMStack for offloading + tasks from the main thread to maintain a responsive user interface. +

    + +
    +

    About Web Workers

    +

    + Web Workers run JavaScript in background threads, allowing you to perform + computations without blocking the main UI thread. This example shows two + worker implementations: +

    +
      +
    • Counter Worker: A simple worker that maintains state
    • +
    • Fibonacci Worker: A computationally intensive worker
    • +
    +
    + +
    +

    Counter Worker Example

    +

    This worker maintains a state and updates it based on messages:

    +
    +
    +

    Counter value: 0

    +

    +
    +
    + + + + +
    +
    +
    + +
    +

    Fibonacci Calculator Worker

    +

    + This worker performs CPU-intensive calculations in a background thread. + Try entering a large number (30-45) to see how the worker prevents UI blocking. +

    +
    +
    + + +
    +
    + +
    +
    +

    Result: -

    +

    +
    +
    +
    + +
    +

    Implementation

    +

    Worker files use the {name}.worker.js naming convention. DOMStack generates a workers.json file with worker path mappings:

    +
    // First, fetch the workers.json to get worker paths
    +async function initializeWorkers() {
    +  const response = await fetch('./workers.json');
    +  const workersData = await response.json();
    +
    +  // Initialize workers with the correct hashed filenames
    +  const counterWorker = new Worker(
    +    new URL(`./${workersData.counter}`, import.meta.url),
    +    { type: 'module' }
    +  );
    +  const fibWorker = new Worker(
    +    new URL(`./${workersData.fibonacci}`, import.meta.url),
    +    { type: 'module' }
    +  );
    +
    +  return { counterWorker, fibWorker };
    +}
    +

    This handles the hashed filenames that get generated during the build process.

    +

    Send messages to workers:

    +
    // Send data to the worker
    +counterWorker.postMessage({ action: 'increment' });
    +fibWorker.postMessage({ n: 42 });
    +

    Receive responses from workers:

    +
    // Listen for worker responses
    +counterWorker.onmessage = (e) => {
    +  console.log('New count:', e.data.count);
    +};
    +
    +
    \ No newline at end of file diff --git a/examples/worker-example/src/worker-page/page.vars.js b/examples/worker-example/src/worker-page/page.vars.js new file mode 100644 index 0000000..ddcfaf1 --- /dev/null +++ b/examples/worker-example/src/worker-page/page.vars.js @@ -0,0 +1,11 @@ +/** + * Variables for the worker example page + * + * These variables are available to the page template and can be used to + * customize the page content and metadata. + */ +export default { + title: 'Web Worker Example', + description: 'Learn how to use web workers in DOMStack for background processing', + layout: 'root' +} diff --git a/examples/worker-example/src/worker-page/style.css b/examples/worker-example/src/worker-page/style.css new file mode 100644 index 0000000..032ef51 --- /dev/null +++ b/examples/worker-example/src/worker-page/style.css @@ -0,0 +1,109 @@ +/* Worker example page styles */ +.worker-example { + background-color: white; + border-radius: 8px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05); + padding: 2rem; + margin-bottom: 2rem; +} + +.worker-example h1 { + color: #0d6efd; + border-bottom: 2px solid #0d6efd; + padding-bottom: 0.5rem; + margin-bottom: 1.5rem; +} + +.worker-example h2 { + color: #495057; + margin-top: 1.5rem; + margin-bottom: 1rem; +} + +.explanation { + margin-bottom: 2rem; +} + +.explanation ul { + margin-left: 1.5rem; +} + +.demo-section { + background-color: #f8f9fa; + border-radius: 8px; + padding: 1.5rem; + margin: 2rem 0; + border: 1px solid #e9ecef; +} + +.demo-container { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.counter-display, .result { + background-color: white; + padding: 1rem; + border-radius: 4px; + border: 1px solid #dee2e6; + position: relative; +} + +.input-group { + display: flex; + align-items: center; + gap: 0.5rem; + flex-wrap: wrap; +} + +.controls { + display: flex; + gap: 0.5rem; + flex-wrap: wrap; + margin-bottom: 0.5rem; +} + +.computation-time { + font-size: 0.9rem; + color: #6c757d; + margin-top: 0.5rem; +} + +input { + padding: 0.5rem; + border: 1px solid #ced4da; + border-radius: 4px; + font-size: 1rem; + width: 80px; +} + +#counter, #fibResult { + font-weight: bold; + color: #0d6efd; + font-size: 1.2rem; +} + +.last-operation { + font-size: 0.85rem; + color: #6c757d; + margin-top: 0.5rem; + font-style: italic; +} + +.code-example { + margin: 2rem 0; +} + +.code-example pre { + background-color: #f8f9fa; + padding: 1rem; + border-radius: 4px; + border: 1px solid #dee2e6; + overflow-x: auto; + margin-bottom: 1.5rem; +} + +.code-example code { + font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; +} \ No newline at end of file diff --git a/fonts/ds-weiss-gotisch/1001fonts-ds-weiss-gotisch-eula.txt b/fonts/ds-weiss-gotisch/1001fonts-ds-weiss-gotisch-eula.txt new file mode 100644 index 0000000..1f4f2b6 --- /dev/null +++ b/fonts/ds-weiss-gotisch/1001fonts-ds-weiss-gotisch-eula.txt @@ -0,0 +1,31 @@ +1001Fonts Free For Commercial Use License (FFC) + +Preamble +In this license, 'DS Weiss-Gotisch' refers to the given .zip file, which may contain one or numerous fonts. These fonts can be of any type (.ttf, .otf, ...) and together they form a 'font family' or in short a 'typeface'. + +1. Copyright +DS Weiss-Gotisch is the intellectual property of its respective author, provided it is original, and is protected by copyright laws in many parts of the world. + +2. Usage +DS Weiss-Gotisch may be downloaded and used free of charge for both personal and commercial use, as long as the usage is not racist or illegal. Personal use refers to all usage that does not generate financial income in a business manner, for instance: + + - personal scrapbooking for yourself + - recreational websites and blogs for friends and family + - prints such as flyers, posters, t-shirts for churches, charities, and non-profit organizations + +Commercial use refers to usage in a business environment, including: + + - business cards, logos, advertising, websites for companies + - t-shirts, books, apparel that will be sold for money + - flyers, posters for events that charge admission + - freelance graphic design work + - anything that will generate direct or indirect income + +3. Modification +DS Weiss-Gotisch may not be modified, altered, adapted or built upon without written permission by its respective author. This pertains all files within the downloadable font zip-file. + +4. Distribution +While DS Weiss-Gotisch may freely be copied and passed along to other individuals for private use as its original downloadable zip-file, it may not be sold or published without written permission by its respective author. + +5. Disclaimer +DS Weiss-Gotisch is offered 'as is' without any warranty. 1001fonts.com and the respective author of DS Weiss-Gotisch shall not be liable for any damage derived from using this typeface. By using DS Weiss-Gotisch you agree to the terms of this license. \ No newline at end of file diff --git a/fonts/ds-weiss-gotisch/DSWeiss-Gotisch.ttf b/fonts/ds-weiss-gotisch/DSWeiss-Gotisch.ttf new file mode 100644 index 0000000..77665a0 Binary files /dev/null and b/fonts/ds-weiss-gotisch/DSWeiss-Gotisch.ttf differ diff --git a/fonts/ds-weiss-gotisch/DSWeiss-GotischAlt.ttf b/fonts/ds-weiss-gotisch/DSWeiss-GotischAlt.ttf new file mode 100644 index 0000000..a5505b9 Binary files /dev/null and b/fonts/ds-weiss-gotisch/DSWeiss-GotischAlt.ttf differ diff --git a/index.js b/index.js index 0a44f38..ef33077 100644 --- a/index.js +++ b/index.js @@ -1,11 +1,21 @@ +/** + * @import { DomStackOpts as DomStackOpts, Results } from './lib/builder.js' + * @import { FSWatcher, Stats } from 'node:fs' + * @import { PostVarsFunction, AsyncPostVarsFunction, AsyncLayoutFunction, LayoutFunction } from './lib/build-pages/page-data.js' + * @import { PageFunction, AsyncPageFunction } from './lib/build-pages/page-builders/page-writer.js' + * @import { TemplateFunction } from './lib/build-pages/page-builders/template-builder.js' + * @import { TemplateAsyncIterator } from './lib/build-pages/page-builders/template-builder.js' + * @import { TemplateOutputOverride } from './lib/build-pages/page-builders/template-builder.js' + * @import { BuildOptions } from 'esbuild' +*/ import { once } from 'events' import assert from 'node:assert' import chokidar from 'chokidar' import { basename, relative, resolve } from 'node:path' -// @ts-ignore +// @ts-expect-error import makeArray from 'make-array' import ignore from 'ignore' -// @ts-ignore +// @ts-expect-error import cpx from 'cpx2' import { inspect } from 'util' import browserSync from 'browser-sync' @@ -13,40 +23,64 @@ import browserSync from 'browser-sync' import { getCopyGlob } from './lib/build-static/index.js' import { getCopyDirs } from './lib/build-copy/index.js' import { builder } from './lib/builder.js' -import { TopBunAggregateError } from './lib/helpers/top-bun-aggregate-error.js' +import { DomStackAggregateError } from './lib/helpers/dom-stack-aggregate-error.js' /** - * @import { TopBunOpts, Results } from './lib/builder.js' - * @import { FSWatcher, Stats } from 'node:fs' -*/ + * @typedef {BuildOptions} BuildOptions + */ + +/** + * @template {Record} Vars - The type of variables passed to the layout function + * @template [PageReturn=any] PageReturn - The return type of the page function (defaults to any) + * @template [LayoutReturn=string] LayoutReturn - The return type of the layout function (defaults to string) + * @typedef {LayoutFunction} LayoutFunction + */ + +/** + * @template {Record} Vars - The type of variables passed to the async layout function + * @template [PageReturn=any] PageReturn - The return type of the page function (defaults to any) + * @template [LayoutReturn=string] LayoutReturn - The return type of the layout function (defaults to string) + * @typedef {AsyncLayoutFunction} AsyncLayoutFunction + */ + +/** + * @template {Record} Vars - The type of variables for the post vars function + * @template [PageReturn=any] PageReturn - The return type of the page function (defaults to any) + * @template [LayoutReturn=string] LayoutReturn - The return type of the layout function (defaults to string) + * @typedef {PostVarsFunction} PostVarsFunction + */ /** - * @template {Record} T - * @typedef {import('./lib/build-pages/resolve-layout.js').LayoutFunction} LayoutFunction + * @template {Record} Vars - The type of variables for the async post vars function + * @template [PageReturn=any] PageReturn - The return type of the page function (defaults to any) + * @template [LayoutReturn=string] LayoutReturn - The return type of the layout function (defaults to string) + * @typedef {AsyncPostVarsFunction} AsyncPostVarsFunction */ /** - * @template {Record} T - * @typedef {import('./lib/build-pages/resolve-vars.js').PostVarsFunction} PostVarsFunction + * @template {Record} Vars - The type of variables passed to the page function + * @template [PageReturn=any] PageReturn - The return type of the page function (defaults to any) + * @typedef {PageFunction} PageFunction */ /** - * @template {Record} T - * @typedef {import('./lib/build-pages/page-builders/page-writer.js').PageFunction} PageFunction + * @template {Record} Vars - The type of variables passed to the async page function + * @template [PageReturn=any] PageReturn - The return type of the page function (defaults to any) + * @typedef {AsyncPageFunction} AsyncPageFunction */ /** - * @template {Record} T - * @typedef {import('./lib/build-pages/page-builders/template-builder.js').TemplateFunction} TemplateFunction + * @template {Record} Vars - The type of variables for the template function + * @typedef {TemplateFunction} TemplateFunction */ /** - * @template {Record} T - * @typedef {import('./lib/build-pages/page-builders/template-builder.js').TemplateAsyncIterator} TemplateAsyncIterator + * @template {Record} Vars - The type of variables for the template async iterator + * @typedef {TemplateAsyncIterator} TemplateAsyncIterator */ /** - * @typedef {import('./lib/build-pages/page-builders/template-builder.js').TemplateOutputOverride} TemplateOutputOverride + * @typedef {TemplateOutputOverride} TemplateOutputOverride */ const DEFAULT_IGNORES = /** @type {const} */ ([ @@ -60,9 +94,9 @@ const DEFAULT_IGNORES = /** @type {const} */ ([ ]) /** - * @template {TopBunOpts} [CurrentOpts=TopBunOpts] + * @template {DomStackOpts} [CurrentOpts=DomStackOpts] - The type of options for the DomStack instance */ -export class TopBun { +export class DomStack { /** @type {string} */ #src = '' /** @type {string} */ #dest = '' /** @type {Readonly} */ opts @@ -118,7 +152,7 @@ export class TopBun { } /** - * Build and watch a top-bun build + * Build and watch a domstack build * @param {object} [params] * @param {boolean} params.serve * @return {Promise} @@ -138,7 +172,7 @@ export class TopBun { console.log('Initial JS, CSS and Page Build Complete') } catch (err) { errorLogger(err) - if (!(err instanceof TopBunAggregateError)) throw new Error('Non-aggregate error thrown', { cause: err }) + if (!(err instanceof DomStackAggregateError)) throw new Error('Non-aggregate error thrown', { cause: err }) report = err.results } diff --git a/lib/build-copy/index.js b/lib/build-copy/index.js index 983705c..5b81b9e 100644 --- a/lib/build-copy/index.js +++ b/lib/build-copy/index.js @@ -1,20 +1,18 @@ -// @ts-ignore +/** + * @import { BuildStepResult, BuildStep } from '../builder.js' + */ + +// @ts-expect-error import cpx from 'cpx2' import { join } from 'node:path' const copy = cpx.copy /** + * @typedef {BuildStepResult<'static', CopyBuilderReport>} CopyBuildStepResult + * @typedef {BuildStep<'static', CopyBuilderReport>} CopyBuildStep * @typedef {Awaited>} CopyBuilderReport */ -/** - * @typedef {import('../builder.js').BuildStepResult<'static', CopyBuilderReport>} CopyBuildStepResult - */ - -/** - * @typedef {import('../builder.js').BuildStep<'static', CopyBuilderReport>} CopyBuildStep - */ - /** * @param {string[]} copy * @return {string[]} diff --git a/lib/build-copy/index.test.js b/lib/build-copy/index.test.js index ec47797..4651210 100644 --- a/lib/build-copy/index.test.js +++ b/lib/build-copy/index.test.js @@ -1,8 +1,11 @@ -import tap from 'tap' +import { test } from 'node:test' +import assert from 'node:assert' import { getCopyDirs } from './index.js' -tap.test('getCopyDirs returns correct src/dest pairs', async (t) => { - const copyDirs = getCopyDirs(['fixtures']) +test.describe('build-copy', () => { + test('getCopyDirs returns correct src/dest pairs', async () => { + const copyDirs = getCopyDirs(['fixtures']) - t.strictSame(copyDirs, ['fixtures/**']) + assert.deepStrictEqual(copyDirs, ['fixtures/**']) + }) }) diff --git a/lib/build-esbuild/index.js b/lib/build-esbuild/index.js index 0619321..6766fd8 100644 --- a/lib/build-esbuild/index.js +++ b/lib/build-esbuild/index.js @@ -1,10 +1,14 @@ +/** + * @import { BuildStep } from '../builder.js' + */ + import { writeFile } from 'fs/promises' import { join, relative, basename } from 'path' import esbuild from 'esbuild' import { resolveVars } from '../build-pages/resolve-vars.js' const __dirname = import.meta.dirname -const TOP_BUN_DEFAULTS_PREFIX = 'top-bun-defaults' +const DOM_STACK_DEFAULTS_PREFIX = 'dom-stack-defaults' /** * @typedef {esbuild.Format} EsbuildFormat @@ -13,7 +17,7 @@ const TOP_BUN_DEFAULTS_PREFIX = 'top-bun-defaults' * @typedef {esbuild.BuildOptions} EsbuildBuildOptions * @typedef {Awaited>} EsbuildBuildResults - * @typedef {import('../builder.js').BuildStep< + * @typedef {BuildStep< * 'esbuild', * { * buildResults?: EsbuildBuildResults @@ -23,7 +27,8 @@ const TOP_BUN_DEFAULTS_PREFIX = 'top-bun-defaults' * >} EsBuildStep */ -/** @typedef {Awaited>} EsBuildStepResults +/** + * @typedef {Awaited>} EsBuildStepResults */ /** @@ -37,14 +42,21 @@ export async function buildEsbuild (src, dest, siteData, opts) { if (siteData.globalStyle) entryPoints.push(join(src, siteData.globalStyle.relname)) if (siteData.defaultLayout) { entryPoints.push( - { in: join(__dirname, '../defaults/default.style.css'), out: join(TOP_BUN_DEFAULTS_PREFIX, 'default.style.css') }, - { in: join(__dirname, '../defaults/default.client.js'), out: join(TOP_BUN_DEFAULTS_PREFIX, 'default.client.js') } + { in: join(__dirname, '../defaults/default.style.css'), out: join(DOM_STACK_DEFAULTS_PREFIX, 'default.style.css') }, + { in: join(__dirname, '../defaults/default.client.js'), out: join(DOM_STACK_DEFAULTS_PREFIX, 'default.client.js') } ) } for (const page of siteData.pages) { if (page.clientBundle) entryPoints.push(join(src, page.clientBundle.relname)) if (page.pageStyle) entryPoints.push(join(src, page.pageStyle.relname)) + + // Add web worker entry points + if (page.workers) { + for (const workerFile of Object.values(page.workers)) { + entryPoints.push(join(src, workerFile.relname)) + } + } } for (const layout of Object.values(siteData.layouts)) { @@ -106,7 +118,7 @@ export async function buildEsbuild (src, dest, siteData, opts) { // @ts-ignore This actually works fine const buildResults = await esbuild.build(extendedBuildOpts) if (buildResults.metafile) { - await writeFile(join(dest, 'top-bun-esbuild-meta.json'), JSON.stringify(buildResults.metafile, null, ' ')) + await writeFile(join(dest, 'dom-stack-esbuild-meta.json'), JSON.stringify(buildResults.metafile, null, ' ')) } /** @type {OutputMap} */ @@ -135,6 +147,17 @@ export async function buildEsbuild (src, dest, siteData, opts) { page.clientBundle.outputName = basename(outputRelname) } } + + // Add output paths for web workers + if (page.workers) { + for (const workerFile of Object.values(page.workers)) { + const outputRelname = outputMap[workerFile.relname] + if (outputRelname) { + workerFile.outputRelname = outputRelname + workerFile.outputName = basename(outputRelname) + } + } + } } if (siteData.globalClient) { @@ -172,8 +195,8 @@ export async function buildEsbuild (src, dest, siteData, opts) { } if (siteData.defaultLayout) { - const defaultClient = Object.values(outputMap).find(p => /^top-bun-defaults.*\.js$/.test(p)) - const defaultStyle = Object.values(outputMap).find(p => /^top-bun-defaults.*\.css$/.test(p)) + const defaultClient = Object.values(outputMap).find(p => /^dom-stack-defaults.*\.js$/.test(p)) + const defaultStyle = Object.values(outputMap).find(p => /^dom-stack-defaults.*\.css$/.test(p)) siteData.defaultClient = defaultClient ?? null siteData.defaultStyle = defaultStyle ?? null } diff --git a/lib/build-pages/index.js b/lib/build-pages/index.js index c6f6077..b8e44ce 100644 --- a/lib/build-pages/index.js +++ b/lib/build-pages/index.js @@ -1,13 +1,17 @@ +/** + * @import { BuilderOptions } from './page-builders/page-writer.js' + * @import { BuildStep } from '../builder.js' + * @import { InternalLayoutFunction } from './page-data.js' + */ + import { Worker } from 'worker_threads' import { join } from 'path' import pMap from 'p-map' import { cpus } from 'os' - import { keyBy } from '../helpers/key-by.js' import { resolveVars } from './resolve-vars.js' -import { resolveLayout } from './resolve-layout.js' import { pageBuilders, templateBuilder } from './page-builders/index.js' -import { PageData } from './page-data.js' +import { PageData, resolveLayout } from './page-data.js' import { pageWriter } from './page-builders/page-writer.js' const MAX_CONCURRENCY = Math.min(cpus().length, 24) @@ -22,10 +26,10 @@ const __dirname = import.meta.dirname */ /** - * @typedef {import('../builder.js').BuildStep< + * @typedef {BuildStep< * 'page', * PageBuilderReport - * >} PageBuildStep + * >} PageBuildStep */ /** @@ -33,14 +37,11 @@ const __dirname = import.meta.dirname */ /** - * @template T - * @typedef {import('./resolve-layout.js').LayoutFunction} LayoutFunction - */ - -/** - * @template T + * @template {Record} T - The type of variables for the layout + * @template [U=any] U - The return type of the page function (defaults to any) + * @template [V=string] V - The return type of the layout function (defaults to string) * @typedef ResolvedLayout - * @property {LayoutFunction} render - The layout function + * @property {InternalLayoutFunction} render - The layout function * @property {string} name - The name of the layout * @property {string | null} layoutStylePath - The string path to the layout style * @property {string | null} layoutClientPath - The string path to the layout client @@ -113,6 +114,8 @@ export async function buildPagesDirect (src, dest, siteData, _opts) { warnings: [], } + // Note: markdown-it settings are now passed directly to builders through builderOptions + const [ defaultVars, bareGlobalVars, @@ -125,7 +128,7 @@ export async function buildPagesDirect (src, dest, siteData, _opts) { }), ]) - /** @type {ResolvedLayout[]} */ + /** @type {ResolvedLayout[]} */ const resolvedLayoutResults = await pMap(Object.values(siteData.layouts), async (layout) => { const render = await resolveLayout(layout.filepath) return { @@ -146,6 +149,12 @@ export async function buildPagesDirect (src, dest, siteData, _opts) { ...bareGlobalVars, } + // Create builder options from siteData + /** @type {BuilderOptions} */ + const builderOptions = { + markdownItSettingsPath: siteData.markdownItSettings?.filepath || null + } + // Mix in resolveVars, renderInnerPage and renderFullPage methods const pages = await pMap(siteData.pages, async (pageInfo) => { const pageData = new PageData({ @@ -155,6 +164,7 @@ export async function buildPagesDirect (src, dest, siteData, _opts) { globalClient: siteData?.globalClient?.outputRelname, defaultStyle: siteData?.defaultStyle, defaultClient: siteData?.defaultClient, + builderOptions, }) try { // Resolves async vars and binds the page to a reference to its layout fn diff --git a/lib/build-pages/page-builders/fs-path-to-url.test.js b/lib/build-pages/page-builders/fs-path-to-url.test.js index 9062f13..6ebcc5f 100644 --- a/lib/build-pages/page-builders/fs-path-to-url.test.js +++ b/lib/build-pages/page-builders/fs-path-to-url.test.js @@ -1,31 +1,34 @@ -import tap from 'tap' +import { test } from 'node:test' +import assert from 'node:assert' import process from 'process' import { fsPathToUrlPath } from './fs-path-to-url.js' const isWin = process.platform === 'win32' -tap.test('fsPathToUrlPath works for all OS file path types', async (t) => { - const tests = [ - { - input: 'foo/bar/baz', - expect: '/foo/bar/baz', - note: 'unix style paths', - winOnly: false, - }, - { - input: 'foo\\bar\\baz', - expect: '/foo/bar/baz', - note: 'windows style paths', - winOnly: true, - }, - ] +test.describe('fs-path-to-url', () => { + test('fsPathToUrlPath works for all OS file path types', async () => { + const tests = [ + { + input: 'foo/bar/baz', + expect: '/foo/bar/baz', + note: 'unix style paths', + winOnly: false, + }, + { + input: 'foo\\bar\\baz', + expect: '/foo/bar/baz', + note: 'windows style paths', + winOnly: true, + }, + ] - for (const test of tests) { - if (isWin && test.winOnly) { - t.equal(fsPathToUrlPath(test.input), test.expect, test.note) - } else if (!isWin && !test.winOnly) { - t.equal(fsPathToUrlPath(test.input), test.expect, test.note) + for (const test of tests) { + if (isWin && test.winOnly) { + assert.equal(fsPathToUrlPath(test.input), test.expect, test.note) + } else if (!isWin && !test.winOnly) { + assert.equal(fsPathToUrlPath(test.input), test.expect, test.note) + } } - } + }) }) diff --git a/lib/build-pages/page-builders/html/index.js b/lib/build-pages/page-builders/html/index.js index dc20a72..2235715 100644 --- a/lib/build-pages/page-builders/html/index.js +++ b/lib/build-pages/page-builders/html/index.js @@ -1,11 +1,15 @@ +/** + * @import { PageBuilderType } from '../page-writer.js' + */ + import assert from 'node:assert' import { readFile } from 'fs/promises' import Handlebars from 'handlebars' /** * Build all of the bundles using esbuild. - * @template {Record} T - * @type {import('../page-writer.js').PageBuilderType} + * @template {Record} T - The type of variables for the page + * @type {PageBuilderType} */ export async function htmlBuilder ({ pageInfo }) { assert(pageInfo.type === 'html', 'html builder requires a "html" page type') diff --git a/lib/build-pages/page-builders/js/index.js b/lib/build-pages/page-builders/js/index.js index fc192da..8696aec 100644 --- a/lib/build-pages/page-builders/js/index.js +++ b/lib/build-pages/page-builders/js/index.js @@ -1,12 +1,17 @@ +/** + * @import { PageBuilderType } from '../page-writer.js' + */ + import assert from 'node:assert' /** * Build all of the bundles using esbuild. - * @template {Record} T - * @type {import('../page-writer.js').PageBuilderType} + * @template {Record} T - The type of variables for the page + * @template [U=any] U - The return type of the pageLayout function + * @type {PageBuilderType} */ export async function jsBuilder ({ pageInfo }) { - assert(pageInfo.type === 'js', 'js page builder requries "js" page type') + assert(pageInfo.type === 'js', 'js page builder requires "js" page type') const { default: pageLayout, vars } = await import(pageInfo.pageFile.filepath) diff --git a/lib/build-pages/page-builders/md/extract-title-from-md.js b/lib/build-pages/page-builders/md/extract-title-from-md.js new file mode 100644 index 0000000..4392280 --- /dev/null +++ b/lib/build-pages/page-builders/md/extract-title-from-md.js @@ -0,0 +1,28 @@ +import markdownit from 'markdown-it' + +const md = markdownit() + +/** + * Extract the first H1 heading from markdown using markdown-it's token API + * @param {string} markdown + * @returns {string | null} + */ +export function extractFirstH1 (markdown) { + const tokens = md.parse(markdown, {}) + + for (let i = 0; i < tokens.length; i++) { + const token = tokens[i] + + // Look for heading_open token with tag 'h1' + if (token && token.type === 'heading_open' && token.tag === 'h1') { + // The next token should be inline with the heading content + const nextToken = tokens[i + 1] + if (nextToken && nextToken.type === 'inline') { + // The inline token's content is the raw text of the heading + return nextToken.content.trim() + } + } + } + + return null +} diff --git a/lib/build-pages/page-builders/md/extract-title-from-md.test.js b/lib/build-pages/page-builders/md/extract-title-from-md.test.js new file mode 100644 index 0000000..eef6773 --- /dev/null +++ b/lib/build-pages/page-builders/md/extract-title-from-md.test.js @@ -0,0 +1,183 @@ +import { test } from 'node:test' +import assert from 'node:assert' + +import { extractFirstH1 } from './extract-title-from-md.js' + +test.describe('extractFirstH1', () => { + test('extracts ATX style H1 headings', async () => { + const tests = [ + { + input: '# Simple Heading', + expect: 'Simple Heading', + note: 'basic ATX H1' + }, + { + input: '# Extra Spaces ', + expect: 'Extra Spaces', + note: 'ATX H1 with extra spaces' + }, + { + input: '# Heading with **bold** and *italic*', + expect: 'Heading with **bold** and *italic*', + note: 'ATX H1 with inline formatting' + }, + { + input: 'Some text\n# First Heading\n## Second Heading', + expect: 'First Heading', + note: 'ATX H1 after other content' + }, + { + input: '## Not H1\n# Real H1\n### Not H1', + expect: 'Real H1', + note: 'ATX H1 between other headings' + } + ] + + for (const testCase of tests) { + const result = extractFirstH1(testCase.input) + assert.equal(result, testCase.expect, testCase.note) + } + }) + + test('extracts Setext style H1 headings', async () => { + const tests = [ + { + input: 'Simple Heading\n==============', + expect: 'Simple Heading', + note: 'basic Setext H1' + }, + { + input: 'Simple Heading\n===', + expect: 'Simple Heading', + note: 'Setext H1 with minimum underline' + }, + { + input: ' Trimmed Heading \n==============', + expect: 'Trimmed Heading', + note: 'Setext H1 with spaces to trim' + }, + { + input: 'Heading with **bold** and *italic*\n==================', + expect: 'Heading with **bold** and *italic*', + note: 'Setext H1 with inline formatting' + }, + { + input: 'Some text\n\nFirst Heading\n=============\n\nMore text', + expect: 'First Heading', + note: 'Setext H1 with surrounding content' + } + ] + + for (const testCase of tests) { + const result = extractFirstH1(testCase.input) + assert.equal(result, testCase.expect, testCase.note) + } + }) + + test('handles edge cases correctly', async () => { + const tests = [ + { + input: '', + expect: null, + note: 'empty string' + }, + { + input: '## Only H2\n### Only H3', + expect: null, + note: 'no H1 present' + }, + { + input: 'Not a heading\n---', + expect: null, + note: 'Setext H2 (dashes) should not match' + }, + { + input: 'Not a heading\n--', + expect: null, + note: 'Setext H2 (dashes) should not match' + }, + { + input: 'Not a heading\n==', + expect: 'Not a heading', + note: 'markdown-it accepts any number of equals for Setext H1' + }, + { + input: '\n========', + expect: null, + note: 'empty line before Setext underline' + }, + { + input: ' # Code block heading', + expect: null, + note: 'indented code block should not match' + }, + { + input: '```\n# Code fence heading\n```', + expect: null, + note: 'fenced code block should not match (simple case)' + } + ] + + for (const testCase of tests) { + const result = extractFirstH1(testCase.input) + assert.equal(result, testCase.expect, testCase.note) + } + }) + + test('prefers first H1 when multiple exist', async () => { + const tests = [ + { + input: '# First ATX\n# Second ATX', + expect: 'First ATX', + note: 'multiple ATX H1s' + }, + { + input: 'First Setext\n============\n\nSecond Setext\n============', + expect: 'First Setext', + note: 'multiple Setext H1s' + }, + { + input: '# ATX First\n\nSetext Second\n=============', + expect: 'ATX First', + note: 'ATX before Setext' + }, + { + input: 'Setext First\n============\n\n# ATX Second', + expect: 'Setext First', + note: 'Setext before ATX' + } + ] + + for (const testCase of tests) { + const result = extractFirstH1(testCase.input) + assert.equal(result, testCase.expect, testCase.note) + } + }) + + test('handles multiline documents correctly', async () => { + const markdown = ` +Some introductory text here +that spans multiple lines + +# The Real Title + +## A subsection + +More content here +` + const result = extractFirstH1(markdown) + assert.equal(result, 'The Real Title', 'finds H1 in realistic document') + }) + + test('handles frontmatter-like content', async () => { + const markdown = `--- +title: Frontmatter Title +--- + +# Actual H1 Title + +Content here` + const result = extractFirstH1(markdown) + assert.equal(result, 'Actual H1 Title', 'ignores frontmatter') + }) +}) diff --git a/lib/build-pages/page-builders/md/get-md.js b/lib/build-pages/page-builders/md/get-md.js index f137d74..01b41c3 100644 --- a/lib/build-pages/page-builders/md/get-md.js +++ b/lib/build-pages/page-builders/md/get-md.js @@ -1,3 +1,5 @@ +/** + */ import markdownIt from 'markdown-it' import markdownItFootnote from 'markdown-it-footnote' import markdownItHighlightjs from 'markdown-it-highlightjs' @@ -31,7 +33,11 @@ const mdOpts = { typographer: true, } -export function getMd () { +/** + * @param {string | null | undefined} [settingsPath] - Path to the markdown-it settings file + * @returns {Promise>} + */ +export async function getMd (settingsPath = null) { const md = markdownIt(mdOpts) .use(markdownItSub) .use(markdownItSup) @@ -52,6 +58,20 @@ export function getMd () { // disable autolinking for filenames md.linkify.tlds('.md', false) // markdown + + // Apply user settings if available + if (settingsPath) { + try { + const settingsModule = await import(settingsPath) + const settingsFunction = settingsModule.default + if (typeof settingsFunction === 'function') { + return await settingsFunction(md) + } + } catch (err) { + console.error('Error loading markdown-it settings:', err) + } + } + return md } @@ -59,11 +79,12 @@ export function getMd () { * Renders markdown, and accepts an optional markdown-it instance * @param {string} mdUnparsed unparsed markdown * @param {object} vars to expose to handlebars - * @param {markdownIt} [md] an instance of markdown - * @return {string} Rendered markdown to html + * @param {markdownIt?} [md] an instance of markdown + * @param {string | null | undefined} [settingsPath] Path to the markdown-it settings file + * @return {Promise} Rendered markdown to html */ -export function renderMd (mdUnparsed, vars, md) { - if (!md) md = getMd() +export async function renderMd (mdUnparsed, vars, md, settingsPath) { + if (!md) md = await getMd(settingsPath) // @ts-ignore if (vars?.vars?.handlebars) { const template = Handlebars.compile(mdUnparsed) diff --git a/lib/build-pages/page-builders/md/index.js b/lib/build-pages/page-builders/md/index.js index 192ea4d..a69144c 100644 --- a/lib/build-pages/page-builders/md/index.js +++ b/lib/build-pages/page-builders/md/index.js @@ -1,19 +1,27 @@ +/** + * @import markdownIt from 'markdown-it' + * @import { PageBuilderType } from '../page-writer.js' + */ import assert from 'node:assert' import { readFile } from 'fs/promises' import yaml from 'js-yaml' -import * as cheerio from 'cheerio' - import { getMd, renderMd } from './get-md.js' +import { extractFirstH1 } from './extract-title-from-md.js' -const md = getMd() +/** @type {markdownIt | null} */ +let md = null /** * Build all of the bundles using esbuild. - * @template {Record} T - * @type {import('../page-writer.js').PageBuilderType} + * @template {Record} T - The type of variables for the page + * @type {PageBuilderType} */ -export async function mdBuilder ({ pageInfo }) { +export async function mdBuilder ({ pageInfo, options }) { assert(pageInfo.type === 'md', 'md builder requires an "md" page type') + + const markdownItSettingsPath = options?.markdownItSettingsPath || null + + if (!md) md = await getMd(markdownItSettingsPath) const fileContents = await readFile(pageInfo.pageFile.filepath, 'utf8') /** @type {object} */ @@ -30,11 +38,11 @@ export async function mdBuilder ({ pageInfo }) { mdUnparsed = fileContents } - const body = renderMd(mdUnparsed, { handlebars: false, ...frontMatter }, md) - const title = cheerio.load(body)('h1').first().text().trim() + // Extract title from first H1 using markdown-it's token API + const title = extractFirstH1(mdUnparsed) return { vars: Object.assign({ title }, frontMatter), - pageLayout: async (vars) => renderMd(mdUnparsed, vars, md), + pageLayout: async (vars) => await renderMd(mdUnparsed, vars, md, markdownItSettingsPath), } } diff --git a/lib/build-pages/page-builders/page-writer.js b/lib/build-pages/page-builders/page-writer.js index 35c29be..b286a71 100644 --- a/lib/build-pages/page-builders/page-writer.js +++ b/lib/build-pages/page-builders/page-writer.js @@ -1,54 +1,94 @@ +/** + * @import { PageInfo } from '../../identify-pages.js' + * @import { PageData } from '../page-data.js' + */ + import { join } from 'path' import { writeFile, mkdir } from 'fs/promises' /** - * @typedef {import('../../identify-pages.js').PageInfo} PageInfo + * @typedef {Object} BuilderOptions + * @property {string | null | undefined} [markdownItSettingsPath] - Path to the markdown-it settings file */ /** * @template {Record} T - * @typedef {import('../page-data.js').PageData} PageData + * @template [U=any] U - The return type of the page function (defaults to any) + * @template [V=string] V - The return type of the layout function (defaults to string) + * @typedef {PageData} PageData */ /** - * pageLayout functions Can be used to type a name.layout.js file + * Common parameters for page functions. * - * @async - * @template {Record} T + * @template {Record} T - The type of variables passed to the page function + * @template [U=any] U - The return type of the page function (defaults to any) + * @typedef {object} PageFunctionParams + * @property {T} vars - All default, global, layout, page, and builder vars shallow merged. + * @property {string[]} [scripts] - Array of script URLs to include. + * @property {string[]} [styles] - Array of stylesheet URLs to include. + * @property {PageInfo} page - Info about the current page + * @property {PageData[]} pages - An array of info about every page + * @property {Object} [workers] - Map of worker names to their output paths + */ + +/** + * Synchronous page function for rendering a page layout. + * + * @template {Record} T - The type of variables passed to the page function + * @template [U=any] U - The return type of the page function (defaults to any) * @callback PageFunction - * @param {object} params - The parameters for the pageLayout. - * @param {T} params.vars - All default, global, layout, page, and builder vars shallow merged. - * @param {string[]} [params.scripts] - Array of script URLs to include. - * @param {string[]} [params.styles] - Array of stylesheet URLs to include. - * @param {PageInfo} params.page - Info about the current page - * @param {PageData[]} params.pages - An array of info about every page - * @returns {Promise} The rendered inner page thats compatible with its matched layout + * @param {PageFunctionParams} params - The parameters for the pageLayout. + * @returns {U | Promise} The rendered inner page thats compatible with its matched layout */ /** - * @template {Record} T + * Asynchronous page function for rendering a page layout. + * + * @template {Record} T - The type of variables passed to the page function + * @template [U=any] U - The return type of the page function (defaults to any) + * @callback AsyncPageFunction + * @param {PageFunctionParams} params - The parameters for the pageLayout. + * @returns {Promise} The rendered inner page thats compatible with its matched layout + */ + +/** + * pageLayout functions can be used to type a name.layout.js file (can be sync or async). + * + * @template {Record} T - The type of variables passed to the page function + * @template [U=any] U - The return type of the page function (defaults to any) + * @typedef {PageFunction | AsyncPageFunction} InternalPageFunction + */ + +/** + * @template {Record} T - The type of variables for the page + * @template [U=any] U - The return type of the pageLayout function * @typedef PageBuilderResult * @property {object} vars - Any variables resolved by the builder - * @property {PageFunction} pageLayout - The function that returns the rendered page + * @property {InternalPageFunction} pageLayout - The function that returns the rendered page */ /** - * @template {Record} T + * @template {Record} T - The type of variables for the page + * @template [U=any] U - The return type of the pageLayout function * @callback PageBuilderType * * @param {object} params * @param {PageInfo} params.pageInfo - * @returns {Promise>} - The results of the build step. + * @param {BuilderOptions} [params.options] + * @returns {Promise>} - The results of the build step. */ /** * Handles rendering and writing a page to disk * @template {Record} T + * @template [U=any] U - The return type of the page function (defaults to any) + * @template [V=string] V - The return type of the layout function (defaults to string) * @param {object} params * @param {string} params.src - The src folder. * @param {string} params.dest - The dest folder. - * @param {PageData} params.page - The PageInfo object of the current page - * @param {PageData[]} params.pages - The PageInfo[] array of all pages + * @param {PageData} params.page - The PageInfo object of the current page + * @param {PageData[]} params.pages - The PageInfo[] array of all pages */ export async function pageWriter ({ dest, @@ -63,5 +103,27 @@ export async function pageWriter ({ await mkdir(pageDir, { recursive: true }) await writeFile(pageFilePath, formattedPageOutput) + // Generate meta.json with worker mappings if page has workers + if (page.pageInfo?.workers) { + /** @type { {[workerName: string]: string } } */ + const workerMappings = {} + + for (const [workerName, workerFile] of Object.entries(page.pageInfo.workers)) { + if (workerFile.outputRelname) { + // Get the basename without the path for client usage + const outputBasename = workerFile.outputName + if (outputBasename) { + workerMappings[workerName] = outputBasename + } + } + } + + if (Object.keys(workerMappings).length > 0) { + const workersFilePath = join(pageDir, 'workers.json') + const workersContent = JSON.stringify(workerMappings, null, 2) + await writeFile(workersFilePath, workersContent) + } + } + return { pageFilePath } } diff --git a/lib/build-pages/page-builders/template-builder.js b/lib/build-pages/page-builders/template-builder.js index 0ad20bc..cdd4bf4 100644 --- a/lib/build-pages/page-builders/template-builder.js +++ b/lib/build-pages/page-builders/template-builder.js @@ -1,14 +1,10 @@ -import { join, resolve, dirname } from 'path' -import { writeFile, mkdir } from 'fs/promises' - /** - * @typedef {import('../../identify-pages.js').TemplateInfo} TemplateInfo + * @import { TemplateInfo } from '../../identify-pages.js' + * @import { PageData } from '../page-data.js' */ -/** - * @template {Record} T - * @typedef {import('../page-data.js').PageData} PageData - */ +import { join, resolve, dirname } from 'path' +import { writeFile, mkdir } from 'fs/promises' /** @typedef {{ * outputName: string, @@ -18,24 +14,24 @@ import { writeFile, mkdir } from 'fs/promises' /** * Callback for rendering a template. * - * @template {Record} T + * @template {Record} T - The type of variables for the template * @callback TemplateFunction * @param {object} params - The parameters for the template. * @param {T} params.vars - All of the site globalVars. * @param {TemplateInfo} params.template - Info about the current template - * @param {PageData[]} params.pages - An array of info about every page + * @param {PageData[]} params.pages - An array of info about every page * @returns {Promise} * } - The results of a template build */ /** - * @template {Record} T + * @template {Record} T - The type of variables for the template function parameters * @typedef {Parameters>} TemplateFunctionParams */ /** * Callback for rendering a template with an async iterator. - * @template T + * @template T - The type of variables for the template async iterator * @callback TemplateAsyncIterator * @param {TemplateFunctionParams[0]} params - Parameters of the template function. * @returns {AsyncIterable} @@ -51,13 +47,13 @@ import { writeFile, mkdir } from 'fs/promises' /** * The template builder renders templates agains the globalVars variables - * @template {Record} T + * @template {Record} T - The type of global variables for the template builder * @param {object} params * @param {string} params.src - The src path of the site build. * @param {string} params.dest - The dest path of the site build. * @param {T} params.globalVars - The resolvedGlobal vars object. * @param {TemplateInfo} params.template - The TemplateInfo of the template. - * @param {PageData[]} params.pages - The array of PageData object. + * @param {PageData[]} params.pages - The array of PageData object. */ export async function templateBuilder ({ dest, diff --git a/lib/build-pages/page-data.js b/lib/build-pages/page-data.js index 2fefbd1..773eaa1 100644 --- a/lib/build-pages/page-data.js +++ b/lib/build-pages/page-data.js @@ -1,46 +1,160 @@ +/** + * @import { PageInfo } from '../identify-pages.js' + * @import { BuilderOptions } from './page-builders/page-writer.js' + * @import { ResolvedLayout } from './index.js' + */ + import { resolveVars, resolvePostVars } from './resolve-vars.js' import { pageBuilders } from './page-builders/index.js' -// @ts-ignore +// @ts-expect-error import pretty from 'pretty' /** - * @typedef {import('../identify-pages.js').PageInfo} PageInfo - * @typedef {import('../builder.js').SiteData} SiteData + * @typedef {Object} WorkerFiles */ /** - * @template T - * @typedef {import('./index.js').ResolvedLayout} ResolvedLayout + * Resolves a layout from an ESM module. + * + * @function + * @template {Record} T - The type of variables for the layout + * @template [U=any] U - The return type of the page function (defaults to any) + * @template [V=string] V - The return type of the layout function (defaults to string) + * @param {string} layoutPath - The string path to the layout ESM module. + * @returns {Promise>} The resolved layout exported as default from the module. */ +export async function resolveLayout (layoutPath) { + const { default: layout } = await import(layoutPath) + + return layout +} + +/** + * Common parameters for layout functions. + * + * @template {Record} T - The type of variables passed to the layout function + * @template [U=any] U - The return type of the page function (defaults to any) + * @template [V=string] V - The return type of the layout function (defaults to string) + * @typedef {object} LayoutFunctionParams + * @property {T} vars - All default, global, layout, page, and builder vars shallow merged. + * @property {string[]} [scripts] - Array of script URLs to include. + * @property {string[]} [styles] - Array of stylesheet URLs to include. + * @property {U} children - The children content, either as a string or a render function. + * @property {PageInfo} page - Info about the current page + * @property {PageData[]} pages - An array of info about every page + * @property {Object} [workers] - Map of worker names to their output paths + */ + +/** + * Synchronous callback for rendering a layout. + * + * @template {Record} T - The type of variables passed to the layout function + * @template [U=any] U - The return type of the page function (defaults to any) + * @template [V=string] V - The return type of the layout function (defaults to string) + * @callback LayoutFunction + * @param {LayoutFunctionParams} params - The parameters for the layout. + * @returns {V | Promise} The rendered content. + */ + +/** + * Asynchronous callback for rendering a layout. + * + * @template {Record} T - The type of variables passed to the layout function + * @template [U=any] U - The return type of the page function (defaults to any) + * @template [V=string] V - The return type of the layout function (defaults to string) + * @callback AsyncLayoutFunction + * @param {LayoutFunctionParams} params - The parameters for the layout. + * @returns {Promise} The rendered content. + */ + +/** + * Callback for rendering a layout (can be sync or async). + * + * @template {Record} T - The type of variables passed to the layout function + * @template [U=any] U - The return type of the page function (defaults to any) + * @template [V=string] V - The return type of the layout function (defaults to string) + * @typedef {LayoutFunction | AsyncLayoutFunction} InternalLayoutFunction + */ + +/** + * Common parameters for postVars functions. + * + * @template {Record} T - The type of variables for the page + * @template [U=any] U - The return type of the page function (defaults to any) + * @template [V=string] V - The return type of the layout function (defaults to string) + * @typedef {object} PostVarsFunctionParams + * @property {T} vars - All default, global, layout, page, and builder vars shallow merged. + * @property {string[]} [scripts] - Array of script URLs to include. + * @property {string[]} [styles] - Array of stylesheet URLs to include. + * @property {PageInfo} page - Info about the current page + * @property {PageData[]} pages - An array of info about every page + * @property {Object} [workers] - Map of worker names to their output paths + */ + +/** + * Synchronous postVars function to generate page vars with access to all page data. + * + * @template {Record} T - The type of variables for the page + * @template [U=any] U - The return type of the page function (defaults to any) + * @template [V=string] V - The return type of the layout function (defaults to string) + * @callback PostVarsFunction + * @param {PostVarsFunctionParams} params - The parameters for the postVars function. + * @returns {T | Promise} The rendered postVars + */ + +/** + * Asynchronous postVars function to generate page vars with access to all page data. + * + * @template {Record} T - The type of variables for the page + * @template [U=any] U - The return type of the page function (defaults to any) + * @template [V=string] V - The return type of the layout function (defaults to string) + * @callback AsyncPostVarsFunction + * @param {PostVarsFunctionParams} params - The parameters for the postVars function. + * @returns {Promise} The rendered postVars + */ + +/** + * postVars functions can be used to generate page vars but access all page data (can be sync or async). + * + * @template {Record} T - The type of variables for the page + * @template [U=any] U - The return type of the page function (defaults to any) + * @template [V=string] V - The return type of the layout function (defaults to string) + * @typedef {PostVarsFunction | AsyncPostVarsFunction} InternalPostVarsFunction + */ /** * Represents the data for a page. - * @template {Record} T + * @template {Record} T - The type of variables for the page data + * @template [U=any] U - The return type of the page function (defaults to any) + * @template [V=string] V - The return type of the layout function (defaults to string) */ export class PageData { /** @type {PageInfo} */ pageInfo - /** @type {ResolvedLayout | null | undefined} */ layout + /** @type {ResolvedLayout | null | undefined} */ layout /** @type {object} */ globalVars /** @type {object?} */ pageVars = null /** @type {function?} */ postVars = null /** @type {object?} */ builderVars = null /** @type {string[]} */ styles = [] /** @type {string[]} */ scripts = [] + /** @type {WorkerFiles} */ workerFiles = {} /** @type {boolean} */ #initialized = false /** @type {T?} */ #renderedPostVars = null /** @type {string?} */ #defaultStyle = null /** @type {string?} */ #defaultClient = null + /** @type {BuilderOptions} */ builderOptions /** * Creates an instance of PageData. * * @param {object} options - The options object. - * @param {PageInfo} options.pageInfo - Page-specific data. - * @param {object} options.globalVars - Global variables available to all pages. - * @param {string | undefined} options.globalStyle - Global style path. - * @param {string | undefined} options.globalClient - Global client-side script path. - * @param {string?} options.defaultStyle - Default style path. - * @param {string?} options.defaultClient - Default client-side script path. + * @param {PageInfo} options.pageInfo - Page-specific data. + * @param {object} options.globalVars - Global variables available to all pages. + * @param {string | undefined} options.globalStyle - Global style path. + * @param {string | undefined} options.globalClient - Global client-side script path. + * @param {string?} options.defaultStyle - Default style path. + * @param {string?} options.defaultClient - Default client-side script path. + * @param {BuilderOptions} options.builderOptions - Options for page builders. */ constructor ({ pageInfo, @@ -49,11 +163,13 @@ export class PageData { globalClient, defaultStyle, defaultClient, + builderOptions, }) { this.pageInfo = pageInfo this.globalVars = globalVars this.#defaultStyle = defaultStyle this.#defaultClient = defaultClient + this.builderOptions = builderOptions if (globalStyle) { this.styles.push(`/${globalStyle}`) @@ -79,9 +195,17 @@ export class PageData { } /** - * @type {import('./resolve-vars.js').PostVarsFunction} + * Access web worker file paths associated with this page + * @return {WorkerFiles} Map of worker names to their output paths */ - async #renderPostVars ({ vars, styles, scripts, pages, page }) { + get workers () { + return this.workerFiles + } + + /** + * @type {AsyncPostVarsFunction} + */ + async #renderPostVars ({ vars, styles, scripts, pages, page, workers }) { if (!this.#initialized) throw new Error('Initialize PageData before accessing renderPostVars') if (!this.postVars) return this.vars if (this.#renderedPostVars) return this.#renderedPostVars @@ -91,7 +215,7 @@ export class PageData { const renderedPostVars = { ...globalVars, ...pageVars, - ...(await this.postVars({ vars, styles, scripts, pages, page })), + ...(await this.postVars({ vars, styles, scripts, pages, page, workers })), ...builderVars, } @@ -103,7 +227,7 @@ export class PageData { /** * [init description] * @param {object} params - Parameters required to initialize - * @param {Record>} params.layouts - The array of ResolvedLayouts + * @param {Record>} params.layouts - The array of ResolvedLayouts */ async init ({ layouts }) { if (this.#initialized) return @@ -119,7 +243,7 @@ export class PageData { }) const builder = pageBuilders[type] - const { vars: builderVars } = await builder({ pageInfo }) + const { vars: builderVars } = await builder({ pageInfo, options: this.builderOptions }) this.builderVars = builderVars /** @type {object} */ @@ -149,6 +273,15 @@ export class PageData { if (pageInfo.clientBundle) { this.scripts.push(`./${pageInfo.clientBundle.outputName}`) } + // Initialize web workers if they exist + if (pageInfo.workers) { + /** @type {WorkerFiles} */ + for (const [workerName, workerFile] of Object.entries(pageInfo.workers)) { + if (workerFile.outputRelname) { + this.workerFiles[workerName] = `./${workerFile.outputName}` + } + } + } // disable-eslint-next-line dot-notation if ('defaultStyle' in finalVars && finalVars.defaultStyle) { @@ -162,30 +295,31 @@ export class PageData { /** * Render the inner contents of a page. * @param {object} params The params required to render the page - * @param {PageData[]} params.pages An array of initialized PageDatas. + * @param {PageData[]} params.pages An array of initialized PageDatas. */ async renderInnerPage ({ pages }) { if (!this.#initialized) throw new Error('Must be initialized before rendering inner pages') - const { pageInfo, styles, scripts, vars } = this + const { pageInfo, styles, scripts, vars, builderOptions, workers } = this if (!pageInfo) throw new Error('A page is required to render') const builder = pageBuilders[pageInfo.type] - const { pageLayout } = await builder({ pageInfo }) - const renderedPostVars = await this.#renderPostVars({ vars, styles, scripts, pages, page: pageInfo }) - // @ts-ignore - return await pageLayout({ vars: renderedPostVars, styles, scripts, pages, page: pageInfo }) + const { pageLayout } = await builder({ pageInfo, options: builderOptions }) + const renderedPostVars = await this.#renderPostVars({ vars, styles, scripts, pages, page: pageInfo, workers }) + // @ts-expect-error - Builder types vary by page type, but the runtime type is correct + const results = await pageLayout({ vars: renderedPostVars, styles, scripts, pages, page: pageInfo, workers }) + return results } /** * Render the full contents of a page with its layout * @param {object} params The params required to render the page - * @param {PageData[]} params.pages An array of initialized PageDatas. + * @param {PageData[]} params.pages An array of initialized PageDatas. */ async renderFullPage ({ pages }) { if (!this.#initialized) throw new Error('Must be initialized before rendering full pages') - const { pageInfo, layout, vars, styles, scripts } = this + const { pageInfo, layout, vars, styles, scripts, workers } = this if (!pageInfo) throw new Error('A page is required to render') if (!layout) throw new Error('A layout is required to render') - const renderedPostVars = await this.#renderPostVars({ vars, styles, scripts, pages, page: pageInfo }) + const renderedPostVars = await this.#renderPostVars({ vars, styles, scripts, pages, page: pageInfo, workers }) const innerPage = await this.renderInnerPage({ pages }) return pretty( @@ -195,7 +329,9 @@ export class PageData { scripts, page: pageInfo, pages, + // @ts-expect-error - innerPage type varies by page builder but layout handles it children: innerPage, + workers: this.workers }) ) } diff --git a/lib/build-pages/resolve-layout.js b/lib/build-pages/resolve-layout.js deleted file mode 100644 index 2d96ffc..0000000 --- a/lib/build-pages/resolve-layout.js +++ /dev/null @@ -1,38 +0,0 @@ -/** - * @typedef {import('../identify-pages.js').PageInfo} PageInfo - */ - -/** - * @template {Record} T - * @typedef {import('./page-data.js').PageData} PageData - */ - -/** - * Callback for rendering a layout. - * - * @template T - * @callback LayoutFunction - * @param {object} params - The parameters for the layout. - * @param {T} params.vars - All default, global, layout, page, and builder vars shallow merged. - * @param {string[]} [params.scripts] - Array of script URLs to include. - * @param {string[]} [params.styles] - Array of stylesheet URLs to include. - * @param {any} params.children - The children content, either as a string or a render function. - * @param {PageInfo} params.page - Info about the current page - * @param {PageData[]} params.pages - An array of info about every page - * @returns {Promise} The rendered HTML string. - */ - -/** - * Resolves a layout from an ESM module. - * - * @async - * @function - * @template T - * @param {string} layoutPath - The string path to the layout ESM module. - * @returns {Promise>} The resolved layout exported as default from the module. - */ -export async function resolveLayout (layoutPath) { - const { default: layout } = await import(layoutPath) - - return layout -} diff --git a/lib/build-pages/resolve-vars.js b/lib/build-pages/resolve-vars.js index 3e181cc..5014945 100644 --- a/lib/build-pages/resolve-vars.js +++ b/lib/build-pages/resolve-vars.js @@ -31,21 +31,6 @@ export async function resolveVars ({ } } -/** - * postVars functions Can be used to generate page vars but access all page data - * - * @async - * @template {Record} T - * @callback PostVarsFunction - * @param {object} params - The parameters for the pageLayout. - * @param {T} params.vars - All default, global, layout, page, and builder vars shallow merged. - * @param {string[]} [params.scripts] - Array of script URLs to include. - * @param {string[]} [params.styles] - Array of stylesheet URLs to include. - * @param {import('../identify-pages.js').PageInfo} params.page - Info about the current page - * @param {import('./page-data.js').PageData[]} params.pages - An array of info about every page - * @returns {Promise} The rendered postVars - */ - /** * Resolve variables by importing them from a specified path. * diff --git a/lib/build-pages/resolve-vars.test.js b/lib/build-pages/resolve-vars.test.js index 2a563e9..cb4b8a2 100644 --- a/lib/build-pages/resolve-vars.test.js +++ b/lib/build-pages/resolve-vars.test.js @@ -1,15 +1,18 @@ -import tap from 'tap' +import { test } from 'node:test' +import assert from 'node:assert' import { resolve } from 'path' import { resolveVars } from './resolve-vars.js' const __dirname = import.meta.dirname -tap.test('resolve vars resolves vars', async (t) => { - const varsPath = resolve(__dirname, '../../test-cases/general-features/src/globals/global.vars.js') +test.describe('resolve-vars', () => { + test('resolve vars resolves vars', async () => { + const varsPath = resolve(__dirname, '../../test-cases/general-features/src/globals/global.vars.js') - const vars = await resolveVars({ varsPath }) + const vars = await resolveVars({ varsPath }) - // @ts-ignore - t.equal(vars.foo, 'global') + // @ts-ignore + assert.equal(vars.foo, 'global') + }) }) diff --git a/lib/build-static/index.js b/lib/build-static/index.js index 8cb3487..934dc33 100644 --- a/lib/build-static/index.js +++ b/lib/build-static/index.js @@ -1,3 +1,7 @@ +/** + * @import { BuildStepResult } from '../builder.js' + * @import { BuildStep } from '../builder.js' + */ // @ts-ignore import cpx from 'cpx2' const copy = cpx.copy @@ -7,11 +11,11 @@ const copy = cpx.copy */ /** - * @typedef {import('../builder.js').BuildStepResult<'static', StaticBuilderReport>} StaticBuildStepResult + * @typedef {BuildStepResult<'static', StaticBuilderReport>} StaticBuildStepResult */ /** - * @typedef {import('../builder.js').BuildStep<'static', StaticBuilderReport>} StaticBuildStep + * @typedef {BuildStep<'static', StaticBuilderReport>} StaticBuildStep */ /** diff --git a/lib/builder.js b/lib/builder.js index 09fcb98..5ac58cc 100644 --- a/lib/builder.js +++ b/lib/builder.js @@ -1,19 +1,23 @@ +/** + * @import {Message as EsbuildMessage} from 'esbuild' + * @import { DomStackWarning } from './helpers/dom-stack-warning.js' + * @import { EsBuildStepResults } from './build-esbuild/index.js' + * @import { PageBuildStepResult } from './build-pages/index.js' + * @import { StaticBuildStepResult } from './build-static/index.js' + * @import { CopyBuildStepResult } from './build-copy/index.js' +*/ + import { buildPages } from './build-pages/index.js' import { identifyPages } from './identify-pages.js' import { buildStatic } from './build-static/index.js' import { buildCopy } from './build-copy/index.js' import { buildEsbuild } from './build-esbuild/index.js' -import { TopBunAggregateError } from './helpers/top-bun-aggregate-error.js' +import { DomStackAggregateError } from './helpers/dom-stack-aggregate-error.js' import { ensureDest } from './helpers/ensure-dest.js' -/** - * @typedef {import('esbuild').Message} EsbuildMessage - * @typedef {import('./helpers/top-bun-warning.js').TopBunWarning} TopBunWarning - */ - /** * @typedef {Array} BuildStepErrors - * @typedef {Array} BuildStepWarnings + * @typedef {Array} BuildStepWarnings */ /** @@ -35,12 +39,12 @@ import { ensureDest } from './helpers/ensure-dest.js' * @param {string} src - The source directory from which the site should be built. * @param {string} dest - The destination directory where the built site should be placed. * @param {SiteData} siteData - Data related to the site being built. - * @param {TopBunOpts?} opts - Additional options for the build step. + * @param {DomStackOpts?} opts - Additional options for the build step. * @returns {Promise>} - The results of the build step. */ /** - * @typedef TopBunOpts + * @typedef DomStackOpts * @property {boolean|undefined} [static=true] - Enable/disable static file processing * @property {boolean|undefined} [metafile=true] - Enable/disable the writing of the esbuild metadata file. * @property {string[]|undefined} [ignore=[]] - Array of ignore strings @@ -54,13 +58,6 @@ import { ensureDest } from './helpers/ensure-dest.js' * @typedef {Awaited>} SiteData */ -/** - * @typedef {import('./build-esbuild/index.js').EsBuildStepResults} EsBuildStepResults - * @typedef {import('./build-pages/index.js').PageBuildStepResult} PageBuildStepResult - * @typedef {import('./build-static/index.js').StaticBuildStepResult} StaticBuildStepResult - * @typedef {import('./build-copy/index.js').CopyBuildStepResult} CopyBuildStepResult - */ - /** * @typedef Results * @property {SiteData} siteData @@ -72,16 +69,15 @@ import { ensureDest } from './helpers/ensure-dest.js' */ /** - * Builds a top-bun site from src to dest with a few options. + * Builds a domstack site from src to dest with a few options. * * - * @async * @function * @export * @param {string} src - The source directory from which the site should be built. * @param {string} dest - The destination directory where the built site should be placed. - * @param {TopBunOpts} opts - Options for the build process. - * @returns Results + * @param {DomStackOpts} opts - Options for the build process. + * @returns {Promise} * * @example * @@ -106,7 +102,7 @@ export async function builder (src, dest, opts) { warnings.push(...siteData.warnings) if (siteData.errors.length > 0) { - const pageWalkErrors = new TopBunAggregateError(siteData.errors, 'Page walk finished but there were errors.', siteData) + const pageWalkErrors = new DomStackAggregateError(siteData.errors, 'Page walk finished but there were errors.', siteData) throw pageWalkErrors } @@ -145,7 +141,7 @@ export async function builder (src, dest, opts) { results.copyResults = copyResults if (errors.length > 0) { - const preBuildError = new TopBunAggregateError(errors, 'Prebuild finished but there were errors.', results) + const preBuildError = new DomStackAggregateError(errors, 'Prebuild finished but there were errors.', results) throw preBuildError } @@ -156,7 +152,7 @@ export async function builder (src, dest, opts) { results.pageBuildResults = pageBuildResults if (errors.length > 0) { - const buildError = new TopBunAggregateError(errors, 'Build finished but there were errors.', results) + const buildError = new DomStackAggregateError(errors, 'Build finished but there were errors.', results) throw buildError } else { return results diff --git a/lib/defaults/default.client.js b/lib/defaults/default.client.js index 4000282..e2babe9 100644 --- a/lib/defaults/default.client.js +++ b/lib/defaults/default.client.js @@ -1,4 +1,4 @@ -// @ts-ignore +// @ts-expect-error import { toggleTheme } from 'mine.css' -// @ts-ignore +// @ts-expect-error window.toggleTheme = toggleTheme diff --git a/lib/defaults/default.root.layout.js b/lib/defaults/default.root.layout.js index a14f0ef..9b8164c 100644 --- a/lib/defaults/default.root.layout.js +++ b/lib/defaults/default.root.layout.js @@ -1,25 +1,28 @@ -// @ts-ignore -import { html, render } from 'uhtml-isomorphic' - /** - * @template {Record} T - * @typedef {import('../build-pages/resolve-layout.js').LayoutFunction} LayoutFunction + * @import { LayoutFunction } from '../../index.js' + * @import { VNode } from 'preact' */ +import { html } from 'htm/preact' +import { render } from 'preact-render-to-string' /** - * Build all of the bundles using esbuild. - * - * @type {LayoutFunction<{ + * @typedef {{ * title: string, * siteName: string, * defaultStyle: boolean, * basePath: string - * }>} + * }} DefaultRootLayoutVars + */ + +/** + * Build all of the bundles using esbuild. + * + * @type {LayoutFunction} */ export default function defaultRootLayout ({ vars: { title, - siteName = 'TopBun', + siteName = 'domstack', basePath, /* defaultStyle = true Set this to false in global or page to disable the default style in the default layout */ }, @@ -29,25 +32,30 @@ export default function defaultRootLayout ({ /* pages */ /* page */ }) { - return render(String, html` + return /* html */` - - - ${title ? `${title}` : ''}${title && siteName ? ' | ' : ''}${siteName} - - ${scripts - ? scripts.map(script => html``) - : null} - ${styles - ? styles.map(style => html``) - : null} - - -
    - ${typeof children === 'string' ? html([children]) : children /* Support both uhtml and string children. Optional. */} -
    - + ${render(html` + + + ${title ? `${title}` : ''}${title && siteName ? ' | ' : ''}${siteName} + + ${scripts + ? scripts.map(script => html``) - : null} - ${styles - ? styles.map(style => html``) - : null} + + + ${scripts?.map(script => + html`