-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
19 changed files
with
38,558 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -59,3 +59,6 @@ typings/ | |
|
||
# next.js build output | ||
.next | ||
|
||
d.json | ||
.DS_Store |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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! | ||
|
||
「雖然我覺得不怎麼可能有人會關注我」 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
Oops, something went wrong.