Skip to content

Commit

Permalink
refactor: first commit (#1)
Browse files Browse the repository at this point in the history
  • Loading branch information
XadillaX committed Jun 15, 2018
1 parent 11eb517 commit 0e031c1
Show file tree
Hide file tree
Showing 19 changed files with 38,558 additions and 1 deletion.
14 changes: 14 additions & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"extends": "eslint-config-xadillax-style",
"parserOptions": {
"ecmaVersion": 2017
},
"globals": {
"it": true,
"describe": true,
"before": true,
"after": true,
"beforeEach": true,
"afterEach": true
}
}
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,6 @@ typings/

# next.js build output
.next

d.json
.DS_Store
11 changes: 11 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
language: node_js
node_js:
- "8"
- "10"

env:
- CI=true

after_script: npm run coverage

sudo: false
93 changes: 92 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,93 @@
# shameimaru
# Shameimaru.js

Shameimaru Aya likes to traverse node_modules and capture the tree.

![Shameimaru](shameimaru.jpg)

## Installation

```shell
$ npm install --save shameimaru
```

## Usage

```js
const Shameimaru = require("shameimaru");

const shameimaru = new Shameimaru("<YOUR_PROJ_ROOT>");
```

> `<YOUR_PROJ_ROOT>` is the root path which contains **node_modules** of your project.
After create the `Shameimaru` instance, you can do `traverse()` through it.

```js
const ret = await shameimaru.traverse();
```

Then you'll get a may-flatten graph-form tree. e.g.

```json
{
"@crand/mt19937": {
"ref": "5c2f5c96-9c29-4f3f-8cc1-ec6ab1f4025b",
"name": "@crand/mt19937",
"version": "2.0.0",
"from": "@crand/mt19937@2.0.0",
"resolved": "http://registry.npm.taobao.org/@crand/mt19937/download/@crand/mt19937-2.0.0.tgz",
"exists": true,
"rawSpec": "*"
},
"any-promise": {
"ref": "78325895-5945-4180-97dd-a01c705b254e",
"name": "any-promise",
"version": "0.2.0",
"from": "any-promise@0.2.0",
"resolved": "http://registry.npm.taobao.org/any-promise/download/any-promise-0.2.0.tgz",
"exists": true,
"rawSpec": "0.2.0"
},
"mz": {
"ref": "63bb611b-232d-4f7a-ba53-3322670ed170",
"name": "mz",
"version": "2.7.0",
"from": "mz@2.7.0",
"resolved": "http://registry.npm.taobao.org/mz/download/mz-2.7.0.tgz",
"exists": true,
"rawSpec": "^2.7.0",
"dependencies": {
"any-promise": {
"ref": "41f0b04f-0904-432f-aa33-13e5cbb8fcdc",
"name": "any-promise",
"version": "1.3.0",
"from": "any-promise@1.3.0",
"resolved": "http://registry.npm.taobao.org/any-promise/download/any-promise-1.3.0.tgz",
"exists": true,
"rawSpec": "^1.0.0"
}
},
...
},
...
}
```

Each element in the result may contains keys as below:

+ `ref`: a random referrence sign in this tree, it's unique; e.g. `63bb611b-232d-4f7a-ba53-3322670ed170`
+ `name`: the name of this package (dependency); e.g. `toshihiko`
+ `version`: the name of this package (dependency); e.g. `2.7.0`
+ `from`: same as `_from` in installed **package.json**; e.g. `mz@^2.0.0`
+ `resolved`: same as `_resolved` in installed **package.json**; `http://registry.npm.taobao.org/mz/download/mz-2.7.0.tgz`
+ `exists`: whether it's really exist in current tree folder; e.g. `true`
+ `ancestor`: if it matches a exactly the same package at any upper directory, it indicates that element's `ref`; e.g. `63bb611b-232d-4f7a-ba53-3322670ed170`
+ `rawSpec`: the raw spec of this package in its parent's **package.json**; e.g. `^2.0.0`
+ `adjustHere`: this package is not need by its parent, but some package need it flatten here; e.g. `true`
+ `missing`: if we can't find this package at any right path, then it will be `true`; e.g. `true`

## Contribute

You're welcome to fork and make pull requests!

「雖然我覺得不怎麼可能有人會關注我」
8 changes: 8 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/**
* XadillaX <i@2333.moe> created at 2018-06-11 12:41:09 with ❤
*
* Copyright (c) 2018 xcoder.in, all rights reserved.
*/
"use strict";

module.exports = require("./lib/shameimaru");
29 changes: 29 additions & 0 deletions lib/shameimaru.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* XadillaX <i@2333.moe> created at 2018-06-11 14:01:00 with ❤
*
* Copyright (c) 2018 xcoder.in, all rights reserved.
*/
"use strict";

const fs = require("fs");
const path = require("path");

const traverse = require("./traverse");
const utils = require("./utils");

class Shameimaru {
constructor(projDir) {
this.projDir = projDir;
this.nodeModuleDir = path.resolve(process.cwd(), projDir, "node_modules");
this.package = JSON.parse(fs.readFileSync(path.join(projDir, "package.json"), "utf8"));
}

async traverse() {
const ret = await traverse(
utils.extraDependenciesFromPackage(this.package),
this.nodeModuleDir);
return ret;
}
}

module.exports = Shameimaru;
215 changes: 215 additions & 0 deletions lib/traverse.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
/**
* XadillaX <i@2333.moe> created at 2018-06-13 20:47:50 with ❤
*
* Copyright (c) 2018 xcoder.in, all rights reserved.
*/
"use strict";

const path = require("path");

const fs = require("mz/fs");
const Linklist = require("algorithmjs").ds.Linklist;
const npa = require("npm-package-arg");
const uuid = require("uuid/v4");

const utils = require("./utils");

const TRAVERSED = Symbol("traversed");

async function getPackagesInDir(nodeModuleDir) {
if(!fs.existsSync(nodeModuleDir) ||
!(await fs.stat(nodeModuleDir)).isDirectory()) {
return [];
}

const depDirs = await fs.readdir(nodeModuleDir);
const pkgs = [];
for(const name of depDirs) {
if(name.startsWith(".") || name.startsWith("_")) continue;

const fullPath = path.join(nodeModuleDir, name);
if(!(await fs.stat(fullPath)).isDirectory()) continue;

// if the directory name starts with '@', then we add its all child
// directories to the result array;
//
// otherwise, we only push this directory itself.

if(name.startsWith("@")) {
const atDirs = await fs.readdir(fullPath);
for(const atName of atDirs) {
if(atName.startsWith(".") || atName.startsWith("_")) continue;

const pkgPath = path.join(fullPath, atName, "package.json");
if(!fs.existsSync(pkgPath)) continue;
const pkg = await utils.readJSON(pkgPath);
pkgs.push({ pkg, moduleDir: path.dirname(pkgPath) });
}
continue;
}

const pkgPath = path.join(fullPath, "package.json");
if(!fs.existsSync(pkgPath)) continue;
const pkg = await utils.readJSON(pkgPath);
pkgs.push({ pkg, moduleDir: fullPath });
}

return pkgs;
}

function searchAncestor(ancestors, currentFolder, pkg) {
// replace `@foo/bar` to `@foo!bar` to let `path.dirname` regards it as one
// folder.
const name = (typeof pkg === "string" ? pkg : pkg.name).replace("/", "!");
pkg = typeof pkg === "string" ? null : pkg;
let onlySlash = false;

do {
if("/" === currentFolder) onlySlash = true;

const tryFolder = path.join(currentFolder, name);
const ancestor = ancestors[tryFolder];
if(ancestor) {
return !pkg || pkg.version === ancestor.version && pkg._resolved === ancestor.resolved ?
ancestor :
null;
}

currentFolder = path.dirname(currentFolder);
} while(!onlySlash);

return null;
}

function genRef(refs) {
do {
const ref = uuid();
if(!refs[ref]) {
refs[ref] = true;
return ref;
}
} while(1);
}

async function scanDir(q, node, ancestors, refs) {
const { dir, dependencies, tree, dummyFolder } = node;
const pkgs = await getPackagesInDir(dir);

for(const info of pkgs) {
const { pkg, moduleDir } = info;

// npmPackageArg eg.
//
// { type: 'range',
// registry: true,
// where: undefined,
// raw: 'toshihiko@^1.0.0-alpha.7',
// name: 'toshihiko',
// escapedName: 'toshihiko',
// scope: undefined,
// rawSpec: '^1.0.0-alpha.7',
// saveSpec: null,
// fetchSpec: '^1.0.0-alpha.7',
// gitRange: undefined,
// gitCommittish: undefined,
// hosted: undefined }
const versionSpec = dependencies[pkg.name];

// add meta information to result tree & ancestors
const meta = tree[pkg.name] = {
ref: genRef(refs),
name: pkg.name,
version: pkg.version,
from: pkg._from,
resolved: pkg._resolved,
exists: true
};

// replace `@foo/bar` to `@foo!bar` to let `path.dirname` regards it as
// one folder.
const nextFolder = path.join(dummyFolder, pkg.name.replace("/", "!"));
ancestors[nextFolder] = meta;

// if dependencies column exists this package, we search for its
// ancestor or add a new task to queue;
//
// otherwise, we consider it as flatten and add a new task to queue.

if(versionSpec) {
const npmPackageArg = npa(`${pkg.name}@${versionSpec}`);
meta.rawSpec = npmPackageArg.rawSpec;
dependencies[pkg.name] = TRAVERSED;

if(dummyFolder !== "/") {
const ancestor = searchAncestor(ancestors, path.dirname(dummyFolder), pkg);
if(ancestor) {
meta.ancestor = ancestor.ref;
continue;
}
}
} else {
meta.adjustHere = true;
}

const nextDependencies = utils.extraDependenciesFromPackage(pkg);
if(Object.keys(nextDependencies).length) {
meta.dependencies = {};

// after setting the package itself, we push it as next search
// status to the queue.
q.pushBack({
dir: path.join(moduleDir, "node_modules"),
dependencies: nextDependencies,
tree: meta.dependencies,
dummyFolder: nextFolder
});
}
}

// pick up the left packages in dependencies (but not in current directory)
for(const name in dependencies) {
if(!dependencies.hasOwnProperty(name) ||
dependencies[name] === TRAVERSED) {
continue;
}

const versionSpec = dependencies[name];
const npmPackageArg = npa(`${name}@${versionSpec}`);
const meta = tree[name] = {
ref: genRef(refs),
name,
exists: false,
rawSpec: npmPackageArg.rawSpec
};

const ancestor = searchAncestor(ancestors, dummyFolder, name);
if(ancestor) {
meta.ancestor = ancestor.ref;
} else {
meta.missing = true;
}
}
}

async function traverse(rootDependencies, rootDir) {
const q = new Linklist();
const rootTree = {};
const ancestors = {};
const refs = {};

q.pushBack({
dir: rootDir,
dependencies: Object.assign({}, rootDependencies),
tree: rootTree,
dummyFolder: "/"
});

while(q.length) {
const node = q.popFront();
await scanDir(q, node, ancestors, refs);
}

return rootTree;
}

module.exports = traverse;

0 comments on commit 0e031c1

Please sign in to comment.