saasaloy 0.5.1 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "saasaloy",
3
- "version": "0.5.1",
3
+ "version": "0.5.2",
4
4
  "description": "Composable SaaS accelerator kit.",
5
5
  "keywords": [
6
6
  "saas",
@@ -39,6 +39,7 @@
39
39
  },
40
40
  "scripts": {
41
41
  "build": "tsup",
42
+ "clean": "rimraf dist coverage",
42
43
  "prepack": "pnpm -w run readme:cli && pnpm run build",
43
44
  "dev": "tsup --watch",
44
45
  "start": "node ./dist/index.js",
@@ -54,6 +55,7 @@
54
55
  "@types/node": "26.4.1",
55
56
  "@vitest/coverage-v8": "5.0.0",
56
57
  "release-it": "21.0.2",
58
+ "rimraf": "6.1.3",
57
59
  "tsup": "8.5.1",
58
60
  "vitest": "5.0.0"
59
61
  },
@@ -36,6 +36,13 @@ export default defineConfig({
36
36
  // `imageService: "compile"` optimises images during the build and serves the results
37
37
  // as plain assets. The adapter's default would reach for Cloudflare's IMAGES binding
38
38
  // at runtime, which needs a Worker this site does not have.
39
+ //
40
+ // package.json pins @cloudflare/vite-plugin, which this file never imports. The
41
+ // adapter takes it through a caret range, and each release of the plugin raises the
42
+ // wrangler version it demands at startup, so an unpinned install pairs a new plugin
43
+ // with our pinned wrangler and `astro dev` exits before it is ready. The direct pin
44
+ // makes pnpm install the same plugin for the adapter. Move it and `wrangler` together:
45
+ // they release as a pair, and apps/api (once added) pins the same two versions.
39
46
  adapter: cloudflare({ imageService: "compile" }),
40
47
  integrations: [react()],
41
48
  // Fixed dev port. Every cross-origin consumer in this repo — the api Worker's CORS
@@ -50,6 +57,20 @@ export default defineConfig({
50
57
  // packages/ui/src/styles/globals.css, which Layout.astro imports via @repo/ui.
51
58
  plugins: [tailwindcss()],
52
59
  server: { strictPort: true },
60
+ // Dev only: pre-bundle every dependency the React components import before the
61
+ // first request. In dev the adapter renders inside workerd, and Vite's scan starts
62
+ // from the pages, so it never walks into @repo/ui (a linked workspace, not an npm
63
+ // package) or the islands a module drops in src/. Without these entries Vite finds
64
+ // lucide-react, @base-ui/react and the rest mid-request, re-bundles, and reloads the
65
+ // worker. The re-run components then import a fresh copy of react while
66
+ // react-dom/server holds the old one, and every hook throws "Invalid hook call".
67
+ // A glob, not an include list, so a component a module adds is covered with no edit
68
+ // here.
69
+ ssr: {
70
+ optimizeDeps: {
71
+ entries: ["src/**/*.tsx", "../../packages/ui/src/**/*.tsx"],
72
+ },
73
+ },
53
74
  resolve: {
54
75
  // `@web` mirrors saasaloy.json's alias of the same name (apps/web/src) — that
55
76
  // alias only drives the CLI's file-placement when a module's files[] target
@@ -4,27 +4,28 @@
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "scripts": {
7
- "dev": "astro dev",
7
+ "dev": "astro dev --host",
8
8
  "build": "astro build",
9
9
  "clean": "rimraf dist .astro .wrangler",
10
10
  "preview": "astro preview",
11
11
  "deploy": "wrangler deploy"
12
12
  },
13
13
  "dependencies": {
14
- "astro": "7.3.1",
15
- "@astrojs/cloudflare": "14.3.0",
16
- "@astrojs/react": "6.0.5",
17
- "react": "19.2.8",
18
- "react-dom": "19.2.8",
14
+ "astro": "7.3.5",
15
+ "@astrojs/cloudflare": "14.3.3",
16
+ "@astrojs/react": "7.0.0",
17
+ "react": "19.3.0",
18
+ "react-dom": "19.3.0",
19
19
  "@repo/ui": "workspace:*"
20
20
  },
21
21
  "devDependencies": {
22
+ "@cloudflare/vite-plugin": "1.62.3",
22
23
  "@tailwindcss/vite": "4.3.3",
23
- "@types/node": "26.4.1",
24
- "@types/react": "19.2.18",
25
- "@types/react-dom": "19.2.7",
24
+ "@types/node": "26.6.3",
25
+ "@types/react": "19.3.0",
26
+ "@types/react-dom": "19.3.0",
26
27
  "rimraf": "6.1.3",
27
28
  "tailwindcss": "4.3.3",
28
- "wrangler": "4.129.0"
29
+ "wrangler": "4.145.0"
29
30
  }
30
31
  }
@@ -2,6 +2,13 @@
2
2
  "$schema": "node_modules/wrangler/config-schema.json",
3
3
  "name": "{{PROJECT_NAME}}-web",
4
4
  "compatibility_date": "2026-07-22",
5
+ // `astro dev` renders inside workerd, and @astrojs/react's server renderer keeps a
6
+ // dynamic `import("node:stream")` for the Node streaming path. That path never runs
7
+ // here (workerd resolves react-dom/server to the edge build), but without this flag
8
+ // @cloudflare/vite-plugin warns "Unexpected Node.js imports" on every dev start. The
9
+ // adapter only adds `nodejs_als` on its own. It costs nothing while the site is
10
+ // assets-only, and the first `prerender = false` page gets Node APIs with no edit.
11
+ "compatibility_flags": ["nodejs_compat"],
5
12
  // Static assets, and still no Worker code — but the layout moved. Registering
6
13
  // @astrojs/cloudflare in astro.config.mjs (for src/pages/500.astro) makes Astro build
7
14
  // to dist/client + dist/server instead of a flat dist/. While every page is
@@ -23,22 +23,22 @@
23
23
  "typecheck": "turbo run typecheck"
24
24
  },
25
25
  "devDependencies": {
26
- "@commitlint/cli": "21.2.2",
27
- "@commitlint/config-conventional": "21.2.2",
26
+ "@commitlint/cli": "21.2.3",
27
+ "@commitlint/config-conventional": "21.2.3",
28
28
  "husky": "9.1.7",
29
- "lint-staged": "17.5.0",
30
- "oxlint": "1.81.0",
31
- "oxlint-tsgolint": "7.0.2001",
32
- "prettier": "3.9.6",
33
- "prettier-plugin-astro": "0.14.1",
29
+ "lint-staged": "17.6.0",
30
+ "oxlint": "1.86.0",
31
+ "oxlint-tsgolint": "7.0.2003",
32
+ "prettier": "3.9.9",
33
+ "prettier-plugin-astro": "1.1.0",
34
34
  "prettier-plugin-tailwindcss": "0.8.1",
35
35
  "rimraf": "6.1.3",
36
36
  "stylelint": "17.15.0",
37
37
  "stylelint-config-idiomatic-order": "10.0.0",
38
38
  "stylelint-config-standard": "40.0.0",
39
39
  "stylelint-prettier": "5.0.3",
40
- "turbo": "2.10.12",
40
+ "turbo": "2.11.6",
41
41
  "typescript": "7.0.2",
42
- "ultracite": "7.11.0"
42
+ "ultracite": "7.12.2"
43
43
  }
44
44
  }
@@ -20,9 +20,9 @@
20
20
  "@base-ui/react": "1.8.0",
21
21
  "class-variance-authority": "0.7.1",
22
22
  "clsx": "2.1.1",
23
- "lucide-react": "1.41.0",
23
+ "lucide-react": "1.49.0",
24
24
  "shadcn": "4.21.0",
25
- "tailwind-merge": "3.6.0",
25
+ "tailwind-merge": "3.7.0",
26
26
  "tailwindcss": "4.3.3",
27
27
  "tw-animate-css": "1.4.0"
28
28
  },
@@ -31,8 +31,8 @@
31
31
  },
32
32
  "devDependencies": {
33
33
  "@repo/tsconfig": "workspace:*",
34
- "@types/react": "19.2.18",
35
- "react": "19.2.8",
34
+ "@types/react": "19.3.0",
35
+ "react": "19.3.0",
36
36
  "rimraf": "6.1.3",
37
37
  "typescript": "7.0.2"
38
38
  }
@@ -124,6 +124,48 @@ export function relabelThemeToggles(theme?: Theme): void {
124
124
  }
125
125
  }
126
126
 
127
+ // The toggle's click handler, installed by `installThemeToggle` below. It lives at module
128
+ // scope because it reads nothing from that function, and one identity per page means a
129
+ // second install adds no second listener.
130
+ function onClick(event: MouseEvent): void {
131
+ const { target } = event;
132
+ if (!(target instanceof Element)) {
133
+ return;
134
+ }
135
+ if (!target.closest(`[${THEME_TOGGLE_ATTRIBUTE}]`)) {
136
+ return;
137
+ }
138
+
139
+ const painted = document.documentElement.getAttribute(THEME_ATTRIBUTE);
140
+ // An unrecognised `data-theme` gives index -1 and starts the cycle at the head of
141
+ // THEME_ORDER.
142
+ const index = isTheme(painted) ? THEME_ORDER.indexOf(painted) : -1;
143
+ const next = THEME_ORDER[(index + 1) % THEME_ORDER.length] ?? "light";
144
+
145
+ setTheme(next);
146
+
147
+ // After `setTheme`, and unconditionally: a `system` press CLEARS THEME_STORAGE_KEY, so
148
+ // this key is the only record that the visitor pressed anything at all.
149
+ try {
150
+ localStorage.setItem(THEME_CHOICE_KEY, "1");
151
+ } catch {
152
+ // Unwritable storage costs persistence, not the current page.
153
+ }
154
+ }
155
+
156
+ // The OS half of the same job, and the reason this is not click-only. THEME_INIT_SCRIPT
157
+ // registers its own matchMedia listener; a host that calls `installThemeToggle` instead
158
+ // does not have that script, so without this listener a visitor on `system` keeps the
159
+ // palette resolved at load until the next reload.
160
+ //
161
+ // It repaints only while the state is `system`. A visitor who picked light or dark said
162
+ // so deliberately, and the OS must not overrule that.
163
+ function onOsChange(): void {
164
+ if (getStoredTheme() === "system") {
165
+ setTheme("system");
166
+ }
167
+ }
168
+
127
169
  /**
128
170
  * Install the toggle's click behaviour on `document`, and answer with a function that
129
171
  * removes it again.
@@ -139,47 +181,8 @@ export function relabelThemeToggles(theme?: Theme): void {
139
181
  * the same value forever and every press would land on the same theme.
140
182
  */
141
183
  export function installThemeToggle(): () => void {
142
- function onClick(event: MouseEvent): void {
143
- const { target } = event;
144
- if (!(target instanceof Element)) {
145
- return;
146
- }
147
- if (!target.closest(`[${THEME_TOGGLE_ATTRIBUTE}]`)) {
148
- return;
149
- }
150
-
151
- const painted = document.documentElement.getAttribute(THEME_ATTRIBUTE);
152
- // An unrecognised `data-theme` gives index -1 and starts the cycle at the head of
153
- // THEME_ORDER.
154
- const index = isTheme(painted) ? THEME_ORDER.indexOf(painted) : -1;
155
- const next = THEME_ORDER[(index + 1) % THEME_ORDER.length] ?? "light";
156
-
157
- setTheme(next);
158
-
159
- // After `setTheme`, and unconditionally: a `system` press CLEARS THEME_STORAGE_KEY, so
160
- // this key is the only record that the visitor pressed anything at all.
161
- try {
162
- localStorage.setItem(THEME_CHOICE_KEY, "1");
163
- } catch {
164
- // Unwritable storage costs persistence, not the current page.
165
- }
166
- }
167
-
168
- // The OS half of the same job, and the reason this is not click-only. THEME_INIT_SCRIPT
169
- // registers its own matchMedia listener; a host that calls this function instead does
170
- // not have that script, so without this listener a visitor on `system` keeps the palette
171
- // resolved at load until the next reload.
172
- //
173
- // It repaints only while the state is `system`. A visitor who picked light or dark said
174
- // so deliberately, and the OS must not overrule that.
175
184
  const media = window.matchMedia(OS_DARK_QUERY);
176
185
 
177
- function onOsChange(): void {
178
- if (getStoredTheme() === "system") {
179
- setTheme("system");
180
- }
181
- }
182
-
183
186
  document.addEventListener("click", onClick);
184
187
  media.addEventListener("change", onOsChange);
185
188