x4js 2.3.11 → 2.3.13
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 +13 -1
- package/aicli.md +250 -0
- package/aicontext.md +6 -1
- package/aiguide.md +7 -0
- package/cli/build-options.mjs +2 -2
- package/cli/build.mjs +8 -2
- package/cli/config.mjs +46 -3
- package/cli/dev.mjs +22 -11
- package/cli/main.mjs +6 -3
- package/package.json +2 -1
- package/src/components/spreadsheet/spreadsheet.ts +28 -4
- package/src/core/core_router.ts +15 -3
package/README.md
CHANGED
|
@@ -7,7 +7,19 @@ see [X4 home page](https://x4js.org)
|
|
|
7
7
|
## API Documentation
|
|
8
8
|
see [GitHib](https://rlibre.github.io/x4/index.html)
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
## Using x4js with an AI assistant
|
|
11
|
+
Three files written for AI assistants ship with the package (in `node_modules/x4js/`). Give them to your assistant before asking it to write x4js code:
|
|
12
|
+
|
|
13
|
+
- [aicontext.md](./aicontext.md): compact API reference, with exact signatures
|
|
14
|
+
- [aiguide.md](./aiguide.md): how to write idiomatic x4js code
|
|
15
|
+
- [aicli.md](./aicli.md): the `x4js` command line and `x4.config.json`
|
|
16
|
+
|
|
17
|
+
With Claude Code, install the x4js plugin instead: its skill reads these files from your project by itself.
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
claude plugin marketplace add rlibre/x4
|
|
21
|
+
claude plugin install x4js@rlibre
|
|
22
|
+
```
|
|
11
23
|
|
|
12
24
|
---
|
|
13
25
|
|
package/aicli.md
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
# x4js command line
|
|
2
|
+
|
|
3
|
+
`x4js` creates, serves and builds x4js applications. It bundles TypeScript and styles with esbuild; there is nothing else to configure.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
x4js create <project> [--template <name>] [--no-install]
|
|
7
|
+
x4js templates
|
|
8
|
+
x4js dev [--config <file>] [--env <file>] [--host <host>] [--port <port>] [--http|--https] [--open]
|
|
9
|
+
x4js build [--config <file>] [--env <file>] [--debug]
|
|
10
|
+
x4js --help | --version
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
The command ships with the `x4js` package. In a project created by `x4js create` it is already a dependency, and the npm scripts use it:
|
|
16
|
+
|
|
17
|
+
```json
|
|
18
|
+
{
|
|
19
|
+
"scripts": {
|
|
20
|
+
"dev": "x4js dev",
|
|
21
|
+
"build": "x4js build"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
npm run dev
|
|
28
|
+
npm run build
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Without a project yet, run it through npx: `npx x4js create my-app`.
|
|
32
|
+
|
|
33
|
+
### Which x4js runs
|
|
34
|
+
|
|
35
|
+
There can be two copies on a machine: the one in the project's `node_modules` and a global one (`npm install -g x4js`).
|
|
36
|
+
|
|
37
|
+
| You type | Copy used |
|
|
38
|
+
|---|---|
|
|
39
|
+
| `npm run dev`, `npx x4js dev` | the project's own copy |
|
|
40
|
+
| `x4js dev` | the global copy |
|
|
41
|
+
|
|
42
|
+
A global copy older than the project's can behave differently. `dev` and `build` print their version on the first line; check it when something looks wrong.
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
x4js 2.3.11
|
|
46
|
+
mode dev
|
|
47
|
+
config C:\work\my-app\x4.config.json
|
|
48
|
+
outdir C:\work\my-app\bin
|
|
49
|
+
server http://127.0.0.1:8000
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Commands
|
|
53
|
+
|
|
54
|
+
### `x4js create <project>`
|
|
55
|
+
|
|
56
|
+
Creates a new project in the folder `<project>` from a template.
|
|
57
|
+
|
|
58
|
+
| Option | Effect |
|
|
59
|
+
|---|---|
|
|
60
|
+
| `--template <name>`, `-t <name>` | Template to use. Default: `app`. |
|
|
61
|
+
| `--no-install` | Do not run `npm install` after creating the files. |
|
|
62
|
+
|
|
63
|
+
- The project name becomes the package name: lowercase letters, digits, `.`, `_` and `-` only.
|
|
64
|
+
- The target folder must be empty or not exist.
|
|
65
|
+
- Templates are downloaded from the `rlibre/x4-templates` repository on GitHub, so the command needs network access.
|
|
66
|
+
- The `x4js` dependency of the new project is set to the version line of the command that created it (for example `2.3`).
|
|
67
|
+
|
|
68
|
+
### `x4js templates`
|
|
69
|
+
|
|
70
|
+
Lists the available templates with a one-line description of each.
|
|
71
|
+
|
|
72
|
+
### `x4js dev`
|
|
73
|
+
|
|
74
|
+
Builds the application, serves the output folder and rebuilds on every change.
|
|
75
|
+
|
|
76
|
+
| Option | Effect |
|
|
77
|
+
|---|---|
|
|
78
|
+
| `--config <file>` | Config file to use. Default: `x4.config.json`. |
|
|
79
|
+
| `--env <file>` | File of variables for `$NAME` in the config (see below). |
|
|
80
|
+
| `--host <host>` | Address to listen on. Default: `127.0.0.1`. |
|
|
81
|
+
| `--port <port>` | Port to listen on. Default: the first free port from 8000. |
|
|
82
|
+
| `--http`, `--https` | Force the protocol, whatever the config says. |
|
|
83
|
+
| `--open` | Open the browser once the server is ready. |
|
|
84
|
+
|
|
85
|
+
- The page reloads when a source or a copied file changes. A change that only touches CSS is applied without reloading.
|
|
86
|
+
- Build errors and warnings are printed in the terminal.
|
|
87
|
+
- Editing the config file restarts the build. The file given to `--env` is read again at that moment; editing it alone restarts nothing.
|
|
88
|
+
- HTTPS needs a certificate and a key, declared in `dev.tls` (see below).
|
|
89
|
+
|
|
90
|
+
### `x4js build`
|
|
91
|
+
|
|
92
|
+
Writes the application to the output folder.
|
|
93
|
+
|
|
94
|
+
| Option | Effect |
|
|
95
|
+
|---|---|
|
|
96
|
+
| `--config <file>` | Config file to use. Default: `x4.config.json`. |
|
|
97
|
+
| `--env <file>` | File of variables for `$NAME` in the config (see below). |
|
|
98
|
+
| `--debug` | Debug build: not minified, with source maps. |
|
|
99
|
+
|
|
100
|
+
The output folder is **emptied before each build**. Do not keep hand-written files in it: put them in the sources and list them in `copy`.
|
|
101
|
+
|
|
102
|
+
| | `dev` | `build --debug` | `build` |
|
|
103
|
+
|---|---|---|---|
|
|
104
|
+
| Minified | no | no | yes |
|
|
105
|
+
| Source maps | separate `.map` files | separate `.map` files | none |
|
|
106
|
+
| `DEBUG_MODE` | `true` | `true` | `false` |
|
|
107
|
+
| Live reload | yes | no | no |
|
|
108
|
+
|
|
109
|
+
## `x4.config.json`
|
|
110
|
+
|
|
111
|
+
The file sits at the root of the project. Every key is optional; without the file, the defaults below apply.
|
|
112
|
+
|
|
113
|
+
```json
|
|
114
|
+
{
|
|
115
|
+
"entryPoints": ["src/main.ts"],
|
|
116
|
+
"outdir": "./bin",
|
|
117
|
+
"copy": [
|
|
118
|
+
{ "from": "./src/assets", "to": "assets" },
|
|
119
|
+
{ "from": "./src/index.html", "to": "index.html" }
|
|
120
|
+
]
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
| Key | Default | Meaning |
|
|
125
|
+
|---|---|---|
|
|
126
|
+
| `entryPoints` | `["src/main.ts"]` | Files to bundle. `src/main.ts` produces `main.js` and `main.css`. |
|
|
127
|
+
| `outdir` | `"./bin"` | Output folder. It cannot be the project root. |
|
|
128
|
+
| `copy` | `[]` | Files or folders copied to the output after each successful build. `to` is relative to `outdir`. |
|
|
129
|
+
| `external` | `[]` | Module names left out of the bundle. |
|
|
130
|
+
| `define` | `{}` | Constants replaced at build time. Values are JavaScript expressions written as strings, for example `"\"prod\""`. |
|
|
131
|
+
| `dev` | | Dev server settings: `host`, `port`, `https`, `tls`. |
|
|
132
|
+
| `esbuild` | `{}` | Native esbuild options, applied last. They override everything above. |
|
|
133
|
+
|
|
134
|
+
Relative paths are resolved from the project root. Paths and `define` values accept environment variables, written `$NAME` or `${NAME}`; an undefined variable is an error.
|
|
135
|
+
|
|
136
|
+
### Environment variables in `define`
|
|
137
|
+
|
|
138
|
+
A `define` value is a JavaScript expression, so a text is written between escaped double quotes. The variable is read when the command runs: its value ends up in the bundle.
|
|
139
|
+
|
|
140
|
+
```json
|
|
141
|
+
{
|
|
142
|
+
"define": {
|
|
143
|
+
"API_URL": "\"$API_URL\"",
|
|
144
|
+
"API_PORT": "$API_PORT"
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
declare const API_URL: string;
|
|
151
|
+
declare const API_PORT: number;
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
With `API_URL=https://example.com` and `API_PORT=8080`, the code sees the text `"https://example.com"` and the number `8080`. A quote or a backslash in the value of a variable is escaped: use double quotes around `$NAME`, not single ones.
|
|
155
|
+
|
|
156
|
+
Everything in the bundle is public: do not put a secret in `define`.
|
|
157
|
+
|
|
158
|
+
### Variables from a file
|
|
159
|
+
|
|
160
|
+
`--env <file>` reads variables from a file of `NAME=value` lines, for `dev` and `build`:
|
|
161
|
+
|
|
162
|
+
```
|
|
163
|
+
# .env
|
|
164
|
+
API_URL=https://example.com
|
|
165
|
+
API_PORT=8080
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
```
|
|
169
|
+
x4js dev --env .env
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
- The file only serves `$NAME` in the config. Its variables are not given to the application: pass the ones it needs through `define`.
|
|
173
|
+
- A variable already defined in the environment keeps its value: the file only adds the missing ones.
|
|
174
|
+
- A file that does not exist is an error.
|
|
175
|
+
|
|
176
|
+
This lets the same config work on a machine and on a host that defines the variables itself:
|
|
177
|
+
|
|
178
|
+
```json
|
|
179
|
+
{
|
|
180
|
+
"scripts": {
|
|
181
|
+
"dev": "x4js dev --env .env",
|
|
182
|
+
"build": "x4js build"
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Keep the file out of Git when it holds values that are not public.
|
|
188
|
+
|
|
189
|
+
### The HTML page
|
|
190
|
+
|
|
191
|
+
The command does not generate an HTML page. Write `src/index.html` and copy it with `copy`, as in the example above. It loads the bundle by its output name:
|
|
192
|
+
|
|
193
|
+
```html
|
|
194
|
+
<!DOCTYPE html>
|
|
195
|
+
<html>
|
|
196
|
+
<head>
|
|
197
|
+
<meta charset="utf8">
|
|
198
|
+
<link rel="stylesheet" href="main.css">
|
|
199
|
+
<script src="main.js"></script>
|
|
200
|
+
</head>
|
|
201
|
+
</html>
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### HTTPS in dev
|
|
205
|
+
|
|
206
|
+
```json
|
|
207
|
+
{
|
|
208
|
+
"dev": {
|
|
209
|
+
"https": true,
|
|
210
|
+
"tls": { "cert": "./certs/dev.crt", "key": "./certs/dev.key" }
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
## What the build handles
|
|
216
|
+
|
|
217
|
+
| Import | Result |
|
|
218
|
+
|---|---|
|
|
219
|
+
| `.ts`, `.js` | Bundled into one script per entry point (browser, ES2020). |
|
|
220
|
+
| `.scss`, `.sass`, `.css` | Compiled and bundled into one stylesheet per entry point. |
|
|
221
|
+
| `.less` | Same, if the `less` package is installed in the project (`npm install -D less`). |
|
|
222
|
+
| `.svg` imported from a script | A `data:` URL string, usable as an icon. |
|
|
223
|
+
| `.png`, `.jpg`, `.jpeg`, `.ttf`, `.woff`, `.woff2` | Copied to `assets/` with a hashed name. |
|
|
224
|
+
|
|
225
|
+
Two constants are always defined:
|
|
226
|
+
|
|
227
|
+
| Constant | Value |
|
|
228
|
+
|---|---|
|
|
229
|
+
| `DEBUG_MODE` | `true` in `dev` and `build --debug`, `false` in `build`. |
|
|
230
|
+
| `VERSION_ID` | Build date as a number, `YYMMDD`. |
|
|
231
|
+
|
|
232
|
+
Declare them once in your project to use them from TypeScript:
|
|
233
|
+
|
|
234
|
+
```ts
|
|
235
|
+
declare const DEBUG_MODE: boolean;
|
|
236
|
+
declare const VERSION_ID: number;
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
## Troubleshooting
|
|
240
|
+
|
|
241
|
+
| Symptom | Cause |
|
|
242
|
+
|---|---|
|
|
243
|
+
| `index.html` is missing from the output folder | It is not listed in `copy`, or the command that ran is an older global copy. Compare the version on the first line with the project's. |
|
|
244
|
+
| A file placed in the output folder disappeared | `x4js build` empties the folder. Move the file to the sources and add it to `copy`. |
|
|
245
|
+
| `Cannot find package 'esbuild'` | The dependencies of the `x4js` copy being run are not installed. Run `npm install` in the project. |
|
|
246
|
+
| `Config file not found` | The file given to `--config` does not exist. Without `--config`, a missing `x4.config.json` is not an error. |
|
|
247
|
+
| `Environment variable '…' is not defined` | A path or a `define` value in the config uses `$NAME` and the variable is not set, in the environment or in the file given to `--env`. |
|
|
248
|
+
| `Env file not found` | The file given to `--env` does not exist. |
|
|
249
|
+
| `LESS support requires the "less" package` | A `.less` file is imported and `less` is not installed in the project. |
|
|
250
|
+
| No colors in the terminal, or unwanted ones | Set `FORCE_COLOR=1` to force them, `NO_COLOR=1` to remove them. |
|
package/aicontext.md
CHANGED
|
@@ -17,6 +17,7 @@ import { ... } from 'x4js' // src/x4.ts
|
|
|
17
17
|
**Archived repository**: https://github.com/rlibre/x4js (read-only since March 2026)
|
|
18
18
|
**License**: MIT
|
|
19
19
|
**Quick start**: `npx x4js create demo` (list the templates with `npx x4js templates`)
|
|
20
|
+
**Claude Code plugin**: `claude plugin marketplace add rlibre/x4` then `claude plugin install x4js@rlibre` (skill that reads these files from the project)
|
|
20
21
|
|
|
21
22
|
---
|
|
22
23
|
|
|
@@ -734,6 +735,7 @@ interface SpreadsheetProps extends ComponentProps {
|
|
|
734
735
|
enterMove?: "down" | "right" | "none" // selection move after Enter in a cell editor (default "down")
|
|
735
736
|
click?, dblClick?, contextMenu?, selectionChange?
|
|
736
737
|
cellChange? // EvCellChange, fired before an edited value is written to the store
|
|
738
|
+
cellKey? // EvCellKey { key, row, col, uievent }, fired for each key before the default handling
|
|
737
739
|
}
|
|
738
740
|
interface SpreadsheetColumn { // GridColumn +
|
|
739
741
|
editable?: boolean | ((row, col) => boolean) // inline edition (off by default), col = column index
|
|
@@ -747,7 +749,9 @@ class Spreadsheet extends Component {
|
|
|
747
749
|
}
|
|
748
750
|
|
|
749
751
|
// Inline edition: F2 / Enter / double-click / typing starts, Enter / Tab / click elsewhere validates, Escape cancels,
|
|
750
|
-
// Delete clears, editable checkboxes toggle on click or Space
|
|
752
|
+
// Delete clears, editable checkboxes toggle on double-click or Space (a single click only selects).
|
|
753
|
+
// Default editor is an Input matching the column type.
|
|
754
|
+
// cellKey: key hook (function keys...), not fired while a cell is edited; ev.preventDefault() skips the default handling.
|
|
751
755
|
// cellChange: ev.preventDefault() refuses the value, ev.value can be replaced.
|
|
752
756
|
// Select / Combobox in a cell (the store holds the id, use `formatter` to display the text):
|
|
753
757
|
// { id: 2, title: "Status", width: 120, editable: true,
|
|
@@ -1392,6 +1396,7 @@ When creating a new x4js project, use these files as a base.
|
|
|
1392
1396
|
|
|
1393
1397
|
`x4js dev` runs the dev server, `x4js build` writes `outdir` (add `--debug` for a debug build).
|
|
1394
1398
|
Both read `x4.config.json` (change the file with `--config <file>`).
|
|
1399
|
+
Full reference of the command line (commands, options, config keys, troubleshooting): `aicli.md`.
|
|
1395
1400
|
|
|
1396
1401
|
### `x4.config.json`
|
|
1397
1402
|
|
package/aiguide.md
CHANGED
|
@@ -636,6 +636,13 @@ selectLanguage("en");
|
|
|
636
636
|
// only now build the application components
|
|
637
637
|
```
|
|
638
638
|
|
|
639
|
+
Type each additional language after the first one, so the compiler reports a missing text:
|
|
640
|
+
|
|
641
|
+
```ts
|
|
642
|
+
const app_fr = { app: { open: "Ouvrir", close: "Fermer" } };
|
|
643
|
+
const app_en: typeof app_fr = { app: { open: "Open", close: "Close" } };
|
|
644
|
+
```
|
|
645
|
+
|
|
639
646
|
---
|
|
640
647
|
|
|
641
648
|
## Final rule
|
package/cli/build-options.mjs
CHANGED
|
@@ -27,12 +27,12 @@ export function createBuildOptions(config, mode) {
|
|
|
27
27
|
charset: "utf8",
|
|
28
28
|
keepNames: true,
|
|
29
29
|
platform: "browser",
|
|
30
|
-
|
|
30
|
+
// Errors and warnings are printed by diagnostic-plugin.mjs.
|
|
31
|
+
logLevel: "silent",
|
|
31
32
|
format: "iife",
|
|
32
33
|
target: "es2020",
|
|
33
34
|
minify: production,
|
|
34
35
|
sourcemap: production ? false : "linked",
|
|
35
|
-
logLevel: "info",
|
|
36
36
|
external: config.external,
|
|
37
37
|
assetNames: "assets/[name]-[hash]",
|
|
38
38
|
loader: {
|
package/cli/build.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import { parseArgs } from "node:util";
|
|
3
3
|
import esbuild from "esbuild";
|
|
4
|
-
import { DEFAULT_CONFIG_FILE, expandEnv, loadConfig } from "./config.mjs";
|
|
4
|
+
import { DEFAULT_CONFIG_FILE, expandEnv, loadConfig, loadEnvFile } from "./config.mjs";
|
|
5
5
|
import { createBuildOptions } from "./build-options.mjs";
|
|
6
6
|
import { info, success } from "./log.mjs";
|
|
7
7
|
import { printBuildError, printWarnings } from "./diagnostic-plugin.mjs";
|
|
@@ -11,6 +11,7 @@ export async function build(argv = [], root = process.cwd()) {
|
|
|
11
11
|
args: argv,
|
|
12
12
|
options: {
|
|
13
13
|
config: { type: "string" },
|
|
14
|
+
env: { type: "string" },
|
|
14
15
|
debug: { type: "boolean", default: false },
|
|
15
16
|
},
|
|
16
17
|
allowPositionals: true,
|
|
@@ -20,12 +21,17 @@ export async function build(argv = [], root = process.cwd()) {
|
|
|
20
21
|
if (positionals.length)
|
|
21
22
|
throw new Error(`Unexpected argument: ${positionals[0]}`);
|
|
22
23
|
|
|
24
|
+
const envFile = values.env ? loadEnvFile(root, values.env) : undefined;
|
|
25
|
+
const env = envFile?.env ?? process.env;
|
|
26
|
+
|
|
23
27
|
const configArg = values.config ? expandEnv(values.config) : DEFAULT_CONFIG_FILE;
|
|
24
|
-
const config = loadConfig(root, configArg,
|
|
28
|
+
const config = loadConfig(root, configArg, env, { explicit: values.config !== undefined });
|
|
25
29
|
const mode = values.debug ? "debug" : "production";
|
|
26
30
|
|
|
27
31
|
info("mode", mode);
|
|
28
32
|
info("config", config.configFile);
|
|
33
|
+
if (envFile)
|
|
34
|
+
info("env file", envFile.filename);
|
|
29
35
|
info("outdir", config.outdir);
|
|
30
36
|
|
|
31
37
|
await fs.rm(config.outdir, { recursive: true, force: true });
|
package/cli/config.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { parseEnv } from "node:util";
|
|
3
4
|
|
|
4
5
|
export const DEFAULT_CONFIG_FILE = "x4.config.json";
|
|
5
6
|
|
|
@@ -15,7 +16,7 @@ const DEFAULTS = Object.freeze({
|
|
|
15
16
|
},
|
|
16
17
|
});
|
|
17
18
|
|
|
18
|
-
export function expandEnv(value, env = process.env) {
|
|
19
|
+
export function expandEnv(value, env = process.env, escape = (text) => text) {
|
|
19
20
|
if (typeof value !== "string")
|
|
20
21
|
return value;
|
|
21
22
|
|
|
@@ -26,11 +27,18 @@ export function expandEnv(value, env = process.env) {
|
|
|
26
27
|
const result = env[name];
|
|
27
28
|
if (result === undefined)
|
|
28
29
|
throw new Error(`Environment variable '${name}' is not defined`);
|
|
29
|
-
return result;
|
|
30
|
+
return escape(result);
|
|
30
31
|
},
|
|
31
32
|
);
|
|
32
33
|
}
|
|
33
34
|
|
|
35
|
+
// A define value is a JavaScript expression, usually a string literal
|
|
36
|
+
// ("\"$NAME\""). The value of the variable is escaped so that a quote or a
|
|
37
|
+
// backslash in it cannot end or corrupt that literal.
|
|
38
|
+
function escapeForDefine(text) {
|
|
39
|
+
return JSON.stringify(text).slice(1, -1);
|
|
40
|
+
}
|
|
41
|
+
|
|
34
42
|
export function resolvePath(root, value, env = process.env) {
|
|
35
43
|
const expanded = expandEnv(value, env);
|
|
36
44
|
return path.isAbsolute(expanded)
|
|
@@ -42,6 +50,39 @@ export function resolveConfigFile(root, configFile = DEFAULT_CONFIG_FILE, env =
|
|
|
42
50
|
return resolvePath(root, configFile, env);
|
|
43
51
|
}
|
|
44
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Reads the file given to --env.
|
|
55
|
+
* Returns the environment to expand the config with: the variables of the
|
|
56
|
+
* file, under those already defined, which win as with `node --env-file`.
|
|
57
|
+
*
|
|
58
|
+
* The option is not named --env-file on purpose: node handles that name
|
|
59
|
+
* itself, even after the script name, before this code runs.
|
|
60
|
+
*/
|
|
61
|
+
export function loadEnvFile(root, file, env = process.env) {
|
|
62
|
+
const filename = resolvePath(root, file, env);
|
|
63
|
+
|
|
64
|
+
let variables;
|
|
65
|
+
try {
|
|
66
|
+
variables = parseEnv(fs.readFileSync(filename, "utf8"));
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
if (error.code === "ENOENT")
|
|
70
|
+
throw new Error(`Env file not found: ${filename}`);
|
|
71
|
+
throw new Error(`Cannot read env file '${filename}': ${error.message}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Inherit from the environment instead of copying it: process.env has
|
|
75
|
+
// its own lookup rules (names are case-insensitive on Windows).
|
|
76
|
+
// defineProperty, because a plain assignment would go through to it.
|
|
77
|
+
const merged = Object.create(env);
|
|
78
|
+
for (const [name, value] of Object.entries(variables)) {
|
|
79
|
+
if (env[name] === undefined)
|
|
80
|
+
Object.defineProperty(merged, name, { value, enumerable: true });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return { filename, env: merged };
|
|
84
|
+
}
|
|
85
|
+
|
|
45
86
|
function readJson(filename) {
|
|
46
87
|
try {
|
|
47
88
|
return JSON.parse(fs.readFileSync(filename, "utf8"));
|
|
@@ -146,7 +187,9 @@ export function loadConfig(root = process.cwd(), configFile = DEFAULT_CONFIG_FIL
|
|
|
146
187
|
to: path.normalize(item.to),
|
|
147
188
|
})),
|
|
148
189
|
external: [...external],
|
|
149
|
-
define:
|
|
190
|
+
define: Object.fromEntries(
|
|
191
|
+
Object.entries(define).map(([name, value]) => [name, expandEnv(value, env, escapeForDefine)]),
|
|
192
|
+
),
|
|
150
193
|
dev: {
|
|
151
194
|
host,
|
|
152
195
|
port,
|
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 { DEFAULT_CONFIG_FILE, expandEnv, loadConfig, resolveConfigFile, resolveTls } from "./config.mjs";
|
|
6
|
+
import { DEFAULT_CONFIG_FILE, expandEnv, loadConfig, loadEnvFile, resolveConfigFile, resolveTls } from "./config.mjs";
|
|
7
7
|
import { createBuildOptions } from "./build-options.mjs";
|
|
8
8
|
import { info, success, failure } from "./log.mjs";
|
|
9
9
|
|
|
@@ -60,6 +60,7 @@ export async function dev(argv = [], root = process.cwd()) {
|
|
|
60
60
|
args: argv,
|
|
61
61
|
options: {
|
|
62
62
|
config: { type: "string" },
|
|
63
|
+
env: { type: "string" },
|
|
63
64
|
host: { type: "string" },
|
|
64
65
|
port: { type: "string" },
|
|
65
66
|
http: { type: "boolean", default: false },
|
|
@@ -86,7 +87,16 @@ export async function dev(argv = [], root = process.cwd()) {
|
|
|
86
87
|
let restartChain = Promise.resolve();
|
|
87
88
|
let browserOpened = false;
|
|
88
89
|
|
|
89
|
-
|
|
90
|
+
// Read again on each (re)start, so an edited env file is taken into
|
|
91
|
+
// account when the config is reloaded.
|
|
92
|
+
function load() {
|
|
93
|
+
const envFile = values.env ? loadEnvFile(root, values.env) : undefined;
|
|
94
|
+
const env = envFile?.env ?? process.env;
|
|
95
|
+
const config = loadConfig(root, configArg, env, { explicit: explicitConfig });
|
|
96
|
+
return { config, env, envFile };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function start({ config, env, envFile }) {
|
|
90
100
|
const host = values.host ?? config.dev.host;
|
|
91
101
|
const port = values.port === undefined ? config.dev.port : Number(values.port);
|
|
92
102
|
if (port !== undefined && (port < 0 || port > 65535))
|
|
@@ -101,7 +111,7 @@ export async function dev(argv = [], root = process.cwd()) {
|
|
|
101
111
|
if (port !== undefined)
|
|
102
112
|
serveOptions.port = port;
|
|
103
113
|
if (useHttps)
|
|
104
|
-
Object.assign(serveOptions, resolveTls(config));
|
|
114
|
+
Object.assign(serveOptions, resolveTls(config, env));
|
|
105
115
|
|
|
106
116
|
const next = await esbuild.context(createBuildOptions(config, "dev"));
|
|
107
117
|
try {
|
|
@@ -113,6 +123,8 @@ export async function dev(argv = [], root = process.cwd()) {
|
|
|
113
123
|
const url = serverUrl(actualHost, result.port, useHttps);
|
|
114
124
|
info("mode", "dev");
|
|
115
125
|
info("config", config.configFile);
|
|
126
|
+
if (envFile)
|
|
127
|
+
info("env file", envFile.filename);
|
|
116
128
|
info("outdir", config.outdir);
|
|
117
129
|
success("server", url);
|
|
118
130
|
|
|
@@ -128,12 +140,12 @@ export async function dev(argv = [], root = process.cwd()) {
|
|
|
128
140
|
}
|
|
129
141
|
|
|
130
142
|
async function reload() {
|
|
131
|
-
let
|
|
143
|
+
let loaded;
|
|
132
144
|
try {
|
|
133
|
-
|
|
134
|
-
protocolConfig(config, values);
|
|
135
|
-
if (values.https || (!values.http && config.dev.https))
|
|
136
|
-
resolveTls(config);
|
|
145
|
+
loaded = load();
|
|
146
|
+
protocolConfig(loaded.config, values);
|
|
147
|
+
if (values.https || (!values.http && loaded.config.dev.https))
|
|
148
|
+
resolveTls(loaded.config, loaded.env);
|
|
137
149
|
}
|
|
138
150
|
catch (error) {
|
|
139
151
|
failure(`config: ${error.message}`);
|
|
@@ -146,7 +158,7 @@ export async function dev(argv = [], root = process.cwd()) {
|
|
|
146
158
|
await previous.dispose();
|
|
147
159
|
|
|
148
160
|
try {
|
|
149
|
-
await start(
|
|
161
|
+
await start(loaded);
|
|
150
162
|
success("config", "reloaded");
|
|
151
163
|
}
|
|
152
164
|
catch (error) {
|
|
@@ -161,8 +173,7 @@ export async function dev(argv = [], root = process.cwd()) {
|
|
|
161
173
|
}, 150);
|
|
162
174
|
}
|
|
163
175
|
|
|
164
|
-
|
|
165
|
-
await start(initialConfig);
|
|
176
|
+
await start(load());
|
|
166
177
|
|
|
167
178
|
const watchDir = path.dirname(watchedConfigFile);
|
|
168
179
|
const watchName = path.basename(watchedConfigFile);
|
package/cli/main.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
-
import { failure } from "./log.mjs";
|
|
4
|
+
import { failure, info } from "./log.mjs";
|
|
5
5
|
|
|
6
6
|
const cliDir = path.dirname(fileURLToPath(import.meta.url));
|
|
7
7
|
const packageFile = path.join(cliDir, "..", "package.json");
|
|
@@ -21,11 +21,12 @@ function printHelp(version) {
|
|
|
21
21
|
Usage:
|
|
22
22
|
x4js create <project> [--template <name>] [--no-install]
|
|
23
23
|
x4js templates
|
|
24
|
-
x4js dev [--config <file>] [--host <host>] [--port <port>] [--http|--https] [--open]
|
|
25
|
-
x4js build [--config <file>] [--debug]
|
|
24
|
+
x4js dev [--config <file>] [--env <file>] [--host <host>] [--port <port>] [--http|--https] [--open]
|
|
25
|
+
x4js build [--config <file>] [--env <file>] [--debug]
|
|
26
26
|
|
|
27
27
|
Options:
|
|
28
28
|
--config <file> Config file (default: x4.config.json)
|
|
29
|
+
--env <file> File of NAME=value lines, for $NAME in the config
|
|
29
30
|
-h, --help Show help
|
|
30
31
|
-v, --version Show version`);
|
|
31
32
|
}
|
|
@@ -46,11 +47,13 @@ try {
|
|
|
46
47
|
break;
|
|
47
48
|
}
|
|
48
49
|
case "dev": {
|
|
50
|
+
info("x4js", version);
|
|
49
51
|
const { dev } = await import("./dev.mjs");
|
|
50
52
|
await dev(argv);
|
|
51
53
|
break;
|
|
52
54
|
}
|
|
53
55
|
case "build": {
|
|
56
|
+
info("x4js", version);
|
|
54
57
|
const { build } = await import("./build.mjs");
|
|
55
58
|
await build(argv);
|
|
56
59
|
break;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "x4js",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.13",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "src/x4.ts",
|
|
6
6
|
"module": "src/x4.ts",
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
"src",
|
|
22
22
|
"bin",
|
|
23
23
|
"cli",
|
|
24
|
+
"aicli.md",
|
|
24
25
|
"aicontext.md",
|
|
25
26
|
"aiguide.md",
|
|
26
27
|
"README.md",
|
|
@@ -72,6 +72,18 @@ export interface EvCellChange extends ComponentEvent {
|
|
|
72
72
|
oldValue: any;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
/**
|
|
76
|
+
* fired for each key pressed on the grid (not while a cell is edited), before the default handling
|
|
77
|
+
* call preventDefault to handle the key yourself
|
|
78
|
+
*/
|
|
79
|
+
|
|
80
|
+
export interface EvCellKey extends ComponentEvent {
|
|
81
|
+
key: string; // "F2", "Enter", "a"...
|
|
82
|
+
row: number; // selected cell, undefined if none
|
|
83
|
+
col: number; // column index
|
|
84
|
+
uievent: KeyboardEvent; // UI event that fire this event
|
|
85
|
+
}
|
|
86
|
+
|
|
75
87
|
interface CellEdit extends CellRef {
|
|
76
88
|
editor: Component;
|
|
77
89
|
initial?: string; // text of the editor at start
|
|
@@ -214,6 +226,7 @@ export interface SpreadsheetEvents extends ComponentEvents {
|
|
|
214
226
|
contextMenu?: EvContextMenu;
|
|
215
227
|
selectionChange?: EvSelectionChange;
|
|
216
228
|
cellChange?: EvCellChange;
|
|
229
|
+
cellKey?: EvCellKey;
|
|
217
230
|
}
|
|
218
231
|
|
|
219
232
|
export interface SpreadsheetProps extends ComponentProps {
|
|
@@ -226,6 +239,7 @@ export interface SpreadsheetProps extends ComponentProps {
|
|
|
226
239
|
enterMove?: "down" | "right" | "none";
|
|
227
240
|
|
|
228
241
|
cellChange?: EventCallback<EvCellChange>;
|
|
242
|
+
cellKey?: EventCallback<EvCellKey>;
|
|
229
243
|
click?: EventCallback<EvClick>;
|
|
230
244
|
dblClick?: EventCallback<EvDblClick>;
|
|
231
245
|
contextMenu?: EventCallback<EvContextMenu>;
|
|
@@ -323,7 +337,7 @@ export class Spreadsheet<P extends SpreadsheetProps = SpreadsheetProps, E extend
|
|
|
323
337
|
|
|
324
338
|
this._columns = props.columns.map(x => x);
|
|
325
339
|
|
|
326
|
-
this.mapPropEvents(props, "click", "dblClick", "contextMenu", "selectionChange", "cellChange");
|
|
340
|
+
this.mapPropEvents(props, "click", "dblClick", "contextMenu", "selectionChange", "cellChange", "cellKey");
|
|
327
341
|
|
|
328
342
|
this.lock(true);
|
|
329
343
|
this.setAttribute("tabindex", 0);
|
|
@@ -365,6 +379,17 @@ export class Spreadsheet<P extends SpreadsheetProps = SpreadsheetProps, E extend
|
|
|
365
379
|
return;
|
|
366
380
|
}
|
|
367
381
|
|
|
382
|
+
// the application first
|
|
383
|
+
const cur = this._curCell( );
|
|
384
|
+
const kev: EvCellKey = { key: ev.key, row: cur?.row, col: cur?.col, uievent: ev };
|
|
385
|
+
this.fire( "cellKey", kev );
|
|
386
|
+
|
|
387
|
+
if( kev.defaultPrevented ) {
|
|
388
|
+
ev.preventDefault();
|
|
389
|
+
ev.stopPropagation();
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
|
|
368
393
|
switch (ev.key) {
|
|
369
394
|
case "ArrowDown": {
|
|
370
395
|
this.navigate(kbNav.next);
|
|
@@ -1148,8 +1173,6 @@ export class Spreadsheet<P extends SpreadsheetProps = SpreadsheetProps, E extend
|
|
|
1148
1173
|
this._clearSelection( false );
|
|
1149
1174
|
this._addSelection(ref.ref,true);
|
|
1150
1175
|
}
|
|
1151
|
-
|
|
1152
|
-
this._toggleCell( ref.row, ref.col );
|
|
1153
1176
|
}
|
|
1154
1177
|
});
|
|
1155
1178
|
|
|
@@ -1170,7 +1193,8 @@ export class Spreadsheet<P extends SpreadsheetProps = SpreadsheetProps, E extend
|
|
|
1170
1193
|
const dev: EvDblClick = { context: { row: ref.row, col: ref.col } };
|
|
1171
1194
|
this.fire( "dblClick", dev );
|
|
1172
1195
|
|
|
1173
|
-
|
|
1196
|
+
// checkboxes are toggled, other cells are edited
|
|
1197
|
+
if( !dev.defaultPrevented && !this._toggleCell( ref.row, ref.col ) ) {
|
|
1174
1198
|
this.editCell( ref.row, ref.col );
|
|
1175
1199
|
}
|
|
1176
1200
|
}
|
package/src/core/core_router.ts
CHANGED
|
@@ -143,7 +143,13 @@ export class Router extends EventSource< RouterEvents > {
|
|
|
143
143
|
window.addEventListener('popstate', (event) => {
|
|
144
144
|
const url = this._getLocation( );
|
|
145
145
|
const found = this._find(url);
|
|
146
|
-
|
|
146
|
+
|
|
147
|
+
if( found.handlers.length==0 ) {
|
|
148
|
+
console.log( 'route not found: '+url );
|
|
149
|
+
this.fire( "error", {code: 404, message: "route not found" } );
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
147
153
|
found.handlers.forEach(h => {
|
|
148
154
|
h(found.params,url);
|
|
149
155
|
});
|
|
@@ -173,11 +179,17 @@ export class Router extends EventSource< RouterEvents > {
|
|
|
173
179
|
|
|
174
180
|
|
|
175
181
|
init() {
|
|
176
|
-
|
|
182
|
+
// the page is already in the history: replace its entry instead of adding one
|
|
183
|
+
this.navigate( this._getLocation(), true, true );
|
|
177
184
|
}
|
|
178
185
|
|
|
179
186
|
private _getLocation( ) {
|
|
180
|
-
|
|
187
|
+
if( !this.m_useHash ) {
|
|
188
|
+
return document.location.pathname;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// "#home" and "#/home" are the same route
|
|
192
|
+
return '/'+document.location.hash.substring(1).replace( /^\/+/, '' );
|
|
181
193
|
}
|
|
182
194
|
|
|
183
195
|
/**
|