x4js 2.3.6 → 2.3.8

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.
@@ -4,7 +4,7 @@ import { sassPlugin } from "esbuild-sass-plugin";
4
4
  import { lessPlugin } from "./less-plugin.mjs";
5
5
  import { copyPlugin } from "./copy-plugin.mjs";
6
6
  import { rawFilePlugin } from "./rawfile-plugin.mjs";
7
- import { diagnosticsPlugin } from "./diagnostic.mjs";
7
+ import { diagnosticsPlugin } from "./diagnostic-plugin.mjs";
8
8
 
9
9
  const cliDir = path.dirname(fileURLToPath(import.meta.url));
10
10
  const devClient = path.join(cliDir, "dev-client.js");
package/cli/build.mjs CHANGED
@@ -1,15 +1,16 @@
1
1
  import fs from "node:fs/promises";
2
2
  import { parseArgs } from "node:util";
3
3
  import esbuild from "esbuild";
4
- import { loadConfig } from "./config.mjs";
4
+ import { DEFAULT_CONFIG_FILE, expandEnv, loadConfig } from "./config.mjs";
5
5
  import { createBuildOptions } from "./build-options.mjs";
6
6
  import { info, success } from "./log.mjs";
7
- import { printBuildError} from "./diagnostic-plugin.mjs";
7
+ import { printBuildError, printWarnings } from "./diagnostic-plugin.mjs";
8
8
 
9
9
  export async function build(argv = [], root = process.cwd()) {
10
10
  const { values, positionals } = parseArgs({
11
11
  args: argv,
12
12
  options: {
13
+ config: { type: "string" },
13
14
  debug: { type: "boolean", default: false },
14
15
  },
15
16
  allowPositionals: true,
@@ -19,29 +20,26 @@ export async function build(argv = [], root = process.cwd()) {
19
20
  if (positionals.length)
20
21
  throw new Error(`Unexpected argument: ${positionals[0]}`);
21
22
 
22
- const config = loadConfig(root);
23
+ const configArg = values.config ? expandEnv(values.config) : DEFAULT_CONFIG_FILE;
24
+ const config = loadConfig(root, configArg, process.env, { explicit: values.config !== undefined });
23
25
  const mode = values.debug ? "debug" : "production";
24
26
 
25
27
  info("mode", mode);
28
+ info("config", config.configFile);
26
29
  info("outdir", config.outdir);
27
30
 
28
31
  await fs.rm(config.outdir, { recursive: true, force: true });
29
32
  await fs.mkdir(config.outdir, { recursive: true });
30
33
 
31
34
  const started = performance.now();
32
- const options = createBuildOptions(config, mode);
33
-
34
- try {
35
- const result = await esbuild.build(options);
36
- if (result.warnings.length)
37
- await printWarnings(result.warnings);
38
-
39
- logSuccess(`built ${Date.now() - start}ms`);
40
- }
41
- catch (error) {
42
- await printBuildError(error);
43
- process.exitCode = 1;
44
- }
45
-
46
- success("built", `${Math.round(performance.now() - started)}ms`);
35
+ try {
36
+ const result = await esbuild.build(createBuildOptions(config, mode));
37
+ if (result.warnings.length)
38
+ await printWarnings(result.warnings);
39
+ success("built", `${Math.round(performance.now() - started)}ms`);
40
+ }
41
+ catch (error) {
42
+ await printBuildError(error);
43
+ process.exitCode = 1;
44
+ }
47
45
  }
package/cli/config.mjs CHANGED
@@ -1,6 +1,8 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
 
4
+ export const DEFAULT_CONFIG_FILE = "x4.config.json";
5
+
4
6
  const DEFAULTS = Object.freeze({
5
7
  entryPoints: ["src/main.ts"],
6
8
  outdir: "./bin",
@@ -36,94 +38,107 @@ export function resolvePath(root, value, env = process.env) {
36
38
  : path.resolve(root, expanded);
37
39
  }
38
40
 
41
+ export function resolveConfigFile(root, configFile = DEFAULT_CONFIG_FILE, env = process.env) {
42
+ return resolvePath(root, configFile, env);
43
+ }
44
+
45
+ function readJson(filename) {
46
+ try {
47
+ return JSON.parse(fs.readFileSync(filename, "utf8"));
48
+ }
49
+ catch (error) {
50
+ throw new Error(`Cannot read config '${filename}': ${error.message}`);
51
+ }
52
+ }
53
+
39
54
  function validateCopy(copy) {
40
55
  if (!Array.isArray(copy))
41
- throw new Error("x4.copy must be an array");
56
+ throw new Error("copy must be an array");
42
57
 
43
58
  for (const item of copy) {
44
59
  if (!item || typeof item !== "object" || typeof item.from !== "string" || typeof item.to !== "string")
45
- throw new Error("Each x4.copy entry must contain string 'from' and 'to' fields");
60
+ throw new Error("Each copy entry must contain string 'from' and 'to' fields");
46
61
  if (path.isAbsolute(item.to))
47
- throw new Error(`x4.copy destination must be relative to outdir: '${item.to}'`);
62
+ throw new Error(`copy destination must be relative to outdir: '${item.to}'`);
48
63
  const normalized = path.normalize(item.to);
49
64
  if (normalized === ".." || normalized.startsWith(`..${path.sep}`))
50
- throw new Error(`x4.copy destination escapes outdir: '${item.to}'`);
65
+ throw new Error(`copy destination escapes outdir: '${item.to}'`);
51
66
  }
52
67
  }
53
68
 
54
- export function loadConfig(root = process.cwd(), env = process.env) {
69
+ export function loadConfig(root = process.cwd(), configFile = DEFAULT_CONFIG_FILE, env = process.env, { explicit = false } = {}) {
55
70
  root = path.resolve(root);
56
- const packageFile = path.join(root, "package.json");
57
-
58
- if (!fs.existsSync(packageFile))
59
- throw new Error(`Cannot find package.json in '${root}'`);
71
+ const filename = resolveConfigFile(root, configFile, env);
60
72
 
61
- let pkg;
62
- try {
63
- pkg = JSON.parse(fs.readFileSync(packageFile, "utf8"));
64
- }
65
- catch (error) {
66
- throw new Error(`Cannot read package.json: ${error.message}`);
67
- }
73
+ let cfg = {};
74
+ if (fs.existsSync(filename))
75
+ cfg = readJson(filename);
76
+ else if (explicit)
77
+ throw new Error(`Config file not found: ${filename}`);
68
78
 
69
- const cfg = pkg.x4 ?? {};
70
- if (cfg === null || typeof cfg !== "object" || Array.isArray(cfg))
71
- throw new Error("package.json field 'x4' must be an object");
79
+ if (!cfg || typeof cfg !== "object" || Array.isArray(cfg))
80
+ throw new Error("x4 config must be an object");
72
81
 
73
82
  const entryPoints = cfg.entryPoints ?? DEFAULTS.entryPoints;
74
83
  if (!Array.isArray(entryPoints) || !entryPoints.length || entryPoints.some((entry) => typeof entry !== "string"))
75
- throw new Error("x4.entryPoints must be a non-empty array of strings");
84
+ throw new Error("entryPoints must be a non-empty array of strings");
76
85
 
77
86
  const copy = cfg.copy ?? DEFAULTS.copy;
78
87
  validateCopy(copy);
79
88
 
80
89
  const external = cfg.external ?? DEFAULTS.external;
81
90
  if (!Array.isArray(external) || external.some((entry) => typeof entry !== "string"))
82
- throw new Error("x4.external must be an array of strings");
91
+ throw new Error("external must be an array of strings");
83
92
 
84
93
  const define = cfg.define ?? DEFAULTS.define;
85
94
  if (!define || typeof define !== "object" || Array.isArray(define))
86
- throw new Error("x4.define must be an object");
95
+ throw new Error("define must be an object");
87
96
  if (Object.values(define).some((value) => typeof value !== "string"))
88
- throw new Error("x4.define values must be strings containing esbuild define expressions");
97
+ throw new Error("define values must be strings containing esbuild define expressions");
89
98
 
90
99
  const devCfg = cfg.dev ?? {};
91
100
  if (!devCfg || typeof devCfg !== "object" || Array.isArray(devCfg))
92
- throw new Error("x4.dev must be an object");
101
+ throw new Error("dev must be an object");
93
102
 
94
103
  const host = devCfg.host ?? DEFAULTS.dev.host;
95
104
  if (typeof host !== "string" || !host)
96
- throw new Error("x4.dev.host must be a non-empty string");
105
+ throw new Error("dev.host must be a non-empty string");
97
106
 
98
107
  const port = devCfg.port;
99
108
  if (port !== undefined && (!Number.isInteger(port) || port < 0 || port > 65535))
100
- throw new Error("x4.dev.port must be an integer between 0 and 65535");
109
+ throw new Error("dev.port must be an integer between 0 and 65535");
101
110
 
102
111
  const https = devCfg.https ?? DEFAULTS.dev.https;
103
112
  if (typeof https !== "boolean")
104
- throw new Error("x4.dev.https must be a boolean");
113
+ throw new Error("dev.https must be a boolean");
105
114
 
106
115
  if (devCfg.tls !== undefined) {
107
116
  if (!devCfg.tls || typeof devCfg.tls !== "object" || Array.isArray(devCfg.tls))
108
- throw new Error("x4.dev.tls must be an object");
117
+ throw new Error("dev.tls must be an object");
109
118
  if (typeof devCfg.tls.cert !== "string" || typeof devCfg.tls.key !== "string")
110
- throw new Error("x4.dev.tls requires string 'cert' and 'key' fields");
119
+ throw new Error("dev.tls requires string 'cert' and 'key' fields");
111
120
  }
112
121
 
113
122
  const outdirRaw = cfg.outdir ?? DEFAULTS.outdir;
114
123
  if (typeof outdirRaw !== "string")
115
- throw new Error("x4.outdir must be a string");
124
+ throw new Error("outdir must be a string");
116
125
 
117
126
  const outdir = resolvePath(root, outdirRaw, env);
118
127
  if (outdir === root)
119
- throw new Error("x4.outdir cannot be the project root");
128
+ throw new Error("outdir cannot be the project root");
120
129
  if (outdir === path.parse(outdir).root)
121
- throw new Error("x4.outdir cannot be a filesystem root");
130
+ throw new Error("outdir cannot be a filesystem root");
131
+
132
+ let esbuildOptions = {};
133
+ if (cfg.esbuild !== undefined) {
134
+ if (!cfg.esbuild || typeof cfg.esbuild !== "object" || Array.isArray(cfg.esbuild))
135
+ throw new Error("esbuild must be an object");
136
+ esbuildOptions = { ...cfg.esbuild };
137
+ }
122
138
 
123
139
  return {
124
140
  root,
125
- packageFile,
126
- package: pkg,
141
+ configFile: filename,
127
142
  entryPoints: entryPoints.map((entry) => resolvePath(root, entry, env)),
128
143
  outdir,
129
144
  copy: copy.map((item) => ({
@@ -132,26 +147,20 @@ export function loadConfig(root = process.cwd(), env = process.env) {
132
147
  })),
133
148
  external: [...external],
134
149
  define: { ...define },
135
- dev: {
150
+ dev: {
136
151
  host,
137
152
  port,
138
153
  https,
139
154
  tls: devCfg.tls ? { ...devCfg.tls } : undefined,
140
155
  },
141
- esbuild: (() => {
142
- if (cfg.esbuild === undefined)
143
- return {};
144
- if (!cfg.esbuild || typeof cfg.esbuild !== "object" || Array.isArray(cfg.esbuild))
145
- throw new Error("x4.esbuild must be an object");
146
- return { ...cfg.esbuild };
147
- })(),
156
+ esbuild: esbuildOptions,
148
157
  };
149
158
  }
150
159
 
151
160
  export function resolveTls(config, env = process.env) {
152
161
  const tls = config.dev.tls;
153
162
  if (!tls)
154
- throw new Error("HTTPS is enabled but x4.dev.tls.cert/key are not configured");
163
+ throw new Error("HTTPS is enabled but dev.tls.cert/key are not configured");
155
164
 
156
165
  const certfile = resolvePath(config.root, tls.cert, env);
157
166
  const keyfile = resolvePath(config.root, tls.key, env);
package/cli/dev.mjs CHANGED
@@ -3,7 +3,7 @@ import path from "node:path";
3
3
  import { spawn } from "node:child_process";
4
4
  import { parseArgs } from "node:util";
5
5
  import esbuild from "esbuild";
6
- import { loadConfig, resolveTls } from "./config.mjs";
6
+ import { DEFAULT_CONFIG_FILE, expandEnv, loadConfig, resolveConfigFile, resolveTls } from "./config.mjs";
7
7
  import { createBuildOptions } from "./build-options.mjs";
8
8
  import { info, success, failure } from "./log.mjs";
9
9
 
@@ -59,6 +59,7 @@ export async function dev(argv = [], root = process.cwd()) {
59
59
  const { values, positionals } = parseArgs({
60
60
  args: argv,
61
61
  options: {
62
+ config: { type: "string" },
62
63
  host: { type: "string" },
63
64
  port: { type: "string" },
64
65
  http: { type: "boolean", default: false },
@@ -71,12 +72,15 @@ export async function dev(argv = [], root = process.cwd()) {
71
72
 
72
73
  if (positionals.length)
73
74
  throw new Error(`Unexpected argument: ${positionals[0]}`);
74
-
75
75
  if (values.port !== undefined && !/^\d+$/.test(values.port))
76
76
  throw new Error("--port must be an integer");
77
77
 
78
+ const configArg = values.config ? expandEnv(values.config) : DEFAULT_CONFIG_FILE;
79
+ const explicitConfig = values.config !== undefined;
80
+ const watchedConfigFile = resolveConfigFile(root, configArg);
81
+
78
82
  let context;
79
- let packageWatcher;
83
+ let configWatcher;
80
84
  let stopping = false;
81
85
  let restartTimer;
82
86
  let restartChain = Promise.resolve();
@@ -96,7 +100,6 @@ export async function dev(argv = [], root = process.cwd()) {
96
100
 
97
101
  if (port !== undefined)
98
102
  serveOptions.port = port;
99
-
100
103
  if (useHttps)
101
104
  Object.assign(serveOptions, resolveTls(config));
102
105
 
@@ -109,6 +112,7 @@ export async function dev(argv = [], root = process.cwd()) {
109
112
  const actualHost = result.hosts.includes(host) ? host : (result.hosts[0] ?? host);
110
113
  const url = serverUrl(actualHost, result.port, useHttps);
111
114
  info("mode", "dev");
115
+ info("config", config.configFile);
112
116
  info("outdir", config.outdir);
113
117
  success("server", url);
114
118
 
@@ -126,9 +130,9 @@ export async function dev(argv = [], root = process.cwd()) {
126
130
  async function reload() {
127
131
  let config;
128
132
  try {
129
- config = loadConfig(root);
133
+ config = loadConfig(root, configArg, process.env, { explicit: explicitConfig });
130
134
  protocolConfig(config, values);
131
- if ((values.https || (!values.http && config.dev.https)))
135
+ if (values.https || (!values.http && config.dev.https))
132
136
  resolveTls(config);
133
137
  }
134
138
  catch (error) {
@@ -147,8 +151,6 @@ export async function dev(argv = [], root = process.cwd()) {
147
151
  }
148
152
  catch (error) {
149
153
  failure(`restart: ${error.message}`);
150
- // The old context cannot be restored after dispose. Keep watching
151
- // package.json so fixing the configuration starts dev again.
152
154
  }
153
155
  }
154
156
 
@@ -159,11 +161,13 @@ export async function dev(argv = [], root = process.cwd()) {
159
161
  }, 150);
160
162
  }
161
163
 
162
- const initialConfig = loadConfig(root);
164
+ const initialConfig = loadConfig(root, configArg, process.env, { explicit: explicitConfig });
163
165
  await start(initialConfig);
164
166
 
165
- packageWatcher = fs.watch(path.resolve(root), { persistent: true }, (_event, filename) => {
166
- if (filename === null || filename.toString() === "package.json")
167
+ const watchDir = path.dirname(watchedConfigFile);
168
+ const watchName = path.basename(watchedConfigFile);
169
+ configWatcher = fs.watch(watchDir, { persistent: true }, (_event, filename) => {
170
+ if (filename === null || filename.toString() === watchName)
167
171
  scheduleReload();
168
172
  });
169
173
 
@@ -172,7 +176,7 @@ export async function dev(argv = [], root = process.cwd()) {
172
176
  return;
173
177
  stopping = true;
174
178
  clearTimeout(restartTimer);
175
- packageWatcher?.close();
179
+ configWatcher?.close();
176
180
  await restartChain.catch(() => {});
177
181
  await context?.dispose();
178
182
  }
package/cli/main.mjs CHANGED
@@ -21,10 +21,11 @@ function printHelp(version) {
21
21
  Usage:
22
22
  x4js create <project> [--template <name>] [--no-install]
23
23
  x4js templates
24
- x4js dev [--host <host>] [--port <port>] [--http|--https] [--open]
25
- x4js build [--debug]
24
+ x4js dev [--config <file>] [--host <host>] [--port <port>] [--http|--https] [--open]
25
+ x4js build [--config <file>] [--debug]
26
26
 
27
27
  Options:
28
+ --config <file> Config file (default: x4.config.json)
28
29
  -h, --help Show help
29
30
  -v, --version Show version`);
30
31
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "x4js",
3
- "version": "2.3.6",
3
+ "version": "2.3.8",
4
4
  "type": "module",
5
5
  "main": "src/x4.ts",
6
6
  "module": "src/x4.ts",
@@ -22,10 +22,6 @@ import { addEvent, DOMEventHandler, GlobalDOMEvents, COMPONENT } from './core_do
22
22
  import { Application, EvMessage } from './core_application';
23
23
  import { makeState } from './core_state.js';
24
24
 
25
- interface RefType<T extends Component> {
26
- dom: T;
27
- }
28
-
29
25
  type ComponentAttributes = Record<string,string|number|boolean>;
30
26
  type CreateComponentCallBack = ( attrs: Record<string,string> ) => ComponentContent;
31
27
 
@@ -95,8 +91,6 @@ export interface ComponentProps {
95
91
  cls?: string;
96
92
  /** Element ID. */
97
93
  id?: string;
98
- /** Reference to the component instance. */
99
- ref?: RefType<any>;
100
94
 
101
95
  // shortcuts
102
96
  /** Width (px or string like `"50%"` or `"3em"`). */
@@ -628,7 +622,7 @@ export class Component<P extends ComponentProps = ComponentProps, E extends Comp
628
622
  set( child );
629
623
  }
630
624
 
631
- d.insertBefore( d.firstChild, fragment );
625
+ d.insertBefore( fragment, d.firstChild );
632
626
  }
633
627
  }
634
628
 
@@ -259,8 +259,10 @@ export class CoreElement<E extends EventMap = EventMap> {
259
259
 
260
260
  cleanUp( ) {
261
261
  this.clearTimeouts( );
262
- this.#cleanup?.forEach( x => x() );
263
- this.#cleanup.length = 0;
262
+ if( this.#cleanup ) {
263
+ this.#cleanup.forEach( x => x() );
264
+ this.#cleanup.length = 0;
265
+ }
264
266
  }
265
267
 
266
268
  // :: EVENTS ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
@@ -46,32 +46,52 @@ import { Constructor, isString } from './core_tools';
46
46
  export class x4_react {
47
47
 
48
48
  static create_element( tag: string, props: any, ...content: any[] ): Component;
49
- static create_element<X extends Component>( tag: string | Constructor<X>, props: X["props"], ...content: any[] ) {
50
-
51
- props = props || {};
49
+ static create_element<X extends Component>( tag: string | Constructor<X>, props: X["props"], ...children: any[] ) {
50
+
51
+ // ref is a JSX-only attribute: never passed to the component
52
+ const { ref, ...attrs } = ( props ?? {} ) as any;
53
+
54
+ // flatten {items.map(...)} and drop null / false from conditional children
55
+ const content = children.flat( Infinity )
56
+ .filter( c => c !== null && c !== undefined && c !== false );
52
57
 
53
58
  let el: Component;
54
59
 
55
- // --- simple div ------------------------
56
- if( isString(tag) ) {
60
+ // --- simple html element ------------------------
61
+ if( isString( tag ) ) {
57
62
  el = new Component( { tag } );
58
- Object.entries( props )
59
- .forEach(([name, value]) => {
60
- if (name.startsWith('on') && name.toLowerCase() in window) {
61
- el.dom.addEventListener(name.toLowerCase().substring(2), value)
63
+
64
+ Object.entries( attrs )
65
+ .forEach( ( [name, value]: [string, any] ) => {
66
+ if( name.startsWith( 'on' ) && name.toLowerCase( ) in window ) {
67
+ el.dom.addEventListener( name.toLowerCase( ).substring( 2 ), value );
62
68
  }
63
69
  else {
64
- el.setAttribute(name, value )
70
+ el.setAttribute( name, value );
65
71
  }
66
72
  });
73
+
74
+ if( content.length ) {
75
+ el.setContent( content );
76
+ }
67
77
  }
68
78
  // --- Component ------------------------
69
79
  else {
70
- el = new tag( props );
80
+ // same path as object construction: <X a={1}>{kids}</X> === new X({ a: 1, content: kids })
81
+ el = new tag( content.length ? { ...attrs, content } : attrs );
71
82
  }
72
83
 
73
- if( content && content.length ) {
74
- el.appendContent( content );
84
+ // ref={[owner, "name"]} stores the element in owner.refs.name
85
+ if( ref ) {
86
+ const [owner, name] = ref;
87
+
88
+ // the owner must be a Box: only Box has refs
89
+ if( !owner?.refs || typeof name !== "string" ) {
90
+ console.error( "invalid JSX ref, expected [ownerBox, \"name\"]:", ref );
91
+ }
92
+ else {
93
+ owner.refs[name] = el;
94
+ }
75
95
  }
76
96
 
77
97
  return el;
package/src/x4tsx.d.ts CHANGED
@@ -21,6 +21,10 @@ declare global {
21
21
  interface IntrinsicElements {
22
22
  [ name: string ]: any;
23
23
  }
24
+
25
+ interface IntrinsicClassAttributes<T> {
26
+ ref?: [ owner: object, name: string ];
27
+ }
24
28
  }
25
29
  }
26
30