@codenotch/codenotch.react 1.0.82 → 2.0.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
@@ -4,15 +4,14 @@ React bindings for [Codenotch](https://codenotch.com) applications.
4
4
 
5
5
  Codenotch is a full-stack development platform: a project combines server-side **BPMN processes**, **SQL tables** / **NoSQL documents**, **i18n** translations and **React** apps. This package is the client-side bridge between those React apps and the Codenotch runtime: it lets a component start BPMN processes, run CNQL queries, translate i18n keys, listen to real-time signals, and manage theme/language.
6
6
 
7
- > **Requirements** — Codenotch apps run on **React 16** (`react@^16.14.0`). Do not use React 17/18 APIs (`createRoot`, `useId`, automatic JSX runtime…); hooks work fine.
7
+ > **Requirements** — Codenotch apps run on **React 19** (`react@^19`, `react-dom@^19`). The package uses the automatic JSX runtime and `createRoot`; all modern React APIs (hooks, `useId`, `use`, Actions…) are available.
8
8
 
9
9
  ## Quick start
10
10
 
11
11
  ```tsx
12
- import React from 'react';
13
12
  import { useCodenotch } from 'codenotch-react';
14
13
 
15
- const MyApp: React.FC = () => {
14
+ const MyApp = () => {
16
15
  const cn = useCodenotch();
17
16
 
18
17
  return <div className="p-4">
@@ -23,7 +22,41 @@ const MyApp: React.FC = () => {
23
22
  export default MyApp;
24
23
  ```
25
24
 
26
- `useCodenotch()` is **not a React hook** despite its name: it is a plain function that returns the API object bound to the current environment. It can be called anywhere components, event handlers, plain modules.
25
+ `useCodenotch()` is a real React hook: the returned object is stable across renders and is replaced re-rendering the component — whenever the environment changes (`setLanguage`, `setTheme`, `init`). Regular hook rules apply. The same API is available everywhere else through two non-hook entry points:
26
+
27
+ | Where | Use |
28
+ |-------|-----|
29
+ | Function component / custom hook | `const cn = useCodenotch();` — re-renders on language/theme change. |
30
+ | Class component | `export default withCodenotch(MyClass);` — injects `this.props.cn`, re-renders on change, forwards `ref`. |
31
+ | Event handler, plain module, service, anywhere | `getCodenotch()` — plain function, never stale, no re-render. |
32
+ | Manual subscription (rare) | `onCodenotchChange(listener)` → `IDisposable`. |
33
+
34
+ ```tsx
35
+ import React from 'react';
36
+ import { withCodenotch, WithCodenotchProps } from 'codenotch-react';
37
+
38
+ interface Props extends WithCodenotchProps {
39
+ userId: string;
40
+ }
41
+
42
+ class TodoList extends React.Component<Props> {
43
+ render() {
44
+ return <h1>{this.props.cn.i18n('todos.title')}</h1>;
45
+ }
46
+ }
47
+
48
+ export default withCodenotch(TodoList); // <TodoList userId="42" /> — `cn` is injected
49
+ ```
50
+
51
+ ```ts
52
+ // services/todos.ts — outside React
53
+ import { getCodenotch } from 'codenotch-react';
54
+
55
+ export async function loadTodos(userId: string) {
56
+ const result = await getCodenotch().startProcess('getTodos', 'start', { UserId: userId });
57
+ return result.output.todos;
58
+ }
59
+ ```
27
60
 
28
61
  The environment (cluster URL, service name, language, translations…) is set up by `init(envVariables)`, which the Codenotch runtime calls automatically from the page hosting the application. You only call `init` yourself in custom hosting scenarios.
29
62
 
@@ -36,7 +69,7 @@ The environment (cluster URL, service name, language, translations…) is set up
36
69
  | `cn.startProcess(name, startNodeId, inputs)` | Start a server-side BPMN process and await its result. |
37
70
  | `cn.requestCnql(cnql, verbose?)` | Run a CNQL query (XML, SELECT-only) against the project's tables. |
38
71
  | `cn.listenSignal(signalId, callback)` | Subscribe to real-time signals emitted by BPMN processes. |
39
- | `cn.showDialog(jsx)` | Render a JSX element in a fullscreen modal dialog. |
72
+ | `cn.showDialog(node)` | Render a React node in a fullscreen modal dialog (own React root, unmounted on `close()`). |
40
73
  | `cn.setTheme(t)` / `cn.getTheme()` | `'light' \| 'dark'`; `setTheme` also toggles the class on `<html>` (Tailwind `dark:`). |
41
74
  | `cn.setLanguage(l)` / `cn.getLanguage()` / `cn.getLanguages()` | Current language and the languages declared in `manifest.json`. |
42
75
  | `cn.getProjectFile(path)` / `cn.getProjectFileUrl(path)` | Read/link a file of the deployed project. |
@@ -84,7 +117,8 @@ Translations come from the project's `.i18n.csv` files and are injected into `cn
84
117
  ```tsx
85
118
  cn.i18n('welcome'); // "Bienvenue"
86
119
  cn.i18n('greeting', 'Ada', 3); // "Bonjour Ada, 3 messages" (from "Bonjour {0}, {1} messages")
87
- cn.setLanguage('en'); // switch language at runtime
120
+ cn.setLanguage('en'); // switch language at runtime — every component using
121
+ // useCodenotch() / withCodenotch() re-renders
88
122
  ```
89
123
 
90
124
  ## Real-time signals
@@ -94,12 +128,15 @@ BPMN processes can broadcast signals; subscribe from the UI with `listenSignal`:
94
128
  ```tsx
95
129
  useEffect(() => {
96
130
  let sub: IDisposable | undefined;
97
- cn.listenSignal('todosChanged', (signal) => refresh(signal.data))
98
- .then(s => { sub = s; });
99
- return () => sub?.dispose();
131
+ let cancelled = false;
132
+ getCodenotch().listenSignal('todosChanged', (signal) => refresh(signal.data))
133
+ .then(s => { if (cancelled) s.dispose(); else sub = s; });
134
+ return () => { cancelled = true; sub?.dispose(); };
100
135
  }, []);
101
136
  ```
102
137
 
138
+ Two details: the `cancelled` flag matters under React 19's StrictMode, which mounts / unmounts / remounts effects in development — without it the first subscription would leak when the cleanup runs before the promise resolves. And the effect uses `getCodenotch()` rather than the `cn` from `useCodenotch()`: the hook's object changes on every language/theme change, so listing it in the dependency array would re-subscribe on each change, while omitting it trips `react-hooks/exhaustive-deps`. `getCodenotch()` sidesteps both.
139
+
103
140
  ## Dialogs
104
141
 
105
142
  ```tsx
@@ -111,7 +148,7 @@ const dialog = cn.showDialog(
111
148
  );
112
149
  ```
113
150
 
114
- The dialog stays open until you call `dialog.close()`.
151
+ The dialog stays open until you call `dialog.close()`, which unmounts the React tree and removes the `<dialog>` element. The content is rendered in its own React root, so it does not share context (providers) with the calling component.
115
152
 
116
153
  ## Type-safe processes and translation keys
117
154
 
@@ -140,6 +177,7 @@ With these in the compilation, keys, inputs and outputs are strictly checked and
140
177
  ## Notes for AI assistants
141
178
 
142
179
  - The complete typed API surface is in `dist/index.d.ts`; the readable implementation ships in `src/` (entry point `src/index.ts`).
143
- - `useCodenotch()` is a plain function, not a hook no hook rules apply.
180
+ - `useCodenotch()` is a real hook (hook rules apply; re-renders on language/theme change). In class components use `withCodenotch(Component)` and read `this.props.cn`; in event handlers, services, plain modules or effects use `getCodenotch()`. Never call `useCodenotch()` outside a function component / custom hook.
181
+ - The object returned by `useCodenotch()` changes when the environment changes: fine in `useMemo` deps, but in `useEffect` prefer `getCodenotch()` unless the effect should re-run on language/theme change.
144
182
  - Never hand-edit a project's `typings/i18n.d.ts` / `typings/process.d.ts`: they are regenerated by the Codenotch IDE.
145
- - Codenotch apps are React 16 + Tailwind CSS; CNQL is SELECT-only (writes go through BPMN processes via `startProcess`).
183
+ - Codenotch apps are React 19 + Tailwind CSS (automatic JSX runtime — no `import React` needed for JSX); CNQL is SELECT-only (writes go through BPMN processes via `startProcess`).
@@ -24,7 +24,7 @@ export default class CodeEditor extends React.Component<ICodeEditorProps> {
24
24
  private isIframeReady;
25
25
  private onChangeTimeoutHandle;
26
26
  private onMessageHandler;
27
- refFrame: React.RefObject<HTMLIFrameElement>;
27
+ refFrame: React.RefObject<HTMLIFrameElement | null>;
28
28
  constructor(props: ICodeEditorProps);
29
29
  setValue(value: string): void;
30
30
  componentDidMount(): void;
@@ -1 +1 @@
1
- {"version":3,"file":"CodeEditor.d.ts","sourceRoot":"","sources":["../../src/components/CodeEditor.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAE,aAAa,EAAE,MAAM,IAAI,CAAC;AAEnC,UAAU,gBAAgB;IACtB,qFAAqF;IACrF,GAAG,CAAC,EAAE,aAAa,CAAC;IACpB,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,kCAAkC;IAClC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,8FAA8F;IAC9F,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,kEAAkE;IAClE,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACtC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,OAAO,OAAO,UAAW,SAAQ,KAAK,CAAC,SAAS,CAAC,gBAAgB,CAAC;IACrE,OAAO,CAAC,aAAa,CAAS;IAC9B,OAAO,CAAC,qBAAqB,CAAa;IAC1C,OAAO,CAAC,gBAAgB,CAAM;IAC9B,QAAQ,qCAAwC;gBAEpC,KAAK,EAAE,gBAAgB;IAKnC,QAAQ,CAAC,KAAK,EAAE,MAAM;IAStB,iBAAiB,IAAI,IAAI;IAKzB,oBAAoB,IAAI,IAAI;IAI5B,kBAAkB,CAAC,SAAS,EAAE,gBAAgB,GAAG,IAAI;IAoBrD,OAAO,CAAC,aAAa;IAgCrB,OAAO,CAAC,YAAY;IAMpB,OAAO,CAAC,WAAW;IA4EnB,MAAM;CAeT"}
1
+ {"version":3,"file":"CodeEditor.d.ts","sourceRoot":"","sources":["../../src/components/CodeEditor.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAE,aAAa,EAAE,MAAM,IAAI,CAAC;AAEnC,UAAU,gBAAgB;IACtB,qFAAqF;IACrF,GAAG,CAAC,EAAE,aAAa,CAAC;IACpB,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,kCAAkC;IAClC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,8FAA8F;IAC9F,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,kEAAkE;IAClE,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACtC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,OAAO,OAAO,UAAW,SAAQ,KAAK,CAAC,SAAS,CAAC,gBAAgB,CAAC;IACrE,OAAO,CAAC,aAAa,CAAS;IAC9B,OAAO,CAAC,qBAAqB,CAAa;IAC1C,OAAO,CAAC,gBAAgB,CAAM;IAC9B,QAAQ,4CAAwC;gBAEpC,KAAK,EAAE,gBAAgB;IAKnC,QAAQ,CAAC,KAAK,EAAE,MAAM;IAStB,iBAAiB,IAAI,IAAI;IAKzB,oBAAoB,IAAI,IAAI;IAI5B,kBAAkB,CAAC,SAAS,EAAE,gBAAgB,GAAG,IAAI;IAoBrD,OAAO,CAAC,aAAa;IAgCrB,OAAO,CAAC,YAAY;IAMpB,OAAO,CAAC,WAAW;IA4EnB,MAAM;CAgBT"}
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ const jsx_runtime_1 = require("react/jsx-runtime");
6
7
  const react_1 = __importDefault(require("react"));
7
8
  /**
8
9
  * Light Monaco code editor embedded in a sandboxed iframe
@@ -92,68 +93,68 @@ class CodeEditor extends react_1.default.Component {
92
93
  if (!iframe)
93
94
  return;
94
95
  const vsBase = 'https://cdn.jsdelivr.net/npm/monaco-editor@0.55.1/min/vs';
95
- const html = `<!DOCTYPE html>
96
- <html>
97
- <head>
98
- <meta charset="utf-8" />
99
- <meta name="viewport" content="width=device-width, initial-scale=1" />
100
- <style>
101
- html, body, #container { height: 100%; width: 100%; margin: 0; padding: 0; overflow: hidden; }
102
- </style>
103
- <script>
104
- (function(){
105
- var vsBase = ${JSON.stringify(vsBase)};
106
- // AMD loader
107
- var s = document.createElement('script');
108
- s.src = vsBase + '/loader.js';
109
- s.onload = function(){
110
- // Configure and load monaco
111
- window.require.config({ paths: { vs: vsBase } });
112
- window.require(['vs/editor/editor.main'], function(){
113
- var editor;
114
-
115
- function post(msg){ parent.postMessage(Object.assign({__from:'MonacoIframe'}, msg), '*'); }
116
-
117
- function ensureEditor(){
118
- if (editor) return editor;
119
- editor = monaco.editor.create(document.getElementById('container'), {
120
- value: '',
121
- language: 'plaintext',
122
- theme: 'vs',
123
- automaticLayout: true,
124
- minimap: { enabled: false },
125
- scrollBeyondLastLine: true,
126
- wordWrap: "on"
127
- });
128
- editor.onDidChangeModelContent(function(){
129
- var v = editor.getValue();
130
- post({ type: 'change', value: v });
131
- });
132
- return editor;
133
- }
134
-
135
- window.addEventListener('message', function(ev){
136
- var data = ev.data || {}; if (data.__from !== 'MonacoIframe') return;
137
- if (data.type === 'init' || data.type === 'update'){
138
- var ed = ensureEditor();
139
- if (typeof data.value === 'string' && ed.getValue() !== data.value){ ed.setValue(data.value); }
140
- if (typeof data.language === 'string') { monaco.editor.setModelLanguage(ed.getModel(), data.language); }
141
- if (typeof data.theme === 'string') { monaco.editor.setTheme(data.theme); }
142
- if (typeof data.readOnly === 'boolean') { ed.updateOptions({ readOnly: data.readOnly }); }
143
- }
144
- });
145
-
146
- // Signal ready after monaco is loaded
147
- post({ type: 'ready' });
148
- });
149
- };
150
- document.head.appendChild(s);
151
- })();
152
- </script>
153
- </head>
154
- <body>
155
- <div id="container"></div>
156
- </body>
96
+ const html = `<!DOCTYPE html>
97
+ <html>
98
+ <head>
99
+ <meta charset="utf-8" />
100
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
101
+ <style>
102
+ html, body, #container { height: 100%; width: 100%; margin: 0; padding: 0; overflow: hidden; }
103
+ </style>
104
+ <script>
105
+ (function(){
106
+ var vsBase = ${JSON.stringify(vsBase)};
107
+ // AMD loader
108
+ var s = document.createElement('script');
109
+ s.src = vsBase + '/loader.js';
110
+ s.onload = function(){
111
+ // Configure and load monaco
112
+ window.require.config({ paths: { vs: vsBase } });
113
+ window.require(['vs/editor/editor.main'], function(){
114
+ var editor;
115
+
116
+ function post(msg){ parent.postMessage(Object.assign({__from:'MonacoIframe'}, msg), '*'); }
117
+
118
+ function ensureEditor(){
119
+ if (editor) return editor;
120
+ editor = monaco.editor.create(document.getElementById('container'), {
121
+ value: '',
122
+ language: 'plaintext',
123
+ theme: 'vs',
124
+ automaticLayout: true,
125
+ minimap: { enabled: false },
126
+ scrollBeyondLastLine: true,
127
+ wordWrap: "on"
128
+ });
129
+ editor.onDidChangeModelContent(function(){
130
+ var v = editor.getValue();
131
+ post({ type: 'change', value: v });
132
+ });
133
+ return editor;
134
+ }
135
+
136
+ window.addEventListener('message', function(ev){
137
+ var data = ev.data || {}; if (data.__from !== 'MonacoIframe') return;
138
+ if (data.type === 'init' || data.type === 'update'){
139
+ var ed = ensureEditor();
140
+ if (typeof data.value === 'string' && ed.getValue() !== data.value){ ed.setValue(data.value); }
141
+ if (typeof data.language === 'string') { monaco.editor.setModelLanguage(ed.getModel(), data.language); }
142
+ if (typeof data.theme === 'string') { monaco.editor.setTheme(data.theme); }
143
+ if (typeof data.readOnly === 'boolean') { ed.updateOptions({ readOnly: data.readOnly }); }
144
+ }
145
+ });
146
+
147
+ // Signal ready after monaco is loaded
148
+ post({ type: 'ready' });
149
+ });
150
+ };
151
+ document.head.appendChild(s);
152
+ })();
153
+ </script>
154
+ </head>
155
+ <body>
156
+ <div id="container"></div>
157
+ </body>
157
158
  </html>`;
158
159
  const doc = iframe.contentWindow?.document;
159
160
  if (!doc)
@@ -163,12 +164,13 @@ class CodeEditor extends react_1.default.Component {
163
164
  doc.close();
164
165
  }
165
166
  render() {
166
- let style = this.props.style || {};
167
+ // Never mutate props: copy the style object before applying defaults.
168
+ const style = { ...this.props.style };
167
169
  style.width = style.width || '100%';
168
170
  style.height = style.height || '100%';
169
171
  style.border = style.border || '0';
170
172
  style.outline = style.outline || '0';
171
- return react_1.default.createElement("iframe", { style: style, title: "Codenotch", ref: this.refFrame, className: this.props.className, sandbox: "allow-scripts allow-same-origin" });
173
+ return (0, jsx_runtime_1.jsx)("iframe", { style: style, title: "Codenotch", ref: this.refFrame, className: this.props.className, sandbox: "allow-scripts allow-same-origin" });
172
174
  }
173
175
  }
174
176
  exports.default = CodeEditor;
package/dist/index.d.ts CHANGED
@@ -1,4 +1,6 @@
1
- import { ICodenotchApi, ICodenotchEnv } from "./models/Codenotch";
1
+ import { type ComponentType, type ComponentProps, type FunctionComponent, type RefAttributes } from "react";
2
+ import { IDisposable } from "./models/Misc";
3
+ import { ICodenotchApi, ICodenotchEnv, WithCodenotchProps } from "./models/Codenotch";
2
4
  import CodeEditor from "./components/CodeEditor";
3
5
  /**
4
6
  * The Codenotch runtime environment, populated by {@link init}.
@@ -35,24 +37,99 @@ declare const env: ICodenotchEnv;
35
37
  */
36
38
  declare function init(envVariables: any): void;
37
39
  /**
38
- * Return the Codenotch client API bound to the current environment.
40
+ * Return the Codenotch client API bound to the current environment, from
41
+ * anywhere: event handlers, plain modules, class components, services…
39
42
  *
40
- * Despite its name this is NOT a React hook it is a plain function with no
41
- * hook rules attached: it can be called anywhere (components, handlers, plain
42
- * modules). `init()` must have been called first, which the Codenotch runtime
43
- * does automatically when serving the application.
43
+ * This is a plain function (not a hook). The returned object is never stale
44
+ * its methods always read the live environment but it does not trigger any
45
+ * re-render when the language or theme changes: inside React components prefer
46
+ * {@link useCodenotch} (function components) or {@link withCodenotch}
47
+ * (class components), which do.
48
+ *
49
+ * `init()` must have been called first, which the Codenotch runtime does
50
+ * automatically when serving the application.
51
+ *
52
+ * @example
53
+ * import { getCodenotch } from 'codenotch-react';
54
+ *
55
+ * export async function loadTodos(userId: string) {
56
+ * const result = await getCodenotch().startProcess('getTodos', 'start', { UserId: userId });
57
+ * return result.output.todos;
58
+ * }
59
+ */
60
+ declare function getCodenotch(): ICodenotchApi;
61
+ /**
62
+ * React hook returning the Codenotch client API.
63
+ *
64
+ * The reference is stable across renders and only changes when the environment
65
+ * changes (`setLanguage`, `setTheme`, `init`), in which case every component
66
+ * using the hook re-renders — so `cn.i18n(...)` output follows the current
67
+ * language automatically, and the object is safe to use in `useMemo` /
68
+ * `useEffect` dependency arrays.
69
+ *
70
+ * Regular hook rules apply (call it unconditionally at the top of a function
71
+ * component or a custom hook). Outside of components — handlers defined in
72
+ * plain modules, services, class components — use {@link getCodenotch} or
73
+ * {@link withCodenotch} instead.
44
74
  *
45
75
  * @returns The Codenotch API: BPMN processes, CNQL queries, i18n, signals, theme…
46
76
  * @example
47
77
  * import { useCodenotch } from 'codenotch-react';
48
78
  *
49
- * const MyApp: React.FC = () => {
79
+ * const MyApp = () => {
50
80
  * const cn = useCodenotch();
51
81
  * return <h1>{cn.i18n('welcome')}</h1>;
52
82
  * };
53
83
  */
54
84
  declare function useCodenotch(): ICodenotchApi;
55
- export { env, useCodenotch, init, CodeEditor };
85
+ /**
86
+ * Register a listener called each time the Codenotch environment changes
87
+ * (`setLanguage`, `setTheme`, `init`). Escape hatch for code that cannot use
88
+ * {@link useCodenotch} or {@link withCodenotch}, e.g. a class component that
89
+ * wants to `forceUpdate()` itself, or a non-React module caching translations.
90
+ *
91
+ * @returns A disposable — call `dispose()` to stop listening.
92
+ * @example
93
+ * componentDidMount() {
94
+ * this.sub = onCodenotchChange(() => this.forceUpdate());
95
+ * }
96
+ * componentWillUnmount() {
97
+ * this.sub.dispose();
98
+ * }
99
+ */
100
+ declare function onCodenotchChange(listener: () => void): IDisposable;
101
+ /** Props of a component wrapped by {@link withCodenotch}, without the injected `cn`. */
102
+ type WithoutCodenotchProps<P> = Omit<P, keyof WithCodenotchProps>;
103
+ /** Instance type of a component (class components only), used to type the forwarded `ref`. */
104
+ type ComponentInstance<C> = C extends new (...args: any[]) => infer I ? I : never;
105
+ /**
106
+ * Higher-order component injecting the Codenotch API as a `cn` prop.
107
+ *
108
+ * Meant for class components, which cannot call {@link useCodenotch}: the
109
+ * wrapped component receives `this.props.cn` and re-renders whenever the
110
+ * environment changes (language, theme…), exactly like the hook. A `ref`
111
+ * passed to the wrapper is forwarded to the wrapped component instance.
112
+ *
113
+ * @param Component A component whose props extend {@link WithCodenotchProps}.
114
+ * @returns A component with the same props minus `cn`.
115
+ * @example
116
+ * import { withCodenotch, WithCodenotchProps } from 'codenotch-react';
117
+ *
118
+ * interface Props extends WithCodenotchProps {
119
+ * userId: string;
120
+ * }
121
+ *
122
+ * class TodoList extends React.Component<Props> {
123
+ * render() {
124
+ * return <h1>{this.props.cn.i18n('todos.title')}</h1>;
125
+ * }
126
+ * }
127
+ *
128
+ * export default withCodenotch(TodoList);
129
+ * // <TodoList userId="42" /> — `cn` is injected
130
+ */
131
+ declare function withCodenotch<C extends ComponentType<any>>(Component: C): FunctionComponent<WithoutCodenotchProps<ComponentProps<C>> & RefAttributes<ComponentInstance<C>>>;
132
+ export { env, useCodenotch, getCodenotch, withCodenotch, onCodenotchChange, init, CodeEditor };
56
133
  export * from "./models/Codenotch";
57
134
  export * from "./models/Misc";
58
135
  export * from "@codenotch/codenotch.core";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,aAAa,EAAoB,aAAa,EAAkB,MAAM,oBAAoB,CAAC;AAEpG,OAAO,UAAU,MAAM,yBAAyB,CAAC;AAIjD;;;GAGG;AACH,QAAA,MAAM,GAAG,EAAE,aAAkB,CAAC;AAM9B;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,iBAAS,IAAI,CAAC,YAAY,EAAE,GAAG,GAAG,IAAI,CAyFrC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,iBAAS,YAAY,IAAI,aAAa,CAsNrC;AAED,OAAO,EACH,GAAG,EACH,YAAY,EACZ,IAAI,EACJ,UAAU,EACb,CAAC;AAMF,cAAc,oBAAoB,CAAC;AACnC,cAAc,eAAe,CAAC;AAC9B,cAAc,2BAA2B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAuC,KAAK,aAAa,EAAE,KAAK,cAAc,EAAE,KAAK,iBAAiB,EAAkB,KAAK,aAAa,EAAE,MAAM,OAAO,CAAC;AAKjK,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAoB,aAAa,EAAkB,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAExH,OAAO,UAAU,MAAM,yBAAyB,CAAC;AAIjD;;;GAGG;AACH,QAAA,MAAM,GAAG,EAAE,aAAkB,CAAC;AA6B9B;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,iBAAS,IAAI,CAAC,YAAY,EAAE,GAAG,GAAG,IAAI,CA2FrC;AA2OD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,iBAAS,YAAY,IAAI,aAAa,CAErC;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,iBAAS,YAAY,IAAI,aAAa,CAErC;AAED;;;;;;;;;;;;;;GAcG;AACH,iBAAS,iBAAiB,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,WAAW,CAG5D;AAED,wFAAwF;AACxF,KAAK,qBAAqB,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,EAAE,MAAM,kBAAkB,CAAC,CAAC;AAElE,8FAA8F;AAC9F,KAAK,iBAAiB,CAAC,CAAC,IAAI,CAAC,SAAS,KAAK,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,MAAM,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;AAElF;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,iBAAS,aAAa,CAAC,CAAC,SAAS,aAAa,CAAC,GAAG,CAAC,EAC/C,SAAS,EAAE,CAAC,GACb,iBAAiB,CAAC,qBAAqB,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,GAAG,aAAa,CAAC,iBAAiB,CAAC,CAAC,CAAC,CAAC,CAAC,CAQnG;AAED,OAAO,EACH,GAAG,EACH,YAAY,EACZ,YAAY,EACZ,aAAa,EACb,iBAAiB,EACjB,IAAI,EACJ,UAAU,EACb,CAAC;AAMF,cAAc,oBAAoB,CAAC;AACnC,cAAc,eAAe,CAAC;AAC9B,cAAc,2BAA2B,CAAC"}
package/dist/index.js CHANGED
@@ -19,8 +19,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
19
19
  Object.defineProperty(exports, "__esModule", { value: true });
20
20
  exports.CodeEditor = exports.env = void 0;
21
21
  exports.useCodenotch = useCodenotch;
22
+ exports.getCodenotch = getCodenotch;
23
+ exports.withCodenotch = withCodenotch;
24
+ exports.onCodenotchChange = onCodenotchChange;
22
25
  exports.init = init;
23
- const react_dom_1 = __importDefault(require("react-dom"));
26
+ const react_1 = require("react");
27
+ const react_dom_1 = require("react-dom");
28
+ const client_1 = require("react-dom/client");
24
29
  const SignalR_1 = require("./core/SignalR");
25
30
  const ProcessUtils_1 = __importDefault(require("./core/ProcessUtils"));
26
31
  const uuid_1 = require("uuid");
@@ -36,6 +41,30 @@ exports.env = env;
36
41
  const MISSING_CLUSTER_URL_ERROR = "Codenotch cluster URL is not defined. Please set it in the Codenotch configuration.";
37
42
  const MISSING_SERVICE_NAME_ERROR = "Codenotch service name is not defined. Please set it in the Codenotch configuration.";
38
43
  const MISSING_TENANT_NAME_ERROR = "Codenotch tenant name is not defined. Please set it in the Codenotch configuration.";
44
+ // ---------------------------------------------------------------------------
45
+ // Change tracking. `env` is a plain mutable object; every mutation done through
46
+ // the public API (init, setTheme, setLanguage) goes through notify(), which
47
+ // rotates the API object (so React sees a new reference) and wakes up the
48
+ // subscribers: useCodenotch() via useSyncExternalStore, withCodenotch(),
49
+ // and any listener registered with onCodenotchChange().
50
+ // ---------------------------------------------------------------------------
51
+ const listeners = new Set();
52
+ let currentApi;
53
+ function notify() {
54
+ currentApi = createApi();
55
+ listeners.forEach((listener) => {
56
+ try {
57
+ listener();
58
+ }
59
+ catch (error) {
60
+ console.error("Error in Codenotch change listener:", error);
61
+ }
62
+ });
63
+ }
64
+ function subscribe(listener) {
65
+ listeners.add(listener);
66
+ return () => { listeners.delete(listener); };
67
+ }
39
68
  /**
40
69
  * Initialize the Codenotch environment from the given key-value pairs.
41
70
  *
@@ -141,26 +170,15 @@ function init(envVariables) {
141
170
  }
142
171
  catch {
143
172
  }
173
+ notify();
144
174
  }
145
175
  /**
146
- * Return the Codenotch client API bound to the current environment.
147
- *
148
- * Despite its name this is NOT a React hook it is a plain function with no
149
- * hook rules attached: it can be called anywhere (components, handlers, plain
150
- * modules). `init()` must have been called first, which the Codenotch runtime
151
- * does automatically when serving the application.
152
- *
153
- * @returns The Codenotch API: BPMN processes, CNQL queries, i18n, signals, theme…
154
- * @example
155
- * import { useCodenotch } from 'codenotch-react';
156
- *
157
- * const MyApp: React.FC = () => {
158
- * const cn = useCodenotch();
159
- * return <h1>{cn.i18n('welcome')}</h1>;
160
- * };
176
+ * Build the API object bound to {@link env}. Every method reads `env` lazily,
177
+ * so an instance never goes stale; a new one is created by {@link notify} only
178
+ * to give React a fresh reference when the environment changes.
161
179
  */
162
- function useCodenotch() {
163
- return {
180
+ function createApi() {
181
+ const api = {
164
182
  env: env,
165
183
  uuid: () => (0, uuid_1.v4)(),
166
184
  getProjectFileUrl: (relativePath) => {
@@ -175,7 +193,7 @@ function useCodenotch() {
175
193
  return url;
176
194
  },
177
195
  getProjectFile: async (relativePath) => {
178
- let url = useCodenotch().getProjectFileUrl(relativePath);
196
+ let url = api.getProjectFileUrl(relativePath);
179
197
  let resp = await fetch(url);
180
198
  if (resp.status.toString().startsWith('2') === false) {
181
199
  throw new Error(`Failed to fetch file content: ${resp.status} ${resp.statusText}`);
@@ -266,9 +284,14 @@ function useCodenotch() {
266
284
  },
267
285
  showDialog: (node) => {
268
286
  let dialogId = "codenotch-dialog-" + Math.random().toString(36).substring(2, 9);
287
+ let root = undefined;
269
288
  let result = {
270
289
  id: dialogId,
271
290
  close: () => {
291
+ // Unmount the React tree first so effects are cleaned up and nothing leaks,
292
+ // then drop the <dialog> element itself.
293
+ root?.unmount();
294
+ root = undefined;
272
295
  let dialogElement = document.getElementById(dialogId);
273
296
  if (dialogElement) {
274
297
  dialogElement.remove();
@@ -311,9 +334,11 @@ function useCodenotch() {
311
334
  dialogElement.style.background = '#78787822';
312
335
  dialogElement.style.backdropFilter = 'blur(2px)';
313
336
  container.appendChild(dialogElement);
314
- react_dom_1.default.render(node, dialogElement, () => {
315
- dialogElement.showModal();
316
- });
337
+ // React 19: ReactDOM.render() is gone. Render synchronously through a dedicated root
338
+ // so the content is in the DOM before showModal() moves the focus into the dialog.
339
+ root = (0, client_1.createRoot)(dialogElement);
340
+ (0, react_dom_1.flushSync)(() => root.render(node));
341
+ dialogElement.showModal();
317
342
  return result;
318
343
  },
319
344
  listenSignal: async (signalId, callback) => {
@@ -334,11 +359,13 @@ function useCodenotch() {
334
359
  document.documentElement.classList.remove(env.theme);
335
360
  env.theme = theme;
336
361
  document.documentElement.classList.add(theme);
362
+ notify();
337
363
  },
338
364
  setLanguage: (lang) => {
339
365
  if (env.language === lang)
340
366
  return;
341
367
  env.language = lang;
368
+ notify();
342
369
  },
343
370
  getLanguages: () => {
344
371
  return env.projectManifest?.languages ?? [];
@@ -359,6 +386,112 @@ function useCodenotch() {
359
386
  return env.appManifest;
360
387
  }
361
388
  };
389
+ return api;
390
+ }
391
+ currentApi = createApi();
392
+ /**
393
+ * Return the Codenotch client API bound to the current environment, from
394
+ * anywhere: event handlers, plain modules, class components, services…
395
+ *
396
+ * This is a plain function (not a hook). The returned object is never stale —
397
+ * its methods always read the live environment — but it does not trigger any
398
+ * re-render when the language or theme changes: inside React components prefer
399
+ * {@link useCodenotch} (function components) or {@link withCodenotch}
400
+ * (class components), which do.
401
+ *
402
+ * `init()` must have been called first, which the Codenotch runtime does
403
+ * automatically when serving the application.
404
+ *
405
+ * @example
406
+ * import { getCodenotch } from 'codenotch-react';
407
+ *
408
+ * export async function loadTodos(userId: string) {
409
+ * const result = await getCodenotch().startProcess('getTodos', 'start', { UserId: userId });
410
+ * return result.output.todos;
411
+ * }
412
+ */
413
+ function getCodenotch() {
414
+ return currentApi;
415
+ }
416
+ /**
417
+ * React hook returning the Codenotch client API.
418
+ *
419
+ * The reference is stable across renders and only changes when the environment
420
+ * changes (`setLanguage`, `setTheme`, `init`), in which case every component
421
+ * using the hook re-renders — so `cn.i18n(...)` output follows the current
422
+ * language automatically, and the object is safe to use in `useMemo` /
423
+ * `useEffect` dependency arrays.
424
+ *
425
+ * Regular hook rules apply (call it unconditionally at the top of a function
426
+ * component or a custom hook). Outside of components — handlers defined in
427
+ * plain modules, services, class components — use {@link getCodenotch} or
428
+ * {@link withCodenotch} instead.
429
+ *
430
+ * @returns The Codenotch API: BPMN processes, CNQL queries, i18n, signals, theme…
431
+ * @example
432
+ * import { useCodenotch } from 'codenotch-react';
433
+ *
434
+ * const MyApp = () => {
435
+ * const cn = useCodenotch();
436
+ * return <h1>{cn.i18n('welcome')}</h1>;
437
+ * };
438
+ */
439
+ function useCodenotch() {
440
+ return (0, react_1.useSyncExternalStore)(subscribe, getCodenotch, getCodenotch);
441
+ }
442
+ /**
443
+ * Register a listener called each time the Codenotch environment changes
444
+ * (`setLanguage`, `setTheme`, `init`). Escape hatch for code that cannot use
445
+ * {@link useCodenotch} or {@link withCodenotch}, e.g. a class component that
446
+ * wants to `forceUpdate()` itself, or a non-React module caching translations.
447
+ *
448
+ * @returns A disposable — call `dispose()` to stop listening.
449
+ * @example
450
+ * componentDidMount() {
451
+ * this.sub = onCodenotchChange(() => this.forceUpdate());
452
+ * }
453
+ * componentWillUnmount() {
454
+ * this.sub.dispose();
455
+ * }
456
+ */
457
+ function onCodenotchChange(listener) {
458
+ const unsubscribe = subscribe(listener);
459
+ return { dispose: unsubscribe };
460
+ }
461
+ /**
462
+ * Higher-order component injecting the Codenotch API as a `cn` prop.
463
+ *
464
+ * Meant for class components, which cannot call {@link useCodenotch}: the
465
+ * wrapped component receives `this.props.cn` and re-renders whenever the
466
+ * environment changes (language, theme…), exactly like the hook. A `ref`
467
+ * passed to the wrapper is forwarded to the wrapped component instance.
468
+ *
469
+ * @param Component A component whose props extend {@link WithCodenotchProps}.
470
+ * @returns A component with the same props minus `cn`.
471
+ * @example
472
+ * import { withCodenotch, WithCodenotchProps } from 'codenotch-react';
473
+ *
474
+ * interface Props extends WithCodenotchProps {
475
+ * userId: string;
476
+ * }
477
+ *
478
+ * class TodoList extends React.Component<Props> {
479
+ * render() {
480
+ * return <h1>{this.props.cn.i18n('todos.title')}</h1>;
481
+ * }
482
+ * }
483
+ *
484
+ * export default withCodenotch(TodoList);
485
+ * // <TodoList userId="42" /> — `cn` is injected
486
+ */
487
+ function withCodenotch(Component) {
488
+ const Wrapped = (props) => {
489
+ const cn = useCodenotch();
490
+ // React 19: `ref` is a regular prop, so spreading `props` forwards it.
491
+ return (0, react_1.createElement)(Component, { ...props, cn });
492
+ };
493
+ Wrapped.displayName = `withCodenotch(${Component.displayName || Component.name || "Component"})`;
494
+ return Wrapped;
362
495
  }
363
496
  // Public models. Re-exported from the package root so that the typings
364
497
  // generated by the Codenotch IDE (typings/i18n.d.ts and typings/process.d.ts,