@jxsuite/compiler 0.29.0 → 0.30.1

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": "@jxsuite/compiler",
3
- "version": "0.29.0",
3
+ "version": "0.30.1",
4
4
  "description": "Jx static HTML compiler, island detector, and site builder",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -42,14 +42,14 @@
42
42
  "upgrade": "bunx npm-check-updates -u && bun install"
43
43
  },
44
44
  "dependencies": {
45
- "@apidevtools/json-schema-ref-parser": "^15.3.6",
46
- "@jxsuite/parser": "^0.29.0",
47
- "@jxsuite/runtime": "^0.29.0",
48
- "@jxsuite/schema": "^0.29.0",
49
- "sharp": "^0.35.1"
45
+ "@apidevtools/json-schema-ref-parser": "^15.4.0",
46
+ "@jxsuite/parser": "^0.30.1",
47
+ "@jxsuite/runtime": "^0.30.1",
48
+ "@jxsuite/schema": "^0.30.1",
49
+ "sharp": "^0.35.2"
50
50
  },
51
51
  "devDependencies": {
52
- "@happy-dom/global-registrator": "^20.10.2",
52
+ "@happy-dom/global-registrator": "^20.10.6",
53
53
  "@types/mdast": "^4.0.4"
54
54
  }
55
55
  }
package/src/cli.ts CHANGED
@@ -9,11 +9,16 @@
9
9
 
10
10
  import { resolve } from "node:path";
11
11
 
12
- const args = process.argv.slice(2);
13
- const [command] = args;
12
+ // The CLI body runs inside an async function rather than via top-level await: when a test pulls this
13
+ // Entry in with a dynamic import(), Bun's test runtime drops the continuation after a top-level await
14
+ // (it never resumes on Windows), so the build step would silently never run. `ready` lets the
15
+ // In-process CLI harness await the same sequence.
16
+ async function main() {
17
+ const args = process.argv.slice(2);
18
+ const [command] = args;
14
19
 
15
- if (!command || command === "--help" || command === "-h") {
16
- console.log(`Usage: jx <command> [options]
20
+ if (!command || command === "--help" || command === "-h") {
21
+ console.log(`Usage: jx <command> [options]
17
22
 
18
23
  Commands:
19
24
  build [root] Build a Jx site to dist/
@@ -21,39 +26,43 @@ Commands:
21
26
  Options:
22
27
  --verbose Print detailed build progress
23
28
  --no-clean Don't clean outDir before building`);
24
- process.exit(0);
25
- }
29
+ process.exit(0);
30
+ }
26
31
 
27
- if (command === "build") {
28
- const rest = args.slice(1);
29
- const flags = new Set(rest.filter((a) => a.startsWith("--")));
30
- const positional = rest.find((a) => !a.startsWith("--"));
32
+ if (command === "build") {
33
+ const rest = args.slice(1);
34
+ const flags = new Set(rest.filter((a) => a.startsWith("--")));
35
+ const positional = rest.find((a) => !a.startsWith("--"));
31
36
 
32
- const projectRoot = resolve(positional ?? ".");
33
- const verbose = flags.has("--verbose");
34
- const clean = !flags.has("--no-clean");
37
+ const projectRoot = resolve(positional ?? ".");
38
+ const verbose = flags.has("--verbose");
39
+ const clean = !flags.has("--no-clean");
35
40
 
36
- console.log(`Building site from ${projectRoot}...`);
41
+ console.log(`Building site from ${projectRoot}...`);
37
42
 
38
- try {
39
- const { buildSite } = await import("./site/site-build.ts");
40
- const result = await buildSite(projectRoot, { clean, verbose });
43
+ try {
44
+ const { buildSite } = await import("./site/site-build.ts");
45
+ const result = await buildSite(projectRoot, { clean, verbose });
41
46
 
42
- if (result.errors.length > 0) {
43
- console.error(`\nBuild completed with ${result.errors.length} error(s):`);
44
- for (const err of result.errors) {
45
- console.error(` - ${err}`);
47
+ if (result.errors.length > 0) {
48
+ console.error(`\nBuild completed with ${result.errors.length} error(s):`);
49
+ for (const err of result.errors) {
50
+ console.error(` - ${err}`);
51
+ }
52
+ process.exit(1);
46
53
  }
54
+
55
+ console.log(`\nDone: ${result.routes} routes → ${result.files} files`);
56
+ } catch (error) {
57
+ const err = error as Error;
58
+ console.error(`Build failed: ${err.message}`);
47
59
  process.exit(1);
48
60
  }
49
-
50
- console.log(`\nDone: ${result.routes} routes → ${result.files} files`);
51
- } catch (error) {
52
- const err = error as Error;
53
- console.error(`Build failed: ${err.message}`);
61
+ } else {
62
+ console.error(`Unknown command: ${command}\nRun "jx --help" for usage.`);
54
63
  process.exit(1);
55
64
  }
56
- } else {
57
- console.error(`Unknown command: ${command}\nRun "jx --help" for usage.`);
58
- process.exit(1);
59
65
  }
66
+
67
+ // oxlint-disable-next-line unicorn/prefer-top-level-await
68
+ export const ready = main();
@@ -1,12 +1,20 @@
1
1
  #!/usr/bin/env node
2
2
  import { runCli } from "./compiler.ts";
3
3
 
4
- const [src, out] = process.argv.slice(2);
5
- if (src) {
6
- try {
7
- await runCli(src, out);
8
- } catch (error) {
9
- console.error(error);
10
- process.exit(1);
4
+ // Body wrapped in an async function rather than a top-level await: Bun's test runtime drops the
5
+ // Continuation after a dynamically-imported module's top-level await (it never resumes on Windows),
6
+ // Which would skip the error handling. `ready` lets the in-process CLI harness await the sequence.
7
+ async function main() {
8
+ const [src, out] = process.argv.slice(2);
9
+ if (src) {
10
+ try {
11
+ await runCli(src, out);
12
+ } catch (error) {
13
+ console.error(error);
14
+ process.exit(1);
15
+ }
11
16
  }
12
17
  }
18
+
19
+ // oxlint-disable-next-line unicorn/prefer-top-level-await
20
+ export const ready = main();
package/src/shared.ts CHANGED
@@ -1024,7 +1024,7 @@ function _walkServerEntries(
1024
1024
 
1025
1025
  // ─── Component pre-rendering ─────────────────────────────────────────────────
1026
1026
 
1027
- const SELF_CLOSING = new Set<string>([
1027
+ export const SELF_CLOSING = new Set<string>([
1028
1028
  "input",
1029
1029
  "br",
1030
1030
  "hr",
@@ -43,6 +43,13 @@ export interface ImageConfig {
43
43
  }
44
44
 
45
45
  type SharpModule = typeof SharpNS;
46
+ /**
47
+ * Accepted output-format type, derived from the Sharp instance's own toFormat signature. Reaching
48
+ * into the `SharpNS.FormatEnum` namespace instead is fragile: sharp ships as `export = sharp`, so
49
+ * whether a default type-import exposes the merged namespace depends on esModuleInterop resolution
50
+ * (differs local vs CI).
51
+ */
52
+ type SharpFormat = Parameters<ReturnType<SharpModule>["toFormat"]>[0];
46
53
 
47
54
  let _sharp: SharpModule | null = null;
48
55
 
@@ -175,7 +182,7 @@ export async function processImage(srcPath: string, cacheImgDir: string, config:
175
182
  const quality = config.quality[format as keyof ImageConfig["quality"]] ?? 80;
176
183
  const task = sharp(srcPath)
177
184
  .resize(width)
178
- .toFormat(format as keyof SharpNS.FormatEnum, { quality })
185
+ .toFormat(format as SharpFormat, { quality })
179
186
  .toFile(absolutePath)
180
187
  .then(() => {});
181
188
 
@@ -17,7 +17,7 @@ import {
17
17
  rmSync,
18
18
  writeFileSync,
19
19
  } from "node:fs";
20
- import { dirname, join, resolve } from "node:path";
20
+ import { basename, dirname, join, resolve } from "node:path";
21
21
  import { isMappedArray, isRef } from "@jxsuite/schema/guards";
22
22
  import { loadProjectConfig } from "./site-loader.ts";
23
23
  import { discoverPages, expandDynamicRoutes, readPageDocument } from "./pages-discovery.ts";
@@ -158,7 +158,11 @@ export async function buildSite(
158
158
  formats: formatRegistry,
159
159
  });
160
160
  for (const f of result.files) {
161
- const outName = f.path.includes("/") ? (f.path.split("/").pop() as string) : f.path;
161
+ // Reduce the emitted path (an absolute source path with .json swapped to .js) to a bare
162
+ // Filename for the dist/components/ output. basename handles both / and \ — a
163
+ // Forward-slash-only split left the full drive path on Windows, so resolve() then wrote
164
+ // The sidecar back into the source tree instead of dist.
165
+ const outName = basename(f.path);
162
166
  writeFileSync(resolve(componentOutDir, outName), f.content, "utf8");
163
167
  if (f.tagName) {
164
168
  compiledComponentTags.push(f.tagName);
@@ -12,6 +12,7 @@ import {
12
12
  escapeHtml,
13
13
  isNodeDynamic,
14
14
  resolveStaticValue,
15
+ SELF_CLOSING,
15
16
  } from "../shared.ts";
16
17
  import { emitElementModule } from "./compile-element.ts";
17
18
  import type { JxDocument, JxMutableNode, JxStyle } from "@jxsuite/schema/types";
@@ -143,6 +144,13 @@ function compileNode(
143
144
 
144
145
  const tag = def.tagName ?? "div";
145
146
  const attrs = buildAttrs(def, nextContext.scope);
147
+
148
+ // Void elements (br, hr, img, …) have no closing tag.
149
+ // Emitting `<br></br>` makes HTML parsers read `</br>` as a second `<br>`.
150
+ if (SELF_CLOSING.has(tag)) {
151
+ return `<${tag}${attrs}>`;
152
+ }
153
+
146
154
  const inner = buildInnerWithIslands(def, raw, nextContext, islands);
147
155
 
148
156
  return `<${tag}${attrs}>${inner}</${tag}>`;