feat(npm-aqua-compiler): create package (#401)

* Add npm-aqua-compiler package

* Release new package

* Remove noUncheckedIndexedAccess from tsconfig.json

* Fix a test script

* Fix length checks

* Fix

* Update error description

* Try to choose a nicer err message

* New import format and API

* Fix error message

* Improve test

* Don't add empty string key when globalImports prop is empty

* Fix exports
This commit is contained in:
Akim
2023-12-15 23:14:07 +07:00
committed by GitHub
parent ac407c204d
commit d6008110cf
49 changed files with 1653 additions and 154 deletions

View File

@ -0,0 +1,115 @@
/**
* Copyright 2023 Fluence Labs Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { join } from "path";
import { fileURLToPath } from "url";
import { assert, describe, expect, it } from "vitest";
import { gatherImportsFromNpm } from "./imports.js";
describe("imports", () => {
/**
* NOTE: This test expects that `npm i` is run
* inside `./__test__/data/transitive-deps/project` folder
*/
it("should resolve transitive dependencies", async () => {
const npmProjectDirPath = "./test/transitive-deps/project";
const aquaToCompileDirPath = "./test/transitive-deps/aqua-project";
const globalImports = ["./.fluence/aqua"];
const expectedResolution: Record<
string,
Record<string, string[] | string>
> = {
[aquaToCompileDirPath]: {
"": globalImports,
A: "./A",
B: "./B",
},
"./A": {
C: "./C",
D: "./D",
},
"./B": {
C: "./B/C",
D: "./B/D",
},
"./C": {
D: "./C/D",
},
"./B/C": {
D: "./B/C/D",
},
};
const prefix = join(
fileURLToPath(new URL("./", import.meta.url)),
"..",
"test",
"transitive-deps",
"project",
);
const buildResolutionKey = (str: string) => {
return (
"./" +
str
.slice(prefix.length)
.split("/node_modules/")
.filter(Boolean)
.join("/")
);
};
const imports = await gatherImportsFromNpm({
npmProjectDirPath,
aquaToCompileDirPath,
globalImports,
});
expect(Object.keys(imports).length).toBe(
Object.keys(expectedResolution).length,
);
Object.entries(imports).forEach(([key, value]) => {
const resolutionKey =
key === aquaToCompileDirPath ? key : buildResolutionKey(key);
const resolutionValues = expectedResolution[resolutionKey];
assert(resolutionValues);
expect(Object.keys(value).length).toBe(
Object.keys(resolutionValues).length,
);
for (const [dep, path] of Object.entries(value)) {
if (Array.isArray(path)) {
expect(dep).toBe("");
expect(expectedResolution[resolutionKey]).toHaveProperty(dep, path);
continue;
}
expect(expectedResolution[resolutionKey]).toHaveProperty(
dep,
buildResolutionKey(path),
);
}
});
});
});

View File

@ -0,0 +1,92 @@
/**
* Copyright 2023 Fluence Labs Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Arborist from "@npmcli/arborist";
import { breadth } from "treeverse";
export interface GatherImportsArg {
npmProjectDirPath: string;
aquaToCompileDirPath?: string; // Default: npmProjectDirPath
globalImports?: string[];
}
export type GatherImportsResult = Record<
string,
Record<string, string[] | string>
>;
export async function gatherImportsFromNpm({
npmProjectDirPath,
aquaToCompileDirPath,
globalImports = [],
}: GatherImportsArg): Promise<GatherImportsResult> {
const arborist = new Arborist({ path: npmProjectDirPath });
const tree = await arborist.loadActual();
/**
* Traverse dependency tree to construct map
* (real path of a package) -> (real paths of its immediate dependencies)
*/
const result: GatherImportsResult = {};
breadth({
tree,
getChildren(node) {
const deps: Arborist.Node[] = [];
for (const edge of node.edgesOut.values()) {
// Skip dependencies that are not installed.
// Looks like Arborist type is incorrect here, so it's possible to have null here
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (edge.to === null) {
continue;
}
// NOTE: Any errors in edge are ignored.
const dep = edge.to;
// Gather dependencies to traverse them.
deps.push(dep);
// Root node should have top-level property pointed to aqua dependency folder
if (node.isRoot) {
const aquaDepPath = aquaToCompileDirPath ?? npmProjectDirPath;
result[aquaDepPath] = {
...(result[aquaDepPath] ??
(globalImports.length > 0
? {
"": globalImports,
}
: {})),
[dep.name]: dep.realpath,
};
} else {
// Gather dependencies real paths.
result[node.realpath] = {
...(result[node.realpath] ?? {}),
[dep.name]: dep.realpath,
};
}
}
return deps;
},
});
return result;
}