N4JS Component Overview shows the N4JS components described in detail in this chapter.[60]
The N4JS platform distinguishes several types of components. The following components can only be created by internal developers:
Definition of a runtime environment such as ECMAScript 5. A Runtime Environment describes the base types provided by the runtime directly which are usually globally available. Other components do not directly rely on runtime environments, but on runtime libraries.
Collections of types provided by the runtime. 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 polyfils to alter types predefined in the environment.
Not yet clear. Environments defined for tests.
Not yet clear. Libraries defined for tests supported by the to enable running tests and viewing test reports directly within the , such as Mangelhaft.
The following components can be created by external developers:
User-written N4JS applications running in a web browser (the reference browser is Chrome).
User-written N4JS processors running on Node.js.
User-written libraries used by apps, processors or other libraries.
These components are described in detail later.
A component is similar to a single project in the N4JS IDE. Generally, it contains the following:
The package.json file describing the components, dependencies and metadata.
Resources such as images, N4ML files etc.
Source files of modules - actual N4JS files used in a project.
Compiled, minified and concatenated versions of the N4JS files and other JS files.
Optional test sources and compiled tests.
Optional source maps.
Components contain modules. Content of a Component describes what can be contained in a component.
Different N4JS component types are described in this section. At compile time and runtime, dependent components have to be available. This is the responsibility of the user interface (IDE or CLI) and described in the N4IDE specification.
A library is a user project providing modules with declaration.
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 are special projects which contain tests for other projects.
Req. IDE-157: Test Project (ver. 1)
Tests have full access to the tested project including elements with project visibility.
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.
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 from the implementation project. When resolving the module specifier, 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.
Req. GH-821002: Type Definition Project Configuration (ver. 1)
For type definition projects, the following constraints must hold true with regard to their project configuration:
They must declare their implementation project via the definesPackage property in their package.json file.
They must not declare an output folder.
A component is similar to a project in the N4JS IDE. It consists of sources and test sources.
These items are contained in separate folders alongside output folders and settings specified in the package.json file.
The package.json file serves as the project description file and is stored at the root of the project (see Package.json File for details).
For build and production purposes, other files such as pom.xml or .project files are automatically derived from the package.json.
These files are not to be added manually.
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 from 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 project.
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.
Used as the globally unique identifier of the component.
The component’s version.
List of components required at runtime and compile time.
List of components required at compile time only.
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).
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:
(string) Must be one of the following strings:
An application. See [Apps].
A library. See Libraries.
For processors running server-side on the N4 platform. Not implemented yet.
An N4JS project containing tests for one or more other N4JS projects specified via property "testedProjects".
For N4JS projects that contain only API (in .n4jsd files) to be implemented by other,
so-called implementation projects. See properties "implementationId", "implementedProjects".
Runtime environments. See Runtime Environment Resolution.
Runtime libraries. See Runtime Environment and Runtime Libraries.
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.
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.
(string) Globally unique identifier for the component’s vendor.
Used for the @Internal accessibility modifier.
(string) Human-readable name of the component’s vendor. Used only for informational purposes.
(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.
(object) Defines various sub-folders where sources, etc. are located. All properties
of the given object must have to 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.
(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).
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.
Files matching this filter are not wrapped into modules and they are not semantically validated. Since they are assumed to be wrapped into modules, declarations inside these modules cannot be referenced by n4js code.
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"
}
],
"noModuleWrap": [
// syntax same as for noValidate above
]
}
(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.
(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.
(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 Components.
Only projects of type "application", "processor", "library", "api" or "validation" may declare this property.
(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 Components. Only components of type "application", "processor", "library", "api" or "validation" may declare this property.
(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.
(string) This property defines what module loaders are supported by the modules in this component. Possible values are the following:
(default) The modules in this component can be loaded with SystemJS or with CommonJS.
Modules in this component must be loaded with CommonJS. When these modules are referenced in
generated code (i.e. when importing from these modules), the module specifier will be prefixed
with @@cjs/.
Modules in this component represent node built-in modules such as fs or https. When these modules
are referenced in generated code (i.e. when importing from these modules), the module specifier will
be prefixed with @node/.
(string) The name of the runtime environment project that is extended by this component. Only components of type "runtime environment" may declare this property.
(array) The list of runtime library components that are provided by this component. Only components of type "runtime environment" may declare this property.
(array) A list of modules in terms of module specifiers (strings), that are executed when the component is initialized. This property may only be declared by components of type "runtime environment" and "runtime library".
These init modules are executed right before the execution of a user-specified module starts. They may be used to initialize polyfills or perform other initialization work with regard to the execution environment (e.g. define global properties).
(string) The module specifier of the exec module of a runtime environment. This property may only be declared by components of type "runtime environment" and "runtime library". The exec module of a runtime environment is executed as the entry-point to the execution of a user-specified module. The underlying script must interpret the execution data and trigger the actual module execution in the runtime environment.
(string) The name of the package this component provides type definitions for. Only components of project type "definition" may declare this property.
All properties described above are optional. The following default values apply:
Property |
Default Value |
name |
name of the folder containing the |
version |
"0.0.1" |
projectType |
"plainjs" |
vendorId |
"vendor.default" |
mainModule |
"index" |
output |
"." |
sources |
a single source-container of type "source" with path "." |
All other properties are undefined if not given in the package.json file.
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"
}
],
"noModuleWrap": [
// syntax same as for noValidate above
]
},
"requiredRuntimeLibraries": [
"n4js-runtime-es2015"
]
}
}
The following constraints apply.
Req. IDE-158: GeneralConstraints (ver. 1)
There must be an output directory specified so the compiler(s) can run.
Req. IDE-159: Paths (ver. 1)
Paths Paths are constrained in the following way:
A path cannot appear more than one time within a source fragment type (same applies to paths in the resources section).
A path cannot be used in different source fragment types at same times.
A path can only be declared exclusively in one of the sections Output, Libraries, Resources or Sources.
A path must not contain wild cards.
A path has to be relative to the project path.
A path has to point to folder.
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).
Req. IDE-160: Module Specifiers (ver. 1)
Module Specifiers are constrained in the following way:
Within a module filter type no duplicate specifiers are allowed.
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
A module specifier can be constrained to be applied only to a certain source container.
A module specifier is allowed to contain wildcards but it must resolve to some existing files in the project
Req. IDE-161: Module Specifier Wildcard Constraints (ver. 1)
All path patterns are case sensitive.
** all module specifiers will be matched.
**/* all module specifiers will be matched.
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.
**/test/**/XYZ - matches all module specifiers whose qualified name contains a segment that matches test and the last segment ends with an ’XYZ’.
A module specifier wild card isn’t allowed to contain ***.
A module specifier wild card isn’t allowed to contain relative navigation.
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 Their Implementations.
Req. GH-821001: Dependencies to Definition Projects (ver. 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.
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:
A project’s plain project name is its name without mentioning the project’s scope (if any),
e.g. myProject.
A project’s scope name is the name of the npm scope a project resides in, including a leading @.
E.g. @myScope.
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).
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.
Req. GH-1026: General Constraints (ver. 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.
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.
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 @.
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.
There are several dependencies between components. We can distinguish between require dependencies and provide dependencies.
N4JS Components require:
APIs
RuntimeLiberaries and
Libraries
Runtime Environments provide Runtime Libraries and maybe extend other Runtime Environments (which means they provide the same runtime libraries as the extended environments and the same base types).
SysLibs implement (provide implementations of) APIs
In order to execute (run, debug, or test) an N4JS Component, an actual runner has to be determined. Since runners support runtime environments, this basically means calculating runtime environments which provide all necessary runtime libraries needed by the component. This is done by computing the transitive closure of required runtime libraries and by comparing that with the transitive closure of runtime libraries provided by an environment.
Definition: Transitive Component Dependencies
We defined the following transitive closures of dependencies:
For a given N4JS Component C we define
For a given Runtime Environment E we define
Req. IDE-162: Runtime Environment Resolution (ver. 1)
An N4JS Component C can be executed in an runtime environment E, written as if the following constraints hold:
The environment must provide all runtime libraries transitively required by the component:
There exist libraries which can be executed by the environment, so that all APIs are implemented:
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.
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.
{
"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.
In case the plain-JS project does not have a main module or the main module is located at the package root,
the mainModule property can be omitted.
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.
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.
We use the following convention to compute the version of type definition packages.
Majortypes.Minortypes.Patchtypes = Majorimpl.Minorimpl.0
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.
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.
"dependencies": {
"@n4jsd/Types": "<=Majorimpl.Minorimpl.*"
}
"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.
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.
All N4JS files are modules, sometimes also called compilation unit (CU). This is the overall structure of a module, based on [ECMA15a(p.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.
This are the properties of script, which can be specified by the user:
annotations Arbitrary annotations, see Annotations and below for details.
scriptElementsThe content of the script.
And we additionally define the following pseudo properties:
path File system path (path delimiter is always ’/’) relative to the source fragment of the file without the extension.
E.g.: given a source folder src, path of a module located at:
src/n4/lang/List.js is n4/lang/List
src/n4/lang/Objects.prototypes is n4/lang/Objects
expandedPath Pseudo property consists of the project name and project version of the module followed by the path,
the concrete syntax is: <project.name>-<project.version>/<module.path> where project version includes all version parts except the qualifier.
E.g. given a module with path n4/lang/List in a project lib with
version 1.0.0, the expandedPath is lib-1.0.0/n4/lang/List.
loadtimeDeps Pseudo property contains all load time dependencies of this module.
runtimeDeps Pseudo property contains all runtime dependencies of this module.
allDeps Pseudo property contains all dependencies of this module. This is the union of loadtimeDeps and runtimeDeps which maintains the ordering of both lists, with the loadtimeDeps at the front.
Pseudo properties to be set via annotations are explained in Annotations.
Properties files have the file extension properties and describe how to localize text in a project.
They basically define keys Primitive Pathselector and I18nKey with their values.
The key is used during runtime to retrieve text localized to the user’s locale.
The syntax of a resource file is defined as:
ResourceFile: Comment* | $entry+=$ Entry*;
Comment: 'pass:[#]' .* EOL;
Entry: $key$ = KeyIdentifier '=' $value$ = .* EOL;
KeyIdentifier: LETTER (DIGIT | LETTER | '.')*;
Properties files have to be stored in source fragment of type source.
The base folder for storing the properties files of a project is .
The language-specific resource files are stored in subfolders of the base folder.
The base language (normally english) has to be located in a subfolder of the base folder.
The resource files for other languages have to be located in a subfolder with the name given by syntax <ISO Language Code>_<ISO Country Code>,
where ISO Language Code is given by the ISO-639 standard and ISO Country Code is given by the ISO-3166 standard.
All resource files stored in a language folder are compiled to a JavaScript file which exports all resource keys as an object literal.
The resource files of a project are automatically loaded.
To access a resource key key stored in a resource file my.properties, you have to use the file name as a prefix (e.g. you have to use the key my.key).
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.
When launching an N4JS component C under runtime environment RE, the user may(!) provide an implementation ID to run. Then, for each API project A in the direct or indirect dependencies of C an implementation project is chosen as follows:
Collect all implementation projects for A (i.e. projects that specify A in their package.json file under implementedProjects).
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!).
If there are no implementation projects left, show an error.
If is defined (i.e. user specified an implementation ID to run), then:
If there is an implementation project left with implementation ID , use that.
Otherwise, show an error.
If is undefined, then
If there is exactly 1 implementation project left, use it.
Otherwise, in UI mode prompt the user for a choice, in headless mode how an error.
Having found an implementation project for each API project , launch as usual except that whenever ’s output folder would be used, use ’s
output folder (esp. when constructing a class path) and when loading or importing a type from return the corresponding type with the same fully-qualified name from .
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.
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.
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.