| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent c8d6b60 commit 2086f33
16 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -2285,6 +2285,13 @@ The V8 platform used by this instance of Node.js does not support creating | |||
| 2285 | 2285 | Workers. This is caused by lack of embedder support for Workers. In particular, | |
| 2286 | 2286 | this error will not occur with standard builds of Node.js. | |
| 2287 | 2287 | ||
| 2288 | + <a id="ERR_MODULE_LINK_MISMATCH"></a> | ||
| 2289 | + | ||
| 2290 | + ### `ERR_MODULE_LINK_MISMATCH` | ||
| 2291 | + | ||
| 2292 | + A module can not be linked because the same module requests in it are not | ||
| 2293 | + resolved to the same module. | ||
| 2294 | + | ||
| 2288 | 2295 | <a id="ERR_MODULE_NOT_FOUND"></a> | |
| 2289 | 2296 | ||
| 2290 | 2297 | ### `ERR_MODULE_NOT_FOUND` | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -419,9 +419,7 @@ class that closely mirrors [Module Record][]s as defined in the ECMAScript | |||
| 419 | 419 | specification. | |
| 420 | 420 | ||
| 421 | 421 | Unlike `vm.Script` however, every `vm.Module` object is bound to a context from | |
| 422 | - its creation. Operations on `vm.Module` objects are intrinsically asynchronous, | ||
| 423 | - in contrast with the synchronous nature of `vm.Script` objects. The use of | ||
| 424 | - 'async' functions can help with manipulating `vm.Module` objects. | ||
| 422 | + its creation. | ||
| 425 | 423 | ||
| 426 | 424 | Using a `vm.Module` object requires three distinct steps: creation/parsing, | |
| 427 | 425 | linking, and evaluation. These three steps are illustrated in the following | |
@@ -449,7 +447,7 @@ const contextifiedObject = vm.createContext({ | |||
| 449 | 447 | // Here, we attempt to obtain the default export from the module "foo", and | |
| 450 | 448 | // put it into local binding "secret". | |
| 451 | 449 | ||
| 452 | - const bar = new vm.SourceTextModule(` | ||
| 450 | + const rootModule = new vm.SourceTextModule(` | ||
| 453 | 451 | import s from 'foo'; | |
| 454 | 452 | s; | |
| 455 | 453 | print(s); | |
@@ -459,47 +457,56 @@ const bar = new vm.SourceTextModule(` | |||
| 459 | 457 | // | |
| 460 | 458 | // "Link" the imported dependencies of this Module to it. | |
| 461 | 459 | // | |
| 462 | - // The provided linking callback (the "linker") accepts two arguments: the | ||
| 463 | - // parent module (`bar` in this case) and the string that is the specifier of | ||
| 464 | - // the imported module. The callback is expected to return a Module that | ||
| 465 | - // corresponds to the provided specifier, with certain requirements documented | ||
| 466 | - // in `module.link()`. | ||
| 467 | - // | ||
| 468 | - // If linking has not started for the returned Module, the same linker | ||
| 469 | - // callback will be called on the returned Module. | ||
| 460 | + // Obtain the requested dependencies of a SourceTextModule by | ||
| 461 | + // `sourceTextModule.moduleRequests` and resolve them. | ||
| 470 | 462 | // | |
| 471 | 463 | // Even top-level Modules without dependencies must be explicitly linked. The | |
| 472 | - // callback provided would never be called, however. | ||
| 473 | - // | ||
| 474 | - // The link() method returns a Promise that will be resolved when all the | ||
| 475 | - // Promises returned by the linker resolve. | ||
| 464 | + // array passed to `sourceTextModule.linkRequests(modules)` can be | ||
| 465 | + // empty, however. | ||
| 476 | 466 | // | |
| 477 | - // Note: This is a contrived example in that the linker function creates a new | ||
| 478 | - // "foo" module every time it is called. In a full-fledged module system, a | ||
| 479 | - // cache would probably be used to avoid duplicated modules. | ||
| 480 | - | ||
| 481 | - async function linker(specifier, referencingModule) { | ||
| 482 | - if (specifier === 'foo') { | ||
| 483 | - return new vm.SourceTextModule(` | ||
| 484 | - // The "secret" variable refers to the global variable we added to | ||
| 485 | - // "contextifiedObject" when creating the context. | ||
| 486 | - export default secret; | ||
| 487 | - `, { context: referencingModule.context }); | ||
| 488 | - | ||
| 489 | - // Using `contextifiedObject` instead of `referencingModule.context` | ||
| 490 | - // here would work as well. | ||
| 491 | - } | ||
| 492 | - throw new Error(`Unable to resolve dependency: ${specifier}`); | ||
| 467 | + // Note: This is a contrived example in that the resolveAndLinkDependencies | ||
| 468 | + // creates a new "foo" module every time it is called. In a full-fledged | ||
| 469 | + // module system, a cache would probably be used to avoid duplicated modules. | ||
| 470 | + | ||
| 471 | + const moduleMap = new Map([ | ||
| 472 | + ['root', rootModule], | ||
| 473 | + ]); | ||
| 474 | + | ||
| 475 | + function resolveAndLinkDependencies(module) { | ||
| 476 | + const requestedModules = module.moduleRequests.map((request) => { | ||
| 477 | + // In a full-fledged module system, the resolveAndLinkDependencies would | ||
| 478 | + // resolve the module with the module cache key `[specifier, attributes]`. | ||
| 479 | + // In this example, we just use the specifier as the key. | ||
| 480 | + const specifier = request.specifier; | ||
| 481 | + | ||
| 482 | + let requestedModule = moduleMap.get(specifier); | ||
| 483 | + if (requestedModule === undefined) { | ||
| 484 | + requestedModule = new vm.SourceTextModule(` | ||
| 485 | + // The "secret" variable refers to the global variable we added to | ||
| 486 | + // "contextifiedObject" when creating the context. | ||
| 487 | + export default secret; | ||
| 488 | + `, { context: referencingModule.context }); | ||
| 489 | + moduleMap.set(specifier, linkedModule); | ||
| 490 | + // Resolve the dependencies of the new module as well. | ||
| 491 | + resolveAndLinkDependencies(requestedModule); | ||
| 492 | + } | ||
| 493 | + | ||
| 494 | + return requestedModule; | ||
| 495 | + }); | ||
| 496 | + | ||
| 497 | + module.linkRequests(requestedModules); | ||
| 493 | 498 | } | |
| 494 | - await bar.link(linker); | ||
| 499 | + | ||
| 500 | + resolveAndLinkDependencies(rootModule); | ||
| 501 | + rootModule.instantiate(); | ||
| 495 | 502 | ||
| 496 | 503 | // Step 3 | |
| 497 | 504 | // | |
| 498 | 505 | // Evaluate the Module. The evaluate() method returns a promise which will | |
| 499 | 506 | // resolve after the module has finished evaluating. | |
| 500 | 507 | ||
| 501 | 508 | // Prints 42. | |
| 502 | - await bar.evaluate(); | ||
| 509 | + await rootModule.evaluate(); | ||
| 503 | 510 | ``` | |
| 504 | 511 | ||
| 505 | 512 | ```cjs | |
@@ -521,7 +528,7 @@ const contextifiedObject = vm.createContext({ | |||
| 521 | 528 | // Here, we attempt to obtain the default export from the module "foo", and | |
| 522 | 529 | // put it into local binding "secret". | |
| 523 | 530 | ||
| 524 | - const bar = new vm.SourceTextModule(` | ||
| 531 | + const rootModule = new vm.SourceTextModule(` | ||
| 525 | 532 | import s from 'foo'; | |
| 526 | 533 | s; | |
| 527 | 534 | print(s); | |
@@ -531,47 +538,56 @@ const contextifiedObject = vm.createContext({ | |||
| 531 | 538 | // | |
| 532 | 539 | // "Link" the imported dependencies of this Module to it. | |
| 533 | 540 | // | |
| 534 | - // The provided linking callback (the "linker") accepts two arguments: the | ||
| 535 | - // parent module (`bar` in this case) and the string that is the specifier of | ||
| 536 | - // the imported module. The callback is expected to return a Module that | ||
| 537 | - // corresponds to the provided specifier, with certain requirements documented | ||
| 538 | - // in `module.link()`. | ||
| 539 | - // | ||
| 540 | - // If linking has not started for the returned Module, the same linker | ||
| 541 | - // callback will be called on the returned Module. | ||
| 541 | + // Obtain the requested dependencies of a SourceTextModule by | ||
| 542 | + // `sourceTextModule.moduleRequests` and resolve them. | ||
| 542 | 543 | // | |
| 543 | 544 | // Even top-level Modules without dependencies must be explicitly linked. The | |
| 544 | - // callback provided would never be called, however. | ||
| 545 | - // | ||
| 546 | - // The link() method returns a Promise that will be resolved when all the | ||
| 547 | - // Promises returned by the linker resolve. | ||
| 545 | + // array passed to `sourceTextModule.linkRequests(modules)` can be | ||
| 546 | + // empty, however. | ||
| 548 | 547 | // | |
| 549 | - // Note: This is a contrived example in that the linker function creates a new | ||
| 550 | - // "foo" module every time it is called. In a full-fledged module system, a | ||
| 551 | - // cache would probably be used to avoid duplicated modules. | ||
| 552 | - | ||
| 553 | - async function linker(specifier, referencingModule) { | ||
| 554 | - if (specifier === 'foo') { | ||
| 555 | - return new vm.SourceTextModule(` | ||
| 556 | - // The "secret" variable refers to the global variable we added to | ||
| 557 | - // "contextifiedObject" when creating the context. | ||
| 558 | - export default secret; | ||
| 559 | - `, { context: referencingModule.context }); | ||
| 548 | + // Note: This is a contrived example in that the resolveAndLinkDependencies | ||
| 549 | + // creates a new "foo" module every time it is called. In a full-fledged | ||
| 550 | + // module system, a cache would probably be used to avoid duplicated modules. | ||
| 551 | + | ||
| 552 | + const moduleMap = new Map([ | ||
| 553 | + ['root', rootModule], | ||
| 554 | + ]); | ||
| 555 | + | ||
| 556 | + function resolveAndLinkDependencies(module) { | ||
| 557 | + const requestedModules = module.moduleRequests.map((request) => { | ||
| 558 | + // In a full-fledged module system, the resolveAndLinkDependencies would | ||
| 559 | + // resolve the module with the module cache key `[specifier, attributes]`. | ||
| 560 | + // In this example, we just use the specifier as the key. | ||
| 561 | + const specifier = request.specifier; | ||
| 562 | + | ||
| 563 | + let requestedModule = moduleMap.get(specifier); | ||
| 564 | + if (requestedModule === undefined) { | ||
| 565 | + requestedModule = new vm.SourceTextModule(` | ||
| 566 | + // The "secret" variable refers to the global variable we added to | ||
| 567 | + // "contextifiedObject" when creating the context. | ||
| 568 | + export default secret; | ||
| 569 | + `, { context: referencingModule.context }); | ||
| 570 | + moduleMap.set(specifier, linkedModule); | ||
| 571 | + // Resolve the dependencies of the new module as well. | ||
| 572 | + resolveAndLinkDependencies(requestedModule); | ||
| 573 | + } | ||
| 574 | + | ||
| 575 | + return requestedModule; | ||
| 576 | + }); | ||
| 560 | 577 | ||
| 561 | - // Using `contextifiedObject` instead of `referencingModule.context` | ||
| 562 | - // here would work as well. | ||
| 563 | - } | ||
| 564 | - throw new Error(`Unable to resolve dependency: ${specifier}`); | ||
| 578 | + module.linkRequests(requestedModules); | ||
| 565 | 579 | } | |
| 566 | - await bar.link(linker); | ||
| 580 | + | ||
| 581 | + resolveAndLinkDependencies(rootModule); | ||
| 582 | + rootModule.instantiate(); | ||
| 567 | 583 | ||
| 568 | 584 | // Step 3 | |
| 569 | 585 | // | |
| 570 | 586 | // Evaluate the Module. The evaluate() method returns a promise which will | |
| 571 | 587 | // resolve after the module has finished evaluating. | |
| 572 | 588 | ||
| 573 | 589 | // Prints 42. | |
| 574 | - await bar.evaluate(); | ||
| 590 | + await rootModule.evaluate(); | ||
| 575 | 591 | })(); | |
| 576 | 592 | ``` | |
| 577 | 593 | ||
@@ -660,6 +676,10 @@ changes: | |||
| 660 | 676 | Link module dependencies. This method must be called before evaluation, and | |
| 661 | 677 | can only be called once per module. | |
| 662 | 678 | ||
| 679 | + Use [`sourceTextModule.linkRequests(modules)`][] and | ||
| 680 | + [`sourceTextModule.instantiate()`][] to link modules either synchronously or | ||
| 681 | + asynchronously. | ||
| 682 | + | ||
| 663 | 683 | The function is expected to return a `Module` object or a `Promise` that | |
| 664 | 684 | eventually resolves to a `Module` object. The returned `Module` must satisfy the | |
| 665 | 685 | following two invariants: | |
@@ -805,8 +825,9 @@ const module = new vm.SourceTextModule( | |||
| 805 | 825 | meta.prop = {}; | |
| 806 | 826 | }, | |
| 807 | 827 | }); | |
| 808 | - // Since module has no dependencies, the linker function will never be called. | ||
| 809 | - await module.link(() => {}); | ||
| 828 | + // The module has an empty `moduleRequests` array. | ||
| 829 | + module.linkRequests([]); | ||
| 830 | + module.instantiate(); | ||
| 810 | 831 | await module.evaluate(); | |
| 811 | 832 | ||
| 812 | 833 | // Now, Object.prototype.secret will be equal to 42. | |
@@ -832,8 +853,9 @@ const contextifiedObject = vm.createContext({ secret: 42 }); | |||
| 832 | 853 | meta.prop = {}; | |
| 833 | 854 | }, | |
| 834 | 855 | }); | |
| 835 | - // Since module has no dependencies, the linker function will never be called. | ||
| 836 | - await module.link(() => {}); | ||
| 856 | + // The module has an empty `moduleRequests` array. | ||
| 857 | + module.linkRequests([]); | ||
| 858 | + module.instantiate(); | ||
| 837 | 859 | await module.evaluate(); | |
| 838 | 860 | // Now, Object.prototype.secret will be equal to 42. | |
| 839 | 861 | // | |
@@ -898,6 +920,69 @@ to disallow any changes to it. | |||
| 898 | 920 | Corresponds to the `[[RequestedModules]]` field of [Cyclic Module Record][]s in | |
| 899 | 921 | the ECMAScript specification. | |
| 900 | 922 | ||
| 923 | + ### `sourceTextModule.instantiate()` | ||
| 924 | + | ||
| 925 | + <!-- YAML | ||
| 926 | + added: REPLACEME | ||
| 927 | + --> | ||
| 928 | + | ||
| 929 | + * Returns: {undefined} | ||
| 930 | + | ||
| 931 | + Instantiate the module with the linked requested modules. | ||
| 932 | + | ||
| 933 | + This resolves the imported bindings of the module, including re-exported | ||
| 934 | + binding names. When there are any bindings that cannot be resolved, | ||
| 935 | + an error would be thrown synchronously. | ||
| 936 | + | ||
| 937 | + If the requested modules include cyclic dependencies, the | ||
| 938 | + [`sourceTextModule.linkRequests(modules)`][] method must be called on all | ||
| 939 | + modules in the cycle before calling this method. | ||
| 940 | + | ||
| 941 | + ### `sourceTextModule.linkRequests(modules)` | ||
| 942 | + | ||
| 943 | + <!-- YAML | ||
| 944 | + added: REPLACEME | ||
| 945 | + --> | ||
| 946 | + | ||
| 947 | + * `modules` {vm.Module\[]} Array of `vm.Module` objects that this module depends on. | ||
| 948 | + The order of the modules in the array is the order of | ||
| 949 | + [`sourceTextModule.moduleRequests`][]. | ||
| 950 | + * Returns: {undefined} | ||
| 951 | + | ||
| 952 | + Link module dependencies. This method must be called before evaluation, and | ||
| 953 | + can only be called once per module. | ||
| 954 | + | ||
| 955 | + The order of the module instances in the `modules` array should correspond to the order of | ||
| 956 | + [`sourceTextModule.moduleRequests`][] being resolved. If two module requests have the same | ||
| 957 | + specifier and import attributes, they must be resolved with the same module instance or an | ||
| 958 | + `ERR_MODULE_LINK_MISMATCH` would be thrown. For example, when linking requests for this | ||
| 959 | + module: | ||
| 960 | + | ||
| 961 | + <!-- eslint-disable no-duplicate-imports --> | ||
| 962 | + | ||
| 963 | + ```mjs | ||
| 964 | + import foo from 'foo'; | ||
| 965 | + import source Foo from 'foo'; | ||
| 966 | + ``` | ||
| 967 | + | ||
| 968 | + <!-- eslint-enable no-duplicate-imports --> | ||
| 969 | + | ||
| 970 | + The `modules` array must contain two references to the same instance, because the two | ||
| 971 | + module requests are identical but in two phases. | ||
| 972 | + | ||
| 973 | + If the module has no dependencies, the `modules` array can be empty. | ||
| 974 | + | ||
| 975 | + Users can use `sourceTextModule.moduleRequests` to implement the host-defined | ||
| 976 | + [HostLoadImportedModule][] abstract operation in the ECMAScript specification, | ||
| 977 | + and using `sourceTextModule.linkRequests()` to invoke specification defined | ||
| 978 | + [FinishLoadingImportedModule][], on the module with all dependencies in a batch. | ||
| 979 | + | ||
| 980 | + It's up to the creator of the `SourceTextModule` to determine if the resolution | ||
| 981 | + of the dependencies is synchronous or asynchronous. | ||
| 982 | + | ||
| 983 | + After each module in the `modules` array is linked, call | ||
| 984 | + [`sourceTextModule.instantiate()`][]. | ||
| 985 | + | ||
| 901 | 986 | ### `sourceTextModule.moduleRequests` | |
| 902 | 987 | ||
| 903 | 988 | <!-- YAML | |
@@ -1017,14 +1102,17 @@ the module to access information outside the specified `context`. Use | |||
| 1017 | 1102 | added: | |
| 1018 | 1103 | - v13.0.0 | |
| 1019 | 1104 | - v12.16.0 | |
| 1105 | + changes: | ||
| 1106 | + - version: REPLACEME | ||
| 1107 | + pr-url: https://github.com/nodejs/node/pull/59000 | ||
| 1108 | + description: No longer need to call `syntheticModule.link()` before | ||
| 1109 | + calling this method. | ||
| 1020 | 1110 | --> | |
| 1021 | 1111 | ||
| 1022 | 1112 | * `name` {string} Name of the export to set. | |
| 1023 | 1113 | * `value` {any} The value to set the export to. | |
| 1024 | 1114 | ||
| 1025 | - This method is used after the module is linked to set the values of exports. If | ||
| 1026 | - it is called before the module is linked, an [`ERR_VM_MODULE_STATUS`][] error | ||
| 1027 | - will be thrown. | ||
| 1115 | + This method sets the module export binding slots with the given value. | ||
| 1028 | 1116 | ||
| 1029 | 1117 | ```mjs | |
| 1030 | 1118 | import vm from 'node:vm'; | |
@@ -1033,7 +1121,6 @@ const m = new vm.SyntheticModule(['x'], () => { | |||
| 1033 | 1121 | m.setExport('x', 1); | |
| 1034 | 1122 | }); | |
| 1035 | 1123 | ||
| 1036 | - await m.link(() => {}); | ||
| 1037 | 1124 | await m.evaluate(); | |
| 1038 | 1125 | ||
| 1039 | 1126 | assert.strictEqual(m.namespace.x, 1); | |
@@ -1045,7 +1132,6 @@ const vm = require('node:vm'); | |||
| 1045 | 1132 | const m = new vm.SyntheticModule(['x'], () => { | |
| 1046 | 1133 | m.setExport('x', 1); | |
| 1047 | 1134 | }); | |
| 1048 | - await m.link(() => {}); | ||
| 1049 | 1135 | await m.evaluate(); | |
| 1050 | 1136 | assert.strictEqual(m.namespace.x, 1); | |
| 1051 | 1137 | })(); | |
@@ -2037,7 +2123,9 @@ const { Script, SyntheticModule } = require('node:vm'); | |||
| 2037 | 2123 | [Cyclic Module Record]: https://tc39.es/ecma262/#sec-cyclic-module-records | |
| 2038 | 2124 | [ECMAScript Module Loader]: esm.md#modules-ecmascript-modules | |
| 2039 | 2125 | [Evaluate() concrete method]: https://tc39.es/ecma262/#sec-moduleevaluation | |
| 2126 | + [FinishLoadingImportedModule]: https://tc39.es/ecma262/#sec-FinishLoadingImportedModule | ||
| 2040 | 2127 | [GetModuleNamespace]: https://tc39.es/ecma262/#sec-getmodulenamespace | |
| 2128 | + [HostLoadImportedModule]: https://tc39.es/ecma262/#sec-HostLoadImportedModule | ||
| 2041 | 2129 | [HostResolveImportedModule]: https://tc39.es/ecma262/#sec-hostresolveimportedmodule | |
| 2042 | 2130 | [ImportDeclaration]: https://tc39.es/ecma262/#prod-ImportDeclaration | |
| 2043 | 2131 | [Link() concrete method]: https://tc39.es/ecma262/#sec-moduledeclarationlinking | |
@@ -2049,13 +2137,14 @@ const { Script, SyntheticModule } = require('node:vm'); | |||
| 2049 | 2137 | [WithClause]: https://tc39.es/ecma262/#prod-WithClause | |
| 2050 | 2138 | [`ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING_FLAG`]: errors.md#err_vm_dynamic_import_callback_missing_flag | |
| 2051 | 2139 | [`ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING`]: errors.md#err_vm_dynamic_import_callback_missing | |
| 2052 | - [`ERR_VM_MODULE_STATUS`]: errors.md#err_vm_module_status | ||
| 2053 | 2140 | [`Error`]: errors.md#class-error | |
| 2054 | 2141 | [`URL`]: url.md#class-url | |
| 2055 | 2142 | [`eval()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval | |
| 2056 | 2143 | [`optionsExpression`]: https://tc39.es/proposal-import-attributes/#sec-evaluate-import-call | |
| 2057 | 2144 | [`script.runInContext()`]: #scriptrunincontextcontextifiedobject-options | |
| 2058 | 2145 | [`script.runInThisContext()`]: #scriptruninthiscontextoptions | |
| 2146 | + [`sourceTextModule.instantiate()`]: #sourcetextmoduleinstantiate | ||
| 2147 | + [`sourceTextModule.linkRequests(modules)`]: #sourcetextmodulelinkrequestsmodules | ||
| 2059 | 2148 | [`sourceTextModule.moduleRequests`]: #sourcetextmodulemodulerequests | |
| 2060 | 2149 | [`url.origin`]: url.md#urlorigin | |
| 2061 | 2150 | [`vm.compileFunction()`]: #vmcompilefunctioncode-params-options | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -359,6 +359,7 @@ class BuiltinModule { | |||
| 359 | 359 | this.setExport('default', builtin.exports); | |
| 360 | 360 | }); | |
| 361 | 361 | // Ensure immediate sync execution to capture exports now | |
| 362 | + this.module.link([]); | ||
| 362 | 363 | this.module.instantiate(); | |
| 363 | 364 | this.module.evaluate(-1, false); | |
| 364 | 365 | return this.module; | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1589,6 +1589,7 @@ E('ERR_MISSING_ARGS', | |||
| 1589 | 1589 | return `${msg} must be specified`; | |
| 1590 | 1590 | }, TypeError); | |
| 1591 | 1591 | E('ERR_MISSING_OPTION', '%s is required', TypeError); | |
| 1592 | + E('ERR_MODULE_LINK_MISMATCH', '%s', TypeError); | ||
| 1592 | 1593 | E('ERR_MODULE_NOT_FOUND', function(path, base, exactUrl) { | |
| 1593 | 1594 | if (exactUrl) { | |
| 1594 | 1595 | lazyInternalUtil().setOwnProperty(this, 'url', `${exactUrl}`); | |
| Back | FazBrowse Home | New Git URL |
0 commit comments