feat(xod-fs, xod-client-electron, xod-cli): load libs from a local workspace and extra lib directories (+4 squashed commits)

Squashed commits:
[12a9428] refactor(xod-cli): bundle std libs with xodc and use them in `xodc transpile` command
[66d1b9d] refactor(xod-fs, workspace): rename `lib` folder into `__lib__` and make it not necessary and don't copy stdlib into user workspace
[f19c366] feat(xod-fs): load project with libs from local workspace and bundled workspace
[ea9ac0e] feat(xod-func-tools): add `uniqLists` function
This commit is contained in:
Kirill Shumilov
2017-10-20 17:40:36 +03:00
parent 54be405128
commit fb248cf16e
242 changed files with 393 additions and 252 deletions

View File

@@ -1 +1,2 @@
bin/
__lib__/

View File

@@ -3,7 +3,9 @@
"version": "0.14.0",
"description": "XOD project: Command Line Interface",
"scripts": {
"build": "babel src/ -d bin/ --source-maps",
"build:workspace": "cpx \"../../workspace/__lib__/**\" \"./__lib__\"",
"build:src": "babel src/ -d bin/ --source-maps",
"build": "yarn run build:src && yarn run build:workspace",
"dev": "yarn run build --watch",
"test": "mocha test/**/*.spec.js"
},
@@ -33,6 +35,7 @@
"devDependencies": {
"chai": "^4.0.0",
"child-process-promise": "^2.2.1",
"cpx": "^1.5.0",
"fs-extra": "^3.0.1"
}
}

View File

@@ -101,8 +101,10 @@ export default function install(swaggerUrl, libUri, path2) {
xodFs.findClosestWorkspaceDir(path2),
])
.then(([libUri2, swaggerClient, workspaceDir]) => {
const orgDir = path.resolve(workspaceDir, 'lib',
xodFs.fsSafeName(libUri2.orgname));
const orgDir = path.resolve(
xodFs.resolveLibPath(workspaceDir),
xodFs.fsSafeName(libUri2.orgname)
);
const libDir = path.resolve(orgDir, xodFs.fsSafeName(libUri2.libname));
return libDirDoesNotExist(libDir, libUri2)
.then(() => getProject(swaggerClient, libUri2))

View File

@@ -12,7 +12,7 @@ export default (projectDir, output) => {
msg.notice(`Packing ${dirName} into ${output} ...`);
loadProjectWithLibs(projectPath, workspace)
loadProjectWithLibs([], projectPath, workspace)
.then(({ project, libs }) => pack(project, libs))
.then(packed => writeJSON(output, packed))
.then(() => {

View File

@@ -8,6 +8,8 @@ import { loadProject, readJSON, writeFile } from 'xod-fs';
import { transformProject, transpile } from 'xod-arduino';
import * as msg from './messages';
const bundledLibs = path.resolve(__dirname, '../__lib__');
const showErrorAndExit = (err) => {
msg.error(err);
process.exit(1);
@@ -38,7 +40,7 @@ export default (input, patchPath, program) => {
}
if (isDirectory) {
loadProject(dir)
loadProject(dir, [bundledLibs])
.then(resolve)
.catch(reject);
}

View File

@@ -7,10 +7,10 @@
"name": "xod-client-electron",
"version": "0.14.0",
"scripts": {
"copy-workspace": "cpx \"../../workspace/**/*\" \"src-babel/workspace\"",
"build:workspace": "cpx \"../../workspace/**/*\" \"src-babel/workspace\"",
"build:gui": "webpack --colors",
"build:app": "babel src/app/ -d src-babel/app/ && babel src/shared/ -d src-babel/shared/",
"build": "yarn build:gui && yarn build:app && yarn copy-workspace",
"build": "yarn build:gui && yarn build:app && yarn build:workspace",
"start": "electron .",
"postinstall": "electron-builder install-app-deps",
"test": "electron-mocha ./test",

View File

@@ -4,8 +4,8 @@ import path from 'path';
import * as XP from 'xod-project';
import {
resolveLibPath,
spawnWorkspaceFile,
spawnStdLib,
spawnDefaultProject,
saveProject,
getLocalProjects,
@@ -14,7 +14,6 @@ import {
getFilePath,
filterDefaultProject,
findProjectMetaByName,
resolveLibPath,
resolveDefaultProjectPath,
ensureWorkspacePath,
ERROR_CODES as FS_ERROR_CODES,
@@ -32,6 +31,7 @@ import * as EVENTS from '../shared/events';
export const getPathToBundledWorkspace = () => path.resolve(__dirname, '../workspace');
export const getPathToBundledLibs = () => resolveLibPath(getPathToBundledWorkspace());
// =============================================================================
//
@@ -86,8 +86,6 @@ export const updateWorkspace = R.curry(
//
// =============================================================================
// :: () -> Path
const getStdLibPath = () => resolveLibPath(getPathToBundledWorkspace());
// :: () -> Path
export const getDefaultProjectPath = () => resolveDefaultProjectPath(getPathToBundledWorkspace());
@@ -150,10 +148,6 @@ export const saveWorkspacePath = workspacePath => R.compose(
settings.load
)();
// :: Path -> Promise Path Error
const spawnWorkspace = workspacePath => spawnWorkspaceFile(workspacePath)
.then(spawnStdLib(getStdLibPath()));
// :: (String -> a -> ()) ->Path -> Promise ProjectMeta[] Error
const spawnAndLoadDefaultProject = (send, workspacePath) =>
spawnDefaultProject(getDefaultProjectPath(), workspacePath)
@@ -204,7 +198,7 @@ export const onOpenProject = R.curry(
// :: (String -> a -> ()) -> (() -> Path) -> Path -> Promise Project Error
export const onSelectProject = R.curry(
(send, pathGetter, projectMeta) => pathGetter()
.then(() => loadProject(getFilePath(projectMeta)))
.then(() => loadProject(getFilePath(projectMeta), [getPathToBundledLibs()]))
.then(requestShowProject(send))
.catch(R.ifElse(
R.prop('errorCode'),
@@ -217,7 +211,7 @@ export const onSelectProject = R.curry(
// :: (String -> a -> ()) -> (Path -> Promise Path Error) -> Path -> Promise ProjectMeta[] Error
export const onCreateWorkspace = R.curry(
(send, pathSaver, workspacePath) => R.pipeP(
spawnWorkspace,
spawnWorkspaceFile,
pathSaver,
updateWorkspace(send, ''),
loadProjectsOrSpawnDefault(send)

View File

@@ -1,7 +1,7 @@
export const WORKSPACE_FILENAME = '.xodworkspace';
export const DEFAULT_WORKSPACE_PATH = '~/xod/';
export const DEFAULT_PROJECT_NAME = 'welcome-to-xod';
export const LIBS_FOLDERNAME = 'lib';
export const LIBS_DIRNAME = '__lib__';
export const IMPL_FILENAMES = {
cpp: 'any.cpp',

View File

@@ -13,7 +13,7 @@ export {
loadProjectWithLibs,
loadProjectWithoutLibs,
} from './load';
export { loadAllLibs } from './loadLibs';
export { loadLibsFromWorkspaceList } from './loadLibs';
export * from './utils';
export {
findClosestProjectDir,

View File

@@ -7,11 +7,12 @@ import * as XP from 'xod-project';
import pack from './pack';
import { findClosestWorkspaceDir } from './find';
import { loadAllLibs } from './loadLibs';
import { loadLibs } from './loadLibs';
import { readDir, readJSON } from './read';
import * as ERROR_CODES from './errorCodes';
import {
resolvePath,
resolveLibPath,
resolveProjectFile,
isLocalProjectDirectory,
basenameEquals,
@@ -122,35 +123,61 @@ export const loadProjectWithoutLibs = projectPath => R.composeP(
readDir
)(projectPath);
// :: Path -> Path -> Path -> Promise [File] Error
export const loadProjectWithLibs = (projectPath, workspace, libWorkspace = workspace) => {
const fullProjectPath = resolvePath(path.resolve(workspace, projectPath));
const libPath = resolvePath(libWorkspace);
// :: [Path] -> Path -> Path -> Promise [File] Error
export const loadProjectWithLibs = R.curry(
(extraLibDirs, projectPath, workspace) => {
const fullProjectPath = resolvePath(path.resolve(workspace, projectPath));
const userLibsPath = resolveLibPath(workspace);
return Promise.all([
loadProjectWithoutLibs(fullProjectPath),
loadAllLibs(libPath),
]).then(([projectFiles, libs]) => ({ project: projectFiles, libs }))
.catch(err => Promise.reject(
Object.assign(err, {
libPath,
fullProjectPath,
workspace,
})
));
};
const libDirPaths = R.compose(
R.concat([userLibsPath]),
R.map(resolvePath)
)(extraLibDirs);
return Promise.all([
loadProjectWithoutLibs(fullProjectPath),
loadLibs(libDirPaths),
]).then(([projectFiles, libs]) => ({ project: projectFiles, libs }))
.catch(err => Promise.reject(
Object.assign(err, {
libPath: userLibsPath,
fullProjectPath,
workspace,
})
));
}
);
// :: Path -> Promise Project Error
//
// Loads a regular XOD project placed in a workspace. The workspace and project
// name are determined by path provided. It is expected to be a path to the
// project directory, e.g. `/path/to/workspace/my-proj`.
//
// Returns a Promise of complete `Project` (see `xod-project`).
export const loadProject = projectPath =>
/**
* Loads a regular XOD project placed in a workspace. Workspace and project
* names are determined by path provided. It is expected to be a path to the
* project directory, e.g. `/path/to/workspace/my-proj`.
*
* Also it loads libraries from libs directory inside of the user workspace
* and from `extraLibDirs` list, if it is not empty.
* The lib directory in the user workspace has a highest priority,
* all extra lib dirs has a smaller priority in accordance to its index
* (more index is less priority).
*
* If lib directories contains libraries with the same names — only one will
* be loaded, from lib directory with the highest priority.
*
* E.G.
* - user workspace contains `xod/units`
* - extraLibDir[0] contains `xod/core`, `xod/common-hardware`, `xod/units`
* - extraLibDir[1] contains `xod/core`, `xod/awesome`
* As a result:
* - `xod/units` will be loaded from user workspace
* - `xod/core` and `xod/common-hardware` will be loaded from extraLibDir[0]
* - `xod/awesome` will be loaded from extraLibDir[1]
*
* Returns a Promise of complete `Project` (see `xod-project`).
*/
export const loadProject = (projectPath, extraLibDirs = []) =>
findClosestWorkspaceDir(projectPath)
.then(workspace => [path.relative(workspace, projectPath), workspace])
.then(R.apply(loadProjectWithLibs))
.then(R.apply(loadProjectWithLibs(extraLibDirs)))
.then(({ project, libs }) => pack(project, libs))
.then(XP.injectProjectTypeHints)
.then(XP.resolveNodeTypesInProject);

View File

@@ -2,10 +2,10 @@ import R from 'ramda';
import path from 'path';
import * as XP from 'xod-project';
import { uniqLists } from 'xod-func-tools';
import { readDir, readJSON } from './read';
import {
resolvePath,
getPatchName,
hasExt,
rejectOnInvalidPatchFileContents,
@@ -17,18 +17,19 @@ import {
addMissingOptionsToPatchFileContents,
} from './convertTypes';
const scanLibsFolder = (libs, libsDir) => Promise.all(
libs.map(
lib => readDir(path.resolve(libsDir, lib))
.then(R.filter(hasExt('.xodp')))
.catch((err) => {
throw Object.assign(err, {
path: path.resolve(libsDir, lib),
libName: lib,
});
})
))
.then(R.zipObj(libs));
const scanLibsFolder = (libs, libsDir) =>
Promise.all(
libs.map(
lib => readDir(path.resolve(libsDir, lib))
.then(R.filter(hasExt('.xodp')))
.catch((err) => {
throw Object.assign(err, {
path: path.resolve(libsDir, lib),
libName: lib,
});
})
))
.then(R.zipObj(libs));
const readLibFiles = (libfiles) => {
const libNames = Object.keys(libfiles);
@@ -55,12 +56,10 @@ const readLibFiles = (libfiles) => {
return Promise.all(libPromises);
};
export const loadLibs = (libs, workspace) => {
const libsDir = path.resolve(resolvePath(workspace), 'lib');
return scanLibsFolder(libs, libsDir)
export const loadLibrary = (libs, libsDir) =>
scanLibsFolder(libs, libsDir)
.then(readLibFiles)
.then(R.indexBy(XP.getPatchPath));
};
// extract libNames from paths to xod-files
// for example: '/Users/vasya/xod/lib/xod/core/and/patch.xodm' -> 'xod/core'
@@ -77,16 +76,15 @@ const extractLibNamesFromFilenames = libsDir => R.compose(
)
);
export const loadAllLibs = (workspace) => {
const libsDir = path.resolve(resolvePath(workspace), 'lib');
// :: Path -> Promise [LibName]
const scanForLibNames = (libDir) => {
const folder = '.';
return scanLibsFolder([folder], libsDir)
.then(R.compose(
extractLibNamesFromFilenames(libsDir),
R.prop(folder)
))
.then(libs => loadLibs(libs, workspace))
return R.composeP(
extractLibNamesFromFilenames(libDir),
R.prop(folder),
scanLibsFolder
)([folder], libDir)
.catch((err) => {
// Catch error ENOENT in case if libsDir is not found.
// E.G. User deleted it before select project.
@@ -95,3 +93,20 @@ export const loadAllLibs = (workspace) => {
return Promise.reject(err);
});
};
// :: [Path] -> Map PatchPath Patch
export const loadLibs = libDirs => R.composeP(
R.mergeAll,
R.unnest,
libPairs => Promise.all(
R.map(
([libDir, libs]) => loadLibrary(libs, libDir),
libPairs
)
),
R.toPairs,
R.reject(R.isEmpty),
R.zipObj(libDirs),
uniqLists,
libDir => Promise.all(R.map(scanForLibNames, libDir))
)(libDirs);

View File

@@ -4,7 +4,7 @@ import copy from 'recursive-copy';
import { rejectWithCode } from 'xod-func-tools';
import { writeFile } from './write';
import { resolvePath, resolveLibPath, resolveDefaultProjectPath } from './utils';
import { resolvePath, resolveDefaultProjectPath } from './utils';
import { WORKSPACE_FILENAME } from './constants';
import * as ERROR_CODES from './errorCodes';
@@ -20,13 +20,6 @@ export const spawnWorkspaceFile = workspacePath =>
.then(() => workspacePath)
.catch(rejectWithCode(ERROR_CODES.CANT_CREATE_WORKSPACE_FILE));
// :: Path -> Promise Path Error
export const spawnStdLib = curry((stdLibPath, workspacePath) =>
copy(stdLibPath, resolveLibPath(workspacePath), copyOptions)
.then(() => workspacePath)
.catch(rejectWithCode(ERROR_CODES.CANT_COPY_STDLIB))
);
// :: Path -> Promise Path Error
export const spawnDefaultProject = curry((defaultProjectPath, workspacePath) =>
copy(defaultProjectPath, resolveDefaultProjectPath(workspacePath), copyOptions)

View File

@@ -16,7 +16,7 @@ import { PatchFileContents, Path, def } from './types';
import {
DEFAULT_WORKSPACE_PATH,
DEFAULT_PROJECT_NAME,
LIBS_FOLDERNAME,
LIBS_DIRNAME,
WORKSPACE_FILENAME,
IMPL_FILENAMES,
} from './constants';
@@ -231,7 +231,7 @@ export const resolveWorkspacePath = R.compose(
export const resolveLibPath = def(
'resolveLibPath :: Path -> Path',
workspacePath => path.resolve(
workspacePath, LIBS_FOLDERNAME
workspacePath, LIBS_DIRNAME
)
);
export const resolveDefaultProjectPath = def(
@@ -265,18 +265,11 @@ const isWorkspaceDirEmptyOrNotExist = def(
R.T
)
);
const doesWorkspaceHaveStdLib = def(
'doesWorkspaceHaveStdLib :: Path -> Boolean',
R.compose(
doesDirectoryExist,
resolveLibPath
)
);
// :: Path -> Promise Path Error
export const isWorkspaceValid = R.cond([
[
R.both(doesWorkspaceFileExist, doesWorkspaceHaveStdLib),
doesWorkspaceFileExist,
Promise.resolve.bind(Promise),
],
[

View File

@@ -0,0 +1,2 @@
{
}

View File

@@ -0,0 +1,52 @@
{
"links": [
{
"id": "1d78b081-ebfe-4a2d-b857-08a3b9ddd90f",
"input": {
"nodeId": "2c03e470-fefd-4f58-be6a-58d209d9158c",
"pinKey": "value"
},
"output": {
"nodeId": "4d716df5-81e7-4845-9b82-31a54c1634d7",
"pinKey": "38395ee5-8634-40f9-b7fa-dfe9eee208e8"
}
},
{
"id": "3ed564cd-71e5-4ad3-9361-e7b9719bc82a",
"input": {
"nodeId": "2c03e470-fefd-4f58-be6a-58d209d9158c",
"pinKey": "value"
},
"output": {
"nodeId": "89c69d91-b484-478d-8990-027e5f0a2e3e",
"pinKey": "b"
}
}
],
"nodes": [
{
"id": "2c03e470-fefd-4f58-be6a-58d209d9158c",
"position": {
"x": 292,
"y": 182
},
"type": "xod/core/pot"
},
{
"id": "4d716df5-81e7-4845-9b82-31a54c1634d7",
"position": {
"x": 363,
"y": 322
},
"type": "@/qux"
},
{
"id": "89c69d91-b484-478d-8990-027e5f0a2e3e",
"position": {
"x": 163,
"y": 314
},
"type": "xod/core/and"
}
]
}

View File

@@ -0,0 +1,7 @@
{
"authors": [
"Amperka team"
],
"name": "awesome-project",
"version": "0.0.42"
}

View File

@@ -0,0 +1,7 @@
# Hello world!
- It's a test file
- If you see a correct content — it passed!
- Also it could contain any UTF-8 characters, like this one 😎 or even кириллица!
Have a nice day!

View File

@@ -0,0 +1 @@
// Impl loaded fine!

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 B

View File

@@ -0,0 +1,33 @@
{
"links": [
{
"id": "30dafe79-37e6-4fc0-a8b8-fa5e912ffef1",
"input": {
"nodeId": "38395ee5-8634-40f9-b7fa-dfe9eee208e8",
"pinKey": "PIN"
},
"output": {
"nodeId": "4da4c29d-f482-47c2-825e-a969084b1116",
"pinKey": "brightness"
}
}
],
"nodes": [
{
"id": "38395ee5-8634-40f9-b7fa-dfe9eee208e8",
"position": {
"x": 276,
"y": 155
},
"type": "xod/patch-nodes/input-number"
},
{
"id": "4da4c29d-f482-47c2-825e-a969084b1116",
"position": {
"x": 310,
"y": 307
},
"type": "xod/core/led"
}
]
}

View File

@@ -70,7 +70,7 @@ describe('Loader', () => {
describe('loadProjectWithLibs', () => {
it('should return project with libs', () =>
Loader.loadProjectWithLibs(projectPath, workspace)
Loader.loadProjectWithLibs([], projectPath, workspace)
.then(({ project, libs }) => {
const quxPatch = R.find(R.pathEq(['content', 'path'], '@/qux'), project);
assert.isDefined(quxPatch);
@@ -90,7 +90,7 @@ describe('Loader', () => {
const okProject = path.resolve(brokenWorkspace, './ok-project');
return expectRejectedWithCode(
Loader.loadProjectWithLibs(okProject, brokenWorkspace),
Loader.loadProjectWithLibs([], okProject, brokenWorkspace),
ERROR_CODES.INVALID_FILE_CONTENTS
);
}
@@ -100,7 +100,7 @@ describe('Loader', () => {
describe('loadProjectWithoutLibs', () => {
it('should return project without libs', () => {
const xodCore = path.resolve(workspace, './lib/xod/core');
const xodCore = path.resolve(workspace, './__lib__/xod/core');
const xodCoreOwner = path.resolve(xodCore, '..');
return Loader.loadProjectWithoutLibs(xodCore)

View File

@@ -2,26 +2,28 @@ import R from 'ramda';
import { assert } from 'chai';
import path from 'path';
import { loadLibs, loadAllLibs } from '../src/loadLibs';
import { loadLibrary, loadLibs } from '../src/loadLibs';
import { resolveLibPath } from '../src/utils';
const workspaceDir = './fixtures/workspace';
describe('Library loader', () => {
const workspace = path.resolve(__dirname, workspaceDir);
const libDir = resolveLibPath(workspace);
it('should load xod/core libs from ./fixtures/workspace/lib', () =>
loadLibs(['xod/core'], workspace).then((libs) => {
it('should load xod/core libs from ./fixtures/workspace/lib', () => {
loadLibrary(['xod/core'], libDir).then((libs) => {
assert.sameMembers(R.keys(libs), [
'xod/core/and',
'xod/core/led',
'xod/core/pot',
'xod/core/test',
]);
})
);
});
});
it('should load all libs from ./fixtures/workspace/lib', () =>
loadAllLibs(workspace).then((libs) => {
loadLibs([libDir]).then((libs) => {
assert.sameMembers(R.keys(libs), [
'user/utils/test',
'user/with-omitted-optionals/empty-lib-patch',

View File

@@ -1,7 +1,7 @@
import { assert } from 'chai';
import fs from 'fs-extra';
import { spawnWorkspaceFile, spawnStdLib, spawnDefaultProject } from '../src/spawn';
import { spawnWorkspaceFile, spawnDefaultProject } from '../src/spawn';
import { doesFileExist, doesDirectoryExist } from '../src/utils';
import * as ERROR_CODES from '../src/errorCodes';
@@ -17,23 +17,6 @@ describe('Spawn', () => {
});
});
it('spawnStdLib resolves to workspace path on success', () => {
after(() => fs.remove(fixture('./new-workspace')));
return spawnStdLib(fixture('./workspace/lib'), fixture('./new-workspace'))
.then(() => {
assert.ok(doesDirectoryExist(fixture('./new-workspace/lib')));
fs.readdir(fixture('./new-workspace/lib'), (err, files) => {
assert.includeMembers(files, ['xod', 'user']);
});
});
});
it('spawnStdLib rejects CANT_COPY_STDLIB on failure', () =>
expectRejectedWithCode(
spawnStdLib(fixture('./no-dir/no-lib'), fixture('./new-workspace')),
ERROR_CODES.CANT_COPY_STDLIB
)
);
it('spawnDefaultProject resolves to workspace path on success', () => {
after(() => fs.remove(fixture('./new-workspace')));
return spawnDefaultProject(fixture('./workspace/awesome-project'), fixture('./new-workspace'))

View File

@@ -53,7 +53,7 @@ describe('Utils', () => {
);
it('isWorkspaceValid: if specified directory is not empty, rejects with error code WORKSPACE_DIR_NOT_EMPTY',
() => expectRejectedWithCode(
U.isWorkspaceValid(fixture('./emptyWorkspace')),
U.isWorkspaceValid(fixture('./notEmpty')),
ERROR_CODES.WORKSPACE_DIR_NOT_EMPTY
)
);

View File

@@ -11,7 +11,7 @@ describe('welcome-to-xod', () => {
const projectPath = 'welcome-to-xod';
it('should load as a valid project', () =>
loadProjectWithLibs(projectPath, workspace)
loadProjectWithLibs([], projectPath, workspace)
.then(({ project, libs }) => {
const packed = pack(project, libs);
assert.isAtLeast(

View File

@@ -332,6 +332,26 @@ export const invertMap = def(
)
);
/**
* Returns the list of list of strings.
* Removes all duplicates from the subsequent list
* on the basis of already filtered values lists.
*
* E.G.
* [['a', 'b', 'c'], ['b','c','d'], ['a','d','e']]
* will become
* [['a', 'b', 'c'], ['d'], ['e']]
*/
export const uniqLists = def(
'uniqLists :: [[String]] -> [[String]]',
R.reduce(
(acc, nextList) => R.append(
R.without(R.unnest(acc), nextList),
acc
),
[]
)
);
export default Object.assign(
{
@@ -358,6 +378,7 @@ export default Object.assign(
mapIndexed,
swap,
reverseLookup,
uniqLists,
},
types
);

View File

@@ -10,6 +10,7 @@ import {
reduceMaybe,
reduceEither,
omitRecursively,
uniqLists,
} from '../src/index';
describe('explode', () => {
@@ -142,3 +143,24 @@ describe('omitRecursively', () => {
assert.deepEqual(cleanObj, [[{ foo: 2 }]]);
});
});
describe('uniqLists', () => {
it('returns filtered list', () => {
assert.deepEqual(
uniqLists([['a', 'b', 'c'], ['b', 'c', 'd'], ['a', 'd', 'e', 'f']]),
[['a', 'b', 'c'], ['d'], ['e', 'f']]
);
});
it('returns filtered list with untouched empty lists', () => {
assert.deepEqual(
uniqLists([['a', 'b', 'c'], ['b', 'c', 'd'], [], []]),
[['a', 'b', 'c'], ['d'], [], []]
);
});
it('returns empty list for empty list', () => {
assert.deepEqual(
uniqLists([]),
[]
);
});
});

Some files were not shown because too many files have changed in this diff Show More