/**
* 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 static com.github.anba.es6draft.runtime.AbstractOperations.Call;
import static com.github.anba.es6draft.runtime.AbstractOperations.Get;
import static com.github.anba.es6draft.runtime.LexicalEnvironment.newObjectEnvironment;
import static com.github.anba.es6draft.runtime.objects.FunctionConstructor.CreateDynamicFunction;
import static com.github.anba.es6draft.runtime.types.Undefined.UNDEFINED;
import static com.github.anba.es6draft.runtime.types.builtins.OrdinaryObject.ObjectCreate;
import java.io.IOException;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import com.github.anba.es6draft.compiler.CompilationException;
import com.github.anba.es6draft.compiler.CompiledFunction;
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.ExecutionContext;
import com.github.anba.es6draft.runtime.LexicalEnvironment;
import com.github.anba.es6draft.runtime.ObjectEnvironmentRecord;
import com.github.anba.es6draft.runtime.Realm;
import com.github.anba.es6draft.runtime.internal.ScriptLoader;
import com.github.anba.es6draft.runtime.internal.Source;
import com.github.anba.es6draft.runtime.modules.ModuleExport;
import com.github.anba.es6draft.runtime.modules.ModuleRecord;
import com.github.anba.es6draft.runtime.modules.ModuleSource;
import com.github.anba.es6draft.runtime.modules.SourceIdentifier;
import com.github.anba.es6draft.runtime.types.Callable;
import com.github.anba.es6draft.runtime.types.ScriptObject;
import com.github.anba.es6draft.runtime.types.Type;
/**
* Module record implementation for standard node modules (CommonJS modules).
*/
public final class NodeModuleRecord implements ModuleRecord {
private final SourceIdentifier sourceId;
private final Source source;
private final CompiledFunction function;
private Realm realm;
private ScriptObject moduleObject;
private LexicalEnvironment<ObjectEnvironmentRecord> environment;
private ScriptObject namespace;
private HashSet<String> exportedNames;
private boolean instantiated;
private boolean evaluated;
NodeModuleRecord(SourceIdentifier sourceId, Source source, CompiledFunction function) {
this.sourceId = sourceId;
this.source = source;
this.function = function;
}
/*package*/Source getSource() {
return this.source;
}
/*package*/Object getModuleExports() {
return Get(realm.defaultContext(), moduleObject, "exports");
}
private ScriptObject getModuleExportsOrNull() {
Object exports = getModuleExports();
if (Type.isObject(exports)) {
return Type.objectValue(exports);
}
return null;
}
private ScriptObject getModuleExportsOrEmpty() {
Object exports = getModuleExports();
if (Type.isObject(exports)) {
return Type.objectValue(exports);
}
// Return an empty object as a placeholder when module exports is a primitive.
return ObjectCreate(realm, (ScriptObject) null);
}
private HashSet<String> ownNames(ScriptObject object) {
HashSet<String> names = new HashSet<>();
for (Object key : object.ownPropertyKeys(realm.defaultContext())) {
if (key instanceof String) {
names.add((String) key);
}
}
return names;
}
private Set<String> instantiateAndGetExportedNames() {
instantiate();
if (!instantiated) {
// Recursive call - return the own names of the current module exports.
return ownNames(getModuleExportsOrEmpty());
}
return exportedNames;
}
@Override
public SourceIdentifier getSourceCodeId() {
return sourceId;
}
@Override
public Realm getRealm() {
return realm;
}
public void setRealm(Realm realm, ScriptObject moduleObject) {
assert this.realm == null : "module already linked";
this.realm = Objects.requireNonNull(realm);
this.moduleObject = Objects.requireNonNull(moduleObject);
}
@Override
public LexicalEnvironment<ObjectEnvironmentRecord> getEnvironment() {
return environment;
}
@Override
public ScriptObject getNamespace() {
assert realm != null : "module is not linked";
ScriptObject exports = getModuleExportsOrNull();
if (exports != null) {
return exports;
}
// Something went wrong, return the current namespace object.
return namespace;
}
@Override
public void setNamespace(ScriptObject namespace) {
assert this.namespace == null : "namespace already created";
this.namespace = Objects.requireNonNull(namespace);
}
@Override
public boolean isEvaluated() {
return evaluated;
}
@Override
public boolean isInstantiated() {
return instantiated;
}
@Override
public Set<String> getExportedNames(Set<ModuleRecord> exportStarSet) {
assert realm != null : "module is not linked";
return Collections.unmodifiableSet(instantiateAndGetExportedNames());
}
@Override
public ModuleExport resolveExport(String exportName, Map<ModuleRecord, Set<String>> resolveSet,
Set<ModuleRecord> exportStarSet) {
assert realm != null : "module is not linked";
Set<String> resolvedExports = resolveSet.get(this);
if (resolvedExports == null) {
resolveSet.put(this, resolvedExports = new HashSet<>());
} else if (resolvedExports.contains(exportName)) {
return null;
}
resolvedExports.add(exportName);
if (instantiateAndGetExportedNames().contains(exportName)) {
return new ModuleExport(this, exportName);
}
return null;
}
@Override
public void instantiate() {
assert realm != null : "module is not linked";
if (environment == null) {
ScriptObject exports = getModuleExportsOrEmpty();
environment = newObjectEnvironment(exports, realm.getGlobalEnv());
// Compile the module.
ExecutionContext cx = realm.defaultContext();
Object compile = Get(cx, moduleObject, "compile");
Callable moduleFn = CreateDynamicFunction(cx, source, function.getFunction());
Callable requireFn = NodeFunctions.createRequireFunction(this);
Call(cx, compile, moduleObject, moduleFn, requireFn);
// Create the module bindings.
ScriptObject currentExports = getModuleExportsOrEmpty();
if (currentExports != exports) {
// Module exports property has changed, update environment with new bindings.
environment = newObjectEnvironment(currentExports, realm.getGlobalEnv());
}
exportedNames = ownNames(currentExports);
instantiated = true;
}
}
@Override
public Object evaluate() {
assert realm != null : "module is not linked";
assert environment != null : "module is not instantiated";
evaluated = true;
return UNDEFINED;
}
/**
* ParseModule ( sourceText )
*
* @param scriptLoader
* the script loader
* @param identifier
* the source code identifier
* @param moduleSource
* the module source code
* @return the parsed module record
* @throws IOException
* if there was any I/O error
* @throws ParserException
* if the module source contains any syntax errors
* @throws CompilationException
* if the parsed module source cannot be compiled
*/
public static NodeModuleRecord ParseModule(ScriptLoader scriptLoader, SourceIdentifier identifier,
ModuleSource moduleSource) throws IOException, ParserException, CompilationException {
Source source = moduleSource.toSource();
String sourceCode = moduleSource.sourceCode();
if (identifier.toUri().toString().endsWith(".json")) {
String jsonScript = JSONParser.parse(sourceCode, new ScriptJSONBuilder());
sourceCode = String.format("module.exports = %s", jsonScript);
}
String parameters = "exports, require, module, __filename, __dirname";
CompiledFunction function = scriptLoader.function(source, parameters, sourceCode);
return new NodeModuleRecord(identifier, source, function);
}
private static final class ScriptJSONBuilder implements JSONBuilder<String, Void, Void, Void> {
private final StringBuilder sb = new StringBuilder();
private static void appendJsonString(StringBuilder sb, String s) {
final int len = s.length();
int begin = 0;
for (int i = 0; i < len; ++i) {
char c = s.charAt(i);
if (c == '\u2028' || c == '\u2029') {
sb.append(s, begin, i);
sb.append('\\').append(c);
begin = i + 1;
}
}
sb.append(s, begin, len);
}
@Override
public String createDocument(Void value) {
return sb.toString();
}
@Override
public Void newObject() {
sb.append('{');
return null;
}
@Override
public Void finishObject(Void object) {
sb.append('}');
return null;
}
@Override
public void newProperty(Void object, String name, String rawName, long index) {
if (index != 0) {
sb.append(',');
}
appendJsonString(sb, rawName);
sb.append(':');
}
@Override
public void finishProperty(Void object, String name, String rawName, long index, Void value) {
// empty
}
@Override
public Void newArray() {
sb.append('[');
return null;
}
@Override
public Void finishArray(Void array) {
sb.append(']');
return null;
}
@Override
public void newElement(Void array, long index) {
if (index != 0) {
sb.append(',');
}
}
@Override
public void finishElement(Void array, long index, Void value) {
// empty
}
@Override
public Void newNull() {
sb.append("null");
return null;
}
@Override
public Void newBoolean(boolean value) {
sb.append(value);
return null;
}
@Override
public Void newNumber(double value, String rawValue) {
sb.append(rawValue);
return null;
}
@Override
public Void newString(String value, String rawValue) {
appendJsonString(sb, rawValue);
return null;
}
}
}