@broxium/compiler 1.3.3 → 1.5.0

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/README.md CHANGED
@@ -123,6 +123,60 @@ Source files are written to a temporary directory under `os.tmpdir()` before com
123
123
 
124
124
  ---
125
125
 
126
+ ## `config.json` format
127
+
128
+ Every component must include a `config.json` file alongside `App.tsx`. This file defines the props exposed in the website builder's property inspector panel.
129
+
130
+ **The format is a flat object** — prop name as key, field definition as value. Do not use a `name`/`slug`/`props` wrapper.
131
+
132
+ ```json
133
+ {
134
+ "title": {
135
+ "label": "Title",
136
+ "type": "text",
137
+ "default": "Hello World"
138
+ },
139
+ "count": {
140
+ "label": "Item Count",
141
+ "type": "number",
142
+ "default": 3
143
+ },
144
+ "theme": {
145
+ "label": "Theme",
146
+ "type": "select",
147
+ "options": ["light", "dark"],
148
+ "default": "light"
149
+ }
150
+ }
151
+ ```
152
+
153
+ ### Supported field types
154
+
155
+ | `type` | Builder input | Extra keys |
156
+ |---|---|---|
157
+ | `text` | Text input | — |
158
+ | `textarea` | Multiline textarea | — |
159
+ | `url` | URL input (validated) | — |
160
+ | `number` | Number input | — |
161
+ | `select` | Dropdown | `options: string[]` |
162
+ | `color` | Color picker + hex | — |
163
+ | `boolean` | Checkbox | — |
164
+ | `range` | Slider | `min`, `max` |
165
+ | `brodox-*` | Custom Brodox field | varies |
166
+
167
+ ### Optional field keys
168
+
169
+ | Key | Type | Description |
170
+ |---|---|---|
171
+ | `label` | `string` | Display name in the inspector panel |
172
+ | `type` | `string` | Input type (required) |
173
+ | `default` | `any` | Initial value when component is first dropped |
174
+ | `render` | `"client"` \| `"server"` | Badge shown in inspector (cosmetic only) |
175
+
176
+ > **Common crash:** If `config.json` is not a flat object (e.g. has a top-level `name` or `props` array key), the builder will crash with `Cannot read properties of undefined (reading 'startsWith')` when the component is dragged onto the canvas.
177
+
178
+ ---
179
+
126
180
  ## `'use client'` and `'use server'` handling
127
181
 
128
182
  The two esbuild builds use custom plugins to handle React-style directives:
@@ -153,11 +207,44 @@ Server-only code never runs in the browser.
153
207
 
154
208
  The directive on the **entry file** (`App.tsx`) determines the `renderMode` stored in the Page Manifest:
155
209
 
156
- | Entry file starts with | `renderMode` | Server renders | Client hydrates |
157
- |---|---|---|---|
158
- | `'use client'` | `client` | No | Yes |
159
- | `'use server'` | `server` | Yes | No |
160
- | _(nothing)_ | `both` | Yes | Yes |
210
+ | Entry file starts with | `renderMode` | Server renders | Client hydrates | Use when |
211
+ |---|---|---|---|---|
212
+ | `'use client'` | `client` | No | Yes (full) | `useState`, `useEffect`, event handlers, browser APIs |
213
+ | `'use server'` | `server` | Yes (full) | No | Display-only, no interactivity needed |
214
+ | _(nothing)_ | `both` | Yes | Yes (islands only) | Static shell with `<Client>` islands for interactive parts |
215
+
216
+ > **Note:** These directives are **not** the same as Next.js. `'use server'` here means "exclude from client bundle" — it does not create a server action.
217
+
218
+ ### Common mistake — missing `'use client'`
219
+
220
+ Using `useState`, `useEffect`, or any hook at the top level of the entry file **without** `'use client'` causes the web engine to crash during SSR:
221
+
222
+ ```
223
+ Cannot read properties of null (reading 'useState')
224
+ ```
225
+
226
+ Fix: add `'use client'` as the very first line of `App.tsx`.
227
+
228
+ ```jsx
229
+ 'use client';
230
+
231
+ import { useState } from 'react';
232
+
233
+ export default function App() {
234
+ const [count, setCount] = useState(0);
235
+ // ...
236
+ }
237
+ ```
238
+
239
+ ### Sub-file directives
240
+
241
+ You can split a multi-file component by marking individual files:
242
+
243
+ ```
244
+ App.tsx ← no directive (both: SSR shell + client hydration)
245
+ ├── Counter.tsx ← 'use client' (stubbed on server, runs in browser)
246
+ └── DataTable.tsx← 'use server' (stubbed in browser, runs on server)
247
+ ```
161
248
 
162
249
  ---
163
250
 
@@ -238,14 +325,46 @@ When `BundleService` catches a compilation error, it blocks the component from b
238
325
 
239
326
  ---
240
327
 
328
+ ## `runtimeServerStubPlugin` — @broxium/runtime server stubs
329
+
330
+ During server bundle compilation, all `@broxium/runtime` imports are replaced
331
+ by inline server-safe stubs so the server bundle has zero external dependencies.
332
+
333
+ Key stub behaviours:
334
+
335
+ | Export | Server stub renders |
336
+ |---|---|
337
+ | `BrodoxLink` | `<a data-brodox-link href={href}>` — the `data-brodox-link` attribute is required for the shell's click interceptor |
338
+ | `BrodoxImage` | `<img src="/api/image?...">` with srcset, or raw `src` if `direct={true}` |
339
+ | `Client` | Empty placeholder div + sibling `<script type="application/json">` with props |
340
+ | `Server` | Transparent passthrough (Fragment) |
341
+ | `useRouter`, `useParams` | Static no-ops (return empty objects) |
342
+ | `BrodoxHead`, `BrodoxFont` | null |
343
+
344
+ **Important:** If you compile a component and the rendered `<a>` tags do not
345
+ have `data-brodox-link`, the shell will not intercept clicks on those links and
346
+ the browser will do a full page reload. This was fixed in v1.3.2 — ensure all
347
+ projects use `@broxium/compiler@^1.3.2` or later. Existing pre-1.3.2 server
348
+ bundles must be patched manually or recompiled.
349
+
350
+ ---
351
+
241
352
  ## Package info
242
353
 
243
354
  | | |
244
355
  |---|---|
245
356
  | Package | `@broxium/compiler` |
246
- | Version | `1.0.0` |
357
+ | Version | `1.3.3` |
247
358
  | Formats | ESM (`dist/index.mjs`), CJS (`dist/index.js`) |
248
359
  | Types | `dist/index.d.ts` |
249
360
  | Runtime dependency | `esbuild ^0.25` |
250
361
  | Node.js requirement | 20+ |
251
362
  | Side effects | Writes files to `outputDir`, creates/removes temp directory |
363
+
364
+ ## Changelog
365
+
366
+ | Version | Change |
367
+ |---|---|
368
+ | 1.3.3 | `BrodoxImage` stub: added `direct` prop support |
369
+ | 1.3.2 | `BrodoxLink` stub: added `data-brodox-link` attribute |
370
+ | 1.3.1 | Initial public release |
package/dist/index.d.mts CHANGED
@@ -13,8 +13,12 @@ interface CompileInput {
13
13
  interface CompileOutput {
14
14
  serverJsPath: string;
15
15
  clientJsPath: string;
16
+ /** Compiled CSS bundle path, or null if no CSS files were imported. */
17
+ cssPath: string | null;
16
18
  serverJsName: string;
17
19
  clientJsName: string;
20
+ /** CSS bundle filename, or null if no CSS files were imported. */
21
+ cssName: string | null;
18
22
  compiledAt: Date;
19
23
  }
20
24
 
package/dist/index.d.ts CHANGED
@@ -13,8 +13,12 @@ interface CompileInput {
13
13
  interface CompileOutput {
14
14
  serverJsPath: string;
15
15
  clientJsPath: string;
16
+ /** Compiled CSS bundle path, or null if no CSS files were imported. */
17
+ cssPath: string | null;
16
18
  serverJsName: string;
17
19
  clientJsName: string;
20
+ /** CSS bundle filename, or null if no CSS files were imported. */
21
+ cssName: string | null;
18
22
  compiledAt: Date;
19
23
  }
20
24
 
package/dist/index.js CHANGED
@@ -154,9 +154,29 @@ export function useRouter() {
154
154
 
155
155
  export function useParams() { return {}; }
156
156
 
157
- export function BrodoxHead() { return null; }
157
+ export function BrodoxHead({ title, description }) {
158
+ if (title && typeof globalThis.__brodoxCollectHead === 'function')
159
+ globalThis.__brodoxCollectHead({ type: 'title', props: { content: title } });
160
+ if (description && typeof globalThis.__brodoxCollectHead === 'function')
161
+ globalThis.__brodoxCollectHead({ type: 'meta', props: { name: 'description', content: description } });
162
+ return null;
163
+ }
158
164
 
159
- export function BrodoxFont() { return null; }
165
+ export function BrodoxFont({ href, family, weights, display }) {
166
+ weights = weights || [400, 700];
167
+ display = display || 'swap';
168
+ var url = href;
169
+ if (!url && family) {
170
+ url = 'https://fonts.googleapis.com/css2?family='
171
+ + encodeURIComponent(family) + ':wght@' + weights.join(';') + '&display=' + display;
172
+ }
173
+ if (url && typeof globalThis.__brodoxCollectHead === 'function') {
174
+ globalThis.__brodoxCollectHead({ type: 'link', props: { rel: 'preconnect', href: 'https://fonts.googleapis.com' } });
175
+ globalThis.__brodoxCollectHead({ type: 'link', props: { rel: 'preconnect', href: 'https://fonts.gstatic.com', crossOrigin: 'anonymous' } });
176
+ globalThis.__brodoxCollectHead({ type: 'link', props: { rel: 'stylesheet', href: url } });
177
+ }
178
+ return null;
179
+ }
160
180
 
161
181
  export function BrodoxRouter({ children }) {
162
182
  return createElement(Fragment, null, children);
@@ -170,7 +190,7 @@ export function BrodoxRouter({ children }) {
170
190
  * The IslandHydrator walks the live DOM and mounts the component from the
171
191
  * parent bundle's __registry__ after page load.
172
192
  */
173
- export function Client({ children }) {
193
+ export function Client({ children, hydration = 'load' }) {
174
194
  var id = __nextIslandId();
175
195
  var child = Children.only(children);
176
196
  var compType = child.type;
@@ -185,13 +205,13 @@ export function Client({ children }) {
185
205
  return createElement(Fragment, null,
186
206
  createElement('div', {
187
207
  'data-brodox-island': id,
188
- 'data-hydration': 'load',
208
+ 'data-hydration': hydration,
189
209
  'data-client-js': '',
190
210
  'data-component-slug': '',
191
211
  'data-version': '',
192
212
  'data-component': name,
193
213
  }),
194
- createElement('script', {
214
+ hydration === 'none' ? null : createElement('script', {
195
215
  type: 'application/json',
196
216
  'data-brodox-props': id,
197
217
  dangerouslySetInnerHTML: { __html: safeProps },
@@ -303,7 +323,9 @@ var BrodoxCompiler = class {
303
323
  outfile: serverJsPath,
304
324
  minify: false,
305
325
  sourcemap: false,
306
- define: { "process.env.NODE_ENV": '"production"' }
326
+ define: { "process.env.NODE_ENV": '"production"' },
327
+ loader: { ".css": "text" }
328
+ // CSS imports return empty string in server bundle
307
329
  });
308
330
  const clientComponents = [];
309
331
  for (const file of input.files) {
@@ -341,14 +363,47 @@ ${registryEntries}
341
363
  minify: true,
342
364
  sourcemap: false,
343
365
  define: { "process.env.NODE_ENV": '"production"' },
344
- banner: { js: 'import React from "react";' }
366
+ banner: { js: 'import React from "react";' },
367
+ loader: { ".css": "text" }
368
+ // CSS imports return the CSS string (injected via BrodoxHead or style tag)
345
369
  });
370
+ const hasCss = input.files.some((f) => /\.css$/.test(f.path));
371
+ const cssName = hasCss ? `${safeName}.css` : null;
372
+ const cssPath = hasCss ? import_node_path3.default.join(input.outputDir, cssName) : null;
373
+ if (hasCss && cssPath) {
374
+ try {
375
+ await esbuild.build({
376
+ entryPoints: [entryPoint],
377
+ bundle: true,
378
+ format: "esm",
379
+ platform: "browser",
380
+ jsx: "automatic",
381
+ external: CLIENT_EXTERNALS,
382
+ plugins: [serverStubPlugin()],
383
+ outfile: cssPath.replace(/\.css$/, ".css.tmp.js"),
384
+ // esbuild needs a JS outfile
385
+ minify: true,
386
+ sourcemap: false,
387
+ define: { "process.env.NODE_ENV": '"production"' },
388
+ loader: { ".css": "css" }
389
+ });
390
+ const defaultCssOut = cssPath.replace(/\.css$/, ".css.tmp.css");
391
+ try {
392
+ await import_promises3.default.rename(defaultCssOut, cssPath);
393
+ } catch {
394
+ }
395
+ await import_promises3.default.rm(cssPath.replace(/\.css$/, ".css.tmp.js"), { force: true });
396
+ } catch {
397
+ }
398
+ }
346
399
  await import_promises3.default.rm(tmpDir, { recursive: true, force: true });
347
400
  return {
348
401
  serverJsPath,
349
402
  clientJsPath,
403
+ cssPath,
350
404
  serverJsName,
351
405
  clientJsName,
406
+ cssName,
352
407
  compiledAt: /* @__PURE__ */ new Date()
353
408
  };
354
409
  }
package/dist/index.mjs CHANGED
@@ -125,9 +125,29 @@ export function useRouter() {
125
125
 
126
126
  export function useParams() { return {}; }
127
127
 
128
- export function BrodoxHead() { return null; }
128
+ export function BrodoxHead({ title, description }) {
129
+ if (title && typeof globalThis.__brodoxCollectHead === 'function')
130
+ globalThis.__brodoxCollectHead({ type: 'title', props: { content: title } });
131
+ if (description && typeof globalThis.__brodoxCollectHead === 'function')
132
+ globalThis.__brodoxCollectHead({ type: 'meta', props: { name: 'description', content: description } });
133
+ return null;
134
+ }
129
135
 
130
- export function BrodoxFont() { return null; }
136
+ export function BrodoxFont({ href, family, weights, display }) {
137
+ weights = weights || [400, 700];
138
+ display = display || 'swap';
139
+ var url = href;
140
+ if (!url && family) {
141
+ url = 'https://fonts.googleapis.com/css2?family='
142
+ + encodeURIComponent(family) + ':wght@' + weights.join(';') + '&display=' + display;
143
+ }
144
+ if (url && typeof globalThis.__brodoxCollectHead === 'function') {
145
+ globalThis.__brodoxCollectHead({ type: 'link', props: { rel: 'preconnect', href: 'https://fonts.googleapis.com' } });
146
+ globalThis.__brodoxCollectHead({ type: 'link', props: { rel: 'preconnect', href: 'https://fonts.gstatic.com', crossOrigin: 'anonymous' } });
147
+ globalThis.__brodoxCollectHead({ type: 'link', props: { rel: 'stylesheet', href: url } });
148
+ }
149
+ return null;
150
+ }
131
151
 
132
152
  export function BrodoxRouter({ children }) {
133
153
  return createElement(Fragment, null, children);
@@ -141,7 +161,7 @@ export function BrodoxRouter({ children }) {
141
161
  * The IslandHydrator walks the live DOM and mounts the component from the
142
162
  * parent bundle's __registry__ after page load.
143
163
  */
144
- export function Client({ children }) {
164
+ export function Client({ children, hydration = 'load' }) {
145
165
  var id = __nextIslandId();
146
166
  var child = Children.only(children);
147
167
  var compType = child.type;
@@ -156,13 +176,13 @@ export function Client({ children }) {
156
176
  return createElement(Fragment, null,
157
177
  createElement('div', {
158
178
  'data-brodox-island': id,
159
- 'data-hydration': 'load',
179
+ 'data-hydration': hydration,
160
180
  'data-client-js': '',
161
181
  'data-component-slug': '',
162
182
  'data-version': '',
163
183
  'data-component': name,
164
184
  }),
165
- createElement('script', {
185
+ hydration === 'none' ? null : createElement('script', {
166
186
  type: 'application/json',
167
187
  'data-brodox-props': id,
168
188
  dangerouslySetInnerHTML: { __html: safeProps },
@@ -274,7 +294,9 @@ var BrodoxCompiler = class {
274
294
  outfile: serverJsPath,
275
295
  minify: false,
276
296
  sourcemap: false,
277
- define: { "process.env.NODE_ENV": '"production"' }
297
+ define: { "process.env.NODE_ENV": '"production"' },
298
+ loader: { ".css": "text" }
299
+ // CSS imports return empty string in server bundle
278
300
  });
279
301
  const clientComponents = [];
280
302
  for (const file of input.files) {
@@ -312,14 +334,47 @@ ${registryEntries}
312
334
  minify: true,
313
335
  sourcemap: false,
314
336
  define: { "process.env.NODE_ENV": '"production"' },
315
- banner: { js: 'import React from "react";' }
337
+ banner: { js: 'import React from "react";' },
338
+ loader: { ".css": "text" }
339
+ // CSS imports return the CSS string (injected via BrodoxHead or style tag)
316
340
  });
341
+ const hasCss = input.files.some((f) => /\.css$/.test(f.path));
342
+ const cssName = hasCss ? `${safeName}.css` : null;
343
+ const cssPath = hasCss ? path3.join(input.outputDir, cssName) : null;
344
+ if (hasCss && cssPath) {
345
+ try {
346
+ await esbuild.build({
347
+ entryPoints: [entryPoint],
348
+ bundle: true,
349
+ format: "esm",
350
+ platform: "browser",
351
+ jsx: "automatic",
352
+ external: CLIENT_EXTERNALS,
353
+ plugins: [serverStubPlugin()],
354
+ outfile: cssPath.replace(/\.css$/, ".css.tmp.js"),
355
+ // esbuild needs a JS outfile
356
+ minify: true,
357
+ sourcemap: false,
358
+ define: { "process.env.NODE_ENV": '"production"' },
359
+ loader: { ".css": "css" }
360
+ });
361
+ const defaultCssOut = cssPath.replace(/\.css$/, ".css.tmp.css");
362
+ try {
363
+ await fs3.rename(defaultCssOut, cssPath);
364
+ } catch {
365
+ }
366
+ await fs3.rm(cssPath.replace(/\.css$/, ".css.tmp.js"), { force: true });
367
+ } catch {
368
+ }
369
+ }
317
370
  await fs3.rm(tmpDir, { recursive: true, force: true });
318
371
  return {
319
372
  serverJsPath,
320
373
  clientJsPath,
374
+ cssPath,
321
375
  serverJsName,
322
376
  clientJsName,
377
+ cssName,
323
378
  compiledAt: /* @__PURE__ */ new Date()
324
379
  };
325
380
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@broxium/compiler",
3
- "version": "1.3.3",
3
+ "version": "1.5.0",
4
4
  "description": "Brodox component compiler — TSX to ESM server + client bundles",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",