Skip to content

Latest commit

 

History

History
923 lines (722 loc) · 44 KB

File metadata and controls

923 lines (722 loc) · 44 KB

Components

Overview

For modularization purposes, N4JS code is managed in so-called components. These components correspond to what node and npm call packages, what OSGi calls bundle, and what Eclipse calls project.

N4JS distinguishes several types of such components. Overview of N4JS Components (OLD) shows the N4JS component types described in detail in this chapter. Overview of N4JS Components and Dependencies (NEW) shows a recently updated diagram, which is not yet fully reflected in the implementation.

cmpd components in n4js
Figure 1. Overview of N4JS Components (OLD)
cmp components
Figure 2. Overview of N4JS Components and Dependencies (NEW)

The following types of components can only be created by internal developers:

Runtime Environment

Definition of a runtime environment such as ECMAScript 5, node.js, or Chrome. A Runtime Environment provides a number of base types which are usually globally available. Other components do not directly rely on runtime environments, but on runtime libraries. NOTE: runtime environments are not fully implemented at this time (see GH-1291).

Runtime Library

Contains type definitions for the base types provided by one or more runtime environments. These types may be extensions to certain language specifications. E.g., the ECMAScript 6 collection classes are already provided by some environments otherwise only supporting ECMAScript 5. The collections are defined in terms of a runtime library which can then be provided by these environments. Runtime libraries may also contain polyfills to alter types predefined in the environment.

The following components can be created by external developers:

App

User-written N4JS applications.

Processor

User-written N4JS code running server-side on the N4 platform. Not implemented yet.

Library

User-written libraries used by apps, processors, or other libraries.

The available component types are described in more detail in Component Types.

A component corresponds to a single project in the N4JS IDE. Generally, it contains the following:

Package.json File

The package.json file describing the contents, dependencies and metadata.

Resources

Resources such as images, data files etc.

Sources

Source files containing either N4JS code or plain Javascript. The actual code used in a project.

Output Code

Compiled and possibly adjusted (e.g. minified, concatenated) versions of the N4JS source files and plain Javascript files.

Tests

Optional test sources and compiled output code.

Source Maps

Optional source maps.

Components contain modules. Content of a Component describes what can be contained in a component.

cmpd component content
Figure 3. Content of a Component

At both compile time and runtime, all components defined as dependency have to be available. Since dependencies are defined in package.json files in a form compliant to node/npm, this can be fully managed by npm (or yarn).

Component Types

Different N4JS component types are described in this section.

Libraries

A library is a user project providing modules with declaration.

Runtime Environment and Runtime Libraries

Note
runtime environments are not fully implemented at this time (see GH-1291).

Runtime environments and libraries define globally available elements (types, variables, functions) provided by the JavaScript engine. Both must contain only definition files (n4jsd) of which all elements are marked as @ProvidedByRuntime ([_runtime-definitions]) and @Global ([_global-definitions]).

Other projects may refer to multiple runtime libraries in their package.json file via the property requiredRuntimeLibraries.

The concrete runtime environment and library are selected by the JavaScript engine. Deployment and execution scripts must ensure that a component can run on the given engine; the required environments and libraries must all be compatible with the provided environment. If no runtime environment is specified, a default an ECMAScript 5 runtime is assumed to be present.

Typical runtime environments are ES5 or ES6, typical runtime libraries are DOM or HTML.

In JavaScript, browsers and other execution environments provide built-in objects. In browsers, for example, the whole DOM is made available via built-in object types. In this case, even the global object also becomes a different type (in N4JS terms). Besides execution environments such as browsers or Node.js, libraries also provide functionality by exposing globally available functions. This is often used to bridge execution environment inconsistencies. When browser API differences are adapted by a library, this is called a polyfil. Other adaptations, such as enabling ECMSScript 6 object types in ECMAScript 5 environments, are known as shim. Instead of directly supporting these kind of 'hacks', other components specify which runtime environment and libraries they depend on by specifying unique runtime ids. Possible shims (in case of environments) or polyfils (in case of libraries) are transparently provided by the execution environment and the bootstrap code.

Tests

Tests are special projects which contain tests for other projects.

Test Project
  1. Tests have full access to the tested project including elements with project visibility.

  2. Only other test projects can depend on tests project. In other words, other components must not depend on test components.

In a test project, the tested projects can be specified via testee.

Type Definitions

Projects of type "definition" are special projects which only provide type information for another so-called implementation project, which only provides executable JS files.

Generally, client projects that depend on a given implementation project may additionally declare a dependency on a corresponding type definitions project, in order to integrate type information on the implementation project. This is implemented by means of module-level shadowing. More specifically, given a client imports a module with module specifier $M$ from the implementation project. When resolving the module specifier, $M$ will first be resolved against the implementation project’s type definitions and only secondarily against the implementation project. As a consequence, type definition projects may only provide partial type information, while the remaining modules of the implementation project remain accessible through dynamic namespace imports.

Type Definition Project Configuration

For type definition projects, the following constraints must hold true with regard to their project configuration:

  1. They must declare their implementation project via the definesPackage property in their package.json file.

  2. They must not declare an output folder.

Package.json File

A folder is a "component" if and only if it contains a package.json file. Being a component means that this folder is recognized by all N4JS-related tools but does not necessarily mean the component contains N4JS code (it could just contain plain Javascript). The main benefit of being a component in this sense is that this unit of code can be used by other N4JS components as a dependency.

For example, a plain npm project containing only plain Javascript can be a component and can therefore be used as a project dependency of a full-blown N4JS component.

Basic Properties

The following standard package.json properties are used by N4JS tooling. Unless otherwise noted, all these properties have the exact same format and meaning as usual in package.json files.

name

Used as the globally unique identifier of the component.

version

The component’s version.

dependencies

List of components required at runtime and compile time.

devDependencies

List of components required at compile time only.

main

Path relative to the component’s root folder, pointing to a .js file located in a source container (the .js file extension is optional, i.e. may be omitted). This file then serves as the component’s default entry point, i.e. project imports pointing to this component from other components will import from the file denoted by this property. In addition, this property may denote a folder and is then assumed to point to a file index.js located in that folder. If this property denotes a file other than a .js file, it will be ignored. In particular, it cannot be used for .n4js files; in that case, property "mainModule" has to be used (see below).

workspaces

(array of strings) Property used by package management tool yarn to denote that a project serves as a "yarn workspace" and to denote the other projects that form the members of this yarn workspace. For details, see here. In N4JS, a project is called a "yarn workspace root" if and only if its package.json file contains top-level property "workspaces", no matter the property’s value (i.e. it will be called "yarn workspace root" even if the value of property "workspaces" is the empty array or an invalid value such as a number). The nested projects referred to via the strings in this property’s array value are called "member projects" of the yarn workspace.

N4JS Properties

In addition to the standard properties above, there is a single N4JS-specific top-level property called "n4js". The value of this property must always be an object that may have any combination of the following properties:

projectType

(string) Must be one of the following strings:

application

An application. See [App].

library

A library. See Libraries.

processor

For processors running server-side on the N4 platform. Not implemented yet.

test

An N4JS project containing tests for one or more other N4JS projects specified via property "testedProjects".

api

For N4JS projects that contain only API (in .n4jsd files) to be implemented by other, so-called implementation projects. See properties "implementationId", "implementedProjects". NOTE: the API/Implementation concept is not fully implemented at this time (see GH-1291).

runtimeEnvironment

Runtime environments. See Runtime Environment and Runtime Libraries. NOTE: runtime environments are not fully implemented at this time (see GH-1291).

runtimeLibrary

Runtime libraries. See Runtime Environment and Runtime Libraries.

validation

A project in which .n4js files are only being validated, not transpiled. This is used for projects that are implemented in terms of .js files but that also provide type information in terms of .n4jsd files.

plainjs

A project which only contains .js files and no N4JS resources. The contained JS files are only indexed to allow for dynamic imports of specific JavaScript modules. Projects of this type are not being transpiled.

vendorId

(string) Globally unique identifier for the component’s vendor. Used for the @Internal accessibility modifier.

vendorName

(string) Human-readable name of the component’s vendor. Used only for informational purposes.

output

(string) Path relative to the component’s root folder, pointing to a folder where all output files will be placed. In particular, this is where the N4JS transpiler will put the .js files created for each .n4js file.

sources

(object) Defines various sub-folders where sources, etc. are located. All properties of the given object must have the following format: the name must be "source", "external", or "test"; the value must be an array of strings, with each string defining a path relative to the component’s root folder, pointing to a folder where source files of the corresponding type are located. For example, paths given via name "source" tell the N4JS transpiler where to look for .n4js source files to be compiled.

moduleFilters

(object) Filters for fine-tuning the validator and compiler. A filter is applied to modules matching the given module specifier which may contain wildcards, optionally restricted to modules defined in a specific source path.

All properties of the given object must have the following format: the name must be a valid module filter type (see below); the value must be an array of strings, with each string defining a pattern of files inside one of the source containers for which validation or module wrapping is to be turned off. Instead of a plain string, the inner array may contain an object with properties "module" and "sourceContainer" to make this filter apply to only one of the source containers (instead of all source containers, which is the default).

noValidate

Modules matching this filter are not semantically validated. That is, they are still syntactically validated. If they are contained in source or test source fragments, it must be possible to bind references to declarations inside these modules. Note that switching off validation for n4js files is disallowed.

Example 1. Module Filters

A simple and a source-container-specific module filter in the n4js section of a package.json file.

"moduleFilters": {
	"noValidate": [
		"abc*",
		{
			"module": "xyz*",
			"sourceContainer": "src/n4js"
		}
	]
}
mainModule

(string) A plain module specifier defining the project’s 'main module'. If this property is defined, other projects can import from this project using imports where the string following keyword from states only the project name and not the complete module specifier (see [import-statement-semantics]). If this property is defined, top-level property main will be ignored.

testedProjects

(array) List of N4JS components being tested by this project.
Only components of project type "test" may declare this property. Furthermore, the referenced projects must all be of the same project type and must not be of type "test" themselves.

implementationId

(string) If this property is defined, this component is called an "implementation project" and the string value provides a unique identifier for the implementation provided in this component. If this is defined, property "implementedProjects" must be defined as well. For details, see API and Implementation Component.

Only projects of type "application", "processor", "library", "api" or "validation" may declare this property.

implementedProjects

(array) A list of API components (components of type "api") that are implemented by this component. If this is defined, property "implementationId" must be defined as well. For details, see API and Implementation Component. Only components of type "application", "processor", "library", "api" or "validation" may declare this property.

requiredRuntimeLibraries

(array) The list of required runtime library components that are required for the execution of this component. All components but components of type "runtime environment" may declare this property. Each required runtime library must also be specified as a dependency using one of the top-level properties dependencies or devDependencies.

extendedRuntimeEnvironment

(string) The name of the runtime environment project that is extended by this component. Only components of type "runtime environment" may declare this property.

providedRuntimeLibraries

(array) The list of runtime library components that are provided by this component. Only components of type "runtime environment" may declare this property.

definesPackage

(string) The name of the package this component provides type definitions for. Only components of project type "definition" may declare this property.

generator

(object) Defines various properties of the n4js generator.

source-maps

(boolean) Iff false, no source maps will be emitted.

rewriteModuleSpecifiers

(object) Each key/value pair of this object defines a mapping of a module specifier: the key is the module specifier as used in the N4JS source code by the programmer, and the value defines the module specifier the transpiler should emit to the output code.

rewriteCjsImports

(boolean) Iff true, the N4JS transpiler will emit special interoperability code whenever an N4JS file imports something from a CommonJS module: named and namespace imports are changed to a default import with destructuring (as recommended by node.js).

d.ts

(boolean) Iff true, TypeScript d.ts files will be created for all n4js files.

All properties described above are optional. The following default values apply:

Property

Default Value

name

name of the folder containing the package.json file

version

"0.0.1"

projectType

"plainjs"

vendorId

"vendor.default"

mainModule

"index"

output

"."

sources

a single source-container of type "source" with path "." (except for yarn workspace roots, see below)

source-maps

true

rewriteModuleSpecifiers

no mappings

rewriteCjsImports

false

d.ts

false

All other properties are undefined if not given in the package.json file. The default source folder of "." does not apply to projects that represent the root folder of a yarn workspace; those projects do not have any source folder, by default.

Example 2. A package.json file with N4JS-specific properties.

The following example illustrates how to use the N4JS-related package.json properties.

{
	"name": "SampleProject",
	"version": "0.0.1",
	"author": "Enfore AG",
	"main": "./src/js/main.js",
	"dependencies": {
		"OtherProject": ">=1.2.3 <2.0.0",
		"n4js-runtime-es2015": "latest"
	},
	"devDependencies": {
		"org.eclipse.n4js.mangelhaft": "latest"
	},
	"n4js": {
		"projectType": "library",
		"vendorId": "org.eclipse.n4js",
		"vendorName": "Eclipse N4JS Project",
		"output": "src-gen",
		"mainModule": "a/b/Main",
		"sources": {
			"source": [
				"src/n4js",
				"src/n4js-gen"
			],
			"external": [
				"src-ext"
			],
			"test": [
				"src-test"
			]
		},
		"moduleFilters": {
			"noValidate": [
				"abc*",
				{
					"module": "xyz*",
					"sourceContainer": "src/n4js"
				}
			]
		},
		"requiredRuntimeLibraries": [
			"n4js-runtime-es2015"
		]
	}
}

Constraints

The following constraints apply.

GeneralConstraints
  1. There must be an output directory specified so the compiler(s) can run.

Paths

Paths Paths are constrained in the following way:

  1. A path cannot appear more than one time within a source fragment type (same applies to paths in the resources section).

  2. A path cannot be used in different source fragment types at same times.

  3. A path can only be declared exclusively in one of the sections Output, Libraries, Resources or Sources.

  4. A path must not contain wild cards.

  5. A path has to be relative to the project path.

  6. A path has to point to folder.

  7. The folder a defined path points to must exist in the project (but in case of non-existent folders of source fragments, only a warning is shown).

Module Specifiers

Module Specifiers are constrained in the following way:

  1. Within a module filter type no duplicate specifiers are allowed.

  2. A module specifier is by default applied relatively to all defined source containers, i.e. if there src and src2 defined as source containers in both folders files are looked up that matches the given module specifier

  3. A module specifier can be constrained to be applied only to a certain source container.

  4. A module specifier is allowed to contain wildcards but it must resolve to some existing files in the project

Module Specifier Wildcard Constraints
  1. All path patterns are case sensitive.

  2. ** all module specifiers will be matched.

  3. **/* all module specifiers will be matched.

  4. test/A?? matches all module specifiers whose qualified name consists of two segments where the first part matches test and the second part starts with an A and then two more characters.

  5. **/test/**/XYZ - matches all module specifiers whose qualified name contains a segment that matches test and the last segment ends with an ’XYZ’.

  6. A module specifier wild card isn’t allowed to contain ***.

  7. A module specifier wild card isn’t allowed to contain relative navigation.

  8. A module specifier wild card shouldn’t contain the file extension (only state the file name (pattern) without extension, valid file extensions will then be used to match the file).

Examples of using external source fragments and filters are given in [_implementation-of-external-declarations], see [external-definitions-and-implementations].

Dependencies to Definition Projects
  1. For each listed project dependency of type "definition", a corresponding dependency (in the (dev)dependencies section) must be declared, whose "name" matches the "definesPackage" property value of the definition project.

Support for NPM Scopes

NPM supports a namespace concept for npm packages. Such namespaces are called "scopes". For details see https://docs.npmjs.com/misc/scope and https://docs.npmjs.com/getting-started/scoped-packages. In N4JS, this is supported too.

Terminology:

  1. A project’s plain project name is its name without mentioning the project’s scope (if any), e.g. myProject.

  2. A project’s scope name is the name of the npm scope a project resides in, including a leading @. E.g. @myScope.

  3. A project’s N4JS project name is its plain project name, prefixed by its scope name (if any), separated by a /. For unscoped projects, this is identical to the plain project name. E.g., myProject (if unscoped), @myScope/myProject (if scoped).

  4. A project’s Eclipse project name is an ancillary name used only within the Eclipse UI for the project in the workspace. It is equal to the N4JS project name, except that : instead of / is used as separator between the scope and plain project name. E.g., myProject (if unscoped), @myScope:myProject (if scoped).

In case the intended meaning is apparent from the context, the "N4JS project name" can simply be referred to as "project name" (as is common practice in the context of npm).

In N4JS, when importing from a module M contained in a scoped project @myScope/myProject, the import statement’s module specifier should have one of the following forms:

  • import * as N from "a/b/c/M";

  • import * as N from "@myScope/myProject/a/b/c/M";

  • import * as N from "@myScope/myProject"; (if M is specified as main module in `myProject’s package.json)

Thus, the N4JS project name, which includes the scope name, is simply used in place of an ordinary, non-scoped project’s name. This is in line with conventions in Javascript.

General Constraints
  1. The name given in the package.json file (i.e. value of top-level property "name") must be equal to the project’s "N4JS project name", as defined above.

  2. The name of the project folder on disk (i.e. folder containing the package.json file) must be equal to the project’s "plain project name", as defined above.

  3. Iff the project is scoped, this project folder must have a parent folder with a name equal to the project’s "scope name", including the leading @.

  4. Within Eclipse, the name of of an N4JS project in the workspace UI must be equal to the project’s "Eclipse project name", as defined above.

N4JS Type Definitions

N4JS Type Definitions

N4JS projects can depend on ordinary JavaScript projects by including them in the package.json file. From there on, modules of those JavaScript projects can be imported when writing N4JS code. However, since JavaScript is untyped there will not be any type information for e.g. classes, functions of ordinary JavaScript projects unless this type information is provided by a type definition project. Type definition projects do only contain n4jsd files that reflect the classes and functions of a specific npm. To refer to a JavaScript npm, the term plain-JS project will be used.

Specify Type Definition

A type definition project is structured like a normal npm. The major difference is that it provides n4jsd files instead of js files. These n4jsd files are named like and located at the exact position in the file tree as their js-counterparts. This ensures the type definition module and the corresponding plain-JS module to have the same fully-qualified name. Besides the usual properties the package.json file usually needs to specify the following properties in the n4js section.

Package.json: Important properties for type definition projects
{
	"n4js": {
		"projectType": "definition"
		"definesPackage": "..."
		"mainModule": "..."
	}
}

The project type declares this project to be a type definition projects. Consequently, it has to also declare the name for which plain-JS project its type definitions are provided (using definesPackage). Lastly, the main module has to be specified since this information will not be taken from the package.json of the plain-JS project.

A type definition project may only define a main module if the corresponding plain-JS project defines a main module. In this case, the main module of the type definition project may have a different fully-qualified name / module specifier (but should, of course, define the types provided by the plain-JS project’s main module). This is possible, because in client code the import of a main module will always look the same, no matter the main module’s fully-qualified name / module specifier.

Name Conventions

A type definition package can have an arbitrary name and define an arbitrary npm package. This can be handy for testing purposes or just creating some temporary type definitions for a local package. However, we chose to use a convention to simplify finding the right type definition package for a specific plain-JS project. First, the scope @n4jsd and second the exact name of the plain-JS project is used. For instance, when a user wants to install type definitions for the plain-JS project express, our related type definitions are called @n4jsd/express.

Version Conventions

Since the plain-JS project will evolve over time and publish different versions, the need arises to also publish the type definition project in different versions that reflect this evolution. In addition to the evolution of the plain-JS project, a new version of the type definition project can also become necessary in case a bug in the type definitions was found or in case the language of N4JS changes and the type definitions have to be adjusted accordingly. Effectively, this will lead to a situation where both the implementation and the type definition project have a version that are technically unrelated from an npm point of view, but still are somehow related to each other from a semantical point of view. To keep the distinct versioning of both of the projects manageable, we propose the following conventions to partially align the type definition project’s version to that of the plain-JS project.

Define a New Type Definition Package

We use the following convention to compute the version of type definition packages.

Convention for initial type definition versions

     Majortypes.Minortypes.Patchtypes = Majorimpl.Minorimpl.0

Example for initial type definition of [email protected]

     Majortypes.Minortypes.Patchtypes = 3.3.0

Let’s say that a new version of a type definition package called types should be created that defines types for an npm called impl of version Majorimpl.Minorimpl.Patchimpl. According to our convention, the major and minor version numbers of the type definition package should just be copied from the version of the impl package. However, the version patch number of types should not be taken from impl. Instead, the patch number of types starts with 0 and increases with every update of this type definition version. For instance when a bug was found in version Majortypes.Minortypes.0, the definitions have been extended, or adjusted to new language features, only the patch number increases to e.g. Majortypes.Minortypes.1.

Using a Type Definition Package

On the client side, a type definition package is listed among the dependency section. Here we use the following convention to specify the required version of a type definition package.

Convention for dependencies

"dependencies": {
     "@n4jsd/Types": "<=Majorimpl.Minorimpl.*"
}

Example of dependencies to express and its type definition project

"dependencies": {
     "express": "^3.3.3",
     "@n4jsd/express": "<=3.3.*"
}

According to this convention, the major and minor version numbers of the implementation package are used, prepended with a smaller-equals and appended with an asterisk for the patch number. This also applies when the implementation version contains a tilde, a caret, etc., or is omitting a minor or patch number. In case a non SemVer version is given (e.g. latest, empty string, url, etc.) it is recommended to plain copy the non SemVer version when possible.

Rational

The rational behind this convention reflects the idea of semantic versioning:

Given a version number MAJOR.MINOR.PATCH, increment the:

  • MAJOR version when you make incompatible API changes,

  • MINOR version when you add functionality in a backwards-compatible manner, and

  • PATCH version when you make backwards-compatible bug fixes.

Patch version increments are always backwards compatible according to SemVer. In addition also no further functionality is added since this would imply at least an increment of the minor version. Consequently, patch versions do not affect the interface or type information of an plain-JS project. This is why patch version number fully suffices to reflect bug fixes and language changes for a given major.minor version.

On client side, we recommend to use a smaller-equals qualifier because most probably there will not be the exact version of a requested type definition project. Instead, only some major.minor versions will have a type definition counterpart. Using a smaller-equals qualifier will make sure that a client will always get the latest version of a requested plain-JS project version. In case a newer version of the plain-JS project was already published, this convention guarantees that a compatible version of the type definition project is installed.

Modules

Each N4JS source file defines a module in the sense of ECMAScript2015, cite:[ECMA15a(S14)]. This is the overall structure of a module, based on cite:[ECMA15a(S14)].

Script: {Script}
    annotations+=ScriptAnnotation*
    scriptElements+=ScriptElement*;

/*
 * The top level elements in a script are type declarations, exports, imports or statements
 */
ScriptElement:
      AnnotatedScriptElement
    | N4ClassDeclaration<Yield=false>
    | N4InterfaceDeclaration<Yield=false>
    | N4EnumDeclaration<Yield=false>
    | ImportDeclaration
    | ExportDeclaration
    | RootStatement<Yield=false>
;

Grammar and semantics of import statement is described in [_import-statement]; of export statement described in [_export-statement].

An import statement imports a variable declaration, function declaration, or N4 type declaration defined and exported by another module into the current module under the given alias (which is similar to the original name if no alias is defined). The name of the module is its project’s source folder’s relative path without any extension, see [_qualified-names] for details.

Cyclic Dependencies between Modules

The module system of ES6 does not support certain cases of cyclic dependencies between modules, causing a ReferenceError to be thrown at runtime when modules are being loaded. N4JS includes validations to disallow code that would produce such behavior. This sections details these validations.

Motivation

As an example for a cyclic dependency between modules that fails at runtime, consider the following two plain Javascript files:

// A.js
import {value} from "./B"

export class A {}

export function foo() {
	console.log(value);
}
// B.js
import {A} from "./A"

export class B extends A {}

export const value = 'value from A';

If a third file imports A.js before B.js (or only imports A.js), then this would throw the following exception at runtime:

export class B extends A {}
                       ^
ReferenceError: Cannot access 'A' before initialization

This is caused by the following load-time behavior of Javascript: when module A is loaded, the import to B is encountered causing B to be loaded before the rest of A (below the import) is being processed; therefore, when the declaration of class B is reached, the symbol A has not been initialized yet.

In contrast, if the third file imports B.js before A.js (or only imports B.js), then everythings works fine at runtime.

Validation

A dependency between two N4JS modules is called pure compile-time dependency if exists only due to type references or identifier-references to elements that do not have a representation at runtime (e.g. @StringBased enums). Because such references are removed by the transpiler (including any corresponding import), such a pure compile-time dependency does not apply to the output code and has no effect at runtime. If a dependency between two N4JS modules isn’t a pure compile-time dependency, then we call it a runtime dependency, to highlight the fact that this dependency will actually be in effect in the output code and at runtime. A runtime dependency that is caused by references of which one or more are located on in the module’s top-level code (e.g. directly on top level, in the initializer expression of a top-level variable or a static field, in the extends or implements clause of a classifier declaration) is called a load-time dependency, because it is required when the module is being loaded.

In the following, we entirely disregard pure compile-time dependencies. We only care about runtime dependencies (of which some may be load-time dependencies).

Given an N4JS project $P$ and the graph defined by $P$'s modules as vertices and the direct runtime dependencies between those modules as edges (i.e. disregarding dependencies to modules outside the project), we call each strongly connected component of modules in this graph with a size greater 1 a runtime cycle cluster of $P$. In such a runtime cycle cluster, each two modules are in a cyclic runtime dependency with each other. Note that it follows from this definition that all modules of a runtime cycle cluster are contained in the same project.

To disallow code that may cause the aforementioned errors at runtime, we introduce the following constraints:

Illegal Load-Time Dependencies
  1. It is an error to form cycles of load-time dependencies between modules.

  2. Within a runtime cycle cluster $C$, no two modules in $C$ may have a direct load-time dependency to the same target module in $C$.

  3. Given

    • a project $P$ with a runtime cycle cluster $C$,

    • two modules $M_T$, $M_S$ that both lie within $C$,

    • a direct load-time dependency from $M_S$ to $M_T$, and

    • a third module $M$ from outside of $C$ (note: $M$ may or may not be contained in $P$),

    then it is an error to import $M_T$ in $M$ unless this import is preceded by an import of another module $M_H$ that lies within $C$ and is not the target of a direct load-time dependency from any module within $C$.

  4. Given a module $M$ that lies within a runtime cycle cluster $C$, it is an error to have a reference in $M$'s top-level code to any named element declared within $C$ (i.e. declared in another module in $C$ or in $M$ itself), except in the extends/implements clause of a classifier declaration.

Note that in some cases the above constraints disallow code that would not produce a ReferenceError at runtime.

The fourth constraint above is required to ensure that only top-level code can produce load-time dependencies; if invoking functions were allowed on top level, for example, the bodies of all functions could potentially introduce additional load-time dependencies.

API and Implementation Component

Note
the API/Implementation concept is not fully implemented at this time (see GH-1291).

Instead of providing an implementation, N4JS components may only define an API by way of one or more n4jsd files which is then implemented by separate implementation projects. For one such API project, several implementation projects may be provided. Client code using the API will always be bound to the API project only, i.e. only the API project will appear in the client project’s package.json file under dependencies. When launching the client code, the launcher will choose an appropriate implementation for each API project in the client code’s direct or indirect dependencies and transparently replace the API project by the implementation project. In other words, instead of the API project’s output folder, the implementation project’s output folder will be put on the class path. Static compile time validations ensure that the implementation projects comply to their corresponding API project.

Note how this concept can be seen as an alternative way of providing the implementation for an n4jsd file: usually n4jsd files are used to define types that are implemented in plain JavaScript code or provided by the runtime; this concept allows for providing the implementation of an n4jsd file in form of ordinary N4JS code.

At this time, the concept of API and implementation components is in a prototype phase and the tool support is limited. The goal is to gain experience from using the early prototype support and then refine the concept over time.

Here is a summary of the most important details of this concept (they are all subject to discussion and change):

  • Support for this concept, esp. validations, should not be built into the core language but rather implemented as a separate validation/analysis tool.

  • Validation is currently provided in the form of a separate view: the API / Implementation compare view.

  • A project that defines one or more other projects in its package.json file under implementedProjects (cf. implementedProjects) is called implementation project. A project that has another project pointing to itself via ImplementedProjects is called API project. Note that, at the moment, there is no explicit definition making a project an API project.

  • An implementation project must define an implementation ID in its package.json file using the implementationId property in the n4js section (cf. implementationId).

  • For each public or public@Internal classifier or enum in an API project, there must be a corresponding type with the same fully-qualified name of the same or higher visibility in the implementation project. For each member of such a type in the API, there must exist a corresponding, owned or inherited type-compatible member in the implementation type.

  • Beyond type compatibility, formal parameters should have the same name on API and implementation side; however, different names are legal but should be highlighted by API / Implementation tool support as a (legal) change.

  • Comments regarding the state of the API or implementation may be added to the JSDoc in the source code using the special tag @apiNote. API / Implementation tool support should extract and present this information to the user in an appropriate form.

  • If an API class C implements an interface I, it has to explicitly (re-) declare all members of I similar to the implementation. This is necessary for abstract classes anyway in order to distinguish the implemented methods from the non-implemented ones. For concrete classes, we want all members in C in order to be complete and avoid problems when the interface is changed or C is made abstract.

Execution of API and Implementation Components

When launching an N4JS component C under runtime environment RE, the user may(!) provide an implementation ID $I\!I\!D$ to run. Then, for each API project A in the direct or indirect dependencies of C an implementation project is chosen as follows:

  1. Collect all implementation projects for A (i.e. projects that specify A in their package.json file under implementedProjects).

  2. Remove implementation projects that cannot be run under runtime environment RE, using the same logic as for running ordinary N4JS components (this step is not implemented yet!).

  3. If there are no implementation projects left, show an error.

  4. If $I\!I\!D$ is defined (i.e. user specified an implementation ID to run), then:

    1. If there is an implementation project left with implementation ID $I\!I\!D$, use that.

    2. Otherwise, show an error.

  5. If $I\!I\!D$ is undefined, then

    1. If there is exactly 1 implementation project left, use it.

    2. Otherwise, in UI mode prompt the user for a choice, in headless mode how an error.

Having found an implementation project $I_n$ for each API project $A_n$, launch as usual except that whenever $A_n$’s output folder would be used, use $I_n$’s output folder (esp. when constructing a class path) and when loading or importing a type from $A_n$ return the corresponding type with the same fully-qualified name from $I_n$.

API and Implementation With DI

API projects may use N4JS DI ([_dependency-injection]) language features which require Implementation projects to provide DI-compatible behaviour in order to allow a Client (implemented against an API project) to be executed with a given Implementation project. This is essential for normal execution and for test execution.

Overview of API tests with DI shows some of those considerations from test client point of view.

diag ApiTestsDI Overview
Figure 4. Overview of API tests with DI

Static DI mechanisms in N4JS allow an API project to enforce Implementation projects to provide all necessary information. This allows clients to work seamlessly with various implementations without specific knowledge about them or without relying on extra tools for proper project wiring.

API tests with static DI shows how API project defines project wiring and enforces certain level of testability.

diag ApiTestsDI StaticDI
Figure 5. API tests with static DI

During Client execution, weather it is test execution or not, N4JS mechanisms will replace the API project with a proper Implementation project. During runtime DI mechanisms will take care of providing proper instances of implantation types.

Types view and Instances view shows Types View perspective of the client, and Instances View perspective of the client.

diag ApiTestsDI Views
Figure 6. Types view and Instances view