Skip to content

Commit

Permalink
Initial commit.
Browse files Browse the repository at this point in the history
  • Loading branch information
zachleat committed Aug 18, 2023
0 parents commit 95dbca0
Show file tree
Hide file tree
Showing 16 changed files with 238 additions and 0 deletions.
22 changes: 22 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
on:
push:
branches-ignore:
- "gh-pages"
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: ["ubuntu-latest", "macos-latest", "windows-latest"]
node: ["16", "18", "20"]
name: Node.js ${{ matrix.node }} on ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
- name: Setup node
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node }}
- run: npm install
- run: npm test
env:
YARN_GPG: no
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules
package-lock.json
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2023 Zach Leatherman

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# `@11ty/dependency-tree-esm`

Returns an unordered array of local paths to dependencies of a Node ES module JavaScript file.

This is used by Eleventy to find dependencies of a JavaScript file to watch for changes to re-run Eleventy’s build.

## Installation

```
npm install --save-dev @11ty/dependency-tree-esm
```

## Features

* Ignores `node_modules`
* Ignores Node’s built-ins (e.g. `path`)
* Handles circular dependencies
* Returns an empty set if the file does not exist.

## Usage

```js
// my-file.js

// if my-local-dependency.js has dependencies, it will include those too
const test = require("./my-local-dependency.js");

// ignored, is a built-in
const path = require("path");
```

```js
const DependencyTree = require("@11ty/dependency-tree");

DependencyTree("./my-file.js");
// returns ["./my-local-dependency.js"]
```
78 changes: 78 additions & 0 deletions main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import * as acorn from "acorn";
import { readFile } from "fs/promises";
import { default as normalizePath } from "normalize-path";
import path from "path";
import { TemplatePath } from "@11ty/eleventy-utils";

// Is *not* a bare specifier (e.g. 'some-package')
// https://nodejs.org/dist/latest-v18.x/docs/api/esm.html#terminology
function isNonBareSpecifier(importSource) {
// Change \\ to / on Windows
let normalized = normalizePath(importSource);
// Relative specifier (e.g. './startup.js')
if(normalized.startsWith("./") || normalized.startsWith("../")) {
return true;
}
// Absolute specifier (e.g. 'file:///opt/nodejs/config.js')
if(normalized.startsWith("file:")) {
return true;
}

return false;
}

function normalizeFilePath(filePath) {
return TemplatePath.addLeadingDotSlash(path.relative(".", filePath));
}

function normalizeImportSourceToFilePath(filePath, source) {
let { dir } = path.parse(filePath);
let normalized = path.join(dir, source);
return TemplatePath.addLeadingDotSlash(path.relative(".", normalized));
}

async function findByContents(contents, filePath, alreadyParsedSet) {
// Should we use dependency-graph for these relationships?
let sources = new Set();

let ast = acorn.parse(contents, {sourceType: "module", ecmaVersion: "latest"});
for(let node of ast.body) {
if(node.type === "ImportDeclaration" && isNonBareSpecifier(node.source.value)) {
let normalized = normalizeImportSourceToFilePath(filePath, node.source.value);
if(sources.has(normalized) || normalized === filePath) {
continue;
}

sources.add(normalized);
}
}

// Recurse for nested deps
for(let source of sources) {
let s = await find(source, alreadyParsedSet);
for(let p of s) {
if(sources.has(p) || p === filePath) {
continue;
}

sources.add(p);
}
}

return Array.from(sources);
}

export async function find(filePath, alreadyParsedSet = new Set()) {
// TODO add a cache here
// Unfortunately we need to read the entire file, imports need to be at the top level but they can be anywhere 🫠
let normalized = normalizeFilePath(filePath);
if(alreadyParsedSet.has(normalized)) {
return [];
}
alreadyParsedSet.add(normalized);

let contents = await readFile(normalized, { encoding: 'utf8' });
let sources = await findByContents(contents, normalized, alreadyParsedSet);

return sources;
}
37 changes: 37 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"name": "@11ty/dependency-tree-esm",
"version": "1.0.0",
"description": "Finds all JavaScript ES Module dependencies from a filename.",
"type": "module",
"main": "main.js",
"scripts": {
"test": "npx ava"
},
"repository": {
"type": "git",
"url": "git+https://github.com/11ty/eleventy-dependency-tree.git"
},
"author": {
"name": "Zach Leatherman",
"email": "[email protected]",
"url": "https://zachleat.com/"
},
"license": "MIT",
"dependencies": {
"@11ty/eleventy-utils": "^1.0.1",
"acorn": "^8.10.0",
"dependency-graph": "^0.11.0",
"normalize-path": "^3.0.0"
},
"devDependencies": {
"ava": "^5.3.1"
},
"ava": {
"files": [
"./test/*.js"
],
"ignoredByWatcher": [
"./test/stubs/**"
]
}
}
1 change: 1 addition & 0 deletions test/stubs/circular-child.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import "./circular-parent.js";
1 change: 1 addition & 0 deletions test/stubs/circular-parent.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import "./circular-child.js";
2 changes: 2 additions & 0 deletions test/stubs/circular-self.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import "./circular-self.js";
import "./empty.js";
Empty file added test/stubs/empty.js
Empty file.
2 changes: 2 additions & 0 deletions test/stubs/file.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import fs from "fs";
import * as fdklsjf from "./imported-secondary.js";
3 changes: 3 additions & 0 deletions test/stubs/imported-secondary.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import "fs";

export function hello() {}
3 changes: 3 additions & 0 deletions test/stubs/imported.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import * as fdklsjf from "./imported-secondary.js";

export function hello() {}
1 change: 1 addition & 0 deletions test/stubs/nested-grandchild.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import * as fdklsjf from "./nested.js";
2 changes: 2 additions & 0 deletions test/stubs/nested.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import fs from "fs";
import * as fdklsjf from "./imported.js";
26 changes: 26 additions & 0 deletions test/test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import test from "ava";
import { find } from "../main.js";

test("Empty", async t => {
t.deepEqual(await find("./test/stubs/empty.js"), []);
});

test("Simple", async t => {
t.deepEqual(await find("./test/stubs/file.js"), ["./test/stubs/imported-secondary.js"]);
});

test("Nested two deep", async t => {
t.deepEqual(await find("./test/stubs/nested.js"), ["./test/stubs/imported.js", "./test/stubs/imported-secondary.js"]);
});

test("Nested three deep", async t => {
t.deepEqual(await find("./test/stubs/nested-grandchild.js"), ["./test/stubs/nested.js", "./test/stubs/imported.js", "./test/stubs/imported-secondary.js"]);
});

test("Circular", async t => {
t.deepEqual(await find("./test/stubs/circular-parent.js"), ["./test/stubs/circular-child.js"]);
});

test("Circular Self Reference", async t => {
t.deepEqual(await find("./test/stubs/circular-self.js"), ["./test/stubs/empty.js"]);
});

0 comments on commit 95dbca0

Please sign in to comment.