Use scaffolded template for remixd

remixd
ioedeveloper 4 years ago
parent cd470fa508
commit 68b8e21441
  1. 2
      libs/README.md
  2. 32
      libs/remixd/.circleci/config.yml
  3. 22
      libs/remixd/.eslintrc
  4. 5
      libs/remixd/.gitignore
  5. 11
      libs/remixd/.npmignore
  6. 17
      libs/remixd/jest.config.js
  7. 4000
      libs/remixd/package-lock.json
  8. 0
      libs/remixd/src/bin/origins.json
  9. 14
      libs/remixd/src/bin/remixd.ts
  10. 6
      libs/remixd/src/services/remixdClient.ts
  11. 2
      libs/remixd/src/types/index.ts
  12. 16
      libs/remixd/src/utils.ts
  13. 13
      libs/remixd/src/websocket.ts
  14. 14
      libs/remixd/tsconfig.json
  15. 12
      libs/remixd/tsconfig.lib.json
  16. 5
      libs/remixd/tsconfig.spec.json
  17. 43150
      package-lock.json
  18. 9
      package.json
  19. 5
      tsconfig.json
  20. 80
      workspace.json

@ -22,7 +22,7 @@ Remix is built out of several different modules. Here is the brief description.
+ [`remix-lib`](remix-lib/README.md): Common place for libraries being used across multiple modules + [`remix-lib`](remix-lib/README.md): Common place for libraries being used across multiple modules
+ [`remix-tests`](remix-tests/README.md): Unit test Solidity smart contracts. It works as a plugin & as CLI both + [`remix-tests`](remix-tests/README.md): Unit test Solidity smart contracts. It works as a plugin & as CLI both
+ [`remix-url-resolver`](remix-url-resolver/README.md): Provide helpers for resolving the content from external URL ( including github, swarm, ipfs etc.). + [`remix-url-resolver`](remix-url-resolver/README.md): Provide helpers for resolving the content from external URL ( including github, swarm, ipfs etc.).
+ [`remixd`](https://github.com/ethereum/remixd/tree/master): Allow accessing local filesystem from Remix IDE by running a daemon + [`remixd`](remixd/README.md): Allow accessing local filesystem from Remix IDE by running a daemon
Each module generally has their own npm package and test suite, as well as basic documentation in their respective `README`s. Usage of modules as plugin is well documented **[here](https://remix-ide.readthedocs.io/en/latest/index.html)**. Each module generally has their own npm package and test suite, as well as basic documentation in their respective `README`s. Usage of modules as plugin is well documented **[here](https://remix-ide.readthedocs.io/en/latest/index.html)**.

@ -1,32 +0,0 @@
# Javascript Node CircleCI 2.0 configuration file
#
# Check https://circleci.com/docs/2.0/language-javascript/ for more details
#
version: 2
jobs:
remixd:
docker:
# specify the version you desire here
- image: circleci/node:9.11.2
# Specify service dependencies here if necessary
# CircleCI maintains a library of pre-built images
# documented at https://circleci.com/docs/2.0/circleci-images/
# - image: circleci/mongo:3.4.4
environment:
- COMMIT_AUTHOR_EMAIL: "yann@ethereum.org"
- COMMIT_AUTHOR: "Circle CI"
- FILES_TO_PACKAGE: "package.json"
working_directory: ~/remixd
steps:
- checkout
- run: npm install
- run: npm run lint
- run: npm run test
workflows:
version: 2
build_all:
jobs:
- remixd

@ -1,21 +1 @@
{ { "extends": "../../.eslintrc", "rules": {}, "ignorePatterns": ["!**/*"] }
"env": {
"browser": true,
"commonjs": true,
"es6": true
},
"extends": [
"plugin:@typescript-eslint/recommended"
],
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly"
},
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint"],
"rules": {
"standard/no-callback-literal": 0,
"@typescript-eslint/no-var-requires": 0,
"@typescript-eslint/ban-types": 0
}
}

@ -1,5 +0,0 @@
node_modules
npm-debug.log
python_modules
lib
shared

@ -1,11 +0,0 @@
node_modules
npm-debug.log
python_modules
shared
/bin
/src
.circleci
tsconfig.json
nodemon.json
.eslintrc.json
/types

@ -1,14 +1,15 @@
/* eslint-disable */ /* eslint-disable */
module.exports = { module.exports = {
name: 'remixd', name: 'remixd',
preset: '../../../jest.config.js', preset: '../../jest.config.js',
globals: {
'ts-jest': {
tsConfig: '<rootDir>/tsconfig.spec.json'
}
},
transform: { transform: {
'^.+\\.[tj]sx?$': [ '^.+\\.[tj]sx?$': 'ts-jest'
'babel-jest',
{ cwd: __dirname, configFile: './babel-jest.config.json' }
]
}, },
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html'], moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html'],
coverageDirectory: '../../../coverage/libs/remixd' coverageDirectory: '../../coverage/libs/remixd'
} };

File diff suppressed because it is too large Load Diff

@ -1,15 +1,14 @@
#!/usr/bin/env node #!/usr/bin/env node
import WebSocket from '../src/websocket' import WebSocket from '../websocket'
import * as servicesList from '../src/serviceList' import * as servicesList from '../serviceList'
import { WS } from '../types/index' import * as WS from 'ws'
import { getDomain } from '../src/utils' import { getDomain } from '../utils'
import Axios from 'axios' import Axios from 'axios'
import * as fs from 'fs-extra' import * as fs from 'fs-extra'
import * as path from 'path' import * as path from 'path'
import * as program from 'commander'
(async () => { (async () => {
const program = require('commander')
program program
.usage('-s <shared folder>') .usage('-s <shared folder>')
.description('Provide a two-way connection between the local computer and Remix IDE') .description('Provide a two-way connection between the local computer and Remix IDE')
@ -19,7 +18,7 @@ import * as path from 'path'
.on('--help', function(){ .on('--help', function(){
console.log('\nExample:\n\n remixd -s ./ --remix-ide http://localhost:8080') console.log('\nExample:\n\n remixd -s ./ --remix-ide http://localhost:8080')
}).parse(process.argv) }).parse(process.argv)
// eslint-disable-next-line
const killCallBack: Array<Function> = [] const killCallBack: Array<Function> = []
if (!program.remixIde) { if (!program.remixIde) {
@ -85,6 +84,7 @@ import * as path from 'path'
return data.includes(origin) ? data.includes(origin) : data.includes(domain) return data.includes(origin) ? data.includes(origin) : data.includes(domain)
} catch (e) { } catch (e) {
try { try {
// eslint-disable-next-line
const origins = require('./origins.json') const origins = require('./origins.json')
const { data } = origins const { data } = origins

@ -1,10 +1,10 @@
import { PluginClient } from '@remixproject/plugin' import { PluginClient } from '@remixproject/plugin'
import { SharedFolderArgs, TrackDownStreamUpdate, WS, Filelist, ResolveDirectory, FileContent } from '../../types' import { SharedFolderArgs, TrackDownStreamUpdate, Filelist, ResolveDirectory, FileContent } from '../types'
import * as WS from 'ws'
import * as utils from '../utils' import * as utils from '../utils'
import * as chokidar from 'chokidar' import * as chokidar from 'chokidar'
import * as fs from 'fs-extra' import * as fs from 'fs-extra'
import * as isbinaryfile from 'isbinaryfile'
const isbinaryfile = require('isbinaryfile')
export class RemixdClient extends PluginClient { export class RemixdClient extends PluginClient {
methods: ['folderIsReadOnly', 'resolveDirectory', 'get', 'exists', 'isFile', 'set', 'list', 'isDirectory'] methods: ['folderIsReadOnly', 'resolveDirectory', 'get', 'exists', 'isFile', 'set', 'list', 'isDirectory']

@ -1,4 +1,4 @@
import * as ServiceList from '../src/serviceList' import * as ServiceList from '../serviceList'
import * as Websocket from 'ws' import * as Websocket from 'ws'
type ServiceListKeys = keyof typeof ServiceList; type ServiceListKeys = keyof typeof ServiceList;

@ -1,10 +1,7 @@
import { ResolveDirectory, Filelist } from '../types' import { ResolveDirectory, Filelist } from './types'
import * as fs from 'fs-extra'
const fs = require('fs-extra') import * as isbinaryfile from 'isbinaryfile'
const path = require('path') import * as pathModule from 'path'
const isbinaryfile = require('isbinaryfile')
const pathModule = require('path')
/** /**
* returns the absolute path of the given @arg path * returns the absolute path of the given @arg path
* *
@ -45,7 +42,7 @@ function walkSync (dir: string, filelist: Filelist, sharedFolder: string): Filel
filelist = filelist || {} filelist = filelist || {}
files.forEach(function (file) { files.forEach(function (file) {
const subElement = path.join(dir, file) const subElement = pathModule.join(dir, file)
if (!fs.lstatSync(subElement).isSymbolicLink()) { if (!fs.lstatSync(subElement).isSymbolicLink()) {
if (fs.statSync(subElement).isDirectory()) { if (fs.statSync(subElement).isDirectory()) {
@ -65,7 +62,7 @@ function resolveDirectory (dir: string, sharedFolder: string): ResolveDirectory
const files: string[] = fs.readdirSync(dir) const files: string[] = fs.readdirSync(dir)
files.forEach(function (file) { files.forEach(function (file) {
const subElement = path.join(dir, file) const subElement = pathModule.join(dir, file)
if (!fs.lstatSync(subElement).isSymbolicLink()) { if (!fs.lstatSync(subElement).isSymbolicLink()) {
const relative: string = relativePath(subElement, sharedFolder) const relative: string = relativePath(subElement, sharedFolder)
@ -83,6 +80,7 @@ function resolveDirectory (dir: string, sharedFolder: string): ResolveDirectory
* @return {String} extracted domain name from url * @return {String} extracted domain name from url
*/ */
function getDomain(url: string) { function getDomain(url: string) {
// eslint-disable-next-line
const domainMatch = url.match(/^(?:https?:\/\/)?(?:[^@\n]+@)?(?:www\.)?([^:\/\n?]+)/img) const domainMatch = url.match(/^(?:https?:\/\/)?(?:[^@\n]+@)?(?:www\.)?([^:\/\n?]+)/img)
return domainMatch ? domainMatch[0] : null return domainMatch ? domainMatch[0] : null

@ -1,17 +1,15 @@
import * as WS from 'ws' import * as WS from 'ws'
import * as http from 'http' import * as http from 'http'
import { WebsocketOpt, SharedFolderClient } from '../types' import { WebsocketOpt, SharedFolderClient } from './types'
import { getDomain } from './utils' import { getDomain } from './utils'
import { createClient } from '@remixproject/plugin-ws'
const { createClient } = require('@remixproject/plugin-ws')
export default class WebSocket { export default class WebSocket {
server: http.Server server: http.Server
wsServer: WS.Server wsServer: WS.Server
constructor (public port: number, public opt: WebsocketOpt, public sharedFolder: SharedFolderClient) {} constructor (public port: number, public opt: WebsocketOpt, public sharedFolder: SharedFolderClient) {}
start (callback?: Function): void { start (callback?: (ws: WS) => void): void {
this.server = http.createServer((request, response) => { this.server = http.createServer((request, response) => {
console.log((new Date()) + ' Received request for ' + request.url) console.log((new Date()) + ' Received request for ' + request.url)
response.writeHead(404) response.writeHead(404)
@ -20,7 +18,7 @@ export default class WebSocket {
const loopback = '127.0.0.1' const loopback = '127.0.0.1'
this.server.listen(this.port, loopback, function () { this.server.listen(this.port, loopback, function () {
console.log((new Date()) + ' Remixd is listening on ' + loopback + ':65520') console.log((new Date()) + ' remixd is listening on ' + loopback + ':65520')
}) })
this.wsServer = new WS.Server({ this.wsServer = new WS.Server({
server: this.server, server: this.server,
@ -36,7 +34,7 @@ export default class WebSocket {
this.wsServer.on('connection', (ws) => { this.wsServer.on('connection', (ws) => {
const { sharedFolder } = this const { sharedFolder } = this
createClient(ws, sharedFolder) createClient(ws, sharedFolder as any)
if(callback) callback(ws) if(callback) callback(ws)
}) })
} }
@ -56,6 +54,7 @@ function originIsAllowed (origin: string, self: WebSocket): boolean {
return origin === self.opt.remixIdeUrl || origin === getDomain(self.opt.remixIdeUrl) return origin === self.opt.remixIdeUrl || origin === getDomain(self.opt.remixIdeUrl)
} else { } else {
try { try {
// eslint-disable-next-line
const origins = require('../bin/origins.json') const origins = require('../bin/origins.json')
const domain = getDomain(origin) const domain = getDomain(origin)
const { data } = origins const { data } = origins

@ -1,9 +1,13 @@
{ {
"extends": "../../tsconfig.json", "extends": "../../tsconfig.json",
"compilerOptions": { "files": [],
"types": ["node"], "include": [],
"module": "commonjs", "references": [
"esModuleInterop": true {
"path": "./tsconfig.lib.json"
}, },
"include": ["**/*.ts"] {
"path": "./tsconfig.spec.json"
}
]
} }

@ -1,11 +1,13 @@
{ {
"extends": "./tsconfig.json", "extends": "./tsconfig.json",
"compilerOptions": { "compilerOptions": {
/* Basic Options */ "target": "es6",
"target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */ "module": "commonjs",
"declaration": true, /* Generates corresponding '.d.ts' file. */ "outDir": "../../dist/out-tsc",
"strict": true, /* Enable all strict type-checking options. */ "declaration": true,
"strictPropertyInitialization": false /* Enable strict checking of property initialization in classes. */ "rootDir": "./src",
"types": ["node"]
}, },
"exclude": ["**/*.spec.ts"],
"include": ["bin", "src", "bin/origins.json"] "include": ["bin", "src", "bin/origins.json"]
} }

@ -1,14 +1,15 @@
{ {
"extends": "./tsconfig.json", "extends": "./tsconfig.json",
"compilerOptions": { "compilerOptions": {
"outDir": "../../../dist/out-tsc", "outDir": "../../dist/out-tsc",
"module": "commonjs", "module": "commonjs",
"types": ["jest", "node"] "types": ["jest", "node"]
}, },
"include": [ "include": [
"**/*.spec.ts", "**/*.spec.ts",
"**/*.spec.tsx",
"**/*.spec.js", "**/*.spec.js",
"**/*.spec.jsx",
"**/*.d.ts" "**/*.d.ts"
] ]
} }

43150
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -43,9 +43,9 @@
"workspace-schematic": "nx workspace-schematic", "workspace-schematic": "nx workspace-schematic",
"dep-graph": "nx dep-graph", "dep-graph": "nx dep-graph",
"help": "nx help", "help": "nx help",
"lint:libs": "nx run-many --target=lint --projects=remix-analyzer,remix-astwalker,remix-debug,remix-lib,remix-simulator,remix-solidity,remix-tests,remix-url-resolver", "lint:libs": "nx run-many --target=lint --projects=remix-analyzer,remix-astwalker,remix-debug,remix-lib,remix-simulator,remix-solidity,remix-tests,remix-url-resolver,remixd",
"build:libs": "nx run-many --target=build --parallel=false --with-deps=true --projects=remix-analyzer,remix-astwalker,remix-debug,remix-lib,remix-simulator,remix-solidity,remix-tests,remix-url-resolver", "build:libs": "nx run-many --target=build --parallel=false --with-deps=true --projects=remix-analyzer,remix-astwalker,remix-debug,remix-lib,remix-simulator,remix-solidity,remix-tests,remix-url-resolver,remixd",
"test:libs": "nx run-many --target=test --projects=remix-analyzer,remix-astwalker,remix-debug,remix-lib,remix-simulator,remix-solidity,remix-tests,remix-url-resolver", "test:libs": "nx run-many --target=test --projects=remix-analyzer,remix-astwalker,remix-debug,remix-lib,remix-simulator,remix-solidity,remix-tests,remix-url-resolver,remixd",
"publish:libs": "npm run build:libs; lerna publish --skip-git; npm run bumpVersion:libs", "publish:libs": "npm run build:libs; lerna publish --skip-git; npm run bumpVersion:libs",
"bumpVersion:libs": "gulp; gulp syncLibVersions;", "bumpVersion:libs": "gulp; gulp syncLibVersions;",
"browsertest": "sleep 5 && npm run nightwatch_local", "browsertest": "sleep 5 && npm run nightwatch_local",
@ -270,6 +270,7 @@
"eslint-plugin-node": "11.1.0", "eslint-plugin-node": "11.1.0",
"eslint-plugin-promise": "4.2.1", "eslint-plugin-promise": "4.2.1",
"eslint-plugin-standard": "4.0.1", "eslint-plugin-standard": "4.0.1",
"nodemon": "^2.0.4" "nodemon": "^2.0.4",
"@types/jest": "25.1.4"
} }
} }

@ -23,7 +23,10 @@
"@remix-project/remix-simulator": ["dist/libs/remix-simulator/index.js"], "@remix-project/remix-simulator": ["dist/libs/remix-simulator/index.js"],
"@remix-project/remix-solidity": ["dist/libs/remix-solidity/index.js"], "@remix-project/remix-solidity": ["dist/libs/remix-solidity/index.js"],
"@remix-project/remix-tests": ["dist/libs/remix-tests/src/index.js"], "@remix-project/remix-tests": ["dist/libs/remix-tests/src/index.js"],
"@remix-project/remix-url-resolver": ["dist/libs/remix-url-resolver/index.js"] "@remix-project/remix-url-resolver": [
"dist/libs/remix-url-resolver/index.js"
],
"@remix-project/remixd": ["libs/remixd/src/index.ts"]
} }
}, },
"exclude": ["node_modules", "tmp"] "exclude": ["node_modules", "tmp"]

@ -55,7 +55,8 @@
"options": { "options": {
"buildTarget": "remix-ide:build", "buildTarget": "remix-ide:build",
"port": 8080, "port": 8080,
"exclude": ["**/node_modules/**", "exclude": [
"**/node_modules/**",
"apps/remix-ide/build/**/*.js", "apps/remix-ide/build/**/*.js",
"apps/remix-ide/src/app/editor/mode-solidity.js", "apps/remix-ide/src/app/editor/mode-solidity.js",
"apps/remix-ide/soljson.js", "apps/remix-ide/soljson.js",
@ -74,9 +75,7 @@
"options": { "options": {
"linter": "eslint", "linter": "eslint",
"config": "apps/remix-ide/.eslintrc", "config": "apps/remix-ide/.eslintrc",
"files": [ "files": ["apps/remix-ide/src/**/*.js"],
"apps/remix-ide/src/**/*.js"
],
"exclude": [ "exclude": [
"**/node_modules/**", "**/node_modules/**",
"apps/remix-ide/src/app/editor/mode-solidity.js", "apps/remix-ide/src/app/editor/mode-solidity.js",
@ -215,9 +214,7 @@
"options": { "options": {
"linter": "eslint", "linter": "eslint",
"config": "libs/remix-debug/.eslintrc", "config": "libs/remix-debug/.eslintrc",
"files": [ "files": ["libs/remix-debug/**/*.js"],
"libs/remix-debug/**/*.js"
],
"exclude": ["**/node_modules/**", "libs/remix-debug/test/**/*"] "exclude": ["**/node_modules/**", "libs/remix-debug/test/**/*"]
} }
}, },
@ -240,8 +237,16 @@
"packageJson": "libs/remix-debug/package.json", "packageJson": "libs/remix-debug/package.json",
"main": "libs/remix-debug/index.js", "main": "libs/remix-debug/index.js",
"assets": [ "assets": [
{ "glob": "rdb", "input": "libs/remix-debug/bin/", "output": "bin/" }, {
{ "glob": "*.md", "input": "libs/remix-debug/", "output": "/" } "glob": "rdb",
"input": "libs/remix-debug/bin/",
"output": "bin/"
},
{
"glob": "*.md",
"input": "libs/remix-debug/",
"output": "/"
}
] ]
} }
} }
@ -296,9 +301,7 @@
"options": { "options": {
"linter": "eslint", "linter": "eslint",
"config": "libs/remix-simulator/.eslintrc", "config": "libs/remix-simulator/.eslintrc",
"files": [ "files": ["libs/remix-simulator/**/*.js"],
"libs/remix-simulator/**/*.js"
],
"exclude": ["**/node_modules/**", "libs/remix-simulator/test/**/*"] "exclude": ["**/node_modules/**", "libs/remix-simulator/test/**/*"]
} }
}, },
@ -321,8 +324,16 @@
"packageJson": "libs/remix-simulator/package.json", "packageJson": "libs/remix-simulator/package.json",
"main": "libs/remix-simulator/index.js", "main": "libs/remix-simulator/index.js",
"assets": [ "assets": [
{ "glob": "ethsim", "input": "libs/remix-simulator/bin/", "output": "bin/" }, {
{ "glob": "*.md", "input": "libs/remix-simulator/", "output": "/" } "glob": "ethsim",
"input": "libs/remix-simulator/bin/",
"output": "bin/"
},
{
"glob": "*.md",
"input": "libs/remix-simulator/",
"output": "/"
}
] ]
} }
} }
@ -339,9 +350,7 @@
"options": { "options": {
"linter": "eslint", "linter": "eslint",
"config": "libs/remix-solidity/.eslintrc", "config": "libs/remix-solidity/.eslintrc",
"tsConfig": [ "tsConfig": ["libs/remix-solidity/tsconfig.lib.json"],
"libs/remix-solidity/tsconfig.lib.json"
],
"exclude": ["**/node_modules/**"] "exclude": ["**/node_modules/**"]
} }
}, },
@ -379,9 +388,7 @@
"options": { "options": {
"linter": "eslint", "linter": "eslint",
"config": "libs/remix-tests/.eslintrc", "config": "libs/remix-tests/.eslintrc",
"tsConfig": [ "tsConfig": ["libs/remix-tests/tsconfig.lib.json"],
"libs/remix-tests/tsconfig.lib.json"
],
"exclude": ["**/node_modules/**", "libs/remix-tests/tests/**/*"] "exclude": ["**/node_modules/**", "libs/remix-tests/tests/**/*"]
} }
}, },
@ -400,8 +407,16 @@
"packageJson": "libs/remix-tests/package.json", "packageJson": "libs/remix-tests/package.json",
"main": "libs/remix-tests/src/index.ts", "main": "libs/remix-tests/src/index.ts",
"assets": [ "assets": [
{ "glob": "remix-tests", "input": "libs/remix-tests/bin/", "output": "bin/" }, {
{ "glob": "*.md", "input": "libs/remix-tests/", "output": "/" } "glob": "remix-tests",
"input": "libs/remix-tests/bin/",
"output": "bin/"
},
{
"glob": "*.md",
"input": "libs/remix-tests/",
"output": "/"
}
] ]
} }
} }
@ -418,10 +433,11 @@
"options": { "options": {
"linter": "eslint", "linter": "eslint",
"config": "libs/remix-url-resolver/.eslintrc", "config": "libs/remix-url-resolver/.eslintrc",
"tsConfig": [ "tsConfig": ["libs/remix-url-resolver/tsconfig.lib.json"],
"libs/remix-url-resolver/tsconfig.lib.json" "exclude": [
], "**/node_modules/**",
"exclude": ["**/node_modules/**", "libs/remix-url-resolver/tests/**/*"] "libs/remix-url-resolver/tests/**/*"
]
} }
}, },
"test": { "test": {
@ -449,7 +465,7 @@
}, },
"remixd": { "remixd": {
"root": "libs/remixd", "root": "libs/remixd",
"sourceRoot": "libs/remixd/src/", "sourceRoot": "libs/remixd/src",
"projectType": "library", "projectType": "library",
"schematics": {}, "schematics": {},
"architect": { "architect": {
@ -457,15 +473,11 @@
"builder": "@nrwl/linter:lint", "builder": "@nrwl/linter:lint",
"options": { "options": {
"linter": "eslint", "linter": "eslint",
"config": "libs/remixd/.eslintrc",
"tsConfig": [ "tsConfig": [
"libs/remixd/tsconfig.lib.json" "libs/remixd/tsconfig.lib.json",
], "libs/remixd/tsconfig.spec.json"
"files": [
"libs/remixd/src/**/*.ts",
"libs/remixd/bin/**/*.ts"
], ],
"exclude": ["**/node_modules/**"] "exclude": ["**/node_modules/**", "!libs/remixd/**/*"]
} }
}, },
"test": { "test": {

Loading…
Cancel
Save