neo.mjs 10.3.0 → 10.3.2

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.
@@ -33,7 +33,9 @@ This new feature is built on a core Neo.mjs principle: **a commitment to a zero-
33
33
  To achieve both developer convenience and production performance, the framework uses a sophisticated dual-mode approach:
34
34
 
35
35
  1. **Development Mode:** Templates are parsed live in the browser using `parse5`. This parser is **only loaded if templates are actually used**, ensuring zero overhead for applications that stick to the JSON VDOM.
36
- 2. **Production Mode:** For `dist/esm`, `dist/dev`, and `dist/prod` builds, a powerful **build-time AST transformation** converts templates directly into optimized VDOM objects. This eliminates the `parse5` dependency from production builds entirely, resulting in **zero runtime parsing overhead** and maximum performance.
36
+ 2. **Production Mode:** For `dist/esm`, `dist/dev`, and `dist/prod` builds, a powerful **build-time AST transformation** converts templates directly into optimized VDOM objects. This eliminates the `parse5` dependency from production builds entirely, resulting in **zero runtime parsing overhead** and maximum performance. The `enableHtmlTemplates` flag is also automatically set to `false` in production environments, ensuring that the runtime parser is never accidentally used.
37
+
38
+ As a developer convenience, if you name your template-generating method `render()`, the build process will automatically rename it to `createVdom()` for you, aligning with the framework's lifecycle methods while providing a familiar entry point.
37
39
 
38
40
  ## Foundational Enhancements
39
41
 
@@ -48,7 +50,22 @@ The introduction of HTML templates was made possible by a series of significant
48
50
 
49
51
  To support this major new feature, we have added two comprehensive guides:
50
52
 
51
- - [Using HTML Templates](../learn/guides/uibuildingblocks/HtmlTemplates.md): A guide focused on the syntax, features, and best practices for using the new template system.
52
- - [Under the Hood: HTML Templates](../learn/guides/uibuildingblocks/HtmlTemplatesUnderTheHood.md): A deep dive into the philosophy and the dual-mode architecture, explaining how templates work in both development and production environments.
53
+ - [Using HTML Templates](https://github.com/neomjs/neo/blob/dev/learn/guides/uibuildingblocks/HtmlTemplates.md): A guide focused on the syntax, features, and best practices for using the new template system.
54
+ - [Under the Hood: HTML Templates](https://github.com/neomjs/neo/blob/dev/learn/guides/uibuildingblocks/HtmlTemplatesUnderTheHood.md): A deep dive into the philosophy and the dual-mode architecture, explaining how templates work in both development and production environments.
55
+
56
+ All changes combined into 1 commit: https://github.com/neomjs/neo/commit/0841e7e1a715c7afe67d07eebb8229e54828eedd
53
57
 
54
58
  We are incredibly excited about this release and believe it makes Neo.mjs more powerful and accessible than ever. Please explore the new feature, read the documentation, and share your feedback!
59
+
60
+ Try it out by yourself:</br>
61
+ https://neomjs.com/examples/functional/nestedTemplateComponent/
62
+
63
+ <img width="1603" height="1292" alt="Screenshot 2025-08-02 at 17 01 25" src="https://github.com/user-attachments/assets/79cb4aad-b791-4b03-bf25-7147bbeeed39" />
64
+
65
+ https://neomjs.com/dist/esm/examples/functional/nestedTemplateComponent/
66
+
67
+ <img width="1606" height="1062" alt="Screenshot 2025-08-02 at 17 00 12" src="https://github.com/user-attachments/assets/7a21638f-cb68-4f28-ba04-6ebaccb3b3f3" />
68
+
69
+ https://neomjs.com/dist/development/examples/functional/nestedTemplateComponent/
70
+
71
+ <img width="1602" height="1179" alt="Screenshot 2025-08-02 at 17 06 24" src="https://github.com/user-attachments/assets/2c1d3ef7-2008-41a1-9025-2859d907ce2d" />
@@ -0,0 +1,14 @@
1
+ # Neo.mjs v10.3.1: Build Process Reliability
2
+
3
+ This is a patch release focused on improving the stability and reliability of the core build process.
4
+
5
+ ## Key Fix: Build Process Dependency Order
6
+
7
+ ### The Problem
8
+ The main `buildAll.mjs` script could fail on a clean repository checkout. The build steps that process HTML templates (e.g., `buildESModules`) have a dependency on the `dist/parse5.mjs` bundle. However, the script did not guarantee that this bundle was created before it was needed, leading to a potential crash.
9
+
10
+ ### The Solution
11
+ We have updated `buildAll.mjs` to explicitly run the `bundleParse5.mjs` script as one of its first actions, immediately after the optional `npm install`.
12
+
13
+ ### The Impact
14
+ This change ensures the `parse5` dependency is always available for subsequent build tasks. It makes the build process more robust and predictable, particularly for developers setting up the project for the first time.
@@ -0,0 +1,10 @@
1
+ # v10.3.2 Hotfix Release
2
+
3
+ This hotfix addresses an issue where changing the `tabBarPosition` config for `tab.Container` did not correctly update the UI. Additionally, a broader refactoring was applied to improve component lifecycle management.
4
+
5
+ ## Fixes
6
+
7
+ - **tab.Container:** Resolved an issue where switching `tabBarPosition` did not correctly update the UI. The `tabBar` component's `dock` config now properly reflects the new position, ensuring correct rendering. This fix involved adding `tabBarPosition` to the `config` object, and implementing `afterSetTabBarPosition` and `beforeSetTabBarPosition` methods to manage UI updates and component creation.
8
+ - **Component Lifecycle:** Replaced `this.rendered` with `this.vnodeInitialized` across various components (`Circle`, `Gallery`, `Helix`, `Dialog`, `Text` field, `GridBody`, `Flexbox`, `List`, `TableBody`). This change refines the component lifecycle management, ensuring that UI updates and other operations are performed when the virtual DOM node is properly initialized.
9
+ - **Font Awesome v7** Adjusted 3 spots inside our theming, where v6 was still in use.
10
+ - **Earthquakes Tutorial** Fixed a malformed code closing tag, added a warning that the labs are currently not working.
package/ServiceWorker.mjs CHANGED
@@ -20,9 +20,9 @@ class ServiceWorker extends ServiceBase {
20
20
  */
21
21
  singleton: true,
22
22
  /**
23
- * @member {String} version='10.3.0'
23
+ * @member {String} version='10.3.2'
24
24
  */
25
- version: '10.3.0'
25
+ version: '10.3.2'
26
26
  }
27
27
 
28
28
  /**
@@ -16,7 +16,7 @@
16
16
  "@type": "Organization",
17
17
  "name": "Neo.mjs"
18
18
  },
19
- "datePublished": "2025-08-02",
19
+ "datePublished": "2025-08-03",
20
20
  "publisher": {
21
21
  "@type": "Organization",
22
22
  "name": "Neo.mjs"
@@ -108,7 +108,7 @@ class FooterContainer extends Container {
108
108
  }, {
109
109
  module: Component,
110
110
  cls : ['neo-version'],
111
- text : 'v10.3.0'
111
+ text : 'v10.3.2'
112
112
  }]
113
113
  }],
114
114
  /**
@@ -129,6 +129,10 @@ if (programOpts.info) {
129
129
  childProcess.status && process.exit(childProcess.status);
130
130
  }
131
131
 
132
+ console.log(chalk.blue('Bundling parse5...'));
133
+ childProcess = spawnSync('node', [`${neoPath}/buildScripts/bundleParse5.mjs`], cpOpts);
134
+ childProcess.status && process.exit(childProcess.status);
135
+
132
136
  if (themes === 'yes') {
133
137
  childProcess = spawnSync('node', [`${neoPath}/buildScripts/buildThemes.mjs`].concat(cpArgs), cpOpts);
134
138
  childProcess.status && process.exit(childProcess.status);
@@ -31,7 +31,7 @@ export default defineComponent({
31
31
  },
32
32
 
33
33
  render(config) {
34
- const [isActive, setIsActive] = useConfig(true);
34
+ const [isActive, setIsActive] = useConfig(true);
35
35
  const [showDetails, setShowDetails] = useConfig(false);
36
36
 
37
37
  // This event listener is for the main container to toggle the active state
@@ -39,15 +39,15 @@ export default defineComponent({
39
39
  // Stop the event from bubbling up to avoid toggling the active state
40
40
  // when the button is clicked. The button has its own handler.
41
41
  if (event.target.id === 'details-button') {
42
- event.stopPropagation();
42
+ event.stopPropagation()
43
43
  } else {
44
- setIsActive(prev => !prev);
44
+ setIsActive(prev => !prev)
45
45
  }
46
46
  });
47
47
 
48
48
  // The idiomatic way to handle a button click is with the handler config.
49
49
  const onButtonClick = () => {
50
- setShowDetails(prev => !prev);
50
+ setShowDetails(prev => !prev)
51
51
  };
52
52
 
53
53
  const cardStyle = {
@@ -40,7 +40,7 @@ To parse HTML strings, we need an HTML parser. Neo.mjs uses `parse5`, a robust a
40
40
  This is why the parser is **only loaded if a component on the page actually uses an HTML template**. This check happens
41
41
  inside the `initAsync` method of `Neo.functional.component.Base`.
42
42
 
43
- ```javascript
43
+ ```javascript readonly
44
44
  // src/functional/component/Base.mjs
45
45
  async initAsync() {
46
46
  await super.initAsync();
@@ -61,7 +61,7 @@ If `enableHtmlTemplates` is true, the component dynamically imports the `HtmlTem
61
61
 
62
62
  When a component's `createVdom()` method returns an `HtmlTemplate` object, it's handed off to the `HtmlTemplateProcessor`.
63
63
  You can inspect its source code here:
64
- [src/functional/util/HtmlTemplateProcessor.mjs](../../../../src/functional/util/HtmlTemplateProcessor.mjs).
64
+ [src/functional/util/HtmlTemplateProcessor.mjs](../../../src/functional/util/HtmlTemplateProcessor.mjs).
65
65
 
66
66
  The processor executes a series of steps to convert the template literal into a VDOM object, which are detailed in the
67
67
  expandable section below.
@@ -93,9 +93,9 @@ parsing overhead**. This is accomplished with a powerful build-time AST (Abstrac
93
93
 
94
94
  This work is handled by two main scripts:
95
95
 
96
- - [buildScripts/util/templateBuildProcessor.mjs](../../../../buildScripts/util/templateBuildProcessor.mjs):
96
+ - [buildScripts/util/templateBuildProcessor.mjs](../../../buildScripts/util/templateBuildProcessor.mjs):
97
97
  Contains the core logic for parsing the template string and converting it to a serializable VDOM object.
98
- - [buildScripts/util/astTemplateProcessor.mjs](../../../../buildScripts/util/astTemplateProcessor.mjs):
98
+ - [buildScripts/util/astTemplateProcessor.mjs](../../../buildScripts/util/astTemplateProcessor.mjs):
99
99
  Orchestrates the overall process of reading a JS file, finding `html` templates, and replacing them with the final
100
100
  VDOM object via AST manipulation.
101
101
 
@@ -119,10 +119,10 @@ renames the method to `createVdom`.
119
119
 
120
120
  This logic is seamlessly integrated into all three of Neo.mjs's production build environments:
121
121
 
122
- - **`dist/esm`:** The [buildScripts/buildESModules.mjs](../../../../buildScripts/buildESModules.mjs) script directly
122
+ - **`dist/esm`:** The [buildScripts/buildESModules.mjs](../../../buildScripts/buildESModules.mjs) script directly
123
123
  invokes the `processFileContent` function from the `astTemplateProcessor` for each JavaScript file before minification.
124
124
  - **`dist/dev` & `dist/prod`:** These environments use Webpack. The transformation is handled by a custom loader:
125
- [buildScripts/webpack/loader/template-loader.mjs](../../../../buildScripts/webpack/loader/template-loader.mjs).
125
+ [buildScripts/webpack/loader/template-loader.mjs](../../../buildScripts/webpack/loader/template-loader.mjs).
126
126
  This loader is strategically applied **only to the App worker's build configuration**, an optimization that saves
127
127
  build time by not processing code for other workers.
128
128
 
@@ -5,6 +5,11 @@ and show the information in two views: a table, and a map.
5
5
 
6
6
  You'll do this in a series of labs.
7
7
 
8
+ <span style='color: red;'>
9
+ Important: The labs inside this Tutorial cannot run currently, since the Google Maps API changed
10
+ and our addon needs adjustments. The chapters still contain valuable input.
11
+ </span>
12
+
8
13
  ## Goals
9
14
 
10
15
  What are the goals of this lengthy topic?
@@ -167,7 +172,8 @@ If you look in `neo-config.json` you should see this content. Note the `mainThre
167
172
  "mainPath" : "../node_modules/neo.mjs/src/Main.mjs",
168
173
  "mainThreadAddons": ["DragDrop", "Navigator", "Stylesheet"],
169
174
  "workerBasePath" : "../../node_modules/neo.mjs/src/worker/"
170
- }```
175
+ }
176
+ ```
171
177
 
172
178
  You're free to edit `neo-config.json` if you were to change your mind later about the theme or need for other add-ons.
173
179
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neo.mjs",
3
- "version": "10.3.0",
3
+ "version": "10.3.2",
4
4
  "description": "Neo.mjs: The multi-threaded UI framework for building ultra-fast, desktop-like web applications with uncompromised responsiveness, inherent security, and a transpilation-free dev mode.",
5
5
  "type": "module",
6
6
  "repository": {
@@ -29,7 +29,6 @@
29
29
  "inject-package-version": "node ./buildScripts/injectPackageVersion.mjs",
30
30
  "server-start": "webpack serve -c ./buildScripts/webpack/webpack.server.config.mjs --open",
31
31
  "test": "echo \"Error: no test specified\" && exit 1",
32
- "build-single-file": "node ./buildScripts/buildSingleFile.mjs",
33
32
  "watch-themes": "node ./buildScripts/watchThemes.mjs"
34
33
  },
35
34
  "keywords": [
@@ -24,7 +24,7 @@
24
24
 
25
25
  .portal-profile-location {
26
26
  color : #777;
27
- font-family : "Font Awesome 6 Free";
27
+ font-family : "Font Awesome 7 Free";
28
28
  font-style : normal;
29
29
  font-weight : 600;
30
30
  margin-right: .5em;
@@ -53,7 +53,7 @@
53
53
 
54
54
  details summary::before {
55
55
  color : #c4c4c4;
56
- font-family: var(--fa-style-family, "Font Awesome 6 Free");
56
+ font-family: var(--fa-style-family, "Font Awesome 7 Free");
57
57
  font-size : 1em;
58
58
  font-weight: var(--fa-style, 900);
59
59
  margin : 0.4em 0.4em 0.4em 0;
@@ -44,7 +44,7 @@
44
44
  flex : 0 0 2rem;
45
45
  height : 2rem;
46
46
  border-radius : 50%;
47
- font-family : var(--fa-style-family,"Font Awesome 6 Free");
47
+ font-family : var(--fa-style-family,"Font Awesome 7 Free");
48
48
  display : flex;
49
49
  align-items : center;
50
50
  justify-content : center;
@@ -299,12 +299,12 @@ const DefaultConfig = {
299
299
  useVdomWorker: true,
300
300
  /**
301
301
  * buildScripts/injectPackageVersion.mjs will update this value
302
- * @default '10.3.0'
302
+ * @default '10.3.2'
303
303
  * @memberOf! module:Neo
304
304
  * @name config.version
305
305
  * @type String
306
306
  */
307
- version: '10.3.0'
307
+ version: '10.3.2'
308
308
  };
309
309
 
310
310
  Object.assign(DefaultConfig, {
@@ -248,7 +248,7 @@ class Circle extends Component {
248
248
  * @protected
249
249
  */
250
250
  afterSetMaxItems(value, oldValue) {
251
- if (oldValue && this.rendered) {
251
+ if (oldValue && this.vnodeInitialized) {
252
252
  let me = this,
253
253
  frontEl = me.getFrontEl();
254
254
 
@@ -289,7 +289,7 @@ class Circle extends Component {
289
289
  afterSetItemSize(value, oldValue) {
290
290
  let me = this;
291
291
 
292
- if (oldValue && me.rendered) {
292
+ if (oldValue && me.vnodeInitialized) {
293
293
  !me.collapsed && me.updateOuterCircle(true);
294
294
  me.updateItemPositions()
295
295
  }
@@ -302,7 +302,7 @@ class Circle extends Component {
302
302
  * @protected
303
303
  */
304
304
  afterSetRotateX(value, oldValue) {
305
- oldValue && this.rendered && this.rotate()
305
+ oldValue && this.vnodeInitialized && this.rotate()
306
306
  }
307
307
 
308
308
  /**
@@ -312,7 +312,7 @@ class Circle extends Component {
312
312
  * @protected
313
313
  */
314
314
  afterSetRotateY(value, oldValue) {
315
- oldValue && this.rendered && this.rotate()
315
+ oldValue && this.vnodeInitialized && this.rotate()
316
316
  }
317
317
 
318
318
  /**
@@ -322,7 +322,7 @@ class Circle extends Component {
322
322
  * @protected
323
323
  */
324
324
  afterSetRotateZ(value, oldValue) {
325
- oldValue && this.rendered && this.rotate()
325
+ oldValue && this.vnodeInitialized && this.rotate()
326
326
  }
327
327
 
328
328
  /**
@@ -344,7 +344,7 @@ class Circle extends Component {
344
344
  * @protected
345
345
  */
346
346
  afterSetSelectionModel(value, oldValue) {
347
- this.rendered && value.register(this);
347
+ this.vnodeInitialized && value.register(this);
348
348
  }
349
349
 
350
350
  /**
@@ -245,7 +245,7 @@ class Gallery extends Component {
245
245
  afterSetMaxItems(value, oldValue) {
246
246
  let me = this;
247
247
 
248
- if (value && me.rendered) {
248
+ if (value && me.vnodeInitialized) {
249
249
  if (oldValue > value) {
250
250
  me.destroyItems(value, oldValue - value)
251
251
  } else {
@@ -310,7 +310,7 @@ class Gallery extends Component {
310
310
  len = Math.min(me.maxItems, me.store.items.length),
311
311
  view = me.getItemsRoot();
312
312
 
313
- if (me.rendered) {
313
+ if (me.vnodeInitialized) {
314
314
  me.refreshImageReflection();
315
315
 
316
316
  me.timeout(50).then(() => {
@@ -338,7 +338,7 @@ class Gallery extends Component {
338
338
  */
339
339
  afterSetSelectionModel(value, oldValue) {
340
340
  oldValue?.destroy();
341
- this.rendered && value.register(this)
341
+ this.vnodeInitialized && value.register(this)
342
342
  }
343
343
 
344
344
  afterSetTranslateX() {this.moveOrigin()}
@@ -348,7 +348,7 @@ class Helix extends Component {
348
348
  afterSetMaxItems(value, oldValue) {
349
349
  let me = this;
350
350
 
351
- if (value && me.rendered) {
351
+ if (value && me.vnodeInitialized) {
352
352
  if (oldValue > value) {
353
353
  me.destroyItems(value, oldValue - value)
354
354
  } else {
@@ -399,7 +399,7 @@ class Helix extends Component {
399
399
  * @protected
400
400
  */
401
401
  afterSetSelectionModel(value, oldValue) {
402
- this.rendered && value.register(this)
402
+ this.vnodeInitialized && value.register(this)
403
403
  }
404
404
 
405
405
  /**
@@ -411,7 +411,7 @@ class Helix extends Component {
411
411
  afterSetUrl(value, oldValue) {
412
412
  let me = this;
413
413
 
414
- if (me.rendered) {
414
+ if (me.vnodeInitialized) {
415
415
  me.destroyItems();
416
416
  me.loadData()
417
417
  }
@@ -269,7 +269,7 @@ class Dialog extends Panel {
269
269
  NeoArray.toggle(me.vdom.cls, 'neo-modal', value);
270
270
  me.update();
271
271
 
272
- me.rendered && me.syncModalMask()
272
+ me.vnodeInitialized && me.syncModalMask()
273
273
  }
274
274
 
275
275
  /**
@@ -432,7 +432,7 @@ class Dialog extends Panel {
432
432
  }
433
433
 
434
434
  // rendered outside the visible area
435
- await me.render(true);
435
+ await me.initVnode(true);
436
436
 
437
437
  let [dialogRect, bodyRect] = await me.waitForDomRect({id: [me.id, 'document.body']});
438
438
 
@@ -1579,8 +1579,7 @@ class Text extends Field {
1579
1579
  }
1580
1580
 
1581
1581
  /**
1582
- * Since triggers do not get rendered, assign the relevant props
1583
- * todo: this could be handled by component.Base
1582
+ * Since triggers do not get vnodeInitialized, assign the relevant props
1584
1583
  */
1585
1584
  updateTriggerVnodes() {
1586
1585
  let me = this,
@@ -1593,8 +1592,8 @@ class Text extends Field {
1593
1592
 
1594
1593
  trigger && Object.assign(trigger, {
1595
1594
  vnode,
1596
- _rendered: true,
1597
- _mounted : true
1595
+ _mounted : true,
1596
+ _vnodeInitialized: true
1598
1597
  })
1599
1598
  })
1600
1599
  }
package/src/grid/Body.mjs CHANGED
@@ -397,7 +397,7 @@ class GridBody extends Component {
397
397
  * @protected
398
398
  */
399
399
  afterSetSelectionModel(value, oldValue) {
400
- this.rendered && value.register(this)
400
+ this.vnodeInitialized && value.register(this)
401
401
  }
402
402
 
403
403
  /**
@@ -295,7 +295,7 @@ class Flexbox extends Base {
295
295
  {container, prefix} = me,
296
296
  {wrapperCls} = container;
297
297
 
298
- if (container?.rendered) {
298
+ if (container?.vnodeInitialized) {
299
299
  NeoArray.remove(wrapperCls, prefix + propertyName + '-' + oldValue);
300
300
 
301
301
  if (value !== null) {
package/src/list/Base.mjs CHANGED
@@ -351,7 +351,7 @@ class List extends Component {
351
351
  * @protected
352
352
  */
353
353
  afterSetSelectionModel(value, oldValue) {
354
- this.rendered && value.register(this)
354
+ this.vnodeInitialized && value.register(this)
355
355
  }
356
356
 
357
357
  /**
@@ -12,10 +12,10 @@ class BodyContainer extends Container {
12
12
  */
13
13
  className: 'Neo.tab.BodyContainer',
14
14
  /**
15
- * @member {String[]} baseCls=['neo-container', 'neo-tab-body-container']
15
+ * @member {String[]} baseCls=['neo-tab-body-container','neo-container']
16
16
  * @protected
17
17
  */
18
- baseCls: ['neo-container', 'neo-tab-body-container']
18
+ baseCls: ['neo-tab-body-container', 'neo-container']
19
19
  }
20
20
 
21
21
  /**
@@ -112,7 +112,7 @@ class TableBody extends Component {
112
112
  * @protected
113
113
  */
114
114
  afterSetSelectionModel(value, oldValue) {
115
- this.rendered && value.register(this)
115
+ this.vnodeInitialized && value.register(this)
116
116
  }
117
117
 
118
118
  /**
@@ -1,46 +0,0 @@
1
- /**
2
- * A regex to check if a string is a valid JavaScript identifier.
3
- * @member {RegExp} validIdentifierRegex=/^[a-zA-Z_$][a-zA-Z0-9_$]*$/
4
- */
5
- const validIdentifierRegex = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
6
-
7
- /**
8
- * Serializes a VDOM object into a JavaScript object literal string.
9
- * This is a custom implementation to ensure keys are unquoted if they are
10
- * valid identifiers, and correctly quoted otherwise. It also handles
11
- * special placeholders for runtime expressions.
12
- * @param {Object} vdom The VDOM object to serialize.
13
- * @returns {String} The string representation of the VDOM.
14
- */
15
- export function vdomToString(vdom) {
16
- if (vdom === null) {
17
- return 'null';
18
- }
19
- if (typeof vdom !== 'object') {
20
- // It's a primitive value (string, number, boolean)
21
- // Check for our special expression placeholder
22
- if (typeof vdom === 'string') {
23
- const match = vdom.match(/##__NEO_EXPR__(.*)##__NEO_EXPR__##/);
24
- if (match) {
25
- return match[1]; // Return the raw expression
26
- }
27
- }
28
- // Otherwise, stringify it normally
29
- return JSON.stringify(vdom);
30
- }
31
-
32
- if (Array.isArray(vdom)) {
33
- return `[${vdom.map(vdomToString).join(',')}]`;
34
- }
35
-
36
- const parts = [];
37
- for (const key in vdom) {
38
- if (Object.prototype.hasOwnProperty.call(vdom, key)) {
39
- const value = vdom[key];
40
- const keyString = validIdentifierRegex.test(key) ? key : `'${key}'`;
41
- parts.push(`${keyString}:${vdomToString(value)}`);
42
- }
43
- }
44
-
45
- return `{${parts.join(',')}}`;
46
- }