add mapper builders

still needs all mapper versions but has list, create, and count working
This commit is contained in:
2021-01-18 00:50:05 -07:00
parent 69466406cd
commit 4e022988b0
13 changed files with 539 additions and 216 deletions

8
.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Datasource local storage ignored files
/../../../../../../:\Users\gomas\src\system-builder\.idea/dataSources/
/dataSources.local.xml
# Editor-based HTTP Client requests
/httpRequests/

8
.idea/modules.xml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/system-builder.iml" filepath="$PROJECT_DIR$/.idea/system-builder.iml" />
</modules>
</component>
</project>

6
.idea/sqldialects.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="SqlDialectMappings">
<file url="PROJECT" dialect="MariaDB" />
</component>
</project>

9
.idea/system-builder.iml generated Normal file
View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="Go" enabled="true" />
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

6
.idea/vcs.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

142
dist/example-def.js vendored
View File

@ -1,142 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var def = {
name: 'Todo',
storage: {
tables: [
{
name: "task",
relations: [
{
type: 'belongs-to',
table: 'list',
}
],
columns: [
{
name: "name",
type: "string",
nullable: false,
},
{
name: "description",
type: "string",
nullable: true,
},
{
name: "completed",
type: "boolean",
nullable: false,
},
{
name: "completed_date",
type: "date",
nullable: true,
},
{
name: "metadata",
type: "blob",
nullable: true,
}
]
},
{
name: "list",
relations: [],
columns: [
{
name: "name",
type: "string",
unique: false,
nullable: false,
},
]
},
{
name: "user",
relations: [],
columns: [
{
name: "name",
type: "string",
nullable: false,
},
{
name: "password",
type: "string",
nullable: false,
}
]
}
],
relations: [
{
left: 'list',
relation: 'many-to-many',
right: 'user',
columns: [
{
name: 'access',
type: 'string',
unique: false,
nullable: false,
}
]
}
]
},
views: [
{
component: 'task',
type: ['list', 'count'],
columns: [
'name',
'description',
'completed',
'completed_date',
],
orderBy: [
{
column: 'modified',
direction: 'desc'
}
],
filters: [
{
param: 'list',
column: 'list_id',
comparison: '=',
required: true
},
{
param: 'completed',
column: 'completed',
comparison: '=',
required: false
},
]
},
{
component: 'list',
type: ['list', 'count'],
columns: [
'name',
],
orderBy: [
{
column: 'modified',
direction: 'desc'
}
],
filters: [
{
param: 'user',
column: 'user_id',
comparison: '=',
required: true
},
]
},
]
};
exports.default = def;

View File

@ -1,8 +1,10 @@
{ {
"dependencies": { "dependencies": {
"@types/node": "^14.0.13", "@types/node": "^14.0.13",
"@types/pluralize": "^0.0.29",
"ncp": "^2.0.0", "ncp": "^2.0.0",
"path": "^0.12.7" "path": "^0.12.7",
"pluralize": "^8.0.0"
}, },
"scripts": { "scripts": {
"start": "rm -rf dist && tsc && cp -r frame dist/ && cd dist && node index.js" "start": "rm -rf dist && tsc && cp -r frame dist/ && cd dist && node index.js"

View File

@ -3,7 +3,7 @@ import * as fs from 'fs';
import { import {
SystemDef, SystemDef,
StorageDef, StorageDef,
ViewDef, EndpointDef,
Order, Order,
Filter, Filter,
TableDef, TableDef,

View File

@ -27,6 +27,7 @@ let def: SystemDef = {
name: "completed", name: "completed",
type: "boolean", type: "boolean",
nullable: false, nullable: false,
default: 'false'
}, },
{ {
name: "completed_date", name: "completed_date",
@ -85,59 +86,197 @@ let def: SystemDef = {
} }
] ]
}, },
views: [ components: [
{ {
component: 'task', component: 'task',
type: ['list', 'count'], endpoints: [
columns: [
'name',
'description',
'completed',
'completed_date',
],
orderBy: [
{ {
column: 'modified', component: 'task',
direction: 'desc' table: 'task',
type: 'list',
columns: [
{
name: 'name',
table: 'task'
},
{
name: 'description',
table: 'task'
},
{
name: 'completed',
table: 'task'
},
{
name: 'completed_date',
table: 'task'
},
],
orderBy: [
{
column: {
name: 'modified',
table: 'task'
},
direction: 'desc'
}
],
filters: [
{
param: 'list',
column: {
name: 'list_id',
table: 'task'
},
comparison: '=',
required: true
},
{
param: 'completed',
column: {
name: 'completed',
table: 'task'
},
comparison: '=',
required: false
},
]
},
{
component: 'task',
table: 'task',
type: 'count',
columns: [
{
name: 'name',
table: 'task'
},
{
name: 'description',
table: 'task'
},
{
name: 'completed',
table: 'task'
},
{
name: 'completed_date',
table: 'task'
},
],
orderBy: [
{
column: {
name: 'modified',
table: 'task'
},
direction: 'desc'
}
],
filters: [
{
param: 'list',
column: {
name: 'list_id',
table: 'task'
},
comparison: '=',
required: true
},
{
param: 'completed',
column: {
name: 'completed',
table: 'task'
},
comparison: '=',
required: false
},
]
},
{
component: 'task',
table: 'task',
type: 'create',
columns: [
{
name: 'name',
table: 'task',
param: 'name',
required: true
},
{
name: 'list_id',
table: 'task',
param: 'list',
required: true
},
{
name: 'description',
table: 'task',
param: 'description'
},
{
name: 'completed',
table: 'task',
param: 'completed'
}
],
} }
],
filters: [
{
param: 'list',
column: 'list_id',
comparison: '=',
required: true
},
{
param: 'completed',
column: 'completed',
comparison: '=',
required: false
},
] ]
}, },
{ {
component: 'list', component: 'list',
type: ['list', 'count'], endpoints: [
columns: [
'name',
],
orderBy: [
{ {
column: 'modified', component: 'list',
direction: 'desc' table: 'list',
} type: 'list',
], columns: [
filters: [ {
{ name: 'name',
param: 'user', table: 'list'
column: 'user_id', },
comparison: '=', ],
required: true join: [
{
table: 'list_user',
on: {
left: {
name: 'list_id',
table: 'list'
},
right: {
name: 'list_id',
table: 'list_user'
}
}
}
],
orderBy: [
{
column: {
name: 'modified',
table: 'list'
},
direction: 'desc'
}
],
filters: [
{
param: 'user',
column: {
name: 'user_id',
table: 'list_user'
},
comparison: '=',
required: true
},
]
}, },
] ]
}, }
] ],
} };
export default def; export default def;

View File

@ -33,7 +33,7 @@ const systemGenService = new SystemGenService();
interface SystemDef { interface SystemDef {
name: string; name: string;
storage: StorageDef; storage: StorageDef;
views: ViewDef[]; components: ComponentDef[];
// TODO: add Views, ACLs, Behaviors, UX // TODO: add Views, ACLs, Behaviors, UX
} }
@ -42,25 +42,32 @@ interface StorageDef {
relations: ManyToManyDef[]; relations: ManyToManyDef[];
} }
interface ViewDef { interface ComponentDef {
component: string; component: string;
type: ('list' | 'count' | 'item' | 'distinct')[]; endpoints: EndpointDef[];
columns: string[]; }
interface EndpointDef {
component: string;
table: string;
type: ('list' | 'count' | 'item' | 'distinct' | 'update' | 'create' | 'delete');
columns: ColumnRef[];
values?: ValueDef[];
join?: JoinDef[];
orderBy?: Order[]; orderBy?: Order[];
filters?: Filter[]; filters?: Filter[];
// if type is 'list' it will always include skip and limit for pagination // if type is 'list' it will always include skip and limit for pagination
} }
interface Order { interface Order {
column: string; column: ColumnRef;
direction: 'asc' | 'desc'; direction: 'asc' | 'desc';
} }
interface Filter { interface Filter {
param: string; // the query param used to get the value param: string; // the query param used to get the value
column: string; column: ColumnRef;
comparison: '=' | '!=' | '>' | '<' | 'contains'; comparison: '=' | '!=' | '>' | '<' | 'contains';
value?: string;
required?: boolean; required?: boolean;
} }
@ -79,6 +86,26 @@ interface ColumnDef {
default?: string; default?: string;
} }
interface ColumnRef {
name: string;
table: string;
param?: string;
required?: boolean;
}
interface ValueDef {
column: string;
required: boolean;
}
interface JoinDef {
table: string;
on: {
left: ColumnRef;
right: ColumnRef;
}
}
interface BelongsToDef { interface BelongsToDef {
type: 'belongs-to'; type: 'belongs-to';
table: string; table: string;
@ -92,14 +119,17 @@ interface ManyToManyDef {
} }
export { export {
SystemDef,
StorageDef,
ViewDef,
Order,
Filter,
TableDef,
ColumnDef,
BelongsToDef, BelongsToDef,
ColumnDef,
ColumnRef,
ComponentDef,
EndpointDef,
Filter,
JoinDef,
ManyToManyDef, ManyToManyDef,
Order,
StorageDef,
SystemDef,
TableDef,
systemGenService systemGenService
} }

View File

@ -1,35 +1,44 @@
import { import {
SystemDef, BelongsToDef,
ViewDef ColumnDef,
Filter, JoinDef, Order,
SystemDef, TableDef,
EndpointDef, ComponentDef, ColumnRef
} from '../systemGenService'; } from '../systemGenService';
import * as path from 'path'; import * as path from 'path';
import * as fs from 'fs'; import * as fs from 'fs';
import pluralize from 'pluralize';
const ncp = require('ncp').ncp; const ncp = require('ncp').ncp;
export function createViews(systemDef: SystemDef) { export function createViews(systemDef: SystemDef) {
let viewsPromises = []; let viewsPromises = [];
for (let i in systemDef.views) { for (let i in systemDef.components) {
viewsPromises.push(createComponent(systemDef.views[i], systemDef.name)); viewsPromises.push(createComponent(systemDef.components[i], systemDef));
} }
return Promise.all(viewsPromises); return Promise.all(viewsPromises);
} }
function createComponent(view: ViewDef, outDir: string) { function createComponent(component: ComponentDef, systemDef: SystemDef) {
let componentPromise = new Promise((componentResolve, componentReject) => { let componentPromise = new Promise<void>((componentResolve, componentReject) => {
ncp(path.join(process.cwd(), 'frame', 'src', 'components', '{{component}}'), path.join(process.cwd(), outDir, 'src', 'components', view.component), (err: any) => { ncp(path.join(process.cwd(), 'frame', 'src', 'components', '{{component}}'), path.join(process.cwd(), systemDef.name, 'src', 'components', component.component), (err: any) => {
if (err) { if (err) {
console.log(err); console.log(err);
} else { } else {
let mapperPromise = initializeComponentFile(path.join(process.cwd(), outDir, 'src', 'components', view.component, '{{component}}Mapper.ets'), path.join(process.cwd(), outDir, 'src', 'components', view.component, `${view.component}Mapper.ts`), view.component, outDir); let mapperPromise = initializeComponentFile(path.join(process.cwd(), systemDef.name, 'src', 'components', component.component, '{{component}}Mapper.ets'), path.join(process.cwd(), systemDef.name, 'src', 'components', component.component, `${component.component}Mapper.ts`), component.component, systemDef.name);
let routesPromise = initializeComponentFile(path.join(process.cwd(), outDir, 'src', 'components', view.component, '{{component}}Routes.ets'), path.join(process.cwd(), outDir, 'src', 'components', view.component, `${view.component}Routes.ts`), view.component, outDir); for (let i in component.endpoints) {
addInitializeRoutesCode(view.component, outDir); mapperPromise = mapperPromise.then(() => {
let serviceFileLocation: string = path.join(process.cwd(), outDir, 'src', 'components', view.component, `${view.component}Service.ts`); return insertMapperCode(component.endpoints[i], systemDef.name);
let servicePromise = initializeComponentFile(path.join(process.cwd(), outDir, 'src', 'components', view.component, '{{component}}Service.ets'), serviceFileLocation, view.component, outDir); });
addInitializeServiceCode(view.component, outDir); }
let routesPromise = initializeComponentFile(path.join(process.cwd(), systemDef.name, 'src', 'components', component.component, '{{component}}Routes.ets'), path.join(process.cwd(), systemDef.name, 'src', 'components', component.component, `${component.component}Routes.ts`), component.component, systemDef.name);
addInitializeRoutesCode(component.component, systemDef.name);
let serviceFileLocation: string = path.join(process.cwd(), systemDef.name, 'src', 'components', component.component, `${component.component}Service.ts`);
let servicePromise = initializeComponentFile(path.join(process.cwd(), systemDef.name, 'src', 'components', component.component, '{{component}}Service.ets'), serviceFileLocation, component.component, systemDef.name);
addInitializeServiceCode(component.component, systemDef.name);
Promise.all([mapperPromise, routesPromise, servicePromise]).then(() => { Promise.all([mapperPromise, routesPromise, servicePromise]).then(() => {
console.log(`success creating component ${view.component}`); console.log(`success creating component ${component.component}`);
componentResolve(); componentResolve();
}); });
} }
@ -39,7 +48,7 @@ function createComponent(view: ViewDef, outDir: string) {
} }
function initializeComponentFile(sourceFile: string, destinationFile: string, component: string, outDir: string) { function initializeComponentFile(sourceFile: string, destinationFile: string, component: string, outDir: string) {
let filePromise = new Promise((resolve, reject) => { let filePromise = new Promise<void>((resolve, reject) => {
fs.rename(sourceFile, destinationFile, (err: any) => { fs.rename(sourceFile, destinationFile, (err: any) => {
let fileContents = fs.readFileSync(destinationFile, 'utf8'); let fileContents = fs.readFileSync(destinationFile, 'utf8');
var uppercaseFirstLetterComponentName = uppercaseFirstLetter(component); var uppercaseFirstLetterComponentName = uppercaseFirstLetter(component);
@ -81,6 +90,207 @@ function addInitializeRoutesCode(component: string, outDir: string) {
fs.writeFileSync(fileLocation, newRoutesFile, 'utf8'); fs.writeFileSync(fileLocation, newRoutesFile, 'utf8');
} }
function insertMapperCode(view: EndpointDef, outDir: string): Promise<void> {
const filePromise: Promise<void> = new Promise<void>((resolve) => {
const separator: string = `// SYSTEM-BUILDER-${view.component}-mapper`;
let fileLocation = path.join(process.cwd(), outDir, 'src', 'components', view.component, `${view.component}Mapper.ts`);
let initMapperFile: string = fs.readFileSync(fileLocation, 'utf8');
let parts = initMapperFile.split(separator);
// TODO: add mapper code here
parts[0] = parts[0] + createMapperFunc(view);
let newMapperFile = parts.join(separator);
fs.writeFileSync(fileLocation, newMapperFile, 'utf8');
resolve();
});
return filePromise;
}
function createMapperFunc(view: EndpointDef): string {
let func: string = '';
let funcName: string = '';
if (view.type.indexOf('list') !== -1 || view.type.indexOf('count') !== -1) {
func = buildGetList(view);
} else if (view.type.indexOf('item') !== -1) {
funcName = `get${uppercaseFirstLetter(view.component)}`;
} else if (view.type.indexOf('update') !== -1) {
funcName = `update${uppercaseFirstLetter(view.component)}`;
} else if (view.type.indexOf('create') !== -1) {
func = buildCreateFunc(view);
}
return func;
}
function buildCreateFunc(view: EndpointDef): string {
let func: string = '';
let funcName = `create${uppercaseFirstLetter(view.component)}`;
let query: string = `INSERT INTO ${view.table} SET `;
let requiredQueryValues: string = '';
let optionalValuesCode: string = '';
for (let i in view.columns) {
let c: ColumnRef = view.columns[i];
if (c.required) {
if (!query.endsWith('SET ')) {
query = query + `, `;
}
query = query + `${c.table}.${c.name} = ?`;
if (requiredQueryValues.length) {
requiredQueryValues = requiredQueryValues + `, `;
}
requiredQueryValues = requiredQueryValues + `params.${c.param}`;
} else {
optionalValuesCode = optionalValuesCode + `
if (params.${c.param}) {
query = query + ', ${c.table}.${c.name} = ?';
queryValues.push(params.${c.param});
}
`;
}
}
func = `
public ${funcName}(params: {[key: string]: string}) {
let query: string = '${query}';
let queryValues: string[] = [${requiredQueryValues}];
// optional params?
${optionalValuesCode}
return super.runQuery(query, [...queryValues]);
}
`;
return func;
}
function buildGetList(view: EndpointDef): string {
// TODO: optionally allow a raw query definition with predefined params
// This would be so much simpler and easy if it were just the queries wanted
let func: string = '';
let funcName: string = '';
let columns: string = view.type === 'count' ? `count(*)` : `${view.component}.${view.component}_id, ${view.columns.map(c => `${c.table}.${c.name}`).join(', ')}`;
let queryValues: string = '';
let query: string = '';
let tables: string = view.join?.length ? buildJoin(view) : `${view.table}`;
let filters: string = '';
funcName = `get${pluralize(uppercaseFirstLetter(view.component))}` + (view.type === 'count' ? 'Count' : '');
let requiredFilterStrings = [];
let requiredQueryValues = '';
let optionalFilterParts = '';
if (view.filters) {
for (let i in view.filters) {
let f: Filter = view.filters[i];
if (f.required) {
requiredFilterStrings.push(`${f.column.table}.${f.column.name} ${f.comparison} ?`); // e.g. 'table.id = ?'
if (requiredQueryValues.length) {
requiredQueryValues = requiredQueryValues + `, `;
}
requiredQueryValues = requiredQueryValues + `params.${f.param}`;
} else {
/* e.g.
if (eventLog.app) {
query = query + ', app = ?';
queryValues.push(eventLog.app);
}
*/
optionalFilterParts = optionalFilterParts + `
if (params.${f.param}) {
query = query + ' AND ${f.column.table}.${f.column.name} ${f.comparison} ?';
queryValues.push(params.${f.param});
}`;
}
}
filters = ` WHERE ${requiredFilterStrings.join(' AND ')}`;
}
query = `SELECT ${columns} FROM ${tables}${filters}`;
let orderByCode = '';
if (view.orderBy) {
orderByCode = `
query = query + '${buildOrderBy(view)}';
`;
}
let funcParams = 'params: {[key: string]: string}, offset: number, limit: number';
let limitOrderCode = `query = query + ' LIMIT ?, ?';
queryValues.push(offset.toString(10));
queryValues.push(limit.toString(10));
${orderByCode}`;
if (view.type === 'count') {
funcParams = 'params: {[key: string]: string}';
limitOrderCode = ``;
}
func = `
public ${funcName}(${funcParams}) {
let query: string = '${query}';
let queryValues: string[] = [${requiredQueryValues}];
// optional params?
${optionalFilterParts}
${limitOrderCode}
return super.runQuery(query, [...queryValues]);
}
`;
return func;
}
function buildJoin(view: EndpointDef): string {
if (view.join?.length) {
let join = `${view.table}`;
for (let i in view.join) {
let j: JoinDef = view.join[i];
join = join + ` JOIN ${j.table} ON ${j.on.left.table}.${j.on.left.name} = ${j.on.right.table}.${j.on.right.name}`;
}
return join;
}
return '';
}
function buildOrderBy(view: EndpointDef): string {
let orderBy: string = '';
if (view.orderBy?.length) {
orderBy = orderBy + ' ORDER BY ';
for (let i in view.orderBy) {
let order: Order = view.orderBy[i];
if (parseInt(i) !== 0) {
orderBy = orderBy + ', ';
}
orderBy = orderBy + `${order.column.table}.${order.column.name} ${order.direction}`;
}
}
return orderBy;
}
function tableHasColumn(tableName: string, column: string, sysDef: SystemDef): boolean {
let answer: boolean = false;
for (let i in sysDef.storage.tables) {
let table: TableDef = sysDef.storage.tables[i];
if (table.name === tableName) {
for (let j in table.columns) {
let c: ColumnDef = table.columns[j];
if (column === c.name) {
answer = true;
break;
}
}
if (!answer && table.relations.length) {
for (let k in table.columns) {
let r: BelongsToDef = table.relations[k];
if (column === `${r.table}_id`) {
answer = true;
break;
}
}
}
}
if (answer) {
break;
}
}
return answer;
}
function uppercaseFirstLetter(input: string) function uppercaseFirstLetter(input: string)
{ {
return input.charAt(0).toUpperCase() + input.slice(1); return input.charAt(0).toUpperCase() + input.slice(1);

37
ui-component-plan.md Normal file
View File

@ -0,0 +1,37 @@
# System Builder
System builder is a full stack builder for apps that allows you to define your data backend, APIs and views of the data and design your own apps or webpages
### UI Components
All UI components need to be able to link into any data/api that has been fetched.
* Text
* Image
* Input (all types) plus Date Picker and Dropdown, checkbox, radio, text, textarea, email, password
* Markdown editor: https://simplemde.com/ plus https://github.com/showdownjs/showdown
* Table (with actions per item, maybe buttons on the list item)
* List (with actions per item, maybe buttons on the list item)
* Button (with text and actions)
* charts?
* div? <- probably just a 'section' that can contain other components
* Loading style/animation?
#### Things to keep track of
There will have to be stuff we keep track of in order to reproduce the page as the user created it.
* All styles -> possibly apply css dynamically this way: https://stackoverflow.com/questions/1720320/how-to-dynamically-create-css-class-in-javascript-and-apply
* Location in the document (could be hard coded but makes it hard for responsive design)
* class and id names for reference in styles and events
* accessibility text and other accessibility info
#### Other things
* Upload images to use in styles for backgrounds or elements
* Save text and such in the database separate from the data source
#### How data sources will work
Many data sources can be defined or connected, but the default will be the app that is created in the system builder UI. Each view gets created with a unique name. All components can pull data from a view by referencing the view's name plus property or index. Data sources will all be pulled using the RXJS pattern used in bx-console and referenced by their unique names. Update calls will allow you to define a list of get requests to refresh after the update calls are made. Each view will have some state stored on it in addition to the data, including loading, and error state/messages.

View File

@ -7,6 +7,11 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.13.tgz#ee1128e881b874c371374c1f72201893616417c9" resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.13.tgz#ee1128e881b874c371374c1f72201893616417c9"
integrity sha512-rouEWBImiRaSJsVA+ITTFM6ZxibuAlTuNOCyxVbwreu6k6+ujs7DfnU9o+PShFhET78pMBl3eH+AGSI5eOTkPA== integrity sha512-rouEWBImiRaSJsVA+ITTFM6ZxibuAlTuNOCyxVbwreu6k6+ujs7DfnU9o+PShFhET78pMBl3eH+AGSI5eOTkPA==
"@types/pluralize@^0.0.29":
version "0.0.29"
resolved "https://registry.yarnpkg.com/@types/pluralize/-/pluralize-0.0.29.tgz#6ffa33ed1fc8813c469b859681d09707eb40d03c"
integrity sha512-BYOID+l2Aco2nBik+iYS4SZX0Lf20KPILP5RGmM1IgzdwNdTs0eebiFriOPcej1sX9mLnSoiNte5zcFxssgpGA==
inherits@2.0.3: inherits@2.0.3:
version "2.0.3" version "2.0.3"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
@ -25,6 +30,11 @@ path@^0.12.7:
process "^0.11.1" process "^0.11.1"
util "^0.10.3" util "^0.10.3"
pluralize@^8.0.0:
version "8.0.0"
resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-8.0.0.tgz#1a6fa16a38d12a1901e0320fa017051c539ce3b1"
integrity sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==
process@^0.11.1: process@^0.11.1:
version "0.11.10" version "0.11.10"
resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"