Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

create new @embroider/reverse-exports package #1652

Merged
merged 4 commits into from
Nov 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
/packages/vite/index.d.ts
/packages/vite/**/*.js
/packages/vite/**/*.d.ts
/packages/reverse-exports/**/*.js
/packages/reverse-exports/**/*.d.ts


# unconventional js
Expand Down
2 changes: 2 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@
/packages/vite/index.mjs
/packages/vite/**/*.js
/packages/vite/**/*.d.ts
/packages/reverse-exports/**/*.js
/packages/reverse-exports/**/*.d.ts

# unconventional js
/blueprints/*/files/
Expand Down
7 changes: 7 additions & 0 deletions packages/reverse-exports/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/node_modules
/src/**/*.js
/src/**/*.d.ts
/src/**/*.map
/tests/**/*.js
/tests/**/*.d.ts
/tests/**/*.map
4 changes: 4 additions & 0 deletions packages/reverse-exports/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module.exports = {
testEnvironment: 'node',
testMatch: ['<rootDir>/tests/**/*.test.js'],
};
19 changes: 19 additions & 0 deletions packages/reverse-exports/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"name": "@embroider/reverse-exports",
"version": "0.0.0",
"description": "",
"main": "src/index.js",
"scripts": {
"test": "jest"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@types/minimatch": "^3.0.4"
},
"dependencies": {
"minimatch": "^3.0.4",
"resolve.exports": "^2.0.2"
}
}
116 changes: 116 additions & 0 deletions packages/reverse-exports/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { posix } from 'path';
import minimatch from 'minimatch';
import { exports as resolveExports } from 'resolve.exports';

type Exports = string | string[] | { [key: string]: Exports };

/**
* An util to find a string value in a nested JSON-like structure.
*
* Receives an object (a netsted JSON-like structure) and a matcher callback
* that is tested against each string value.
*
* When a value is found, returns an object containing a `value` and a `key`.
* The key is one of the parent keys of the found value — the one that starts
* with `.`.
*
* When a value is not found, returns `undefined`.
*/
export function _findPathRecursively(
exportsObj: Exports,
matcher: (path: string) => boolean,
key = '.'
): { key: string; value: Exports } | undefined {
if (typeof exportsObj === 'string') {
return matcher(exportsObj) ? { key, value: exportsObj } : undefined;
}

if (Array.isArray(exportsObj)) {
const value = exportsObj.find(path => matcher(path));

if (value) {
return { key, value };
} else {
return undefined;
}
}

if (typeof exportsObj === 'object') {
let result: { key: string; value: Exports } | undefined = undefined;

for (const candidateKey in exportsObj) {
if (!exportsObj.hasOwnProperty(candidateKey)) {
return;
}

const candidate = _findPathRecursively(exportsObj[candidateKey], matcher, key);

if (candidate) {
result = {
key: candidateKey,
value: candidate.value,
};

break;
}
}

if (result) {
if (result.key.startsWith('./')) {
if (key !== '.') {
throw new Error(`exportsObj contains doubly nested path keys: "${key}" and "${result.key}"`);
}

return { key: result.key, value: result.value };
} else {
return { key, value: result.value };
}
} else {
return undefined;
}
}

throw new Error(`Unexpected type of obj: ${typeof exportsObj}`);
}

export default function reversePackageExports(
{ exports: exportsObj, name }: { exports?: Exports; name: string },
relativePath: string
): string {
if (!exportsObj) {
return posix.join(name, relativePath);
}

const maybeKeyValuePair = _findPathRecursively(exportsObj, candidate => {
// miminatch does not treat directories as full of content without glob
if (candidate.endsWith('/')) {
candidate += '**';
}

return minimatch(relativePath, candidate);
});

if (!maybeKeyValuePair) {
throw new Error(
`You tried to reverse exports for the file \`${relativePath}\` in package \`${name}\` but it does not match any of the exports rules defined in package.json. This means it should not be possible to access directly.`
);
}

const { key, value } = maybeKeyValuePair;

if (typeof value !== 'string') {
throw new Error('Expected value to be a string');
}

const maybeResolvedPaths = resolveExports({ name, exports: { [value]: key } }, relativePath);

if (!maybeResolvedPaths) {
throw new Error(
`Bug Discovered! \`_findPathRecursively()\` must always return a string value but instead it found a ${typeof value}. Please report this as an issue to https://github.com/embroider-build/embroider/issues/new`
);
}

const [resolvedPath] = maybeResolvedPaths;

return resolvedPath.replace(/^./, name);
}