feat(providence): traverseHtml improvement + node builtin support

This commit is contained in:
Thijs Louisse 2023-05-09 16:09:29 +02:00
parent 0315b33acd
commit f4448394b9
21 changed files with 290 additions and 101 deletions

View file

@ -29,32 +29,32 @@
"scripts": {
"dashboard": "node ./dashboard/server.js --run-server --serve-from-package-root",
"postinstall": "npx patch-package",
"match-lion-imports": "npm run providence -- analyze match-subclasses --search-target-collection @lion-targets --reference-collection @lion-references --measure-perf --add-system-paths",
"match-lion-imports": "npm run providence -- analyze match-imports --search-target-collection @lion-targets --reference-collection @lion-references --measure-perf --skip-check-match-compatibility",
"providence": "node --max-old-space-size=8192 ./src/cli/index.js",
"publish-docs": "node ../../packages-node/publish-docs/src/cli.js --github-url https://github.com/ing-bank/lion/ --git-root-dir ../../",
"prepublishOnly": "npm run publish-docs",
"test:node": "npm run test:node:unit && npm run test:node:e2e",
"test:node:e2e": "mocha './test-node/**/*.e2e.{j,mj}s' --timeout 60000",
"test:node:unit": "mocha './test-node/**/*.test.{j,mj}s'"
"test:node:e2e": "mocha './test-node/**/*.e2e.js' --timeout 60000",
"test:node:unit": "mocha './test-node/**/*.test.js'"
},
"dependencies": {
"@babel/core": "^7.21.3",
"@babel/parser": "^7.21.3",
"@babel/core": "^7.21.4",
"@babel/parser": "^7.21.4",
"@babel/plugin-proposal-class-properties": "^7.18.6",
"@babel/plugin-syntax-export-default-from": "^7.18.6",
"@babel/plugin-syntax-import-assertions": "^7.20.0",
"@babel/register": "^7.21.0",
"@babel/traverse": "^7.21.3",
"@babel/types": "^7.21.3",
"@rollup/plugin-node-resolve": "^15.0.1",
"@swc/core": "^1.3.42",
"@web/dev-server": "^0.1.37",
"@babel/traverse": "^7.21.4",
"@babel/types": "^7.21.4",
"@rollup/plugin-node-resolve": "^15.0.2",
"@swc/core": "^1.3.46",
"@web/dev-server": "^0.1.38",
"anymatch": "^3.1.3",
"commander": "^2.20.3",
"glob": "^7.2.3",
"glob": "^8.1.0",
"inquirer": "^9.1.5",
"is-negated-glob": "^1.0.0",
"lit-element": "~2.4.0",
"lit-element": "~3.3.1",
"parse5": "^7.1.2",
"read-package-tree": "5.3.1",
"semver": "^7.3.8",

View file

@ -34,6 +34,6 @@ export default {
// Usually the references are different from the targets.
// In this demo file, we test @lion usage amongst itself
// Select via " providence analyze --reference-collection 'exampleCollection' "
'@lion-references': ['../../packages/ui/'],
'@lion-references': lionScopedPackagePaths,
},
};

View file

@ -1,17 +0,0 @@
#!/usr/bin/env bash
# See https://gist.github.com/myusuf3/7f645819ded92bda6677
if [ -z "$1" ]; then
echo "Please define 'path/to/submodule'";
exit;
fi
# Remove the submodule entry from .git/config
git submodule deinit -f $1
# Remove the submodule directory from the superproject's .git/modules directory
rm -rf .git/modules/$1
# Remove the entry in .gitmodules and remove the submodule directory located at path/to/submodule
git rm -rf $1

View file

@ -1,5 +1,5 @@
/* eslint-disable no-shadow, no-param-reassign */
import pathLib from 'path';
import path from 'path';
import t from '@babel/types';
import babelTraverse from '@babel/traverse';
import { Analyzer } from '../core/Analyzer.js';
@ -251,7 +251,7 @@ export default class FindClassesAnalyzer extends Analyzer {
/** @type {FindClassesAnalyzerOutput} */
const queryOutput = await this._traverse(async (ast, { relativePath }) => {
const projectPath = cfg.targetProjectPath;
const fullPath = pathLib.resolve(projectPath, relativePath);
const fullPath = path.resolve(projectPath, relativePath);
const transformedEntry = await findMembersPerAstEntry(ast, fullPath, projectPath);
return { result: transformedEntry };
});

View file

@ -109,9 +109,9 @@ export default class FindCustomelementsAnalyzer extends Analyzer {
/**
* Prepare
*/
const analyzerResult = this._prepare(cfg);
if (analyzerResult) {
return analyzerResult;
const cachedAnalyzerResult = this._prepare(cfg);
if (cachedAnalyzerResult) {
return cachedAnalyzerResult;
}
/**

View file

@ -170,13 +170,13 @@ function findExportsPerAstFile(babelAst, { skipFileImports }) {
}
},
},
ExportNamedDeclaration(path) {
const exportSpecifiers = getExportSpecifiers(path.node);
const localMap = getLocalNameSpecifiers(path.node);
const source = path.node.source?.value;
const entry = { exportSpecifiers, localMap, source, __tmp: { path } };
if (path.node.assertions?.length) {
entry.assertionType = path.node.assertions[0].value?.value;
ExportNamedDeclaration(astPath) {
const exportSpecifiers = getExportSpecifiers(astPath.node);
const localMap = getLocalNameSpecifiers(astPath.node);
const source = astPath.node.source?.value;
const entry = { exportSpecifiers, localMap, source, __tmp: { astPath } };
if (astPath.node.assertions?.length) {
entry.assertionType = astPath.node.assertions[0].value?.value;
}
transformedFile.push(entry);
},
@ -194,7 +194,7 @@ function findExportsPerAstFile(babelAst, { skipFileImports }) {
source = importOrDeclPath.parentPath.node.source.value;
}
}
transformedFile.push({ exportSpecifiers, source, __tmp: { path: defaultExportPath } });
transformedFile.push({ exportSpecifiers, source, __tmp: { astPath: defaultExportPath } });
},
});
@ -238,9 +238,9 @@ export default class FindExportsAnalyzer extends Analyzer {
/**
* Prepare
*/
const analyzerResult = this._prepare(cfg);
if (analyzerResult) {
return analyzerResult;
const cachedAnalyzerResult = this._prepare(cfg);
if (cachedAnalyzerResult) {
return cachedAnalyzerResult;
}
/**

View file

@ -25,7 +25,13 @@ import { toPosixPath } from '../../utils/to-posix-path.js';
*/
export async function fromImportToExportPerspective({ importee, importer, importeeProjectPath }) {
if (isRelativeSourcePath(importee)) {
LogService.warn('[fromImportToExportPerspective] Please only provide external import paths');
LogService.warn(
`[fromImportToExportPerspective] Please only provide external import paths for ${{
importee,
importer,
importeeProjectPath,
}}`,
);
return null;
}

View file

@ -1,5 +1,5 @@
/* eslint-disable no-param-reassign */
import pathLib from 'path';
import path from 'path';
import { isRelativeSourcePath } from '../../utils/relative-source-path.js';
import { resolveImportPath } from '../../utils/resolve-import-path.js';
import { toPosixPath } from '../../utils/to-posix-path.js';
@ -7,7 +7,7 @@ import { toPosixPath } from '../../utils/to-posix-path.js';
/**
* @typedef {import('../../../../types/index.js').PathRelative} PathRelative
* @typedef {import('../../../../types/index.js').PathFromSystemRoot} PathFromSystemRoot
* @typedef {import('../../../../types/index.js').QueryOutput} QueryOutput
* @typedef {import('../../../../types/index.js').FindImportsAnalyzerEntry} FindImportsAnalyzerEntry
*/
/**
@ -16,9 +16,9 @@ import { toPosixPath } from '../../utils/to-posix-path.js';
* @returns {PathRelative}
*/
function toLocalPath(currentDirPath, resolvedPath) {
let relativeSourcePath = pathLib.relative(currentDirPath, resolvedPath);
let relativeSourcePath = path.relative(currentDirPath, resolvedPath);
if (!relativeSourcePath.startsWith('.')) {
// correction on top of pathLib.resolve, which resolves local paths like
// correction on top of path.resolve, which resolves local paths like
// (from import perspective) external modules.
// so 'my-local-files.js' -> './my-local-files.js'
relativeSourcePath = `./${relativeSourcePath}`;
@ -28,36 +28,39 @@ function toLocalPath(currentDirPath, resolvedPath) {
/**
* Resolves and converts to normalized local/absolute path, based on file-system information.
* - from: { source: '../../relative/file' }
* - to: {
* fullPath: './absolute/path/from/root/to/relative/file.js',
* normalizedPath: '../../relative/file.js'
* }
* @param {QueryOutput} queryOutput
* - from: '../../relative/file'
* - to: './src/relative/file.js'
* @param {string} oldSource
* @param {string} relativePath
* @param {string} rootPath
*/
export async function normalizeSourcePath(oldSource, relativePath, rootPath = process.cwd()) {
const currentFilePath = /** @type {PathFromSystemRoot} */ (path.resolve(rootPath, relativePath));
const currentDirPath = /** @type {PathFromSystemRoot} */ (path.dirname(currentFilePath));
if (isRelativeSourcePath(oldSource) && relativePath) {
// This will be a source like '../my/file.js' or './file.js'
const resolvedPath = /** @type {PathFromSystemRoot} */ (
await resolveImportPath(oldSource, currentFilePath)
);
return resolvedPath && toLocalPath(currentDirPath, resolvedPath);
}
// This will be a source from a project, like 'lion-based-ui/x.js' or '@open-wc/testing/y.js'
return oldSource;
}
/**
* @param {Partial<FindImportsAnalyzerEntry>[]} queryOutput
* @param {string} relativePath
* @param {string} rootPath
*/
export async function normalizeSourcePaths(queryOutput, relativePath, rootPath = process.cwd()) {
const currentFilePath = /** @type {PathFromSystemRoot} */ (
pathLib.resolve(rootPath, relativePath)
);
const currentDirPath = /** @type {PathFromSystemRoot} */ (pathLib.dirname(currentFilePath));
const normalizedQueryOutput = [];
for (const specifierResObj of queryOutput) {
if (specifierResObj.source) {
if (isRelativeSourcePath(specifierResObj.source) && relativePath) {
// This will be a source like '../my/file.js' or './file.js'
const resolvedPath = /** @type {PathFromSystemRoot} */ (
await resolveImportPath(specifierResObj.source, currentFilePath)
);
specifierResObj.normalizedSource =
resolvedPath && toLocalPath(currentDirPath, resolvedPath);
// specifierResObj.fullSource = resolvedPath && toRelativeSourcePath(resolvedPath, rootPath);
} else {
// This will be a source from a project, like 'lion-based-ui/x.js' or '@open-wc/testing/y.js'
specifierResObj.normalizedSource = specifierResObj.source;
// specifierResObj.fullSource = specifierResObj.source;
const x = await normalizeSourcePath(specifierResObj.source, relativePath, rootPath);
if (x) {
specifierResObj.normalizedSource = x;
}
}
normalizedQueryOutput.push(specifierResObj);

View file

@ -1,5 +1,6 @@
/**
* @typedef {import('../../../../types/index.js').FindExportsAnalyzerResult} FindExportsAnalyzerResult
* @typedef {import('../../../../types/index.js').IterableFindExportsAnalyzerEntry} IterableFindExportsAnalyzerEntry
*/
/**

View file

@ -1,5 +1,6 @@
/**
* @typedef {import('../../../../types/index.js').FindImportsAnalyzerResult} FindImportsAnalyzerResult
* @typedef {import('../../../../types/index.js').IterableFindImportsAnalyzerEntry} IterableFindImportsAnalyzerEntry
*/
/**
@ -43,13 +44,12 @@ export function transformIntoIterableFindImportsOutput(importsAnalyzerResult) {
continue;
}
for (const importSpecifier of importSpecifiers) {
/** @type {IterableFindImportsAnalyzerEntry} */
const resultEntry = {
const resultEntry = /** @type {IterableFindImportsAnalyzerEntry} */ ({
file,
specifier: importSpecifier,
source,
normalizedSource,
};
});
iterableEntries.push(resultEntry);
}
}

View file

@ -17,6 +17,8 @@ import { getFilePathRelativeFromRoot } from '../utils/get-file-path-relative-fro
* @typedef {import('../../../types/index.js').ProjectInputDataWithMeta} ProjectInputDataWithMeta
* @typedef {import('../../../types/index.js').AnalyzerQueryResult} AnalyzerQueryResult
* @typedef {import('../../../types/index.js').MatchAnalyzerConfig} MatchAnalyzerConfig
* @typedef {import('@babel/types').File} File
* @typedef {(ast: File, astContext: {code:string; relativePath:string; projectData: ProjectInputDataWithMeta}) => object} FileAstTraverseFn
*/
/**
@ -126,7 +128,7 @@ function ensureAnalyzerResultFormat(queryOutput, cfg, analyzer) {
* Before running the analyzer, we need two conditions for a 'compatible match':
* - 1. referenceProject is imported by targetProject at all
* - 2. referenceProject and targetProject have compatible major versions
* @typedef {(referencePath:PathFromSystemRoot,targetPath:PathFromSystemRoot) => {compatible:boolean}} CheckForMatchCompatibilityFn
* @typedef {(referencePath:PathFromSystemRoot,targetPath:PathFromSystemRoot) => {compatible:boolean; reason?:string}} CheckForMatchCompatibilityFn
* @type {CheckForMatchCompatibilityFn}
*/
const checkForMatchCompatibility = memoize(
@ -172,7 +174,7 @@ export class Analyzer {
name = /** @type {typeof Analyzer} */ (this.constructor).analyzerName;
/** @type {'babel'|'swc-to-babel'|'swc-to-babel'} */
/** @type {'babel'|'swc-to-babel'} */
requiredAst = 'babel';
/**
@ -183,10 +185,10 @@ export class Analyzer {
* @param {MatchAnalyzerConfig} cfg
*/
static __unwindProvidedResults(cfg) {
if (cfg.targetProjectResult && !cfg.targetProjectResult.analyzerMeta) {
if (cfg.targetProjectResult && !cfg.targetProjectResult?.analyzerMeta) {
cfg.targetProjectResult = unwindJsonResult(cfg.targetProjectResult);
}
if (cfg.referenceProjectResult && !cfg.referenceProjectResult.analyzerMeta) {
if (cfg.referenceProjectResult && !cfg.referenceProjectResult?.analyzerMeta) {
cfg.referenceProjectResult = unwindJsonResult(cfg.referenceProjectResult);
}
}
@ -296,7 +298,7 @@ export class Analyzer {
}
/**
* @param {function|{traverseEntryFn: function; filePaths:string[]; projectPath: string}} traverseEntryOrConfig
* @param {FileAstTraverseFn|{traverseEntryFn: FileAstTraverseFn; filePaths:string[]; projectPath: string}} traverseEntryOrConfig
*/
async _traverse(traverseEntryOrConfig) {
LogService.debug(`Analyzer "${this.name}": started _traverse method`);

View file

@ -77,7 +77,7 @@ export class AstService {
/**
* Returns the Babel AST
* @param { string } code
* @param { 'babel'|'swc-to-babel' } astType
* @param { 'babel'|'swc-to-babel'} astType
* @param { {filePath?: PathFromSystemRoot} } options
* @returns {File|undefined}
*/

View file

@ -63,6 +63,17 @@ export function getReferencedDeclaration({ referencedIdentifierName, globalScope
}
/**
* @example
* ```js
* // ------ input file --------
* const x = 88;
* const y = x;
* export const myIdentifier = y;
* // --------------------------
*
* await getSourceCodeFragmentOfDeclaration(code) // finds "88"
* ```
*
* @param {{ filePath: PathFromSystemRoot; exportedIdentifier: string; projectRootPath: PathFromSystemRoot }} opts
* @returns {Promise<{ sourceNodePath: string; sourceFragment: string|null; externalImportSource: string; }>}
*/

View file

@ -1,4 +1,5 @@
import pathLib from 'path';
import { isBuiltin } from 'node:module';
import path from 'path';
import { nodeResolve } from '@rollup/plugin-node-resolve';
import { LogService } from '../core/LogService.js';
import { memoize } from './memoize.js';
@ -36,11 +37,15 @@ const fakePluginContext = {
* @param {SpecifierSource} importee source like '@lion/core' or '../helpers/index.js'
* @param {PathFromSystemRoot} importer importing file, like '/my/project/importing-file.js'
* @param {{customResolveOptions?: {preserveSymlinks:boolean}}} [opts] nodeResolve options
* @returns {Promise<PathFromSystemRoot|null>} the resolved file system path, like '/my/project/node_modules/@lion/core/index.js'
* @returns {Promise<PathFromSystemRoot|null|'[node-builtin]'>} the resolved file system path, like '/my/project/node_modules/@lion/core/index.js'
*/
async function resolveImportPathFn(importee, importer, opts) {
if (isBuiltin(importee)) {
return '[node-builtin]';
}
const rollupResolve = nodeResolve({
rootDir: pathLib.dirname(importer),
rootDir: path.dirname(importer),
// allow resolving polyfills for nodejs libs
preferBuiltins: false,
// extensions: ['.mjs', '.js', '.json', '.node'],
@ -48,23 +53,24 @@ async function resolveImportPathFn(importee, importer, opts) {
});
const preserveSymlinks =
(opts && opts.customResolveOptions && opts.customResolveOptions.preserveSymlinks) || false;
// @ts-ignore
(opts?.customResolveOptions && opts.customResolveOptions.preserveSymlinks) || false;
// @ts-expect-error
rollupResolve.buildStart.call(fakePluginContext, { preserveSymlinks });
// @ts-ignore
// @ts-expect-error
const result = await rollupResolve.resolveId.handler.call(
fakePluginContext,
importee,
importer,
{},
);
// @ts-ignore
if (!result?.id) {
LogService.warn(`importee ${importee} not found in filesystem for importer '${importer}'.`);
LogService.warn(
`[resolveImportPath] importee ${importee} not found in filesystem for importer '${importer}'.`,
);
return null;
}
// @ts-ignore
return toPosixPath(result.id);
}

View file

@ -1,25 +1,41 @@
/**
* @param {Node} curNode Node to start from. Will loop over its children
* @typedef {import('parse5/dist/tree-adapters/default.js').Node} Node
*/
/**
* Creates an api similar to Babel traverse for parse5 trees
* @param {Parse5AstNode} curNode Node to start from. Will loop over its children
* @param {object} processObject Will be executed for every node
*/
export function traverseHtml(curNode, processObject) {
export function traverseHtml(curNode, processObject, config = {}) {
function pathify(node) {
return {
node,
traverse(obj) {
traverseHtml(obj) {
traverseHtml(node, obj);
},
stop() {
// eslint-disable-next-line no-param-reassign
config.stopped = true;
},
};
}
// let done = processFn(curNode, parentNode);
// Match...
if (processObject[curNode.nodeName]) {
processObject[curNode.nodeName](pathify(curNode));
}
if (curNode.childNodes) {
curNode.childNodes.forEach(childNode => {
traverseHtml(childNode, processObject, curNode);
let { childNodes } = curNode;
if (curNode.nodeName === 'template') {
childNodes = curNode.content.childNodes;
}
if (!config.stopped && childNodes) {
childNodes.forEach(childNode => {
if (!config.stopped) {
traverseHtml(childNode, processObject, config);
}
});
}
}

View file

@ -33,7 +33,6 @@ const options = {
*/
// eslint-disable-next-line no-unused-vars
function getResultPerAstFile(ast) {
console.debug('myAnalyzerPerAstEntry');
// Visit AST...
const transformedEntryResult = [];
// Do the traverse: https://babeljs.io/docs/en/babel-traverse

View file

@ -9,7 +9,6 @@ import {
mockProject,
mock,
} from '../../../test-helpers/mock-project-helpers.js';
// import { setupAnalyzerTest } from '../../../test-helpers/setup-analyzer-test.js';
/**
* @typedef {import('../../../types/index.js').PathFromSystemRoot} PathFromSystemRoot

View file

@ -115,6 +115,25 @@ describe('resolveImportPath', () => {
expect(foundPath).to.equal('/target/node_modules/ref/packages/x/index.js');
});
it(`resolves native node modules`, async () => {
mockProject(
{
'./src/someFile.js': `
import { readFile } from 'fs';
export const x = readFile('/path/to/someOtherFile.js');
`,
},
{
projectName: 'my-project',
projectPath: '/my/project',
},
);
const foundPath = await resolveImportPath('fs', '/my/project/someFile.js');
expect(foundPath).to.equal('[node-builtin]');
});
/**
* All edge cases are covered by https://github.com/rollup/plugins/tree/master/packages/node-resolve/test
*/

View file

@ -0,0 +1,140 @@
import { expect } from 'chai';
import { it } from 'mocha';
import * as parse5 from 'parse5';
import { traverseHtml } from '../../../src/program/utils/traverse-html.js';
function getId(p5Path) {
return p5Path.node.attrs.find(a => a.name === 'id').value;
}
describe('traverseHtml', () => {
it('finds different tag names', async () => {
const htmlCode = `
<div id="a-lvl1">
<span id="a-lvl2">
<my-tag id="a-lvl3">
<not-found></notfound>
</my-tag>
</span>
</div>
<div id="b"></div>
`;
const ast = parse5.parseFragment(htmlCode);
const foundDivs = [];
const foundSpans = [];
const foundMyTags = [];
traverseHtml(ast, {
div(p5Path) {
foundDivs.push(getId(p5Path));
},
span(p5Path) {
foundSpans.push(getId(p5Path));
},
// eslint-disable-next-line object-shorthand
'my-tag'(p5Path) {
foundMyTags.push(getId(p5Path));
},
});
expect(foundDivs).to.eql(['a-lvl1', 'b']);
expect(foundSpans).to.eql(['a-lvl2']);
expect(foundMyTags).to.eql(['a-lvl3']);
});
it('traverses different levels in DOM order', async () => {
const htmlCode = `
<div id="a-lvl1">
<span id="a-lvl2">
<my-tag id="a-lvl3">
<not-found></notfound>
</my-tag>
</span>
</div>
<div id="b"></div>
`;
const ast = parse5.parseFragment(htmlCode);
const callOrder = [];
const processObj = {
span(p5Path) {
callOrder.push(`span#${getId(p5Path)}`);
},
div(p5Path) {
callOrder.push(`div#${getId(p5Path)}`);
},
// eslint-disable-next-line object-shorthand
'my-tag'(p5Path) {
callOrder.push(`my-tag#${getId(p5Path)}`);
},
};
traverseHtml(ast, processObj);
// call order based on dom tree
expect(callOrder).to.eql(['div#a-lvl1', 'span#a-lvl2', 'my-tag#a-lvl3', 'div#b']);
});
it('allows to stop traversal (for performance)', async () => {
const htmlCode = `
<div id="a-lvl1">
<span id="a-lvl2">
<my-tag id="a-lvl3">
<not-found></notfound>
</my-tag>
</span>
</div>
<div id="b"></div>
`;
const ast = parse5.parseFragment(htmlCode);
const callOrder = [];
const processObj = {
div(p5Path) {
callOrder.push(`div#${getId(p5Path)}`);
p5Path.stop();
},
span(p5Path) {
callOrder.push(`span#${getId(p5Path)}`);
},
// eslint-disable-next-line object-shorthand
'my-tag'(p5Path) {
callOrder.push(`my-tag#${getId(p5Path)}`);
},
};
traverseHtml(ast, processObj);
expect(callOrder).to.eql(['div#a-lvl1']);
});
it('allows to traverse within a path', async () => {
const htmlCode = `
<div id="a-lvl1">
<span id="a-lvl2">
<my-tag id="a-lvl3">
<not-found id="a-lvl4"></notfound>
</my-tag>
</span>
</div>
<div id="b"></div>
`;
const ast = parse5.parseFragment(htmlCode);
const callOrder = [];
const processObj = {
// eslint-disable-next-line object-shorthand
'my-tag'(p5Path) {
callOrder.push(`my-tag#${getId(p5Path)}`);
p5Path.traverseHtml({
// eslint-disable-next-line object-shorthand, no-shadow
'not-found'(p5Path) {
callOrder.push(`not-found#${getId(p5Path)}`);
},
});
},
};
traverseHtml(ast, processObj);
expect(callOrder).to.eql(['my-tag#a-lvl3', 'not-found#a-lvl4']);
});
});

View file

@ -4,9 +4,11 @@ export type TargetDep = `${PkgName}#${PkgVersion}`;
export type TargetDepsObj = {
[key: TargetDep]: TargetDep[];
};
export type TargetOrRefCollectionsObj = {
[key: PkgName]: PkgName[];
};
export type ProvidenceCliConf = {
metaConfig: {
categoryConfig: {
@ -35,3 +37,5 @@ export type ProvidenceCliConf = {
[referenceCollection: string]: string[];
};
};
// export { Node as Parse5Node } from 'parse5/dist/tree-adapters/default/index.js';