Resolver: Add support for Node exports manifest field
MODIFY
packages/@romejs/codec-js-manifest/convert.ts
packages/@romejs/codec-js-manifest/convert.ts
import {stringifySPDXLicense} from '@romejs/codec-spdx-license';
import {ManifestDependencies, stringifyDependencyPattern} from './dependencies';
-import {Manifest, JSONManifest} from './types';
+import {
+ Manifest,
+ JSONManifest,
+ JSONManifestExports,
+ ManifestExports,
+} from './types';
import {stringifySemver} from '@romejs/codec-semver';
import {Dict} from '@romejs/typescript-helpers';
import {stringifyPathPattern} from '@romejs/path-match';
@@ -26,10 +31,8 @@ export function convertManifestToJSON(manifest: Manifest): JSONManifest {
repository: manifest.repository,
bugs: manifest.bugs,
- browser: manifest.browser,
main: manifest.main,
- 'rome:main': manifest['rome:main'],
- 'jsnext:main': manifest['jsnext:main'],
+ exports: exportsToObject(manifest.exports),
author: manifest.author,
contributors: manifest.contributors,
@@ -66,6 +69,42 @@ export function convertManifestToJSON(manifest: Manifest): JSONManifest {
};
}
+function exportsToObject(
+ exports: boolean | ManifestExports,
+): undefined | false | JSONManifestExports {
+ if (exports === false) {
+ return false;
+ }
+
+ if (exports === true) {
+ return;
+ }
+
+ if (exports.size === 0) {
+ return {};
+ }
+
+ const obj: JSONManifestExports = {};
+
+ for (const [key, entries] of exports) {
+ if (entries.size === 1) {
+ const def = entries.get('default');
+ if (def !== undefined) {
+ obj[key.join()] = def.relative.join();
+ continue;
+ }
+ }
+
+ const entriesObj: Dict<string> = {};
+ for (const [type, alias] of entries) {
+ entriesObj[type] = alias.relative.join();
+ }
+ obj[key.join()] = entriesObj;
+ }
+
+ return obj;
+}
+
function maybeArray<T>(items: Array<T>): undefined | Array<T> {
if (items.length === 0) {
return undefined;

MODIFY
packages/@romejs/codec-js-manifest/index.ts
packages/@romejs/codec-js-manifest/index.ts
@@ -18,11 +18,17 @@ import {
Manifest,
ManifestPerson,
ManifestRepository,
+ ManifestExports,
+ ManifestExportConditions,
} from './types';
import {tryParseWithOptionalOffsetPosition} from '@romejs/parser-core';
import {normalizeName} from './name';
import {PartialDiagnostics} from '@romejs/diagnostics';
-import {AbsoluteFilePath} from '@romejs/path';
+import {
+ AbsoluteFilePath,
+ createRelativeFilePath,
+ RelativeFilePathMap,
+} from '@romejs/path';
import {toCamelCase} from '@romejs/string-utils';
import {PathPatterns, parsePathPattern} from '@romejs/path-match';
@@ -411,6 +417,61 @@ function normalizeRepo(
}
}
+function normalizeExports(consumer: Consumer): boolean | ManifestExports {
+ if (typeof consumer.asUnknown() === 'boolean') {
+ return consumer.asBoolean();
+ }
+
+ if (!consumer.exists()) {
+ return true;
+ }
+
+ const exports: ManifestExports = new RelativeFilePathMap();
+
+ const dotConditions: ManifestExportConditions = new Map();
+
+ for (const [relative, value] of consumer.asMap()) {
+ // If it's not a relative path then it's a platform for the root
+ if (relative[0] !== '.') {
+ dotConditions.set(relative, {
+ consumer: value,
+ relative: createRelativeFilePath(value.asString()),
+ });
+ continue;
+ }
+
+ if (dotConditions.size > 0) {
+ value.unexpected(
+ 'Cannot mix a root conditional export with relative paths',
+ );
+ }
+
+ const conditions: ManifestExportConditions = new Map();
+
+ if (typeof value.asUnknown() === 'string') {
+ conditions.set('default', {
+ consumer: value,
+ relative: createRelativeFilePath(value.asString()),
+ });
+ } else {
+ for (const [type, relativeAlias] of value.asMap()) {
+ conditions.set(type, {
+ consumer: relativeAlias,
+ relative: createRelativeFilePath(relativeAlias.asString()),
+ });
+ }
+ }
+
+ if (dotConditions.size > 0) {
+ exports.set(createRelativeFilePath('.'), dotConditions);
+ }
+
+ exports.set(createRelativeFilePath(relative), conditions);
+ }
+
+ return exports;
+}
+
function normalizeBugs(
consumer: Consumer,
loose: boolean,
@@ -559,11 +620,8 @@ export async function normalizeManifest(
cpu: normalizeStringArray(consumer.get('cpu'), loose),
os: normalizeStringArray(consumer.get('os'), loose),
- // Entry fields
- browser: undefined, // normalizeString(consumer, 'browser'),
main: normalizeString(consumer, 'main'),
- 'rome:main': normalizeString(consumer, 'rome:main'),
- 'jsnext:main': normalizeString(consumer, 'jsnext:main'),
+ exports: normalizeExports(consumer.get('exports')),
// Dependency fields
dependencies: normalizeDependencies(consumer, 'dependencies', loose),

MODIFY
packages/@romejs/codec-js-manifest/types.ts
packages/@romejs/codec-js-manifest/types.ts
@@ -9,7 +9,11 @@ import {ManifestDependencies} from './dependencies';
import {SPDXExpressionNode} from '@romejs/codec-spdx-license';
import {SemverVersionNode} from '@romejs/codec-semver';
import {Consumer} from '@romejs/consume';
-import {AbsoluteFilePath} from '@romejs/path';
+import {
+ AbsoluteFilePath,
+ RelativeFilePath,
+ RelativeFilePathMap,
+} from '@romejs/path';
import {JSONObject, JSONPropertyValue} from '@romejs/codec-json';
import {Dict} from '@romejs/typescript-helpers';
import {PathPatterns} from '@romejs/path-match';
@@ -41,6 +45,16 @@ export type ManifestBugs = {
email: MString;
};
+export type ManifestExports = RelativeFilePathMap<ManifestExportConditions>;
+
+export type ManifestExportConditions = Map<
+ string,
+ {
+ consumer: Consumer;
+ relative: RelativeFilePath;
+ }
+>;
+
export type Manifest = {
name: MString;
description: MString;
@@ -53,10 +67,8 @@ export type Manifest = {
repository: undefined | ManifestRepository;
bugs: undefined | ManifestBugs;
- browser: MString;
main: MString;
- 'rome:main': MString;
- 'jsnext:main': MString;
+ exports: boolean | ManifestExports;
author: undefined | ManifestPerson;
contributors: undefined | Array<ManifestPerson>;
@@ -93,10 +105,8 @@ export type JSONManifest = {
repository: Manifest['repository'];
bugs: Manifest['bugs'];
- browser: Manifest['browser'];
main: Manifest['main'];
- 'rome:main': Manifest['rome:main'];
- 'jsnext:main': Manifest['jsnext:main'];
+ exports: undefined | false | JSONManifestExports;
author: Manifest['author'];
contributors: Manifest['contributors'];
@@ -120,6 +130,8 @@ export type JSONManifest = {
[key: string]: JSONPropertyValue;
};
+export type JSONManifestExports = Dict<Dict<string> | string>;
+
export type ManifestDefinition = {
path: AbsoluteFilePath;
folder: AbsoluteFilePath;

MODIFY
packages/@romejs/core/master/fs/Resolver.ts
packages/@romejs/core/master/fs/Resolver.ts
* LICENSE file in the root directory of this source tree.
*/
-import {Manifest} from '@romejs/codec-js-manifest';
+import {Manifest, ManifestDefinition} from '@romejs/codec-js-manifest';
import Master from '../Master';
import {Platform} from '../../common/types/platform';
import {ProjectDefinition, DEFAULT_PROJECT_CONFIG} from '@romejs/project';
@@ -14,15 +14,18 @@ import {FileReference} from '../../common/types/files';
import resolverSuggest from './resolverSuggest';
import {
AbsoluteFilePath,
- createUnknownFilePath,
UnknownFilePath,
URLFilePath,
+ createRelativeFilePath,
+ RelativeFilePath,
+ createFilePathFromSegments,
} from '@romejs/path';
import {DiagnosticPointer, PartialDiagnosticAdvice} from '@romejs/diagnostics';
import {IMPLICIT_JS_EXTENSIONS} from '../../common/fileHandlers';
import {writeFile} from '@romejs/fs';
import https = require('https');
import {MOCKS_FOLDER_NAME} from '@romejs/core/common/constants';
+import {Consumer} from '@romejs/consume';
function request(
url: string,
@@ -177,15 +180,94 @@ export type ResolverOptions = {
scale?: number;
};
+type ExportAlias = {key: Consumer; value: RelativeFilePath};
+
+function attachExportAliasIfUnresolved(
+ res: ResolverQueryResponse,
+ alias: ExportAlias,
+) {
+ if (res.type === 'FOUND') {
+ return res;
+ }
+
+ const pointer = alias.key.getDiagnosticPointer('value');
+
+ return {
+ ...res,
+ source:
+ pointer === undefined
+ ? undefined
+ : {
+ pointer,
+ source: alias.value.join(),
+ },
+ };
+}
+
+function getExportsAlias({
+ manifest,
+ relative,
+ platform,
+}: {
+ manifest: Manifest;
+ relative: UnknownFilePath;
+ platform?: Platform;
+}): undefined | ExportAlias {
+ if (typeof manifest.exports === 'boolean') {
+ return;
+ }
+
+ if (platform === undefined) {
+ return;
+ }
+
+ if (!relative.isRelative()) {
+ return;
+ }
+
+ const aliases = manifest.exports.get(relative.assertRelative());
+ if (aliases === undefined) {
+ return;
+ }
+
+ const alias = aliases.get(platform);
+ if (alias !== undefined) {
+ return {
+ key: alias.consumer,
+ value: alias.relative,
+ };
+ }
+
+ const def = aliases.get('default');
+ if (def !== undefined) {
+ return {
+ key: def.consumer,
+ value: def.relative,
+ };
+ }
+
+ // TODO check for folder aliases
+}
+
function getPreferredMainKey(
+ consumer: Consumer,
manifest: Manifest,
-): undefined | {key: string; value: string} {
- if (manifest['jsnext:main'] !== undefined) {
- return {key: 'jsnext:main', value: manifest['jsnext:main']};
+ platform?: Platform,
+): undefined | ExportAlias {
+ const alias = getExportsAlias({
+ manifest: manifest,
+ relative: createRelativeFilePath('.'),
+ platform,
+ });
+ if (alias !== undefined) {
+ return alias;
}
if (manifest.main !== undefined) {
- return {key: 'main', value: manifest.main};
+ return {
+ key: consumer.get('main'),
+ value: createRelativeFilePath(manifest.main),
+ };
}
}
@@ -513,36 +595,23 @@ export default class Resolver {
return this.finishResolverQueryResponse(resolvedOrigin, types);
}
- const main = getPreferredMainKey(manifestDef.manifest);
+ const main = getPreferredMainKey(
+ manifestDef.consumer,
+ manifestDef.manifest,
+ query.platform,
+ );
if (main !== undefined) {
const resolved = this.resolvePath(
{
...query,
origin: resolvedOrigin,
- source: createUnknownFilePath(main.value),
+ source: main.value,
},
true,
['package'],
);
- if (resolved.type === 'FOUND') {
- return resolved;
- } else {
- const pointer = manifestDef.consumer
- .get(main.key)
- .getDiagnosticPointer('value');
-
- return {
- ...resolved,
- source:
- pointer === undefined
- ? undefined
- : {
- pointer,
- source: main.value,
- },
- };
- }
+ return attachExportAliasIfUnresolved(resolved, main);
}
}
@@ -571,7 +640,7 @@ export default class Resolver {
resolvePackageFolder(
query: ResolverLocalQuery,
moduleName: string,
- ): undefined | AbsoluteFilePath {
+ ): undefined | ManifestDefinition {
// Find the project
const project = this.master.projectManager.findProjectExisting(
query.origin,
@@ -588,7 +657,7 @@ export default class Resolver {
for (const project of projects) {
const pkg = project.packages.get(moduleName);
if (pkg !== undefined) {
- return pkg.folder;
+ return pkg;
}
}
}
@@ -598,22 +667,62 @@ export default class Resolver {
moduleName: string,
moduleNameParts: Array<string>,
): ResolverQueryResponse {
- const packageDir = this.resolvePackageFolder(query, moduleName);
+ const manifestDef = this.resolvePackageFolder(query, moduleName);
+ return this.resolveManifest(query, manifestDef, moduleNameParts);
+ }
+
+ resolveManifest(
+ query: ResolverLocalQuery,
+ manifestDef: undefined | ManifestDefinition,
+ moduleNameParts: Array<string>,
+ ): ResolverQueryResponse {
+ if (manifestDef === undefined) {
+ return QUERY_RESPONSE_MISSING;
+ }
- if (packageDir === undefined) {
+ if (moduleNameParts.length > 0) {
+ // Submodules of this package are private
+ if (manifestDef.manifest.exports === false) {
return QUERY_RESPONSE_MISSING;
- } else {
- return this.resolvePath(
+ }
+
+ // Check if we're allowed to touch this submodule
+ if (manifestDef.manifest.exports !== true) {
+ const alias = getExportsAlias({
+ manifest: manifestDef.manifest,
+ relative: createFilePathFromSegments(moduleNameParts),
+ platform: query.platform,
+ });
+
+ if (alias === undefined) {
+ // No submodule found
+ return QUERY_RESPONSE_MISSING;
+ }
+
+ // Alias found!
+ const resolved = this.resolvePath(
{
...query,
- source: packageDir.append(moduleNameParts),
+ source: manifestDef.folder.append(alias.value),
},
true,
['package'],
);
+ return attachExportAliasIfUnresolved(resolved, alias);
}
}
+ // All exports are enabled or we are importing the root
+ return this.resolvePath(
+ {
+ ...query,
+ source: manifestDef.folder.append(moduleNameParts),
+ },
+ true,
+ ['package'],
+ );
+ }
+
resolveMock(
query: ResolverLocalQuery,
project: ProjectDefinition | undefined,
@@ -745,21 +854,12 @@ export default class Resolver {
// Check all parent directories for node_modules
for (const dir of parentDirectories) {
- // Check for node_modules/*
- const nodeModulesLoc = dir.append([
- NODE_MODULES,
- source.assertRelative(),
- ]);
- const nodeModulesResolved = this.resolvePath(
- {
- ...query,
- source: nodeModulesLoc,
- },
- true,
- ['package'],
+ const modulePath = dir.append(NODE_MODULES).append(moduleName);
+ const manifestDef = this.master.memoryFs.getManifestDefinition(
+ modulePath,
);
- if (shouldReturnQueryResponse(nodeModulesResolved)) {
- return nodeModulesResolved;
+ if (manifestDef !== undefined) {
+ return this.resolveManifest(query, manifestDef, moduleNameParts);
}
}

MODIFY
packages/@romejs/core/master/fs/resolverSuggest.ts
packages/@romejs/core/master/fs/resolverSuggest.ts
@@ -215,6 +215,8 @@ export default function resolverSuggest(
}
}
+ // TODO check if this would have been successful if not for exports access control
+
const source =
querySource.source === undefined ? query.source.join() : querySource.source;
let message = '';

MODIFY
packages/@romejs/path/index.ts
packages/@romejs/path/index.ts
@@ -361,6 +361,10 @@ class BaseFilePath<Super extends UnknownFilePath> {
const items: Array<FilePathOrString> = Array.isArray(raw) ? raw : [raw];
+ if (items.length === 0) {
+ return this._assert();
+ }
+
let segments: PathSegments = this.getSegments();
for (const item of items) {

Resolver: Add support for Node exports manifest field