@bytebrand/export-cars 1.26.38 → 1.26.39
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/dist/interfaces/export-item.interface.d.ts +5 -5
- package/dist/transformers/criteo.transformer.js +13 -8
- package/dist/transformers/criteo.transformer.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +65 -65
- package/src/interfaces/export-item.interface.ts +3 -3
- package/src/transformers/criteo.transformer.ts +22 -10
- package/test/criteo-mapping.test.ts +122 -0
package/package.json
CHANGED
|
@@ -1,65 +1,65 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@bytebrand/export-cars",
|
|
3
|
-
"version": "1.26.
|
|
4
|
-
"description": "",
|
|
5
|
-
"author": "bytebrand",
|
|
6
|
-
"license": "UNLICENSED",
|
|
7
|
-
"scripts": {
|
|
8
|
-
"prebuild": "rimraf dist",
|
|
9
|
-
"build": "nest build",
|
|
10
|
-
"prepublishOnly": "npm run build",
|
|
11
|
-
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
|
12
|
-
"lint": "eslint -c .eslintrc.js --ext .ts src/",
|
|
13
|
-
"start:dev": "nest start --watch",
|
|
14
|
-
"test": "ts-node --transpile-only test/google-feed-envkv.test.ts && ts-node --transpile-only test/google-feed-item-row.test.ts && ts-node --transpile-only test/publication-gate.test.ts && ts-node --transpile-only test/images-regression.test.ts && ts-node --transpile-only test/clean-images.test.ts && ts-node --transpile-only test/image-url-validator.test.ts && ts-node --transpile-only test/openai-image-fields.test.ts && ts-node --transpile-only test/openai-ads-mapping.test.ts && ts-node --transpile-only test/openai-ads-feed-run.test.ts && ts-node --transpile-only test/openai-ads-finance.test.ts && ts-node --transpile-only test/openai-ads-price-cap.test.ts && ts-node --transpile-only test/meinstadt-mapping.test.ts && ts-node --transpile-only test/meinstadt-feed-run.test.ts && ts-node --transpile-only test/autouncle-mapping.test.ts && ts-node --transpile-only test/autouncle-feed-run.test.ts && ts-node --transpile-only test/criteo-images.test.ts"
|
|
15
|
-
},
|
|
16
|
-
"dependencies": {
|
|
17
|
-
"@bytebrand/autode-request": "^1.19.2",
|
|
18
|
-
"@bytebrand/bigquery-logger": "^2.1.2",
|
|
19
|
-
"@bytebrand/car-basic-data-scope": "^9.11.24",
|
|
20
|
-
"@bytebrand/car-description": "^1.17.20",
|
|
21
|
-
"@bytebrand/car-schema": "^3.14.15",
|
|
22
|
-
"@bytebrand/chunkable": "^5.19.8",
|
|
23
|
-
"@bytebrand/cronicle-helper": "^1.21.8",
|
|
24
|
-
"@bytebrand/fe-financing": "^5.14.19",
|
|
25
|
-
"@bytebrand/i18n-dictionaries": "^1.5.0",
|
|
26
|
-
"@bytebrand/import-helper": "^0.45.0",
|
|
27
|
-
"@bytebrand/schema-tools": "^3.18.8",
|
|
28
|
-
"@bytebrand/zipper": "^2.2.0",
|
|
29
|
-
"@nestjs/cache-manager": "^2.1.0",
|
|
30
|
-
"@nestjs/common": "^10.2.7",
|
|
31
|
-
"@nestjs/core": "^10.2.7",
|
|
32
|
-
"adm-zip": "0.5.5",
|
|
33
|
-
"axios": "^1.5.1",
|
|
34
|
-
"bottleneck": "^2.19.5",
|
|
35
|
-
"cache-manager": "^5.2.4",
|
|
36
|
-
"csv-write-stream": "^2.0.0",
|
|
37
|
-
"google-spreadsheet": "^3.3.0",
|
|
38
|
-
"lodash": "^4.17.21",
|
|
39
|
-
"moment": "^2.29.4",
|
|
40
|
-
"nest-commander": "~3.15.0",
|
|
41
|
-
"reflect-metadata": "^0.1.13",
|
|
42
|
-
"vin-generator": "^1.0.1"
|
|
43
|
-
},
|
|
44
|
-
"devDependencies": {
|
|
45
|
-
"@nestjs/cli": "^10.1.18",
|
|
46
|
-
"@types/google-spreadsheet": "^3.3.0",
|
|
47
|
-
"@types/inquirer": "^8.2.4",
|
|
48
|
-
"@types/lodash": "^4.14.186",
|
|
49
|
-
"@types/node": "^16.18.58",
|
|
50
|
-
"@typescript-eslint/eslint-plugin": "^6.8.0",
|
|
51
|
-
"@typescript-eslint/parser": "^6.8.0",
|
|
52
|
-
"eslint": "^8.0.1",
|
|
53
|
-
"eslint-config-prettier": "^8.3.0",
|
|
54
|
-
"eslint-plugin-prettier": "^4.0.0",
|
|
55
|
-
"gts": "^3.1.1",
|
|
56
|
-
"prettier": "^2.3.2",
|
|
57
|
-
"ts-node": "^10.0.0",
|
|
58
|
-
"tsconfig-paths": "4.1.0",
|
|
59
|
-
"typescript": "^5.2.2"
|
|
60
|
-
},
|
|
61
|
-
"engines": {
|
|
62
|
-
"node": ">=14.0.0"
|
|
63
|
-
},
|
|
64
|
-
"gitHead": "e827f35865032821bf3a081f03a026d3d3f8a540"
|
|
65
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@bytebrand/export-cars",
|
|
3
|
+
"version": "1.26.39",
|
|
4
|
+
"description": "",
|
|
5
|
+
"author": "bytebrand",
|
|
6
|
+
"license": "UNLICENSED",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"prebuild": "rimraf dist",
|
|
9
|
+
"build": "nest build",
|
|
10
|
+
"prepublishOnly": "npm run build",
|
|
11
|
+
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
|
12
|
+
"lint": "eslint -c .eslintrc.js --ext .ts src/",
|
|
13
|
+
"start:dev": "nest start --watch",
|
|
14
|
+
"test": "ts-node --transpile-only test/google-feed-envkv.test.ts && ts-node --transpile-only test/google-feed-item-row.test.ts && ts-node --transpile-only test/publication-gate.test.ts && ts-node --transpile-only test/images-regression.test.ts && ts-node --transpile-only test/clean-images.test.ts && ts-node --transpile-only test/image-url-validator.test.ts && ts-node --transpile-only test/openai-image-fields.test.ts && ts-node --transpile-only test/openai-ads-mapping.test.ts && ts-node --transpile-only test/openai-ads-feed-run.test.ts && ts-node --transpile-only test/openai-ads-finance.test.ts && ts-node --transpile-only test/openai-ads-price-cap.test.ts && ts-node --transpile-only test/meinstadt-mapping.test.ts && ts-node --transpile-only test/meinstadt-feed-run.test.ts && ts-node --transpile-only test/autouncle-mapping.test.ts && ts-node --transpile-only test/autouncle-feed-run.test.ts && ts-node --transpile-only test/criteo-images.test.ts && ts-node --transpile-only test/criteo-mapping.test.ts"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@bytebrand/autode-request": "^1.19.2",
|
|
18
|
+
"@bytebrand/bigquery-logger": "^2.1.2",
|
|
19
|
+
"@bytebrand/car-basic-data-scope": "^9.11.24",
|
|
20
|
+
"@bytebrand/car-description": "^1.17.20",
|
|
21
|
+
"@bytebrand/car-schema": "^3.14.15",
|
|
22
|
+
"@bytebrand/chunkable": "^5.19.8",
|
|
23
|
+
"@bytebrand/cronicle-helper": "^1.21.8",
|
|
24
|
+
"@bytebrand/fe-financing": "^5.14.19",
|
|
25
|
+
"@bytebrand/i18n-dictionaries": "^1.5.0",
|
|
26
|
+
"@bytebrand/import-helper": "^0.45.0",
|
|
27
|
+
"@bytebrand/schema-tools": "^3.18.8",
|
|
28
|
+
"@bytebrand/zipper": "^2.2.0",
|
|
29
|
+
"@nestjs/cache-manager": "^2.1.0",
|
|
30
|
+
"@nestjs/common": "^10.2.7",
|
|
31
|
+
"@nestjs/core": "^10.2.7",
|
|
32
|
+
"adm-zip": "0.5.5",
|
|
33
|
+
"axios": "^1.5.1",
|
|
34
|
+
"bottleneck": "^2.19.5",
|
|
35
|
+
"cache-manager": "^5.2.4",
|
|
36
|
+
"csv-write-stream": "^2.0.0",
|
|
37
|
+
"google-spreadsheet": "^3.3.0",
|
|
38
|
+
"lodash": "^4.17.21",
|
|
39
|
+
"moment": "^2.29.4",
|
|
40
|
+
"nest-commander": "~3.15.0",
|
|
41
|
+
"reflect-metadata": "^0.1.13",
|
|
42
|
+
"vin-generator": "^1.0.1"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@nestjs/cli": "^10.1.18",
|
|
46
|
+
"@types/google-spreadsheet": "^3.3.0",
|
|
47
|
+
"@types/inquirer": "^8.2.4",
|
|
48
|
+
"@types/lodash": "^4.14.186",
|
|
49
|
+
"@types/node": "^16.18.58",
|
|
50
|
+
"@typescript-eslint/eslint-plugin": "^6.8.0",
|
|
51
|
+
"@typescript-eslint/parser": "^6.8.0",
|
|
52
|
+
"eslint": "^8.0.1",
|
|
53
|
+
"eslint-config-prettier": "^8.3.0",
|
|
54
|
+
"eslint-plugin-prettier": "^4.0.0",
|
|
55
|
+
"gts": "^3.1.1",
|
|
56
|
+
"prettier": "^2.3.2",
|
|
57
|
+
"ts-node": "^10.0.0",
|
|
58
|
+
"tsconfig-paths": "4.1.0",
|
|
59
|
+
"typescript": "^5.2.2"
|
|
60
|
+
},
|
|
61
|
+
"engines": {
|
|
62
|
+
"node": ">=14.0.0"
|
|
63
|
+
},
|
|
64
|
+
"gitHead": "e827f35865032821bf3a081f03a026d3d3f8a540"
|
|
65
|
+
}
|
|
@@ -152,10 +152,10 @@ export interface CriteoXmlItem {
|
|
|
152
152
|
brand: any;
|
|
153
153
|
price: string;
|
|
154
154
|
sale_price: string;
|
|
155
|
-
energy_efficiency_class
|
|
155
|
+
energy_efficiency_class?: string;
|
|
156
156
|
color: string;
|
|
157
|
-
installment
|
|
158
|
-
custom_label_0
|
|
157
|
+
installment?: { paybackPeriod: number; monthlyInstallment: number };
|
|
158
|
+
custom_label_0?: string;
|
|
159
159
|
}
|
|
160
160
|
|
|
161
161
|
export interface GoogleXmlItem {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { get, join, round, toString, trim } from 'lodash';
|
|
2
2
|
import { CarDescriptionFactory, DescriptionTypeEnum } from '@bytebrand/car-description';
|
|
3
3
|
import { DataSchemes } from '@bytebrand/car-schema';
|
|
4
4
|
import { TranslatorExtended } from '@bytebrand/schema-tools';
|
|
@@ -34,6 +34,11 @@ export class CriteoTransformer {
|
|
|
34
34
|
return null;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
const energyClass = this.getEnergyClass(car);
|
|
38
|
+
const installment = this.getInstallment(car);
|
|
39
|
+
|
|
40
|
+
// Unknown energy class and a missing rate leave their elements out of the item,
|
|
41
|
+
// instead of sending "default" or a "0 EUR" monthly rate.
|
|
37
42
|
return {
|
|
38
43
|
id: carId,
|
|
39
44
|
title: this.getTitle(car),
|
|
@@ -47,17 +52,20 @@ export class CriteoTransformer {
|
|
|
47
52
|
brand: get(car, 'mainData.make', ''),
|
|
48
53
|
price: this.getRetailPrice(car),
|
|
49
54
|
sale_price: this.getPrice(car),
|
|
50
|
-
energy_efficiency_class:
|
|
55
|
+
...(energyClass ? { energy_efficiency_class: energyClass } : {}),
|
|
51
56
|
color: this.getColor(car),
|
|
52
|
-
installment
|
|
53
|
-
|
|
57
|
+
...(installment
|
|
58
|
+
? { installment, custom_label_0: `${installment.monthlyInstallment} EUR mtl.` }
|
|
59
|
+
: {}),
|
|
54
60
|
};
|
|
55
61
|
}
|
|
56
62
|
|
|
63
|
+
// No HTML escaping here: GoogleXmlBuilder escapes every value for XML.
|
|
64
|
+
// Escaping twice turned "cee'd" into "cee'd".
|
|
57
65
|
private getTitle(car: DataSchemes.Car): string {
|
|
58
66
|
const model = toString(get(car, 'mainData.model', ''));
|
|
59
67
|
|
|
60
|
-
return trim(
|
|
68
|
+
return trim(join([this.getBrand(car), model], ' '));
|
|
61
69
|
}
|
|
62
70
|
|
|
63
71
|
private async getDescription(car: DataSchemes.Car): Promise<string> {
|
|
@@ -143,16 +151,20 @@ export class CriteoTransformer {
|
|
|
143
151
|
selector_energyEfficiencyClass_e: 'E',
|
|
144
152
|
selector_energyEfficiencyClass_f: 'F',
|
|
145
153
|
selector_energyEfficiencyClass_g: 'G',
|
|
146
|
-
selector_unknown: 'default',
|
|
147
154
|
};
|
|
148
155
|
// ADV-187: canonical CO2-derived WLTP class, not the unvalidated legacy NEFZ class.
|
|
149
|
-
|
|
156
|
+
// Unknown or unmapped class -> undefined, so the element is left out.
|
|
157
|
+
const energyClass = get(car, 'environmentEmissions.wltpEnergyEfficiencyClass');
|
|
150
158
|
return get(available, energyClass);
|
|
151
159
|
}
|
|
152
160
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
const
|
|
161
|
+
// A rate is sent only when the car has a real one: both values present and above zero.
|
|
162
|
+
private getInstallment(car: DataSchemes.Car): CriteoXmlItem['installment'] | undefined {
|
|
163
|
+
const monthlyInstallment = round(Number(get(car, 'rate.monthlyInstallment')));
|
|
164
|
+
const paybackPeriod = round(Number(get(car, 'rate.paybackPeriod')));
|
|
165
|
+
if (!(monthlyInstallment > 0) || !(paybackPeriod > 0)) {
|
|
166
|
+
return undefined;
|
|
167
|
+
}
|
|
156
168
|
|
|
157
169
|
return {
|
|
158
170
|
paybackPeriod,
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// Run: npm test (ts-node, no test framework needed)
|
|
2
|
+
// ADV-319: Criteo item mapping bugs found in the DEV feed of 27.10.2026.
|
|
3
|
+
// - custom_label_0 was "[object Object] EUR mtl." for every car;
|
|
4
|
+
// - a car without a rate got "<g:months>0</g:months><g:amount>0 EUR</g:amount>";
|
|
5
|
+
// - a title with an apostrophe was escaped twice ("cee&#39;d");
|
|
6
|
+
// - an unknown energy class was sent as the literal "default".
|
|
7
|
+
import * as assert from 'node:assert/strict';
|
|
8
|
+
import * as fs from 'node:fs';
|
|
9
|
+
import * as os from 'node:os';
|
|
10
|
+
import * as path from 'node:path';
|
|
11
|
+
import { CriteoTransformer } from '../src/transformers/criteo.transformer';
|
|
12
|
+
import { GoogleXmlBuilder } from '../src/formats/google-xml.builder';
|
|
13
|
+
|
|
14
|
+
const IMAGE = 'https://images.autode-dev.de/generated_images/0/0/0/00000000-0000-4000-8000-000000000001/criteo-Test.jpg';
|
|
15
|
+
|
|
16
|
+
const baseCar = (patch: any = {}): any => ({
|
|
17
|
+
_id: '00000000-0000-4000-8000-000000000001',
|
|
18
|
+
mainData: { make: 'Volkswagen', model: 'Golf', condition: 'selector_condition_used', category: 'selector_category_limousine' },
|
|
19
|
+
price: { currentSalesPrice: 19990 },
|
|
20
|
+
offer: { availabilityMode: 'selector_availabilityMode_always' },
|
|
21
|
+
environmentEmissions: { wltpEnergyEfficiencyClass: 'selector_energyEfficiencyClass_d' },
|
|
22
|
+
rate: { monthlyInstallment: 164.6, paybackPeriod: 60 },
|
|
23
|
+
metaData: { ownerId: 'test-dealer', imagesCount: 5 },
|
|
24
|
+
...patch,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const transformer = new CriteoTransformer(
|
|
28
|
+
{} as any,
|
|
29
|
+
{ config: { isCriteo: true, imageType: 'marketing', domain: 'autode-dev.de' } } as any,
|
|
30
|
+
{ getImages: async () => [IMAGE] } as any,
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
const renderItem = async (item: any): Promise<string> => {
|
|
34
|
+
const file = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'criteo-mapping-')), 'feed.xml');
|
|
35
|
+
const builder = new GoogleXmlBuilder(file);
|
|
36
|
+
await builder.buildStart('autode-dev.de');
|
|
37
|
+
await builder.addItem(item);
|
|
38
|
+
await builder.buildEnd();
|
|
39
|
+
return fs.readFileSync(file, 'utf8');
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const tests: Array<[string, () => Promise<void>]> = [
|
|
43
|
+
['custom_label_0 carries the rounded monthly rate, installment keeps months and amount', async () => {
|
|
44
|
+
const item = await transformer.transform({ car: baseCar(), images: [] });
|
|
45
|
+
assert.equal(item?.custom_label_0, '165 EUR mtl.');
|
|
46
|
+
assert.deepEqual(item?.installment, { paybackPeriod: 60, monthlyInstallment: 165 });
|
|
47
|
+
const xml = await renderItem(item);
|
|
48
|
+
assert.match(xml, /<g:months>60<\/g:months>/);
|
|
49
|
+
assert.match(xml, /<g:amount>165 EUR<\/g:amount>/);
|
|
50
|
+
assert.ok(!xml.includes('[object Object]'));
|
|
51
|
+
}],
|
|
52
|
+
['car without a rate has no installment and no custom_label_0', async () => {
|
|
53
|
+
for (const rate of [undefined, null, {}, { monthlyInstallment: 0, paybackPeriod: 0 }, { monthlyInstallment: 199 }, { paybackPeriod: 48 }]) {
|
|
54
|
+
const item = await transformer.transform({ car: baseCar({ rate }), images: [] });
|
|
55
|
+
assert.ok(item, `item for rate ${JSON.stringify(rate)}`);
|
|
56
|
+
assert.ok(!('installment' in item), `installment for rate ${JSON.stringify(rate)}`);
|
|
57
|
+
assert.ok(!('custom_label_0' in item), `custom_label_0 for rate ${JSON.stringify(rate)}`);
|
|
58
|
+
const xml = await renderItem(item);
|
|
59
|
+
assert.ok(!xml.includes('g:installment') && !xml.includes('g:custom_label_0'));
|
|
60
|
+
}
|
|
61
|
+
}],
|
|
62
|
+
['title with an apostrophe is escaped once, by the XML builder', async () => {
|
|
63
|
+
const item = await transformer.transform({
|
|
64
|
+
car: baseCar({ mainData: { make: 'Kia', model: "cee'd", condition: 'selector_condition_used' } }),
|
|
65
|
+
images: [],
|
|
66
|
+
});
|
|
67
|
+
assert.equal(item?.title, "Kia cee'd");
|
|
68
|
+
const xml = await renderItem(item);
|
|
69
|
+
assert.match(xml, /<g:title>Kia cee'd<\/g:title>/);
|
|
70
|
+
assert.ok(!xml.includes('&#39;'));
|
|
71
|
+
}],
|
|
72
|
+
['title keeps the Mercedes / VW brand aliases (ACF-6332, ACF-6534)', async () => {
|
|
73
|
+
const vw = await transformer.transform({ car: baseCar(), images: [] });
|
|
74
|
+
assert.equal(vw?.title, 'VW Golf');
|
|
75
|
+
assert.equal(vw?.brand, 'Volkswagen');
|
|
76
|
+
const mb = await transformer.transform({
|
|
77
|
+
car: baseCar({ mainData: { make: 'Mercedes-Benz', model: 'A 180', condition: 'selector_condition_used' } }),
|
|
78
|
+
images: [],
|
|
79
|
+
});
|
|
80
|
+
assert.equal(mb?.title, 'Mercedes A 180');
|
|
81
|
+
}],
|
|
82
|
+
['known energy class is sent, unknown or missing class leaves the element out', async () => {
|
|
83
|
+
const known = await transformer.transform({ car: baseCar(), images: [] });
|
|
84
|
+
assert.equal(known?.energy_efficiency_class, 'D');
|
|
85
|
+
for (const environmentEmissions of [{ wltpEnergyEfficiencyClass: 'selector_unknown' }, {}, undefined]) {
|
|
86
|
+
const item = await transformer.transform({ car: baseCar({ environmentEmissions }), images: [] });
|
|
87
|
+
assert.ok(item && !('energy_efficiency_class' in item));
|
|
88
|
+
const xml = await renderItem(item);
|
|
89
|
+
assert.ok(!xml.includes('g:energy_efficiency_class') && !xml.includes('>default<'));
|
|
90
|
+
}
|
|
91
|
+
}],
|
|
92
|
+
['unchanged legacy fields: id, link params, price, condition, availability', async () => {
|
|
93
|
+
const item = await transformer.transform({ car: baseCar(), images: [] });
|
|
94
|
+
assert.equal(item?.id, '00000000-0000-4000-8000-000000000001');
|
|
95
|
+
assert.equal(
|
|
96
|
+
item?.link,
|
|
97
|
+
'https://www.autode-dev.de/search/vehicle/00000000-0000-4000-8000-000000000001?source=criteo&medium=cpc&campaign=retargeting',
|
|
98
|
+
);
|
|
99
|
+
assert.equal(item?.price, '19990 EUR');
|
|
100
|
+
assert.equal(item?.sale_price, '19990 EUR');
|
|
101
|
+
assert.equal(item?.condition, 'Gebraucht');
|
|
102
|
+
assert.equal(item?.availability, 'Sofort');
|
|
103
|
+
assert.deepEqual(Object.keys(item as any), [
|
|
104
|
+
'id', 'title', 'description', 'google_product_category', 'product_type', 'link', 'image_link', 'condition',
|
|
105
|
+
'availability', 'brand', 'price', 'sale_price', 'energy_efficiency_class', 'color', 'installment', 'custom_label_0',
|
|
106
|
+
]);
|
|
107
|
+
}],
|
|
108
|
+
];
|
|
109
|
+
|
|
110
|
+
(async () => {
|
|
111
|
+
let failed = 0;
|
|
112
|
+
for (const [name, fn] of tests) {
|
|
113
|
+
try {
|
|
114
|
+
await fn();
|
|
115
|
+
console.log(`ok ${name}`);
|
|
116
|
+
} catch (e) {
|
|
117
|
+
failed += 1;
|
|
118
|
+
console.error(`FAIL ${name}\n ${e.message}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (failed) process.exit(1);
|
|
122
|
+
})();
|