as-test 1.4.1 → 1.5.1
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/CHANGELOG.md +64 -0
- package/assembly/src/expectation.ts +2 -2
- package/assembly/src/stringify.ts +29 -4
- package/bin/commands/build-core.js +62 -6
- package/bin/commands/fuzz-core.js +22 -1
- package/bin/commands/init-core.js +371 -36
- package/bin/commands/run-core.js +130 -21
- package/bin/commands/web-session.js +50 -3
- package/bin/index.js +6 -2
- package/bin/wipc.js +46 -7
- package/lib/build/index.js +45 -15
- package/lib/build/web-runner/worker.js +27 -0
- package/lib/src/index.ts +66 -15
- package/package.json +18 -17
- package/transform/lib/equals.js +3 -3
- package/transform/lib/index.js +10 -1
- package/transform/lib/mock.js +63 -20
package/lib/src/index.ts
CHANGED
|
@@ -247,7 +247,11 @@ async function instantiateRawInstance(
|
|
|
247
247
|
if (typeof helper.instantiate != "function") {
|
|
248
248
|
throw new Error("bindings helper missing instantiate export");
|
|
249
249
|
}
|
|
250
|
-
const mergedImports =
|
|
250
|
+
const mergedImports = stubMissingImports(
|
|
251
|
+
module,
|
|
252
|
+
mergeImports(withNodeIo({}), imports),
|
|
253
|
+
["wasi_snapshot_preview1"],
|
|
254
|
+
);
|
|
251
255
|
const instance = await captureHelperInstance(async () => {
|
|
252
256
|
await helper.instantiate!(module, mergedImports);
|
|
253
257
|
});
|
|
@@ -268,6 +272,10 @@ async function instantiateEsmInstance(
|
|
|
268
272
|
"esm bindings do not support custom imports in as-test/lib; pass {} or switch to raw bindings",
|
|
269
273
|
);
|
|
270
274
|
}
|
|
275
|
+
// The esm helper auto-instantiates at import time and writes the WIPC report
|
|
276
|
+
// by calling the global process.stdout.write with an ArrayBuffer. Patch node
|
|
277
|
+
// IO before importing so that write (and stdin.read) accept the raw buffer.
|
|
278
|
+
patchNodeIo();
|
|
271
279
|
const instance = await captureHelperInstance(async () => {
|
|
272
280
|
await import(`${pathToFileURL(helperPath).href}?t=${Date.now()}`);
|
|
273
281
|
});
|
|
@@ -347,6 +355,8 @@ async function instantiateWebInstance(
|
|
|
347
355
|
let ready = false;
|
|
348
356
|
let wsSocket: Duplex | null = null;
|
|
349
357
|
let wsBuffer = Buffer.alloc(0);
|
|
358
|
+
let wsFragmentOpcode = 0;
|
|
359
|
+
let wsFragments: Buffer[] = [];
|
|
350
360
|
let stdinBuffer = Buffer.alloc(0);
|
|
351
361
|
let browserProcess: ChildProcess | null = null;
|
|
352
362
|
let browserStderr = "";
|
|
@@ -488,15 +498,33 @@ async function instantiateWebInstance(
|
|
|
488
498
|
payload = Buffer.from(payload);
|
|
489
499
|
}
|
|
490
500
|
wsBuffer = wsBuffer.subarray(offset + maskLength + length);
|
|
491
|
-
|
|
501
|
+
// Reassemble fragmented messages: a non-final data frame (0x1/0x2 with
|
|
502
|
+
// FIN=0) starts a sequence continued by 0x0 frames until FIN=1. Chromium
|
|
503
|
+
// fragments large (64KB) frames, so without this the wipc stream
|
|
504
|
+
// desyncs and the report payload is corrupted.
|
|
505
|
+
const fin = (first & 0x80) !== 0;
|
|
506
|
+
let effectiveOpcode = opcode;
|
|
507
|
+
if (opcode == 0x0) {
|
|
508
|
+
wsFragments.push(payload);
|
|
509
|
+
if (!fin) continue;
|
|
510
|
+
payload = Buffer.concat(wsFragments);
|
|
511
|
+
wsFragments = [];
|
|
512
|
+
effectiveOpcode = wsFragmentOpcode;
|
|
513
|
+
wsFragmentOpcode = 0;
|
|
514
|
+
} else if ((opcode == 0x1 || opcode == 0x2) && !fin) {
|
|
515
|
+
wsFragmentOpcode = opcode;
|
|
516
|
+
wsFragments = [payload];
|
|
517
|
+
continue;
|
|
518
|
+
}
|
|
519
|
+
if (effectiveOpcode == 0x8) {
|
|
492
520
|
finish(0);
|
|
493
521
|
return;
|
|
494
522
|
}
|
|
495
|
-
if (
|
|
523
|
+
if (effectiveOpcode == 0x1) {
|
|
496
524
|
onControl(payload.toString("utf8"));
|
|
497
525
|
continue;
|
|
498
526
|
}
|
|
499
|
-
if (
|
|
527
|
+
if (effectiveOpcode == 0x2) {
|
|
500
528
|
process.stdout.write(payload);
|
|
501
529
|
}
|
|
502
530
|
}
|
|
@@ -703,20 +731,33 @@ function createWasmImports(
|
|
|
703
731
|
module: WebAssembly.Module,
|
|
704
732
|
imports: AnyImports,
|
|
705
733
|
): AnyImports {
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
734
|
+
return stubMissingImports(module, mergeImports(withNodeIo({}), imports));
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// Fills any function import the module declares but the caller didn't provide
|
|
738
|
+
// with a no-op stub, so instantiation never fails on a missing import. This
|
|
739
|
+
// covers imports an unmocked `mockImport` target reintroduces (which keep
|
|
740
|
+
// their real binding but have no host implementation in tests). `skipModules`
|
|
741
|
+
// leaves bindings-helper-owned modules (env / wasi) for the helper to fill.
|
|
742
|
+
function stubMissingImports(
|
|
743
|
+
module: WebAssembly.Module,
|
|
744
|
+
imports: AnyImports,
|
|
745
|
+
skipModules: readonly string[] = [],
|
|
746
|
+
): AnyImports {
|
|
747
|
+
const skip = new Set(skipModules);
|
|
748
|
+
const bag = imports as Record<string, Record<string, unknown>>;
|
|
710
749
|
for (const entry of WebAssembly.Module.imports(module)) {
|
|
711
|
-
if (
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
750
|
+
if (entry.kind != "function" || skip.has(entry.module)) continue;
|
|
751
|
+
let mod = bag[entry.module];
|
|
752
|
+
if (!mod || typeof mod != "object") {
|
|
753
|
+
mod = {};
|
|
754
|
+
bag[entry.module] = mod;
|
|
755
|
+
}
|
|
756
|
+
if (!(entry.name in mod)) {
|
|
757
|
+
mod[entry.name] = () => 0;
|
|
717
758
|
}
|
|
718
759
|
}
|
|
719
|
-
return
|
|
760
|
+
return imports;
|
|
720
761
|
}
|
|
721
762
|
|
|
722
763
|
let patchedWasiWarning = false;
|
|
@@ -1306,6 +1347,16 @@ async function captureHelperInstance(
|
|
|
1306
1347
|
source: BufferSource | WebAssembly.Module,
|
|
1307
1348
|
importObject?: WebAssembly.Imports,
|
|
1308
1349
|
) => {
|
|
1350
|
+
// Stub any import the bindings helper left unprovided (e.g. an unmocked
|
|
1351
|
+
// mockImport target with no host implementation), so esm/raw bindings
|
|
1352
|
+
// instantiate instead of failing with a LinkError. env/wasi stay with the
|
|
1353
|
+
// helper.
|
|
1354
|
+
if (source instanceof WebAssembly.Module && importObject) {
|
|
1355
|
+
stubMissingImports(source, importObject as AnyImports, [
|
|
1356
|
+
"env",
|
|
1357
|
+
"wasi_snapshot_preview1",
|
|
1358
|
+
]);
|
|
1359
|
+
}
|
|
1309
1360
|
const result = await originalInstantiate(source, importObject);
|
|
1310
1361
|
if (result instanceof WebAssembly.Instance) {
|
|
1311
1362
|
instance = result;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "as-test",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.1",
|
|
4
4
|
"author": "Jairus Tanaka",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -74,30 +74,31 @@
|
|
|
74
74
|
"access": "public"
|
|
75
75
|
},
|
|
76
76
|
"scripts": {
|
|
77
|
-
"
|
|
78
|
-
"
|
|
79
|
-
"
|
|
80
|
-
"
|
|
77
|
+
"build": "npm run build:cli && npm run build:lib && npm run build:transform",
|
|
78
|
+
"build:cli": "rm -rf ./bin/ && tsc -p cli && prettier -w ./cli/ && chmod +x ./bin/index.js",
|
|
79
|
+
"build:lib": "tsc -p ./tsconfig.lib.json",
|
|
80
|
+
"build:transform": "rm -rf ./transform/lib && tsc -p ./transform && prettier -w ./transform/",
|
|
81
|
+
"test": "node ./bin/index.js test --parallel",
|
|
82
|
+
"test:integration": "node --test tests/*.test.mjs",
|
|
83
|
+
"test:modes": "npm run test -- --mode node:bindings,node:bindings:raw,node:bindings:esm,node:wasi,wasmtime,wasmer,wazero,firefox,chromium,webkit,firefox:headless,chromium:headless,webkit:headless",
|
|
84
|
+
"test:examples": "npm --prefix ./examples run test",
|
|
85
|
+
"test:all": "npm run build && npm run test:modes && npm run test:integration && npm run test:examples",
|
|
86
|
+
"test:ci": "npm run test -- --mode node:bindings,node:wasi,wasmtime",
|
|
81
87
|
"fuzz": "node ./bin/index.js fuzz",
|
|
82
88
|
"bench:seed": "node ./bin/index.js fuzz --config ./as-test.bench.config.json --clean",
|
|
83
89
|
"bench:seed:compare": "bash ./tools/bench-seed-compare.sh 7",
|
|
84
|
-
"test:examples": "npm --prefix ./examples run test",
|
|
85
|
-
"ci:act": "bash ./tools/act.sh push",
|
|
86
|
-
"ci:act:pr": "bash ./tools/act.sh pull_request",
|
|
87
|
-
"ci:act:tests": "act push -W .github/workflows/as-test.yml",
|
|
88
|
-
"ci:act:examples": "act push -W .github/workflows/examples.yml",
|
|
89
90
|
"typecheck": "tsc -p cli --noEmit && tsc -p tsconfig.lib.json --noEmit && tsc -p transform --noEmit",
|
|
90
91
|
"lint": "eslint transform/src/**/*.ts tools/**/*.js eslint.config.js",
|
|
91
|
-
"
|
|
92
|
-
"build:transform": "rm -rf ./transform/lib && tsc -p ./transform && prettier -w ./transform/",
|
|
93
|
-
"build:cli": "rm -rf ./bin/ && tsc -p cli && prettier -w ./cli/ && chmod +x ./bin/index.js",
|
|
94
|
-
"build:run": "npm run build:cli",
|
|
92
|
+
"format": "prettier -w .",
|
|
95
93
|
"docs:dev": "vitepress dev docs",
|
|
96
94
|
"docs:build": "vitepress build docs",
|
|
97
95
|
"docs:preview": "vitepress preview docs",
|
|
98
|
-
"
|
|
99
|
-
"
|
|
100
|
-
"
|
|
96
|
+
"release:check": "npm run build && npm run test && npm run test:examples && npm pack --dry-run --cache /tmp/as-test-npm-cache",
|
|
97
|
+
"prepublishOnly": "npm run build && npm run test && npm run format",
|
|
98
|
+
"ci:act": "bash ./tools/act.sh push",
|
|
99
|
+
"ci:act:pr": "bash ./tools/act.sh pull_request",
|
|
100
|
+
"ci:act:tests": "act push -W .github/workflows/as-test.yml",
|
|
101
|
+
"ci:act:examples": "act push -W .github/workflows/examples.yml",
|
|
101
102
|
"commitmsg:verify": "bash ./scripts/commit-msg.sh",
|
|
102
103
|
"precommit:verify": "bash ./scripts/pre-commit.sh",
|
|
103
104
|
"prepare": "husky"
|
package/transform/lib/equals.js
CHANGED
|
@@ -4,7 +4,7 @@ import { readFileSync } from "fs";
|
|
|
4
4
|
import { join } from "path";
|
|
5
5
|
import { SimpleParser, isStdlib } from "./util.js";
|
|
6
6
|
const EQUALS_METHOD = "__AS_TEST_EQUALS";
|
|
7
|
-
const
|
|
7
|
+
const GENERATED_TOJSON_METHOD = "__AS_TEST_TO_JSON";
|
|
8
8
|
const REFLECT_LOCAL = "__AS_TEST_REFLECT_EQUALS_INTERNAL";
|
|
9
9
|
const STRINGIFY_LOCAL = "__AS_TEST_STRINGIFY_INTERNAL";
|
|
10
10
|
const ALREADY_INJECTED_EQUALS = new WeakSet();
|
|
@@ -165,7 +165,7 @@ export class EqualsTransform {
|
|
|
165
165
|
injectToJSONMethod(klass) {
|
|
166
166
|
if (ALREADY_INJECTED_TOJSON.has(klass))
|
|
167
167
|
return false;
|
|
168
|
-
if (declaresMethod(klass,
|
|
168
|
+
if (declaresMethod(klass, GENERATED_TOJSON_METHOD))
|
|
169
169
|
return false;
|
|
170
170
|
if (hasAnyDecorator(klass, JSON_DECORATORS))
|
|
171
171
|
return false;
|
|
@@ -189,7 +189,7 @@ export class EqualsTransform {
|
|
|
189
189
|
const body = parts.length
|
|
190
190
|
? `return "{" + ${parts.join(" + ")} + "}";`
|
|
191
191
|
: `return "{}";`;
|
|
192
|
-
const code =
|
|
192
|
+
const code = `${GENERATED_TOJSON_METHOD}(): string { ${body} }`;
|
|
193
193
|
try {
|
|
194
194
|
const method = SimpleParser.parseClassMember(code, klass);
|
|
195
195
|
klass.members.push(method);
|
package/transform/lib/index.js
CHANGED
|
@@ -37,6 +37,10 @@ export default class Transformer extends Transform {
|
|
|
37
37
|
for (const target of mockedImportTargets) {
|
|
38
38
|
mock.importMocked.add(target);
|
|
39
39
|
}
|
|
40
|
+
const unmockedImportTargets = collectUnmockImportTargets(sources);
|
|
41
|
+
for (const target of unmockedImportTargets) {
|
|
42
|
+
mock.importUnmocked.add(target);
|
|
43
|
+
}
|
|
40
44
|
for (const source of sources) {
|
|
41
45
|
const sourceInfo = analyzeSourceText(source.text);
|
|
42
46
|
const shouldInjectRunCall = source.sourceKind == 1 &&
|
|
@@ -110,8 +114,13 @@ function patchModeName(parser, modeName) {
|
|
|
110
114
|
}
|
|
111
115
|
}
|
|
112
116
|
function collectMockImportTargets(sources) {
|
|
117
|
+
return collectImportTargets(sources, /\bmockImport\s*\(\s*["']([^"']+)["']/g);
|
|
118
|
+
}
|
|
119
|
+
function collectUnmockImportTargets(sources) {
|
|
120
|
+
return collectImportTargets(sources, /\bunmockImport\s*\(\s*["']([^"']+)["']/g);
|
|
121
|
+
}
|
|
122
|
+
function collectImportTargets(sources, pattern) {
|
|
113
123
|
const out = new Set();
|
|
114
|
-
const pattern = /\bmockImport\s*\(\s*["']([^"']+)["']/g;
|
|
115
124
|
for (const source of sources) {
|
|
116
125
|
const text = stripComments(source.text);
|
|
117
126
|
for (const match of text.matchAll(pattern)) {
|
package/transform/lib/mock.js
CHANGED
|
@@ -6,8 +6,11 @@ export class MockTransform extends Visitor {
|
|
|
6
6
|
globalStatements = [];
|
|
7
7
|
mocked = new Set();
|
|
8
8
|
importMocked = new Set();
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
importUnmocked = new Set();
|
|
10
|
+
pendingHoist = [];
|
|
11
|
+
pendingRemoval = [];
|
|
12
|
+
visitCallExpression(node, ref = null) {
|
|
13
|
+
super.visitCallExpression(node, ref);
|
|
11
14
|
const name = normalizeName(expressionName(node.expression));
|
|
12
15
|
if (this.mocked.has(name + "_mock")) {
|
|
13
16
|
node.expression = Node.createIdentifierExpression(name + "_mock", node.expression.range);
|
|
@@ -25,6 +28,7 @@ export class MockTransform extends Visitor {
|
|
|
25
28
|
if (!oldFn)
|
|
26
29
|
return;
|
|
27
30
|
this.mocked.delete(normalizeName(expressionName(oldFn)) + "_mock");
|
|
31
|
+
this.scheduleRemoval(node, ref);
|
|
28
32
|
return;
|
|
29
33
|
}
|
|
30
34
|
if (name == "unmockImport") {
|
|
@@ -34,26 +38,21 @@ export class MockTransform extends Visitor {
|
|
|
34
38
|
return;
|
|
35
39
|
const oldValue = node.args[0];
|
|
36
40
|
const callback = node.args[1];
|
|
37
|
-
if (!oldValue || !callback)
|
|
41
|
+
if (!oldValue || !callback || !callback.declaration)
|
|
38
42
|
return;
|
|
39
|
-
const
|
|
40
|
-
const newFn = Node.createFunctionDeclaration(Node.createIdentifierExpression(
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
if (stmt.range.start != node.range.start)
|
|
49
|
-
continue;
|
|
50
|
-
index = i;
|
|
51
|
-
break;
|
|
52
|
-
}
|
|
53
|
-
if (index === -1)
|
|
43
|
+
const mockName = normalizeName(expressionName(oldValue)) + "_mock";
|
|
44
|
+
const newFn = Node.createFunctionDeclaration(Node.createIdentifierExpression(mockName, callback.range), callback.declaration.decorators, 0, callback.declaration.typeParameters, callback.declaration.signature, callback.declaration.body, callback.declaration.arrowKind, callback.range);
|
|
45
|
+
this.pendingHoist.push(newFn);
|
|
46
|
+
this.mocked.add(mockName);
|
|
47
|
+
this.scheduleRemoval(node, ref);
|
|
48
|
+
}
|
|
49
|
+
scheduleRemoval(node, ref) {
|
|
50
|
+
const container = asStatementContainer(ref);
|
|
51
|
+
if (!container)
|
|
54
52
|
return;
|
|
55
|
-
|
|
56
|
-
|
|
53
|
+
const stmt = container.statements.find((s) => s.expression === node);
|
|
54
|
+
if (stmt)
|
|
55
|
+
this.pendingRemoval.push({ container, stmt });
|
|
57
56
|
}
|
|
58
57
|
visitFunctionDeclaration(node, isDefault) {
|
|
59
58
|
if (this.mocked.has(node.name.text))
|
|
@@ -62,11 +61,23 @@ export class MockTransform extends Visitor {
|
|
|
62
61
|
}
|
|
63
62
|
visitSource(node) {
|
|
64
63
|
this.mocked = new Set();
|
|
64
|
+
this.pendingHoist = [];
|
|
65
|
+
this.pendingRemoval = [];
|
|
65
66
|
this.srcCurrent = node;
|
|
66
67
|
super.visitSource(node);
|
|
67
68
|
const currentSource = this.srcCurrent;
|
|
68
69
|
if (!currentSource)
|
|
69
70
|
return;
|
|
71
|
+
for (const { container, stmt } of this.pendingRemoval) {
|
|
72
|
+
const i = container.statements.indexOf(stmt);
|
|
73
|
+
if (i !== -1)
|
|
74
|
+
container.statements.splice(i, 1);
|
|
75
|
+
}
|
|
76
|
+
this.pendingRemoval = [];
|
|
77
|
+
if (this.pendingHoist.length) {
|
|
78
|
+
currentSource.statements.unshift(...this.pendingHoist);
|
|
79
|
+
this.pendingHoist = [];
|
|
80
|
+
}
|
|
70
81
|
const stmts = currentSource.statements;
|
|
71
82
|
for (let index = 0; index < stmts.length; index++) {
|
|
72
83
|
const node = stmts[index];
|
|
@@ -98,6 +109,17 @@ export class MockTransform extends Visitor {
|
|
|
98
109
|
index++;
|
|
99
110
|
continue;
|
|
100
111
|
}
|
|
112
|
+
if (this.importUnmocked.has(path) && node.signature.returnType) {
|
|
113
|
+
const realName = "__as_test_real_" + node.name.text;
|
|
114
|
+
const r = node.range;
|
|
115
|
+
const wrapper = buildFallbackWrapper(node, path, realName, r);
|
|
116
|
+
const mutable = node;
|
|
117
|
+
mutable.name = Node.createIdentifierExpression(realName, r);
|
|
118
|
+
mutable.flags &= ~2;
|
|
119
|
+
stmts.splice(index + 1, 0, wrapper);
|
|
120
|
+
index++;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
101
123
|
const args = [
|
|
102
124
|
Node.createCallExpression(Node.createPropertyAccessExpression(Node.createIdentifierExpression("__mock_import", node.range), Node.createIdentifierExpression("get", node.range), node.range), null, [Node.createStringLiteralExpression(path, node.range)], node.range),
|
|
103
125
|
];
|
|
@@ -120,6 +142,27 @@ function isBodylessTopLevelFunction(node) {
|
|
|
120
142
|
"signature" in candidate &&
|
|
121
143
|
candidate.body == null);
|
|
122
144
|
}
|
|
145
|
+
function buildFallbackWrapper(node, path, realName, range) {
|
|
146
|
+
const mockImportGet = Node.createCallExpression(Node.createPropertyAccessExpression(Node.createIdentifierExpression("__mock_import", range), Node.createIdentifierExpression("get", range), range), null, [Node.createStringLiteralExpression(path, range)], range);
|
|
147
|
+
const indirectArgs = [mockImportGet];
|
|
148
|
+
const forwardArgs = [];
|
|
149
|
+
for (const param of node.signature.parameters) {
|
|
150
|
+
indirectArgs.push(Node.createIdentifierExpression(param.name.text, range));
|
|
151
|
+
forwardArgs.push(Node.createIdentifierExpression(param.name.text, range));
|
|
152
|
+
}
|
|
153
|
+
const ifMocked = Node.createIfStatement(Node.createCallExpression(Node.createPropertyAccessExpression(Node.createIdentifierExpression("__mock_import", range), Node.createIdentifierExpression("has", range), range), null, [Node.createStringLiteralExpression(path, range)], range), Node.createReturnStatement(Node.createCallExpression(Node.createIdentifierExpression("call_indirect", range), null, indirectArgs, range), range), null, range);
|
|
154
|
+
const callReal = Node.createReturnStatement(Node.createCallExpression(Node.createIdentifierExpression(realName, range), null, forwardArgs, range), range);
|
|
155
|
+
const signature = Node.createFunctionType(node.signature.parameters, node.signature.returnType, node.signature.explicitThisType, node.signature.isNullable, range);
|
|
156
|
+
const exported = (node.flags & 2) != 0;
|
|
157
|
+
return Node.createFunctionDeclaration(Node.createIdentifierExpression(node.name.text, range), null, exported ? 2 : 0, node.typeParameters, signature, Node.createBlockStatement([ifMocked, callReal], range), 0, range);
|
|
158
|
+
}
|
|
159
|
+
function asStatementContainer(ref) {
|
|
160
|
+
const candidate = ref;
|
|
161
|
+
if (candidate && Array.isArray(candidate.statements)) {
|
|
162
|
+
return candidate;
|
|
163
|
+
}
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
123
166
|
function normalizeName(value) {
|
|
124
167
|
return value
|
|
125
168
|
.replaceAll(".", "_")
|