| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent e10dc7b commit 6277058
16 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -2301,6 +2301,13 @@ The V8 platform used by this instance of Node.js does not support creating | |||
| 2301 | 2301 | Workers. This is caused by lack of embedder support for Workers. In particular, | |
| 2302 | 2302 | this error will not occur with standard builds of Node.js. | |
| 2303 | 2303 | ||
| 2304 | + <a id="ERR_MODULE_LINK_MISMATCH"></a> | ||
| 2305 | + | ||
| 2306 | + ### `ERR_MODULE_LINK_MISMATCH` | ||
| 2307 | + | ||
| 2308 | + A module can not be linked because the same module requests in it are not | ||
| 2309 | + resolved to the same module. | ||
| 2310 | + | ||
| 2304 | 2311 | <a id="ERR_MODULE_NOT_FOUND"></a> | |
| 2305 | 2312 | ||
| 2306 | 2313 | ### `ERR_MODULE_NOT_FOUND` | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -417,9 +417,7 @@ class that closely mirrors [Module Record][]s as defined in the ECMAScript | |||
| 417 | 417 | specification. | |
| 418 | 418 | ||
| 419 | 419 | Unlike `vm.Script` however, every `vm.Module` object is bound to a context from | |
| 420 | - its creation. Operations on `vm.Module` objects are intrinsically asynchronous, | ||
| 421 | - in contrast with the synchronous nature of `vm.Script` objects. The use of | ||
| 422 | - 'async' functions can help with manipulating `vm.Module` objects. | ||
| 420 | + its creation. | ||
| 423 | 421 | ||
| 424 | 422 | Using a `vm.Module` object requires three distinct steps: creation/parsing, | |
| 425 | 423 | linking, and evaluation. These three steps are illustrated in the following | |
@@ -447,7 +445,7 @@ const contextifiedObject = vm.createContext({ | |||
| 447 | 445 | // Here, we attempt to obtain the default export from the module "foo", and | |
| 448 | 446 | // put it into local binding "secret". | |
| 449 | 447 | ||
| 450 | - const bar = new vm.SourceTextModule(` | ||
| 448 | + const rootModule = new vm.SourceTextModule(` | ||
| 451 | 449 | import s from 'foo'; | |
| 452 | 450 | s; | |
| 453 | 451 | print(s); | |
@@ -457,47 +455,56 @@ const bar = new vm.SourceTextModule(` | |||
| 457 | 455 | // | |
| 458 | 456 | // "Link" the imported dependencies of this Module to it. | |
| 459 | 457 | // | |
| 460 | - // The provided linking callback (the "linker") accepts two arguments: the | ||
| 461 | - // parent module (`bar` in this case) and the string that is the specifier of | ||
| 462 | - // the imported module. The callback is expected to return a Module that | ||
| 463 | - // corresponds to the provided specifier, with certain requirements documented | ||
| 464 | - // in `module.link()`. | ||
| 465 | - // | ||
| 466 | - // If linking has not started for the returned Module, the same linker | ||
| 467 | - // callback will be called on the returned Module. | ||
| 458 | + // Obtain the requested dependencies of a SourceTextModule by | ||
| 459 | + // `sourceTextModule.moduleRequests` and resolve them. | ||
| 468 | 460 | // | |
| 469 | 461 | // Even top-level Modules without dependencies must be explicitly linked. The | |
| 470 | - // callback provided would never be called, however. | ||
| 471 | - // | ||
| 472 | - // The link() method returns a Promise that will be resolved when all the | ||
| 473 | - // Promises returned by the linker resolve. | ||
| 462 | + // array passed to `sourceTextModule.linkRequests(modules)` can be | ||
| 463 | + // empty, however. | ||
| 474 | 464 | // | |
| 475 | - // Note: This is a contrived example in that the linker function creates a new | ||
| 476 | - // "foo" module every time it is called. In a full-fledged module system, a | ||
| 477 | - // cache would probably be used to avoid duplicated modules. | ||
| 478 | - | ||
| 479 | - async function linker(specifier, referencingModule) { | ||
| 480 | - if (specifier === 'foo') { | ||
| 481 | - return new vm.SourceTextModule(` | ||
| 482 | - // The "secret" variable refers to the global variable we added to | ||
| 483 | - // "contextifiedObject" when creating the context. | ||
| 484 | - export default secret; | ||
| 485 | - `, { context: referencingModule.context }); | ||
| 486 | - | ||
| 487 | - // Using `contextifiedObject` instead of `referencingModule.context` | ||
| 488 | - // here would work as well. | ||
| 489 | - } | ||
| 490 | - throw new Error(`Unable to resolve dependency: ${specifier}`); | ||
| 465 | + // Note: This is a contrived example in that the resolveAndLinkDependencies | ||
| 466 | + // creates a new "foo" module every time it is called. In a full-fledged | ||
| 467 | + // module system, a cache would probably be used to avoid duplicated modules. | ||
| 468 | + | ||
| 469 | + const moduleMap = new Map([ | ||
| 470 | + ['root', rootModule], | ||
| 471 | + ]); | ||
| 472 | + | ||
| 473 | + function resolveAndLinkDependencies(module) { | ||
| 474 | + const requestedModules = module.moduleRequests.map((request) => { | ||
| 475 | + // In a full-fledged module system, the resolveAndLinkDependencies would | ||
| 476 | + // resolve the module with the module cache key `[specifier, attributes]`. | ||
| 477 | + // In this example, we just use the specifier as the key. | ||
| 478 | + const specifier = request.specifier; | ||
| 479 | + | ||
| 480 | + let requestedModule = moduleMap.get(specifier); | ||
| 481 | + if (requestedModule === undefined) { | ||
| 482 | + requestedModule = new vm.SourceTextModule(` | ||
| 483 | + // The "secret" variable refers to the global variable we added to | ||
| 484 | + // "contextifiedObject" when creating the context. | ||
| 485 | + export default secret; | ||
| 486 | + `, { context: referencingModule.context }); | ||
| 487 | + moduleMap.set(specifier, linkedModule); | ||
| 488 | + // Resolve the dependencies of the new module as well. | ||
| 489 | + resolveAndLinkDependencies(requestedModule); | ||
| 490 | + } | ||
| 491 | + | ||
| 492 | + return requestedModule; | ||
| 493 | + }); | ||
| 494 | + | ||
| 495 | + module.linkRequests(requestedModules); | ||
| 491 | 496 | } | |
| 492 | - await bar.link(linker); | ||
| 497 | + | ||
| 498 | + resolveAndLinkDependencies(rootModule); | ||
| 499 | + rootModule.instantiate(); | ||
| 493 | 500 | ||
| 494 | 501 | // Step 3 | |
| 495 | 502 | // | |
| 496 | 503 | // Evaluate the Module. The evaluate() method returns a promise which will | |
| 497 | 504 | // resolve after the module has finished evaluating. | |
| 498 | 505 | ||
| 499 | 506 | // Prints 42. | |
| 500 | - await bar.evaluate(); | ||
| 507 | + await rootModule.evaluate(); | ||
| 501 | 508 | ``` | |
| 502 | 509 | ||
| 503 | 510 | ```cjs | |
@@ -519,7 +526,7 @@ const contextifiedObject = vm.createContext({ | |||
| 519 | 526 | // Here, we attempt to obtain the default export from the module "foo", and | |
| 520 | 527 | // put it into local binding "secret". | |
| 521 | 528 | ||
| 522 | - const bar = new vm.SourceTextModule(` | ||
| 529 | + const rootModule = new vm.SourceTextModule(` | ||
| 523 | 530 | import s from 'foo'; | |
| 524 | 531 | s; | |
| 525 | 532 | print(s); | |
@@ -529,47 +536,56 @@ const contextifiedObject = vm.createContext({ | |||
| 529 | 536 | // | |
| 530 | 537 | // "Link" the imported dependencies of this Module to it. | |
| 531 | 538 | // | |
| 532 | - // The provided linking callback (the "linker") accepts two arguments: the | ||
| 533 | - // parent module (`bar` in this case) and the string that is the specifier of | ||
| 534 | - // the imported module. The callback is expected to return a Module that | ||
| 535 | - // corresponds to the provided specifier, with certain requirements documented | ||
| 536 | - // in `module.link()`. | ||
| 537 | - // | ||
| 538 | - // If linking has not started for the returned Module, the same linker | ||
| 539 | - // callback will be called on the returned Module. | ||
| 539 | + // Obtain the requested dependencies of a SourceTextModule by | ||
| 540 | + // `sourceTextModule.moduleRequests` and resolve them. | ||
| 540 | 541 | // | |
| 541 | 542 | // Even top-level Modules without dependencies must be explicitly linked. The | |
| 542 | - // callback provided would never be called, however. | ||
| 543 | - // | ||
| 544 | - // The link() method returns a Promise that will be resolved when all the | ||
| 545 | - // Promises returned by the linker resolve. | ||
| 543 | + // array passed to `sourceTextModule.linkRequests(modules)` can be | ||
| 544 | + // empty, however. | ||
| 546 | 545 | // | |
| 547 | - // Note: This is a contrived example in that the linker function creates a new | ||
| 548 | - // "foo" module every time it is called. In a full-fledged module system, a | ||
| 549 | - // cache would probably be used to avoid duplicated modules. | ||
| 550 | - | ||
| 551 | - async function linker(specifier, referencingModule) { | ||
| 552 | - if (specifier === 'foo') { | ||
| 553 | - return new vm.SourceTextModule(` | ||
| 554 | - // The "secret" variable refers to the global variable we added to | ||
| 555 | - // "contextifiedObject" when creating the context. | ||
| 556 | - export default secret; | ||
| 557 | - `, { context: referencingModule.context }); | ||
| 546 | + // Note: This is a contrived example in that the resolveAndLinkDependencies | ||
| 547 | + // creates a new "foo" module every time it is called. In a full-fledged | ||
| 548 | + // module system, a cache would probably be used to avoid duplicated modules. | ||
| 549 | + | ||
| 550 | + const moduleMap = new Map([ | ||
| 551 | + ['root', rootModule], | ||
| 552 | + ]); | ||
| 553 | + | ||
| 554 | + function resolveAndLinkDependencies(module) { | ||
| 555 | + const requestedModules = module.moduleRequests.map((request) => { | ||
| 556 | + // In a full-fledged module system, the resolveAndLinkDependencies would | ||
| 557 | + // resolve the module with the module cache key `[specifier, attributes]`. | ||
| 558 | + // In this example, we just use the specifier as the key. | ||
| 559 | + const specifier = request.specifier; | ||
| 560 | + | ||
| 561 | + let requestedModule = moduleMap.get(specifier); | ||
| 562 | + if (requestedModule === undefined) { | ||
| 563 | + requestedModule = new vm.SourceTextModule(` | ||
| 564 | + // The "secret" variable refers to the global variable we added to | ||
| 565 | + // "contextifiedObject" when creating the context. | ||
| 566 | + export default secret; | ||
| 567 | + `, { context: referencingModule.context }); | ||
| 568 | + moduleMap.set(specifier, linkedModule); | ||
| 569 | + // Resolve the dependencies of the new module as well. | ||
| 570 | + resolveAndLinkDependencies(requestedModule); | ||
| 571 | + } | ||
| 572 | + | ||
| 573 | + return requestedModule; | ||
| 574 | + }); | ||
| 558 | 575 | ||
| 559 | - // Using `contextifiedObject` instead of `referencingModule.context` | ||
| 560 | - // here would work as well. | ||
| 561 | - } | ||
| 562 | - throw new Error(`Unable to resolve dependency: ${specifier}`); | ||
| 576 | + module.linkRequests(requestedModules); | ||
| 563 | 577 | } | |
| 564 | - await bar.link(linker); | ||
| 578 | + | ||
| 579 | + resolveAndLinkDependencies(rootModule); | ||
| 580 | + rootModule.instantiate(); | ||
| 565 | 581 | ||
| 566 | 582 | // Step 3 | |
| 567 | 583 | // | |
| 568 | 584 | // Evaluate the Module. The evaluate() method returns a promise which will | |
| 569 | 585 | // resolve after the module has finished evaluating. | |
| 570 | 586 | ||
| 571 | 587 | // Prints 42. | |
| 572 | - await bar.evaluate(); | ||
| 588 | + await rootModule.evaluate(); | ||
| 573 | 589 | })(); | |
| 574 | 590 | ``` | |
| 575 | 591 | ||
@@ -658,6 +674,10 @@ changes: | |||
| 658 | 674 | Link module dependencies. This method must be called before evaluation, and | |
| 659 | 675 | can only be called once per module. | |
| 660 | 676 | ||
| 677 | + Use [`sourceTextModule.linkRequests(modules)`][] and | ||
| 678 | + [`sourceTextModule.instantiate()`][] to link modules either synchronously or | ||
| 679 | + asynchronously. | ||
| 680 | + | ||
| 661 | 681 | The function is expected to return a `Module` object or a `Promise` that | |
| 662 | 682 | eventually resolves to a `Module` object. The returned `Module` must satisfy the | |
| 663 | 683 | following two invariants: | |
@@ -803,8 +823,9 @@ const module = new vm.SourceTextModule( | |||
| 803 | 823 | meta.prop = {}; | |
| 804 | 824 | }, | |
| 805 | 825 | }); | |
| 806 | - // Since module has no dependencies, the linker function will never be called. | ||
| 807 | - await module.link(() => {}); | ||
| 826 | + // The module has an empty `moduleRequests` array. | ||
| 827 | + module.linkRequests([]); | ||
| 828 | + module.instantiate(); | ||
| 808 | 829 | await module.evaluate(); | |
| 809 | 830 | ||
| 810 | 831 | // Now, Object.prototype.secret will be equal to 42. | |
@@ -830,8 +851,9 @@ const contextifiedObject = vm.createContext({ secret: 42 }); | |||
| 830 | 851 | meta.prop = {}; | |
| 831 | 852 | }, | |
| 832 | 853 | }); | |
| 833 | - // Since module has no dependencies, the linker function will never be called. | ||
| 834 | - await module.link(() => {}); | ||
| 854 | + // The module has an empty `moduleRequests` array. | ||
| 855 | + module.linkRequests([]); | ||
| 856 | + module.instantiate(); | ||
| 835 | 857 | await module.evaluate(); | |
| 836 | 858 | // Now, Object.prototype.secret will be equal to 42. | |
| 837 | 859 | // | |
@@ -896,6 +918,69 @@ to disallow any changes to it. | |||
| 896 | 918 | Corresponds to the `[[RequestedModules]]` field of [Cyclic Module Record][]s in | |
| 897 | 919 | the ECMAScript specification. | |
| 898 | 920 | ||
| 921 | + ### `sourceTextModule.instantiate()` | ||
| 922 | + | ||
| 923 | + <!-- YAML | ||
| 924 | + added: REPLACEME | ||
| 925 | + --> | ||
| 926 | + | ||
| 927 | + * Returns: {undefined} | ||
| 928 | + | ||
| 929 | + Instantiate the module with the linked requested modules. | ||
| 930 | + | ||
| 931 | + This resolves the imported bindings of the module, including re-exported | ||
| 932 | + binding names. When there are any bindings that cannot be resolved, | ||
| 933 | + an error would be thrown synchronously. | ||
| 934 | + | ||
| 935 | + If the requested modules include cyclic dependencies, the | ||
| 936 | + [`sourceTextModule.linkRequests(modules)`][] method must be called on all | ||
| 937 | + modules in the cycle before calling this method. | ||
| 938 | + | ||
| 939 | + ### `sourceTextModule.linkRequests(modules)` | ||
| 940 | + | ||
| 941 | + <!-- YAML | ||
| 942 | + added: REPLACEME | ||
| 943 | + --> | ||
| 944 | + | ||
| 945 | + * `modules` {vm.Module\[]} Array of `vm.Module` objects that this module depends on. | ||
| 946 | + The order of the modules in the array is the order of | ||
| 947 | + [`sourceTextModule.moduleRequests`][]. | ||
| 948 | + * Returns: {undefined} | ||
| 949 | + | ||
| 950 | + Link module dependencies. This method must be called before evaluation, and | ||
| 951 | + can only be called once per module. | ||
| 952 | + | ||
| 953 | + The order of the module instances in the `modules` array should correspond to the order of | ||
| 954 | + [`sourceTextModule.moduleRequests`][] being resolved. If two module requests have the same | ||
| 955 | + specifier and import attributes, they must be resolved with the same module instance or an | ||
| 956 | + `ERR_MODULE_LINK_MISMATCH` would be thrown. For example, when linking requests for this | ||
| 957 | + module: | ||
| 958 | + | ||
| 959 | + <!-- eslint-disable no-duplicate-imports --> | ||
| 960 | + | ||
| 961 | + ```mjs | ||
| 962 | + import foo from 'foo'; | ||
| 963 | + import source Foo from 'foo'; | ||
| 964 | + ``` | ||
| 965 | + | ||
| 966 | + <!-- eslint-enable no-duplicate-imports --> | ||
| 967 | + | ||
| 968 | + The `modules` array must contain two references to the same instance, because the two | ||
| 969 | + module requests are identical but in two phases. | ||
| 970 | + | ||
| 971 | + If the module has no dependencies, the `modules` array can be empty. | ||
| 972 | + | ||
| 973 | + Users can use `sourceTextModule.moduleRequests` to implement the host-defined | ||
| 974 | + [HostLoadImportedModule][] abstract operation in the ECMAScript specification, | ||
| 975 | + and using `sourceTextModule.linkRequests()` to invoke specification defined | ||
| 976 | + [FinishLoadingImportedModule][], on the module with all dependencies in a batch. | ||
| 977 | + | ||
| 978 | + It's up to the creator of the `SourceTextModule` to determine if the resolution | ||
| 979 | + of the dependencies is synchronous or asynchronous. | ||
| 980 | + | ||
| 981 | + After each module in the `modules` array is linked, call | ||
| 982 | + [`sourceTextModule.instantiate()`][]. | ||
| 983 | + | ||
| 899 | 984 | ### `sourceTextModule.moduleRequests` | |
| 900 | 985 | ||
| 901 | 986 | <!-- YAML | |
@@ -1005,14 +1090,17 @@ the module to access information outside the specified `context`. Use | |||
| 1005 | 1090 | added: | |
| 1006 | 1091 | - v13.0.0 | |
| 1007 | 1092 | - v12.16.0 | |
| 1093 | + changes: | ||
| 1094 | + - version: REPLACEME | ||
| 1095 | + pr-url: https://github.com/nodejs/node/pull/59000 | ||
| 1096 | + description: No longer need to call `syntheticModule.link()` before | ||
| 1097 | + calling this method. | ||
| 1008 | 1098 | --> | |
| 1009 | 1099 | ||
| 1010 | 1100 | * `name` {string} Name of the export to set. | |
| 1011 | 1101 | * `value` {any} The value to set the export to. | |
| 1012 | 1102 | ||
| 1013 | - This method is used after the module is linked to set the values of exports. If | ||
| 1014 | - it is called before the module is linked, an [`ERR_VM_MODULE_STATUS`][] error | ||
| 1015 | - will be thrown. | ||
| 1103 | + This method sets the module export binding slots with the given value. | ||
| 1016 | 1104 | ||
| 1017 | 1105 | ```mjs | |
| 1018 | 1106 | import vm from 'node:vm'; | |
@@ -1021,7 +1109,6 @@ const m = new vm.SyntheticModule(['x'], () => { | |||
| 1021 | 1109 | m.setExport('x', 1); | |
| 1022 | 1110 | }); | |
| 1023 | 1111 | ||
| 1024 | - await m.link(() => {}); | ||
| 1025 | 1112 | await m.evaluate(); | |
| 1026 | 1113 | ||
| 1027 | 1114 | assert.strictEqual(m.namespace.x, 1); | |
@@ -1033,7 +1120,6 @@ const vm = require('node:vm'); | |||
| 1033 | 1120 | const m = new vm.SyntheticModule(['x'], () => { | |
| 1034 | 1121 | m.setExport('x', 1); | |
| 1035 | 1122 | }); | |
| 1036 | - await m.link(() => {}); | ||
| 1037 | 1123 | await m.evaluate(); | |
| 1038 | 1124 | assert.strictEqual(m.namespace.x, 1); | |
| 1039 | 1125 | })(); | |
@@ -2083,7 +2169,9 @@ const { Script, SyntheticModule } = require('node:vm'); | |||
| 2083 | 2169 | [Cyclic Module Record]: https://tc39.es/ecma262/#sec-cyclic-module-records | |
| 2084 | 2170 | [ECMAScript Module Loader]: esm.md#modules-ecmascript-modules | |
| 2085 | 2171 | [Evaluate() concrete method]: https://tc39.es/ecma262/#sec-moduleevaluation | |
| 2172 | + [FinishLoadingImportedModule]: https://tc39.es/ecma262/#sec-FinishLoadingImportedModule | ||
| 2086 | 2173 | [GetModuleNamespace]: https://tc39.es/ecma262/#sec-getmodulenamespace | |
| 2174 | + [HostLoadImportedModule]: https://tc39.es/ecma262/#sec-HostLoadImportedModule | ||
| 2087 | 2175 | [HostResolveImportedModule]: https://tc39.es/ecma262/#sec-hostresolveimportedmodule | |
| 2088 | 2176 | [ImportDeclaration]: https://tc39.es/ecma262/#prod-ImportDeclaration | |
| 2089 | 2177 | [Link() concrete method]: https://tc39.es/ecma262/#sec-moduledeclarationlinking | |
@@ -2095,13 +2183,14 @@ const { Script, SyntheticModule } = require('node:vm'); | |||
| 2095 | 2183 | [WithClause]: https://tc39.es/ecma262/#prod-WithClause | |
| 2096 | 2184 | [`ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING_FLAG`]: errors.md#err_vm_dynamic_import_callback_missing_flag | |
| 2097 | 2185 | [`ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING`]: errors.md#err_vm_dynamic_import_callback_missing | |
| 2098 | - [`ERR_VM_MODULE_STATUS`]: errors.md#err_vm_module_status | ||
| 2099 | 2186 | [`Error`]: errors.md#class-error | |
| 2100 | 2187 | [`URL`]: url.md#class-url | |
| 2101 | 2188 | [`eval()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval | |
| 2102 | 2189 | [`optionsExpression`]: https://tc39.es/proposal-import-attributes/#sec-evaluate-import-call | |
| 2103 | 2190 | [`script.runInContext()`]: #scriptrunincontextcontextifiedobject-options | |
| 2104 | 2191 | [`script.runInThisContext()`]: #scriptruninthiscontextoptions | |
| 2192 | + [`sourceTextModule.instantiate()`]: #sourcetextmoduleinstantiate | ||
| 2193 | + [`sourceTextModule.linkRequests(modules)`]: #sourcetextmodulelinkrequestsmodules | ||
| 2105 | 2194 | [`sourceTextModule.moduleRequests`]: #sourcetextmodulemodulerequests | |
| 2106 | 2195 | [`url.origin`]: url.md#urlorigin | |
| 2107 | 2196 | [`vm.compileFunction()`]: #vmcompilefunctioncode-params-options | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -360,6 +360,7 @@ class BuiltinModule { | |||
| 360 | 360 | this.setExport('default', builtin.exports); | |
| 361 | 361 | }); | |
| 362 | 362 | // Ensure immediate sync execution to capture exports now | |
| 363 | + this.module.link([]); | ||
| 363 | 364 | this.module.instantiate(); | |
| 364 | 365 | this.module.evaluate(-1, false); | |
| 365 | 366 | return this.module; | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1592,6 +1592,7 @@ E('ERR_MISSING_ARGS', | |||
| 1592 | 1592 | return `${msg} must be specified`; | |
| 1593 | 1593 | }, TypeError); | |
| 1594 | 1594 | E('ERR_MISSING_OPTION', '%s is required', TypeError); | |
| 1595 | + E('ERR_MODULE_LINK_MISMATCH', '%s', TypeError); | ||
| 1595 | 1596 | E('ERR_MODULE_NOT_FOUND', function(path, base, exactUrl) { | |
| 1596 | 1597 | if (exactUrl) { | |
| 1597 | 1598 | lazyInternalUtil().setOwnProperty(this, 'url', `${exactUrl}`); | |
| Back | FazBrowse Home | New Git URL |
0 commit comments