codify-plugin-lib 1.0.182-beta30 → 1.0.182-beta32
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/plugin/plugin.d.ts +3 -3
- package/dist/plugin/plugin.js +17 -3
- package/dist/resource/resource-controller.js +2 -2
- package/dist/resource/resource-settings.d.ts +1 -1
- package/dist/resource/resource-settings.js +2 -2
- package/dist/utils/load-resources.d.ts +1 -0
- package/dist/utils/load-resources.js +43 -0
- package/dist/utils/package-json-utils.d.ts +12 -0
- package/dist/utils/package-json-utils.js +34 -0
- package/package.json +1 -1
- package/src/plugin/plugin.test.ts +1 -1
- package/src/plugin/plugin.ts +19 -3
- package/src/resource/parsed-resource-settings.test.ts +1 -1
- package/src/resource/parsed-resource-settings.ts +0 -1
- package/src/resource/resource-controller.ts +2 -2
- package/src/resource/resource-settings.ts +3 -3
- package/src/utils/load-resources.ts +48 -0
- package/src/utils/package-json-utils.ts +40 -0
package/dist/plugin/plugin.d.ts
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { ApplyRequestData, GetResourceInfoRequestData, GetResourceInfoResponseData, ImportRequestData, ImportResponseData, InitializeRequestData, InitializeResponseData, MatchRequestData, MatchResponseData, PlanRequestData, PlanResponseData, ResourceConfig, ResourceJson, ValidateRequestData, ValidateResponseData } from 'codify-schemas';
|
|
2
2
|
import { Plan } from '../plan/plan.js';
|
|
3
3
|
import { BackgroundPty } from '../pty/background-pty.js';
|
|
4
|
-
import { Resource } from '../resource/resource.js';
|
|
5
4
|
import { ResourceController } from '../resource/resource-controller.js';
|
|
6
5
|
export declare class Plugin {
|
|
7
6
|
name: string;
|
|
7
|
+
version: string;
|
|
8
8
|
resourceControllers: Map<string, ResourceController<ResourceConfig>>;
|
|
9
9
|
planStorage: Map<string, Plan<any>>;
|
|
10
10
|
planPty: BackgroundPty;
|
|
11
|
-
constructor(name: string, resourceControllers: Map<string, ResourceController<ResourceConfig>>);
|
|
12
|
-
static create(
|
|
11
|
+
constructor(name: string, version: string, resourceControllers: Map<string, ResourceController<ResourceConfig>>);
|
|
12
|
+
static create(): Promise<Plugin>;
|
|
13
13
|
initialize(data: InitializeRequestData): Promise<InitializeResponseData>;
|
|
14
14
|
getResourceInfo(data: GetResourceInfoRequestData): GetResourceInfoResponseData;
|
|
15
15
|
match(data: MatchRequestData): Promise<MatchResponseData>;
|
package/dist/plugin/plugin.js
CHANGED
|
@@ -4,23 +4,37 @@ import { BackgroundPty } from '../pty/background-pty.js';
|
|
|
4
4
|
import { getPty } from '../pty/index.js';
|
|
5
5
|
import { SequentialPty } from '../pty/seqeuntial-pty.js';
|
|
6
6
|
import { ResourceController } from '../resource/resource-controller.js';
|
|
7
|
+
import { listAllResources } from '../utils/load-resources.js';
|
|
8
|
+
import { readNearestPackageJson } from '../utils/package-json-utils.js';
|
|
7
9
|
import { ptyLocalStorage } from '../utils/pty-local-storage.js';
|
|
8
10
|
import { VerbosityLevel } from '../utils/verbosity-level.js';
|
|
9
11
|
export class Plugin {
|
|
10
12
|
name;
|
|
13
|
+
version;
|
|
11
14
|
resourceControllers;
|
|
12
15
|
planStorage;
|
|
13
16
|
planPty = new BackgroundPty();
|
|
14
|
-
constructor(name, resourceControllers) {
|
|
17
|
+
constructor(name, version, resourceControllers) {
|
|
15
18
|
this.name = name;
|
|
19
|
+
this.version = version;
|
|
16
20
|
this.resourceControllers = resourceControllers;
|
|
17
21
|
this.planStorage = new Map();
|
|
18
22
|
}
|
|
19
|
-
static create(
|
|
23
|
+
static async create() {
|
|
24
|
+
const packageJson = readNearestPackageJson();
|
|
25
|
+
if (!packageJson) {
|
|
26
|
+
throw new Error('Failed to read nearest package.json');
|
|
27
|
+
}
|
|
28
|
+
const { name, version } = packageJson;
|
|
29
|
+
const resourceLocations = await listAllResources();
|
|
30
|
+
const resources = await Promise.all(resourceLocations.map((l) => {
|
|
31
|
+
return import(l);
|
|
32
|
+
}));
|
|
33
|
+
console.log(resources);
|
|
20
34
|
const controllers = resources
|
|
21
35
|
.map((resource) => new ResourceController(resource));
|
|
22
36
|
const controllersMap = new Map(controllers.map((r) => [r.typeId, r]));
|
|
23
|
-
return new Plugin(name, controllersMap);
|
|
37
|
+
return new Plugin(name, version, controllersMap);
|
|
24
38
|
}
|
|
25
39
|
async initialize(data) {
|
|
26
40
|
if (data.verbosityLevel) {
|
|
@@ -374,8 +374,8 @@ ${JSON.stringify(refresh, null, 2)}
|
|
|
374
374
|
.sort((a, b) => this.parsedSettings.statefulParameterOrder.get(a.name) - this.parsedSettings.statefulParameterOrder.get(b.name));
|
|
375
375
|
}
|
|
376
376
|
getAllParameterKeys() {
|
|
377
|
-
return this.
|
|
378
|
-
? Object.keys(this.
|
|
377
|
+
return this.parsedSettings.schema
|
|
378
|
+
? Object.keys(this.parsedSettings.schema?.properties)
|
|
379
379
|
: Object.keys(this.parsedSettings.parameterSettings);
|
|
380
380
|
}
|
|
381
381
|
getParametersToRefreshForImport(parameters, context) {
|
|
@@ -2,8 +2,8 @@ import { JSONSchemaType } from 'ajv';
|
|
|
2
2
|
import { OS, StringIndexedObject } from 'codify-schemas';
|
|
3
3
|
import { ZodObject } from 'zod';
|
|
4
4
|
import { ArrayStatefulParameter, StatefulParameter } from '../stateful-parameter/stateful-parameter.js';
|
|
5
|
-
import { RefreshContext } from './resource.js';
|
|
6
5
|
import { ParsedResourceSettings } from './parsed-resource-settings.js';
|
|
6
|
+
import { RefreshContext } from './resource.js';
|
|
7
7
|
export interface InputTransformation {
|
|
8
8
|
to: (input: any) => Promise<any> | any;
|
|
9
9
|
from: (current: any, original: any) => Promise<any> | any;
|
|
@@ -3,7 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import { addVariablesToPath, areArraysEqual, resolvePathWithVariables, tildify, untildify } from '../utils/functions.js';
|
|
4
4
|
const ParameterEqualsDefaults = {
|
|
5
5
|
'boolean': (a, b) => Boolean(a) === Boolean(b),
|
|
6
|
-
'directory'
|
|
6
|
+
'directory'(a, b) {
|
|
7
7
|
let transformedA = resolvePathWithVariables(untildify(String(a)));
|
|
8
8
|
let transformedB = resolvePathWithVariables(untildify(String(b)));
|
|
9
9
|
if (transformedA.startsWith('.')) { // Only relative paths start with '.'
|
|
@@ -65,7 +65,7 @@ export function resolveFnFromEqualsFnOrString(fnOrString) {
|
|
|
65
65
|
const ParameterTransformationDefaults = {
|
|
66
66
|
'directory': {
|
|
67
67
|
to: (a) => resolvePathWithVariables((untildify(String(a)))),
|
|
68
|
-
from
|
|
68
|
+
from(a, original) {
|
|
69
69
|
if (ParameterEqualsDefaults.directory(a, original)) {
|
|
70
70
|
return original;
|
|
71
71
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const listAllResources: () => Promise<string[]>;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
export const listAllResources = async () => {
|
|
4
|
+
const resourceDir = await fs.readdir('./src/resources');
|
|
5
|
+
const dedupSet = new Set();
|
|
6
|
+
const result = new Set();
|
|
7
|
+
for (const folder of resourceDir) {
|
|
8
|
+
if (await fs.stat(path.join('./src/resources', folder)).then(s => s.isDirectory()).catch(() => false)) {
|
|
9
|
+
for (const folderContents of await fs.readdir(path.join('./src/resources', folder))) {
|
|
10
|
+
const isDirectory = await fs.stat(path.join('./src/resources', folder, folderContents)).then(s => s.isDirectory());
|
|
11
|
+
// console.log(folderContents, isDirectory);
|
|
12
|
+
if (isDirectory) {
|
|
13
|
+
for (const innerContents of await fs.readdir(path.join('./src/resources', folder, folderContents))) {
|
|
14
|
+
if (!dedupSet.has(path.join('./src/resources', folder, folderContents))) {
|
|
15
|
+
dedupSet.add(path.join('./src/resources', folder, folderContents));
|
|
16
|
+
addResourceFromDir(path.join('./src/resources', folder, folderContents), result);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
if (!dedupSet.has(path.join('./src/resources', folder))) {
|
|
22
|
+
dedupSet.add(path.join('./src/resources', folder));
|
|
23
|
+
addResourceFromDir(path.join('./src/resources', folder), result);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
throw new Error('Only directories are allowed in resources folder');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return [...result];
|
|
33
|
+
};
|
|
34
|
+
function addResourceFromDir(dir, result) {
|
|
35
|
+
try {
|
|
36
|
+
const resourceFile = path.resolve(path.join(dir, 'resource.ts'));
|
|
37
|
+
if (!(fs.stat(resourceFile).then((s) => s.isFile())).catch(() => false)) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
result.add(resourceFile);
|
|
41
|
+
}
|
|
42
|
+
catch { }
|
|
43
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Find the nearest package.json starting from a directory and walking upward.
|
|
3
|
+
* @param {string} startDir - Directory to start searching from
|
|
4
|
+
* @returns {string|null} Absolute path to package.json or null if not found
|
|
5
|
+
*/
|
|
6
|
+
export declare function findNearestPackageJson(startDir?: string): string | null;
|
|
7
|
+
/**
|
|
8
|
+
* Read and parse the nearest package.json
|
|
9
|
+
* @param {string} startDir
|
|
10
|
+
* @returns {object|null}
|
|
11
|
+
*/
|
|
12
|
+
export declare function readNearestPackageJson(startDir?: string): any;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
/**
|
|
4
|
+
* Find the nearest package.json starting from a directory and walking upward.
|
|
5
|
+
* @param {string} startDir - Directory to start searching from
|
|
6
|
+
* @returns {string|null} Absolute path to package.json or null if not found
|
|
7
|
+
*/
|
|
8
|
+
export function findNearestPackageJson(startDir = process.cwd()) {
|
|
9
|
+
let currentDir = path.resolve(startDir);
|
|
10
|
+
while (true) {
|
|
11
|
+
const pkgPath = path.join(currentDir, "package.json");
|
|
12
|
+
if (fs.existsSync(pkgPath)) {
|
|
13
|
+
return pkgPath;
|
|
14
|
+
}
|
|
15
|
+
const parentDir = path.dirname(currentDir);
|
|
16
|
+
if (parentDir === currentDir) {
|
|
17
|
+
// Reached filesystem root
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
currentDir = parentDir;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Read and parse the nearest package.json
|
|
25
|
+
* @param {string} startDir
|
|
26
|
+
* @returns {object|null}
|
|
27
|
+
*/
|
|
28
|
+
export function readNearestPackageJson(startDir = process.cwd()) {
|
|
29
|
+
const pkgPath = findNearestPackageJson(startDir);
|
|
30
|
+
if (!pkgPath)
|
|
31
|
+
return null;
|
|
32
|
+
const contents = fs.readFileSync(pkgPath, 'utf8');
|
|
33
|
+
return JSON.parse(contents);
|
|
34
|
+
}
|
package/package.json
CHANGED
|
@@ -177,7 +177,7 @@ describe('Plugin tests', () => {
|
|
|
177
177
|
plugins: z
|
|
178
178
|
.array(z.string())
|
|
179
179
|
.describe(
|
|
180
|
-
|
|
180
|
+
'Asdf plugins to install. See: https://github.com/asdf-community for a full list'
|
|
181
181
|
)
|
|
182
182
|
})
|
|
183
183
|
.strict()
|
package/src/plugin/plugin.ts
CHANGED
|
@@ -22,8 +22,9 @@ import { Plan } from '../plan/plan.js';
|
|
|
22
22
|
import { BackgroundPty } from '../pty/background-pty.js';
|
|
23
23
|
import { getPty } from '../pty/index.js';
|
|
24
24
|
import { SequentialPty } from '../pty/seqeuntial-pty.js';
|
|
25
|
-
import { Resource } from '../resource/resource.js';
|
|
26
25
|
import { ResourceController } from '../resource/resource-controller.js';
|
|
26
|
+
import { listAllResources } from '../utils/load-resources.js';
|
|
27
|
+
import { readNearestPackageJson } from '../utils/package-json-utils.js';
|
|
27
28
|
import { ptyLocalStorage } from '../utils/pty-local-storage.js';
|
|
28
29
|
import { VerbosityLevel } from '../utils/verbosity-level.js';
|
|
29
30
|
|
|
@@ -33,12 +34,27 @@ export class Plugin {
|
|
|
33
34
|
|
|
34
35
|
constructor(
|
|
35
36
|
public name: string,
|
|
37
|
+
public version: string,
|
|
36
38
|
public resourceControllers: Map<string, ResourceController<ResourceConfig>>
|
|
37
39
|
) {
|
|
38
40
|
this.planStorage = new Map();
|
|
39
41
|
}
|
|
40
42
|
|
|
41
|
-
static create(
|
|
43
|
+
static async create() {
|
|
44
|
+
const packageJson = readNearestPackageJson();
|
|
45
|
+
if (!packageJson) {
|
|
46
|
+
throw new Error('Failed to read nearest package.json');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const { name, version } = packageJson;
|
|
50
|
+
|
|
51
|
+
const resourceLocations = await listAllResources();
|
|
52
|
+
const resources = await Promise.all(resourceLocations.map((l) => {
|
|
53
|
+
return import(l);
|
|
54
|
+
}))
|
|
55
|
+
console.log(resources);
|
|
56
|
+
|
|
57
|
+
|
|
42
58
|
const controllers = resources
|
|
43
59
|
.map((resource) => new ResourceController(resource))
|
|
44
60
|
|
|
@@ -46,7 +62,7 @@ export class Plugin {
|
|
|
46
62
|
controllers.map((r) => [r.typeId, r] as const)
|
|
47
63
|
);
|
|
48
64
|
|
|
49
|
-
return new Plugin(name, controllersMap);
|
|
65
|
+
return new Plugin(name, version, controllersMap);
|
|
50
66
|
}
|
|
51
67
|
|
|
52
68
|
async initialize(data: InitializeRequestData): Promise<InitializeResponseData> {
|
|
@@ -3,7 +3,7 @@ import { ResourceSettings } from './resource-settings.js';
|
|
|
3
3
|
import { ParsedResourceSettings } from './parsed-resource-settings.js';
|
|
4
4
|
import { TestConfig } from '../utils/test-utils.test.js';
|
|
5
5
|
import { z } from 'zod';
|
|
6
|
-
import { OS
|
|
6
|
+
import { OS } from 'codify-schemas';
|
|
7
7
|
|
|
8
8
|
describe('Resource options parser tests', () => {
|
|
9
9
|
it('Parses default values from options', () => {
|
|
@@ -15,7 +15,6 @@ import {
|
|
|
15
15
|
resolveMatcher,
|
|
16
16
|
resolveParameterTransformFn
|
|
17
17
|
} from './resource-settings.js';
|
|
18
|
-
import { JSONSchema } from '@apidevtools/json-schema-ref-parser';
|
|
19
18
|
|
|
20
19
|
export interface ParsedStatefulParameterSetting extends DefaultParameterSetting {
|
|
21
20
|
type: 'stateful',
|
|
@@ -525,8 +525,8 @@ ${JSON.stringify(refresh, null, 2)}
|
|
|
525
525
|
}
|
|
526
526
|
|
|
527
527
|
private getAllParameterKeys(): string[] {
|
|
528
|
-
return this.
|
|
529
|
-
? Object.keys((this.
|
|
528
|
+
return this.parsedSettings.schema
|
|
529
|
+
? Object.keys((this.parsedSettings.schema as any)?.properties)
|
|
530
530
|
: Object.keys(this.parsedSettings.parameterSettings);
|
|
531
531
|
}
|
|
532
532
|
|
|
@@ -12,8 +12,8 @@ import {
|
|
|
12
12
|
tildify,
|
|
13
13
|
untildify
|
|
14
14
|
} from '../utils/functions.js';
|
|
15
|
-
import { RefreshContext } from './resource.js';
|
|
16
15
|
import { ParsedResourceSettings } from './parsed-resource-settings.js';
|
|
16
|
+
import { RefreshContext } from './resource.js';
|
|
17
17
|
|
|
18
18
|
export interface InputTransformation {
|
|
19
19
|
to: (input: any) => Promise<any> | any;
|
|
@@ -356,7 +356,7 @@ export interface StatefulParameterSetting extends DefaultParameterSetting {
|
|
|
356
356
|
|
|
357
357
|
const ParameterEqualsDefaults: Partial<Record<ParameterSettingType, (a: unknown, b: unknown) => boolean>> = {
|
|
358
358
|
'boolean': (a: unknown, b: unknown) => Boolean(a) === Boolean(b),
|
|
359
|
-
'directory'
|
|
359
|
+
'directory'(a: unknown, b: unknown) {
|
|
360
360
|
let transformedA = resolvePathWithVariables(untildify(String(a)))
|
|
361
361
|
let transformedB = resolvePathWithVariables(untildify(String(b)))
|
|
362
362
|
|
|
@@ -437,7 +437,7 @@ export function resolveFnFromEqualsFnOrString(
|
|
|
437
437
|
const ParameterTransformationDefaults: Partial<Record<ParameterSettingType, InputTransformation>> = {
|
|
438
438
|
'directory': {
|
|
439
439
|
to: (a: unknown) => resolvePathWithVariables((untildify(String(a)))),
|
|
440
|
-
from
|
|
440
|
+
from(a: unknown, original) {
|
|
441
441
|
if (ParameterEqualsDefaults.directory!(a, original)) {
|
|
442
442
|
return original;
|
|
443
443
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
export const listAllResources = async () => {
|
|
5
|
+
const resourceDir = await fs.readdir('./src/resources');
|
|
6
|
+
const dedupSet = new Set();
|
|
7
|
+
const result = new Set<string>();
|
|
8
|
+
|
|
9
|
+
for (const folder of resourceDir) {
|
|
10
|
+
if (await fs.stat(path.join('./src/resources', folder)).then(s => s.isDirectory()).catch(() => false)) {
|
|
11
|
+
for (const folderContents of await fs.readdir(path.join('./src/resources', folder))) {
|
|
12
|
+
const isDirectory = await fs.stat(path.join('./src/resources', folder, folderContents)).then(s => s.isDirectory());
|
|
13
|
+
|
|
14
|
+
// console.log(folderContents, isDirectory);
|
|
15
|
+
if (isDirectory) {
|
|
16
|
+
for (const innerContents of await fs.readdir(path.join('./src/resources', folder, folderContents))) {
|
|
17
|
+
if (!dedupSet.has(path.join('./src/resources', folder, folderContents))) {
|
|
18
|
+
dedupSet.add(path.join('./src/resources', folder, folderContents));
|
|
19
|
+
addResourceFromDir(path.join('./src/resources', folder,folderContents), result);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
} else {
|
|
23
|
+
if (!dedupSet.has(path.join('./src/resources', folder))) {
|
|
24
|
+
dedupSet.add(path.join('./src/resources', folder));
|
|
25
|
+
addResourceFromDir(path.join('./src/resources', folder), result);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
} else {
|
|
30
|
+
throw new Error('Only directories are allowed in resources folder')
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return [...result];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
function addResourceFromDir(dir: string, result: Set<string>): void {
|
|
40
|
+
try {
|
|
41
|
+
const resourceFile = path.resolve(path.join(dir, 'resource.ts'));
|
|
42
|
+
if (!(fs.stat(resourceFile).then((s) => s.isFile())).catch(() => false)) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
result.add(resourceFile);
|
|
47
|
+
} catch {}
|
|
48
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Find the nearest package.json starting from a directory and walking upward.
|
|
6
|
+
* @param {string} startDir - Directory to start searching from
|
|
7
|
+
* @returns {string|null} Absolute path to package.json or null if not found
|
|
8
|
+
*/
|
|
9
|
+
export function findNearestPackageJson(startDir = process.cwd()) {
|
|
10
|
+
let currentDir = path.resolve(startDir);
|
|
11
|
+
|
|
12
|
+
while (true) {
|
|
13
|
+
const pkgPath = path.join(currentDir, "package.json");
|
|
14
|
+
|
|
15
|
+
if (fs.existsSync(pkgPath)) {
|
|
16
|
+
return pkgPath;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const parentDir = path.dirname(currentDir);
|
|
20
|
+
if (parentDir === currentDir) {
|
|
21
|
+
// Reached filesystem root
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
currentDir = parentDir;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Read and parse the nearest package.json
|
|
31
|
+
* @param {string} startDir
|
|
32
|
+
* @returns {object|null}
|
|
33
|
+
*/
|
|
34
|
+
export function readNearestPackageJson(startDir: string = process.cwd()) {
|
|
35
|
+
const pkgPath = findNearestPackageJson(startDir);
|
|
36
|
+
if (!pkgPath) return null;
|
|
37
|
+
|
|
38
|
+
const contents = fs.readFileSync(pkgPath, 'utf8');
|
|
39
|
+
return JSON.parse(contents);
|
|
40
|
+
}
|