diff --git a/.changeset/README.md b/.changeset/README.md deleted file mode 100644 index 654c6d4..0000000 --- a/.changeset/README.md +++ /dev/null @@ -1,8 +0,0 @@ -# Changesets - -Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works -with multi-package repos, or single-package repos to help you version and publish your code. You can -find the full documentation for it [in our repository](https://github.com/changesets/changesets). - -We have a quick list of common questions to get you started engaging with this project in -[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md). diff --git a/.changeset/bold-heads-play.md b/.changeset/bold-heads-play.md new file mode 100644 index 0000000..f7df642 --- /dev/null +++ b/.changeset/bold-heads-play.md @@ -0,0 +1,6 @@ +--- +"@michthemaker/vanjs": patch +--- + +- Added repository subdirectory in package.json +- Use `dist` folder in dev to mirror production build output diff --git a/.changeset/config.json b/.changeset/config.json index a173979..ccf0ddc 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -7,5 +7,10 @@ "access": "public", "baseBranch": "main", "updateInternalDependencies": "patch", - "ignore": ["basic-reactivity", "plugin-test", "plugin-test-manual"] + "ignore": [ + "basic-reactivity", + "plugin-test", + "plugin-test-manual", + "vanjs-ts-starter" + ] } diff --git a/.changeset/five-radios-taste.md b/.changeset/five-radios-taste.md new file mode 100644 index 0000000..1465ac0 --- /dev/null +++ b/.changeset/five-radios-taste.md @@ -0,0 +1,7 @@ +--- +"@michthemaker/vite-plugin-vanjs": patch +--- + +- Fix README.md content to cater to @michthemaker/vite-plugin-vanjs +- Use .ts file extension for all source imports for consistency +- Use `dist` folder in dev to mirror production build output diff --git a/.changeset/short-monkeys-poke.md b/.changeset/short-monkeys-poke.md new file mode 100644 index 0000000..4d9e78e --- /dev/null +++ b/.changeset/short-monkeys-poke.md @@ -0,0 +1,5 @@ +--- +"@michthemaker/vite-plugin-vanjs": patch +--- + +Added repository subdirectory in package.json diff --git a/.changeset/sour-toes-bow.md b/.changeset/sour-toes-bow.md new file mode 100644 index 0000000..78689fe --- /dev/null +++ b/.changeset/sour-toes-bow.md @@ -0,0 +1,52 @@ +--- +"create-van-app": minor +--- + +Project scaffolder for VanJS apps with support for JavaScript, TypeScript, and Tailwind CSS. + +## Features + +- Interactive CLI — prompts for project name, framework variant, and styling approach +- Non-interactive mode — fully scriptable via flags for CI and AI agent environments +- Detects AI agent environments and suggests one-shot usage automatically +- Auto-detects package manager (npm, pnpm, yarn, bun, deno) from the environment +- Optional immediate install and dev server start with `--immediate` +- Handles existing directories — offers to overwrite, ignore, or cancel + +## Templates + +| Template | Language | Styling | +| ------------- | ---------- | --------------- | +| `ts-tailwind` | TypeScript | Tailwind CSS v3 | +| `ts-css` | TypeScript | Plain CSS | +| `js-tailwind` | JavaScript | Tailwind CSS v3 | +| `js-css` | JavaScript | Plain CSS | + +## TypeScript + Tailwind template includes + +- `@michthemaker/vanjs` + `@michthemaker/vite-plugin-vanjs` with HMR +- Tailwind CSS v3 with PostCSS and Autoprefixer +- `clsx` + `tailwind-merge` via a `cn()` utility +- `darkMode: "media"` — dark mode via `prefers-color-scheme` +- `experimental.classRegex` configured for Tailwind IntelliSense in plain `.ts` files (works in VS Code and Zed) +- `@src` path alias preconfigured in Vite and TypeScript +- Starter `App.ts` with a working counter example + +## Usage + +```bash +# npm +npm create van-app@latest + +# pnpm +pnpm create van-app + +# yarn +yarn create van-app + +# bun +bun create van-app + +# non-interactive +pnpm create van-app my-app --template ts-tailwind --no-interactive +``` diff --git a/.changeset/sunny-bottles-juggle.md b/.changeset/sunny-bottles-juggle.md new file mode 100644 index 0000000..95d6f4e --- /dev/null +++ b/.changeset/sunny-bottles-juggle.md @@ -0,0 +1,16 @@ +--- +"@michthemaker/vanjs": minor +--- + +Tags can now be created with a **ref** prop to get a reference to the underlying DOM element. + +```ts +import { van, type Ref } from "@michthemaker/vanjs"; + +const { div } = van.tags; + +const ref: Ref = { current: null }; +return div({ ref }); +``` + +A ref is just a plain JavaScript object with a `current` property that holds the DOM element. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 354a89f..be6d992 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,6 +26,13 @@ jobs: - name: Install dependencies run: pnpm install + # we build packages before typechecking to ensure examples/ dir use correct packages they are up-to-date + - name: Build Packages + run: pnpm --filter "./packages/**" build + + - name: List packages dir before build + run: ls ./packages -R + - name: Typecheck # use tsgo instead of tsc run: pnpm tsgo -b --noEmit diff --git a/.gitignore b/.gitignore index dc54eb0..96f15c2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /*.sh /node_modules **/node_modules +/packages/test-template-dir diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..a7b87ec --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +public-hoist-pattern[]=magic-string diff --git a/.zed/settings.json b/.zed/settings.json index ce00db1..fb41076 100644 --- a/.zed/settings.json +++ b/.zed/settings.json @@ -9,6 +9,16 @@ { // lint and formatting "lsp": { + "tailwindcss-language-server": { + "settings": { + "experimental": { + "classRegex": [ + ["cn\\(([^)]*)\\)", "\"'`([^\"'`]*).*?[\"'`]"], + ["class:\\s*[\"'`]([^\"'`]*)[\"'`]"] + ] + } + } + }, "oxlint": { "initialization_options": { "settings": { diff --git a/CICD.md b/CICD.md new file mode 100644 index 0000000..dd8e973 --- /dev/null +++ b/CICD.md @@ -0,0 +1,127 @@ +# CI/CD Flow + +## Branches + +``` +main ─────────────────────────────────────────► (stable, protected) + └── release ───────────────────────────────► (publish trigger) +``` + +--- + +## Pull Request Flow (main) + +``` +your feature branch + │ + │ git push origin + ▼ + open PR → main + │ + ├── ✅ Typecheck (tsgo -b --noEmit) + │ + ├── pass → PR can be merged + └── fail → PR is blocked, fix and push again +``` + +> Only admins can merge PRs. No force pushing to main. + +--- + +## Release Flow + +``` +main (up to date) + │ + │ git checkout release + │ git merge main + │ git push origin release + ▼ +release branch + │ + ├── pnpm install (packages/ only) + ├── pnpm build (packages/ only) + │ + └── changesets/action + │ + ├── finds .changeset/*.md files? + │ │ + │ ├── YES → opens "Version Packages" PR + │ │ bumps versions + │ │ generates CHANGELOG.md per package + │ │ deletes consumed .changeset files + │ │ + │ └── NO → nothing to publish, workflow exits + │ + └── "Version Packages" PR merged? + │ + └── YES → publishes to npm + creates GitHub release +``` + +--- + +## Changeset Workflow (per PR) + +``` +finish your work + │ + │ pnpm changeset + ▼ +CLI asks: + ├── which packages changed? (@michthemaker/vanjs, @michthemaker/vite-plugin-vanjs) + ├── how significant? (patch / minor / major) + └── describe what changed? (write while context is fresh) + │ + ▼ +.changeset/random-name.md created + │ + │ git add .changeset/ + │ git commit -m "chore: add changeset" + ▼ +committed alongside your code changes in the same PR +``` + +--- + +## Bump Types + +| Type | When to use | Example | +| ------- | --------------------------------- | ----------------- | +| `patch` | Bug fix, no API change | `0.1.0` → `0.1.1` | +| `minor` | New feature, backwards compatible | `0.1.0` → `0.2.0` | +| `major` | Breaking change | `0.1.0` → `1.0.0` | + +--- + +## Per-package versioning + +Packages version and publish **independently** — if only `vanjs` has changesets, only `vanjs` gets a new npm version. + +``` +.changeset/ + ├── fuzzy-lion.md → affects @michthemaker/vanjs (minor) + └── odd-bikes.md → affects @michthemaker/vite-plugin-vanjs (patch) + │ + ▼ + @michthemaker/vanjs 0.1.0 → 0.2.0 ✅ published + @michthemaker/vite-plugin-vanjs 0.1.0 → 0.1.1 ✅ published +``` + +--- + +## Full Picture + +``` +feature branch + │ + ▼ +PR → main ──── typecheck CI + │ + ▼ +merge main → release ──── build + publish CI + │ + ▼ + npm registry 📦 + GitHub release 🚀 +``` diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ce5032e..1c6d51d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -34,16 +34,21 @@ vanjs/ │ ├── reactive-lists.ts # list binding logic │ └── event-handlers.ts # event handler types │ - └── vite-plugin-vanjs/ # vite HMR plugin + ├── vite-plugin-vanjs/ # vite HMR plugin + │ └── src/ + │ ├── index.ts # plugin entry point + │ └── plugin.ts # HMR transformation logic + └── create-van-app/ # project scaffolding cli tool └── src/ - ├── index.ts # plugin entry point - └── plugin.ts # HMR transformation logic + ├── index.ts # binary entry point ``` If you're fixing a core reactivity bug → `packages/vanjs/src/index.ts` If you're fixing an HMR or transform bug → `packages/vite-plugin-vanjs/src/plugin.ts` +If you're fixing a Project Scaffolding bug → `packages/create-van-app/src/index.ts` + If you're adding an example → `apps/examples//` --- @@ -85,6 +90,12 @@ pnpm build pnpm dev ``` +### Working on the Project Scaffolding + +```bash +cd packages/create-van-app +``` + ### Running the example app The example app is the best way to test changes end-to-end — it uses both `packages/vanjs` and `packages/vite-plugin-vanjs` locally via workspace linking. @@ -114,7 +125,7 @@ type(scope): short description Optional longer explanation if needed. ``` -Scope should be the package name: `vanjs` or `vite-plugin-vanjs` +Scope should be the package name: `vanjs` or `vite-plugin-vanjs` or `create-van-app` Types: `fix`, `feat`, `docs`, `refactor`, `test`, `chore` @@ -125,6 +136,7 @@ fix(vanjs): handle empty array in reactive list binding feat(vanjs): add rawVal to state for non-tracking reads fix(vite-plugin-vanjs): support async arrow function components docs(vanjs): clarify van.derive vs useEffect comparison +feat(create-van-app): add vanjs-tailwind template ``` --- @@ -136,7 +148,7 @@ A good bug report includes: - A **minimal reproduction** — the smallest possible code that shows the problem - What you **expected** to happen - What **actually** happened -- Which package is affected — `vanjs` or `vite-plugin-vanjs` +- Which package is affected — `vanjs` or `vite-plugin-vanjs` or `create-van-app` - Your environment — browser, Node version, pnpm version --- diff --git a/apps/examples/basic-reactivity/package.json b/apps/examples/basic-reactivity/package.json index 0c9258c..15feddd 100644 --- a/apps/examples/basic-reactivity/package.json +++ b/apps/examples/basic-reactivity/package.json @@ -1,6 +1,7 @@ { "name": "basic-reactivity", "version": "1.0.0", + "private": true, "description": "", "keywords": [], "license": "ISC", diff --git a/apps/examples/plugin-test-manual/package.json b/apps/examples/plugin-test-manual/package.json index ae917fc..dec2da6 100644 --- a/apps/examples/plugin-test-manual/package.json +++ b/apps/examples/plugin-test-manual/package.json @@ -1,6 +1,7 @@ { "name": "plugin-test-manual", "version": "1.0.0", + "private": true, "description": "", "keywords": [], "license": "ISC", diff --git a/apps/examples/plugin-test/package.json b/apps/examples/plugin-test/package.json index 3946c39..330dfe7 100644 --- a/apps/examples/plugin-test/package.json +++ b/apps/examples/plugin-test/package.json @@ -1,6 +1,7 @@ { "name": "plugin-test", "version": "1.0.0", + "private": true, "description": "", "keywords": [], "license": "ISC", diff --git a/apps/examples/plugin-test/src/main.ts b/apps/examples/plugin-test/src/main.ts index a3d7384..b046f7b 100644 --- a/apps/examples/plugin-test/src/main.ts +++ b/apps/examples/plugin-test/src/main.ts @@ -1,4 +1,4 @@ -import van from "@michthemaker/vanjs"; +import van, { type Ref } from "@michthemaker/vanjs"; import { Counter } from "./barrel-export"; const { div, h1, button } = van.tags; @@ -6,6 +6,7 @@ const { div, h1, button } = van.tags; // Component with props - using named export const App = (props: { name: string }) => { const myName = van.state("Mich"); + const ref: Ref = { current: null }; return div( { style: @@ -15,6 +16,7 @@ const App = (props: { name: string }) => { { style: "color: #333; border-bottom: 2px solid #eee; padding-bottom: 10px;", + ref: ref, }, "VanJS Multi-File HMR Test - me us ", props.name, diff --git a/apps/examples/test-template/README.md b/apps/examples/test-template/README.md new file mode 100644 index 0000000..d222eea --- /dev/null +++ b/apps/examples/test-template/README.md @@ -0,0 +1,28 @@ +## Usage + +```bash +$ npm install # or pnpm install or yarn install +``` + +### Learn more on the [VanJS Website](https://vanjs.org) + +## Available Scripts + +In the project directory, you can run: + +### `npm run dev` + +Runs the app in the development mode.
+Open [http://localhost:5173](http://localhost:5173) to view it in the browser. + +### `npm run build` + +Builds the app for production to the `dist` folder.
+It correctly bundles VanJS in production mode and optimizes the build for the best performance. + +The build is minified and the filenames include the hashes.
+Your app is ready to be deployed! + +## Deployment + +Learn more about deploying your application with the [documentations](https://vite.dev/guide/static-deploy.html) diff --git a/apps/examples/test-template/_gitignore b/apps/examples/test-template/_gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/apps/examples/test-template/_gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/apps/examples/test-template/index.html b/apps/examples/test-template/index.html new file mode 100644 index 0000000..f1833d2 --- /dev/null +++ b/apps/examples/test-template/index.html @@ -0,0 +1,13 @@ + + + + + + + VanJS + JavaScript + + +
+ + + diff --git a/apps/examples/test-template/package.json b/apps/examples/test-template/package.json new file mode 100644 index 0000000..eede3b7 --- /dev/null +++ b/apps/examples/test-template/package.json @@ -0,0 +1,19 @@ +{ + "name": "vanjs-ts-starter", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "@michthemaker/vanjs": "workspace:*" + }, + "devDependencies": { + "@michthemaker/vite-plugin-vanjs": "workspace:*", + "@types/node": "^25.3.0", + "vite": "latest" + } +} diff --git a/apps/examples/test-template/public/favicon.svg b/apps/examples/test-template/public/favicon.svg new file mode 100644 index 0000000..e096922 --- /dev/null +++ b/apps/examples/test-template/public/favicon.svg @@ -0,0 +1 @@ + diff --git a/apps/examples/test-template/public/icons.svg b/apps/examples/test-template/public/icons.svg new file mode 100644 index 0000000..e952219 --- /dev/null +++ b/apps/examples/test-template/public/icons.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/examples/test-template/src/App.js b/apps/examples/test-template/src/App.js new file mode 100644 index 0000000..d09c691 --- /dev/null +++ b/apps/examples/test-template/src/App.js @@ -0,0 +1,117 @@ +import van from "@michthemaker/vanjs"; +import ViteLogo from "./assets/vite.svg"; +import VanJSLogo from "./assets/vanjs.svg"; + +const { div, h1, h2, p, a, button, code, section, ul, li, img } = van.tags; +const { svg, use } = van.tags("http://www.w3.org/2000/svg"); + +const App = () => { + const count = van.state(0); + + return div( + { class: "app" }, + + // ── Center ──────────────────────────────────────────────────────────────── + section( + { id: "center" }, + + // Hero logos + div( + { class: "hero" }, + img({ src: VanJSLogo, class: "logo", alt: "VanJS logo", width: "64", height: "64" }), + img({ src: ViteLogo, class: "logo", alt: "Vite logo", width: "64", height: "64" }), + ), + + // Headline + subtitle + div( + h1("VanJS + Vite"), + p( + { class: "subtitle" }, + "Edit ", + code("src/App.js"), + " and save to test HMR" + ) + ), + + // Counter + button( + { + class: "counter", + onclick: () => count.val++, + }, + () => `Count is ${count.val}` + ) + ), + + // ── Ticks ───────────────────────────────────────────────────────────────── + Ticks(), + + // ── Next steps ──────────────────────────────────────────────────────────── + section( + { id: "next-steps" }, + + // Docs column + div( + { class: "column" }, + svg( + { class: "column-icon", role: "presentation", "aria-hidden": "true" }, + use({ href: "/icons.svg#documentation-icon" }) + ), + h2("Documentation"), + p({ class: "column-desc" }, "Your questions, answered"), + ul( + { class: "link-list" }, + li( + a( + { href: "https://vite.dev/", target: "_blank", class: "link" }, + img({ class: "link-icon", src: ViteLogo, alt: "", width: "18", height: "18" }), + "Explore Vite" + ) + ), + li( + a( + { href: "https://github.com/michthemaker/vanjs", target: "_blank", class: "link" }, + img({ class: "link-icon", src: VanJSLogo, alt: "", width: "18", height: "18" }), + "Learn VanJS" + ) + ) + ) + ), + + // Social column + div( + { class: "column" }, + svg( + { class: "column-icon", role: "presentation", "aria-hidden": "true" }, + use({ href: "/icons.svg#social-icon" }) + ), + h2("Connect with us"), + p({ class: "column-desc" }, "Join the community"), + ul( + { class: "link-list" }, + li( + a( + { href: "https://github.com/michthemaker/vanjs", target: "_blank", class: "link" }, + svg( + { class: "link-icon", role: "presentation", "aria-hidden": "true" }, + use({ href: "/icons.svg#github-icon" }) + ), + "GitHub" + ) + ), + ) + ) + ), + + // ── Ticks ───────────────────────────────────────────────────────────────── + Ticks(), + + // ── Spacer ──────────────────────────────────────────────────────────────── + section({ class: "spacer" }) + ); +}; + +// Decorative tick marks at section boundaries +const Ticks = () => div({ class: "ticks" }); + +export default App; diff --git a/apps/examples/test-template/src/assets/vanjs.svg b/apps/examples/test-template/src/assets/vanjs.svg new file mode 100644 index 0000000..e096922 --- /dev/null +++ b/apps/examples/test-template/src/assets/vanjs.svg @@ -0,0 +1 @@ + diff --git a/apps/examples/test-template/src/assets/vite.svg b/apps/examples/test-template/src/assets/vite.svg new file mode 100644 index 0000000..5101b67 --- /dev/null +++ b/apps/examples/test-template/src/assets/vite.svg @@ -0,0 +1 @@ +Vite diff --git a/apps/examples/test-template/src/index.css b/apps/examples/test-template/src/index.css new file mode 100644 index 0000000..1b278fc --- /dev/null +++ b/apps/examples/test-template/src/index.css @@ -0,0 +1,451 @@ +/* ── Reset & base ──────────────────────────────────────────────────────────── */ + +*, +*::before, +*::after { + box-sizing: border-box; + text-box-trim: trim-both; + font-variant-numeric: tabular-nums; +} + +html { + font: + 18px/1.45 system-ui, + "Segoe UI", + Roboto, + sans-serif; + letter-spacing: 0.18px; + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + color-scheme: light dark; +} + +body { + margin: 0; + color: #6b7280; + background: #fff; + overflow: auto; + height: 100vh; + width: 100vw; +} + +.no-scrollbar::-webkit-scrollbar { + display: "none"; +} + +.no-scrollbar { + -ms-overflow-style: none; + scrollbar-width: none; +} + +.thin-scrollbar { + scrollbar-width: 0; + scrollbar-color: #d3d3d3; + scroll-padding-left: 10px; +} + +.thin-scrollbar::-webkit-scrollbar { + width: 3px; + height: 3px; + background-color: transparent; +} + +.thin-scrollbar::-webkit-scrollbar-thumb:hover { + scale: 2; +} + +.thin-scrollbar::-webkit-scrollbar-thumb { + background-color: #d3d3d3; + border-radius: 10px; +} + +div#root { + overflow: auto; + height: 100%; + width: 100%; +} + +@media (prefers-color-scheme: dark) { + body { + color: #9ca3af; + background: #111827; + } +} + +h1, +h2 { + font-family: system-ui, "Segoe UI", Roboto, sans-serif; + font-weight: 500; + color: #111827; + margin: 0; +} + +@media (prefers-color-scheme: dark) { + h1, + h2 { + color: #f3f4f6; + } +} + +h1 { + font-size: 56px; + letter-spacing: -1.68px; + margin: 32px 0; +} + +h2 { + font-size: 24px; + line-height: 118%; + letter-spacing: -0.24px; + margin: 0 0 8px; +} + +p { + margin: 0; +} + +code { + font-family: ui-monospace, Consolas, monospace; + font-size: 15px; + line-height: 135%; + padding: 4px 8px; + background: #f3f4f6; + color: #111827; + border-radius: 4px; + display: inline-flex; +} + +@media (prefers-color-scheme: dark) { + code { + background: #1f2937; + color: #f3f4f6; + } +} + +@media (max-width: 1024px) { + html { + font-size: 16px; + } + + h1 { + font-size: 36px; + margin: 20px 0; + } + + h2 { + font-size: 20px; + } +} + +/* ── App shell ──────────────────────────────────────────────────────────────── */ + +.app { + width: 100%; + max-width: 1126px; + margin: 0 auto; + text-align: center; + border-left: 1px solid #e5e7eb; + border-right: 1px solid #e5e7eb; + min-height: 100svh; + display: flex; + flex-direction: column; +} + +@media (prefers-color-scheme: dark) { + .app { + border-color: #1f2937; + } +} + +/* ── Center section ─────────────────────────────────────────────────────────── */ + +#center { + display: flex; + flex-direction: column; + gap: 18px; + place-content: center; + place-items: center; + flex-grow: 1; + padding: 32px 20px; +} + +@media (min-width: 1024px) { + #center { + gap: 25px; + padding: 32px 0; + } +} + +/* ── Hero ───────────────────────────────────────────────────────────────────── */ + +.hero { + display: flex; + align-items: center; + gap: 32px; +} + +.logo { + width: 64px; + height: 64px; +} + +/* ── Subtitle ───────────────────────────────────────────────────────────────── */ + +.subtitle { + color: #6b7280; +} + +@media (prefers-color-scheme: dark) { + .subtitle { + color: #9ca3af; + } +} + +/* ── Counter button ─────────────────────────────────────────────────────────── */ + +.counter { + font-family: ui-monospace, Consolas, monospace; + display: inline-flex; + border-radius: 4px; + color: #111827; + background: rgb(168 85 247 / 0.1); + border: 2px solid transparent; + padding: 5px 10px; + font-size: 16px; + transition: border-color 0.3s; + margin-bottom: 24px; + cursor: pointer; +} + +.counter:hover { + border-color: rgb(168 85 247 / 0.5); +} + +.counter:focus-visible { + outline: 2px solid #a855f7; + outline-offset: 2px; +} + +@media (prefers-color-scheme: dark) { + .counter { + color: #f3f4f6; + background: rgb(192 132 252 / 0.15); + } + + .counter:hover { + border-color: rgb(192 132 252 / 0.5); + } + + .counter:focus-visible { + outline-color: #c084fc; + } +} + +/* ── Ticks ──────────────────────────────────────────────────────────────────── */ + +.ticks { + position: relative; + width: 100%; +} + +.ticks::before, +.ticks::after { + content: ""; + position: absolute; + top: -4.5px; + border: 5px solid transparent; +} + +.ticks::before { + left: 0; + border-left-color: #e5e7eb; +} + +.ticks::after { + right: 0; + border-right-color: #e5e7eb; +} + +@media (prefers-color-scheme: dark) { + .ticks::before { + border-left-color: #1f2937; + } + + .ticks::after { + border-right-color: #1f2937; + } +} + +/* ── Next steps section ─────────────────────────────────────────────────────── */ + +#next-steps { + display: flex; + border-top: 1px solid #e5e7eb; + text-align: left; +} + +@media (prefers-color-scheme: dark) { + #next-steps { + border-color: #1f2937; + } +} + +@media (max-width: 1024px) { + #next-steps { + flex-direction: column; + text-align: center; + } +} + +/* ── Column ─────────────────────────────────────────────────────────────────── */ + +.column { + flex: 1 1 0; + padding: 32px; + border-right: 1px solid #e5e7eb; +} + +.column:last-child { + border-right: none; +} + +@media (prefers-color-scheme: dark) { + .column { + border-right-color: #1f2937; + } +} + +@media (max-width: 1024px) { + .column { + padding: 24px 20px; + border-right: none; + border-bottom: 1px solid #e5e7eb; + } + + .column:last-child { + border-bottom: none; + } + + @media (prefers-color-scheme: dark) { + .column { + border-bottom-color: #1f2937; + } + } +} + +.column-icon { + display: block; + width: 22px; + height: 22px; + margin-bottom: 16px; +} + +.column-desc { + color: #6b7280; +} + +@media (prefers-color-scheme: dark) { + .column-desc { + color: #9ca3af; + } +} + +/* ── Link list ──────────────────────────────────────────────────────────────── */ + +.link-list { + list-style: none; + padding: 0; + margin: 32px 0 0; + display: flex; + gap: 8px; +} + +@media (max-width: 1024px) { + .link-list { + margin-top: 20px; + flex-wrap: wrap; + justify-content: center; + } + + .link-list li { + flex: 1 1 calc(50% - 8px); + } +} + +/* ── Link ───────────────────────────────────────────────────────────────────── */ + +.link { + color: #111827; + font-size: 16px; + border-radius: 8px; + background: rgb(243 244 246 / 0.5); + box-shadow: 0 1px 2px rgb(0 0 0 / 0.1); + outline: 1px solid rgb(0 0 0 / 0.1); + display: flex; + padding: 6px 12px; + align-items: center; + gap: 8px; + text-decoration: none; + transition: box-shadow 0.3s; +} + +.link:hover { + box-shadow: + 0 10px 15px -3px rgb(0 0 0 / 0.1), + 0 4px 6px -2px rgb(0 0 0 / 0.05); +} + +@media (prefers-color-scheme: dark) { + .link { + color: #f3f4f6; + background: rgb(31 41 55 / 0.5); + box-shadow: 0 1px 2px rgb(0 0 0 / 0.3); + outline-color: rgb(255 255 255 / 0.08); + } + + .link:hover { + box-shadow: + 0 10px 15px -3px rgb(0 0 0 / 0.4), + 0 4px 6px -2px rgb(0 0 0 / 0.25); + } +} + +@media (max-width: 1024px) { + .link { + width: 100%; + justify-content: center; + } +} + +.link-icon { + width: 18px; + height: 18px; + display: inline; +} + +@media (max-width: 360px) { + .link-icon { + display: none; + } +} + +/* ── Spacer ─────────────────────────────────────────────────────────────────── */ + +.spacer { + height: 88px; + border-top: 1px solid #e5e7eb; +} + +@media (prefers-color-scheme: dark) { + .spacer { + border-color: #1f2937; + } +} + +@media (max-width: 1024px) { + .spacer { + height: 48px; + } +} diff --git a/apps/examples/test-template/src/main.js b/apps/examples/test-template/src/main.js new file mode 100644 index 0000000..95f9876 --- /dev/null +++ b/apps/examples/test-template/src/main.js @@ -0,0 +1,7 @@ +import van from "@michthemaker/vanjs"; +import "./index.css"; +import App from "./App.js"; + +const root = document.getElementById("root"); + +van.add(root, App()); diff --git a/apps/examples/test-template/tsconfig.json b/apps/examples/test-template/tsconfig.json new file mode 100644 index 0000000..8aa3bf9 --- /dev/null +++ b/apps/examples/test-template/tsconfig.json @@ -0,0 +1,33 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "types": ["vite/client"], + "skipLibCheck": true, + "paths": { + "@components/*": ["./components/*"], + "@src/*": ["./src/*"], + "@lib/*": ["./lib/*"] + }, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src", "components", "lib"], + "exclude": [] +} diff --git a/apps/examples/test-template/vite.config.js b/apps/examples/test-template/vite.config.js new file mode 100644 index 0000000..d441efc --- /dev/null +++ b/apps/examples/test-template/vite.config.js @@ -0,0 +1,20 @@ +import { defineConfig } from "vite"; +import { resolve } from "path"; +import vanjs from "@michthemaker/vite-plugin-vanjs"; + +// https://vite.dev/config/ +export default defineConfig({ + resolve: { + alias: { + "@src": resolve("./src/"), + "@components": resolve("./components/"), + }, + }, + plugins: [ + vanjs({ + hmr: { + smartStateChecking: true, + }, + }), + ], +}); diff --git a/package.json b/package.json index ed807aa..ce46fc4 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,8 @@ "format:check": "oxfmt --check .", "changeset": "changeset", "version": "changeset version", - "release": "changeset publish" + "release": "changeset publish", + "clean": "pnpm -r exec rm -rf dist" }, "devDependencies": { "@changesets/cli": "^2.30.0", diff --git a/packages/create-van-app/.gitignore b/packages/create-van-app/.gitignore new file mode 100644 index 0000000..1019782 --- /dev/null +++ b/packages/create-van-app/.gitignore @@ -0,0 +1,4 @@ +/node_modules/ +/dist/ +/*.sh +/__tests__ diff --git a/packages/create-van-app/CONTRIBUTING.md b/packages/create-van-app/CONTRIBUTING.md new file mode 100644 index 0000000..1c6d51d --- /dev/null +++ b/packages/create-van-app/CONTRIBUTING.md @@ -0,0 +1,158 @@ +# Contributing to VanJS + +Thanks for taking the time to contribute. VanJS is a small, focused project and every contribution matters. + +## Code of Conduct + +Be respectful. Constructive criticism is welcome, personal attacks are not. We're all here to build something good. + +--- + +## Ways to Contribute + +- **Bug reports** — found something broken? Open an issue with a minimal reproduction +- **Bug fixes** — check the [good first issues](https://github.com/michthemaker/vanjs/labels/good%20first%20issue) label to get started +- **Documentation** — typos, unclear explanations, missing examples — all fair game +- **Feature proposals** — open an issue first before writing code, so we can discuss the direction + +--- + +## Repository Structure + +This is a **pnpm monorepo**. The codebase is split into packages and apps: + +``` +vanjs/ +├── apps/ +│ └── examples/ # example app for testing & development +│ +└── packages/ + ├── vanjs/ # core framework + │ └── src/ + │ ├── van.ts # type definitions & interfaces + │ ├── index.ts # core implementation + │ ├── reactive-lists.ts # list binding logic + │ └── event-handlers.ts # event handler types + │ + ├── vite-plugin-vanjs/ # vite HMR plugin + │ └── src/ + │ ├── index.ts # plugin entry point + │ └── plugin.ts # HMR transformation logic + └── create-van-app/ # project scaffolding cli tool + └── src/ + ├── index.ts # binary entry point +``` + +If you're fixing a core reactivity bug → `packages/vanjs/src/index.ts` + +If you're fixing an HMR or transform bug → `packages/vite-plugin-vanjs/src/plugin.ts` + +If you're fixing a Project Scaffolding bug → `packages/create-van-app/src/index.ts` + +If you're adding an example → `apps/examples//` + +--- + +## Development Setup + +This repo uses **pnpm workspaces**. Make sure you have [pnpm](https://pnpm.io) installed before anything else. + +```bash +# Install pnpm if you don't have it +npm install -g pnpm + +# Clone the repo +git clone https://github.com/michthemaker/vanjs.git +cd vanjs + +# Install all workspace dependencies in one shot +pnpm install +``` + +### Working on the core framework + +```bash +cd packages/vanjs + +# Build the package +pnpm build + +# Run in watch mode while developing +pnpm dev +``` + +### Working on the Vite plugin + +```bash +cd packages/vite-plugin-vanjs + +pnpm build +pnpm dev +``` + +### Working on the Project Scaffolding + +```bash +cd packages/create-van-app +``` + +### Running the example app + +The example app is the best way to test changes end-to-end — it uses both `packages/vanjs` and `packages/vite-plugin-vanjs` locally via workspace linking. + +```bash +cd apps/examples + +pnpm dev +``` + +Any changes you make to `packages/vanjs` or `packages/vite-plugin-vanjs` will reflect immediately in the example app since pnpm workspace links them directly. + +--- + +## Pull Request Guidelines + +1. **Open an issue first** for non-trivial changes — saves everyone time +2. **Keep PRs focused** — one fix or feature per PR, not a bundle of unrelated changes +3. **Write clear commit messages** — describe what changed and why, not just what +4. **Don't break the example app** — run it and verify your changes work end-to-end + +### Commit message format + +``` +type(scope): short description + +Optional longer explanation if needed. +``` + +Scope should be the package name: `vanjs` or `vite-plugin-vanjs` or `create-van-app` + +Types: `fix`, `feat`, `docs`, `refactor`, `test`, `chore` + +Examples: + +``` +fix(vanjs): handle empty array in reactive list binding +feat(vanjs): add rawVal to state for non-tracking reads +fix(vite-plugin-vanjs): support async arrow function components +docs(vanjs): clarify van.derive vs useEffect comparison +feat(create-van-app): add vanjs-tailwind template +``` + +--- + +## Reporting Bugs + +A good bug report includes: + +- A **minimal reproduction** — the smallest possible code that shows the problem +- What you **expected** to happen +- What **actually** happened +- Which package is affected — `vanjs` or `vite-plugin-vanjs` or `create-van-app` +- Your environment — browser, Node version, pnpm version + +--- + +## License + +By contributing to VanJS, you agree that your contributions will be licensed under the [MIT License](./LICENSE). diff --git a/packages/create-van-app/LICENSE b/packages/create-van-app/LICENSE new file mode 100644 index 0000000..9487c2e --- /dev/null +++ b/packages/create-van-app/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Michthemaker + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/create-van-app/README.md b/packages/create-van-app/README.md new file mode 100644 index 0000000..44166a2 --- /dev/null +++ b/packages/create-van-app/README.md @@ -0,0 +1,66 @@ +# create-van-app npm package + +## Scaffolding Your First Van Project + +> **Compatibility Note:** +> Create Van App requires [Node.js](https://nodejs.org/en/) version 20.19+, 22.12+. However, some templates require a higher Node.js version to work, please upgrade if your package manager warns about it. + +With NPM: + +```bash +npm create van-app@latest +``` + +With Yarn: + +```bash +yarn create van-app +``` + +With PNPM: + +```bash +pnpm create van-app +``` + +With Bun: + +```bash +bun create van-app +``` + +With Deno: + +```bash +deno init --npm van-app +``` + +Then follow the prompts! + +You can also directly specify the project name and the template you want to use via additional command line options. For example, to scaffold a VanJS + Tailwind project, run: + +```bash +# npm 7+, extra double-dash is needed: +npm create van-app@latest my-van-app -- --template vanjs-ts-tailwind + +# yarn +yarn create van-app my-van-app --template vanjs-ts-tailwind + +# pnpm +pnpm create van-app my-van-app --template vanjs-ts-tailwind + +# Bun +bun create van-app my-van-app --template vanjs-ts-tailwind + +# Deno +deno init --npm van-app my-van-app --template vanjs-ts-tailwind +``` + +Currently supported template presets include: + +- `vanjs-ts` + `tailwind` ← default +- `vanjs-ts` + `css` +- `vanjs` + `tailwind` +- `vanjs` + `css` + +You can use `.` for the project name to scaffold in the current directory. diff --git a/packages/create-van-app/package.json b/packages/create-van-app/package.json new file mode 100644 index 0000000..98223f5 --- /dev/null +++ b/packages/create-van-app/package.json @@ -0,0 +1,60 @@ +{ + "name": "create-van-app", + "version": "0.0.0", + "description": "VanJS CLI for creating new projects", + "keywords": [ + "Minimalist", + "Reactive", + "UI", + "UI Framework", + "Ultra-lightweight", + "Van", + "Vanilla", + "Vanjs" + ], + "homepage": "https://github.com/michthemaker/vanjs/tree/main/packages/create-van-app", + "bugs": { + "url": "https://github.com/michthemaker/vanjs/issues" + }, + "license": "MIT", + "author": "Michthebrand ", + "repository": { + "type": "git", + "url": "git+https://github.com/michthemaker/vanjs.git", + "directory": "packages/create-van-app" + }, + "funding": "https://github.com/michthemaker/vanjs?sponsor=1", + "bin": { + "create-van-app": "dist/index.js" + }, + "files": [ + "template-*", + "dist" + ], + "type": "module", + "main": "./dist/index.js", + "publishConfig": { + "exports": { + ".": { + "import": "./dist/index.js" + } + }, + "main": "./dist/index.js" + }, + "scripts": { + "build": "tsup" + }, + "devDependencies": { + "@clack/prompts": "^1.1.0", + "@types/cross-spawn": "^6.0.6", + "@vercel/detect-agent": "^1.1.0", + "cross-spawn": "^7.0.6", + "mri": "^1.2.0", + "picocolors": "^1.1.1", + "tsup": "^8.5.1" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "packageManager": "pnpm@10.28.0" +} diff --git a/packages/create-van-app/src/index.ts b/packages/create-van-app/src/index.ts new file mode 100644 index 0000000..bf61695 --- /dev/null +++ b/packages/create-van-app/src/index.ts @@ -0,0 +1,535 @@ +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import type { SpawnOptions } from "node:child_process"; +import spawn from "cross-spawn"; +import mri from "mri"; +import * as prompts from "@clack/prompts"; +import colors from "picocolors"; +import { determineAgent } from "@vercel/detect-agent"; + +const { blue, green, underline, yellow } = colors; + +const argv = mri<{ + template?: string; + help?: boolean; + overwrite?: boolean; + immediate?: boolean; + interactive?: boolean; +}>(process.argv.slice(2), { + boolean: ["help", "overwrite", "immediate", "interactive"], + alias: { h: "help", t: "template", i: "immediate" }, + string: ["template"], +}); +const cwd = process.cwd(); + +// prettier-ignore +const helpMessage = `\ +Usage: create-van-app [OPTION]... [DIRECTORY] + +Create a new VanJS project in JavaScript or TypeScript. +When running in TTY, the CLI will start in interactive mode. + +Options: + -t, --template NAME use a specific template + -i, --immediate install dependencies and start dev + --interactive / --no-interactive force interactive / non-interactive mode + +Available templates: +${yellow ('vanjs-ts Typescript' )} +${green ('vanjs JavaScript' )} +` + +type ColorFunc = (str: string | number) => string; +type Framework = { + name: string; + display: string; + color: ColorFunc; + variants: FrameworkVariant[]; +}; +type FrameworkVariant = { + name: string; + display: string; + link?: `https://github.com/michthemaker/vanjs`; + color: ColorFunc; + customCommand?: string; +}; + +const FRAMEWORKS: Framework[] = [ + { + name: "vanjs-ts", + display: "VanJS TypeScript", + color: yellow, + variants: [ + { + name: "ts-tailwind", + display: "Tailwind CSS", + color: blue, + }, + { + name: "ts-css", + display: "CSS", + color: yellow, + }, + ], + }, + { + name: "vanjs", + display: "VanJS JavaScript", + color: green, + variants: [ + { + name: "js-tailwind", + display: "Tailwind CSS", + color: blue, + }, + { + name: "js-css", + display: "CSS", + color: yellow, + }, + ], + }, +]; + +const TEMPLATES = FRAMEWORKS.map((f) => f.variants.map((v) => v.name)).reduce( + (a, b) => a.concat(b), + [] +); + +const renameFiles: Record = { + _gitignore: ".gitignore", +}; + +const defaultTargetDir = "van-app"; + +function run([command, ...args]: string[], options?: SpawnOptions) { + const { status, error } = spawn.sync(command, args, options); + if (status != null && status > 0) { + process.exit(status); + } + + if (error) { + console.error(`\n${command} ${args.join(" ")} error!`); + console.error(error); + process.exit(1); + } +} + +function install(root: string, agent: string) { + if (process.env._VITE_TEST_CLI) { + prompts.log.step( + `Installing dependencies with ${agent}... (skipped in test)` + ); + return; + } + prompts.log.step(`Installing dependencies with ${agent}...`); + run(getInstallCommand(agent), { + stdio: "inherit", + cwd: root, + }); +} + +function start(root: string, agent: string) { + if (process.env._VITE_TEST_CLI) { + prompts.log.step("Starting dev server... (skipped in test)"); + return; + } + prompts.log.step("Starting dev server..."); + run(getRunCommand(agent, "dev"), { + stdio: "inherit", + cwd: root, + }); +} + +async function init() { + const argTargetDir = argv._[0] + ? formatTargetDir(String(argv._[0])) + : undefined; + const argTemplate = argv.template; + const argOverwrite = argv.overwrite; + const argImmediate = argv.immediate; + const argInteractive = argv.interactive; + + const help = argv.help; + if (help) { + console.log(helpMessage); + return; + } + + const interactive = argInteractive ?? process.stdin.isTTY; + + // Detect AI agent environment for better agent experience (AX) + const { isAgent } = await determineAgent(); + if (isAgent && interactive) { + console.log( + "\nTo create in one go, run: create-van-app --no-interactive --template