/**
* Copyright (c) 2012-2016 André Bargull
* Alle Rechte vorbehalten / All Rights Reserved. Use is subject to license terms.
*
* <https://github.com/anba/es6draft>
*/
package com.github.anba.es6draft.repl.loader;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Iterator;
import com.github.anba.es6draft.parser.JSONBuilder;
import com.github.anba.es6draft.parser.JSONParser;
import com.github.anba.es6draft.parser.ParserException;
import com.github.anba.es6draft.runtime.internal.SimpleIterator;
import com.github.anba.es6draft.runtime.modules.MalformedNameException;
import com.github.anba.es6draft.runtime.modules.SourceIdentifier;
import com.github.anba.es6draft.runtime.modules.loader.FileSourceIdentifier;
/**
* Node module file resolution.
*
* @see https://iojs.org/api/modules.html#modules_all_together
*/
final class NodeModuleResolution {
private static final String INDEX_FILE_NAME = "index";
private static final String PACKAGE_FILE_NAME = "package.json";
private static final String MODULES_DIR_NAME = "node_modules";
private static final String EXECUTABLE_NAME = "main";
private static final String[] FILE_EXTENSIONS = { ".js", ".json" };
private NodeModuleResolution() {
}
/**
* Resolves a module name.
*
* @param baseDirectory
* the base directory
* @param normalizedName
* the normalized module identifier
* @param unnormalizedName
* the unnormalized module identifier
* @param referrerId
* the referrer module identifier or {@code null}
* @return the resolved and normalized module identifier
* @throws MalformedNameException
* if the name cannot be normalized
*/
public static FileSourceIdentifier resolve(Path baseDirectory, FileSourceIdentifier normalizedName,
String unnormalizedName, SourceIdentifier referrerId) throws MalformedNameException {
try {
Path normalizedPath = normalizedName.getPath();
boolean isRelative = unnormalizedName.startsWith("./") || unnormalizedName.startsWith("../");
if (isRelative) {
Path file = findModuleFile(baseDirectory, normalizedPath, true);
if (file != null) {
return new FileSourceIdentifier(baseDirectory, file);
}
} else if (referrerId != null) {
for (Path p : new NodeModulePaths(baseDirectory, referrerId)) {
Path file = findModuleFile(baseDirectory, p.resolve(normalizedPath), true);
if (file != null) {
return new FileSourceIdentifier(baseDirectory, file);
}
}
}
return normalizedName;
} catch (InvalidPathException e) {
throw new MalformedNameException(unnormalizedName);
}
}
private static Path findModuleFile(Path dir, Path path, boolean searchPackage) {
path = dir.resolve(path);
if (Files.exists(path)) {
if (Files.isRegularFile(path)) {
return path;
}
if (Files.isDirectory(path)) {
if (searchPackage) {
Path executable = readPackage(path);
if (executable != null) {
Path executablePath = findModuleFile(path, executable, false);
if (executablePath != null) {
return executablePath;
}
}
}
for (String ext : FILE_EXTENSIONS) {
Path indexFile = path.resolve(INDEX_FILE_NAME + ext);
if (Files.isRegularFile(indexFile)) {
return indexFile;
}
}
}
} else {
for (String ext : FILE_EXTENSIONS) {
Path pathWithExt = Paths.get(path + ext);
if (Files.isRegularFile(pathWithExt)) {
return pathWithExt;
}
}
}
return null;
}
private static Path readPackage(Path path) {
Path jsonPackage = path.resolve(PACKAGE_FILE_NAME);
if (!Files.isRegularFile(jsonPackage)) {
return null;
}
String executable;
try {
String json = new String(Files.readAllBytes(jsonPackage), StandardCharsets.UTF_8);
executable = JSONParser.parse(json, new ExecJSONBuilder());
} catch (IOException | ParserException e) {
// ignore?
return null;
}
if (executable == null || executable.isEmpty()) {
return null;
}
return Paths.get(executable);
}
private static final class NodeModulePaths implements Iterable<Path> {
private final Path referrer;
NodeModulePaths(Path base, SourceIdentifier referrerId) {
this.referrer = base.relativize(Paths.get(base.toUri().resolve(referrerId.toUri())));
}
@Override
public Iterator<Path> iterator() {
return new SimpleIterator<Path>() {
final Path node_modules = Paths.get(MODULES_DIR_NAME);
Path p = referrer;
@Override
protected Path findNext() {
if (p != null) {
while ((p = p.getParent()) != null) {
Path fileName = p.getFileName();
if (fileName != null && !fileName.equals(node_modules)) {
return p.resolve(node_modules);
}
}
return node_modules;
}
return null;
}
};
}
}
private static final class ExecJSONBuilder implements JSONBuilder<String, Void, Void, String> {
private int depth;
private String executablePath;
@Override
public String createDocument(String value) {
return executablePath;
}
@Override
public Void newObject() {
depth += 1;
return null;
}
@Override
public String finishObject(Void object) {
depth -= 1;
return null;
}
@Override
public void newProperty(Void object, String name, String rawName, long index) {
// empty
}
@Override
public void finishProperty(Void object, String name, String rawName, long index, String value) {
if (depth == 1 && EXECUTABLE_NAME.equals(name)) {
executablePath = value;
}
}
@Override
public Void newArray() {
depth += 1;
return null;
}
@Override
public String finishArray(Void array) {
depth -= 1;
return null;
}
@Override
public void newElement(Void array, long index) {
// empty
}
@Override
public void finishElement(Void array, long index, String value) {
// empty
}
@Override
public String newNull() {
return null;
}
@Override
public String newBoolean(boolean value) {
return null;
}
@Override
public String newNumber(double value, String rawValue) {
return null;
}
@Override
public String newString(String value, String rawValue) {
return value;
}
}
}