Skip to content

Instantly share code, notes, and snippets.

@SMotaal
Last active December 14, 2018 14:16
Show Gist options
  • Save SMotaal/cf28227df46de80ec0892160695886c9 to your computer and use it in GitHub Desktop.
Save SMotaal/cf28227df46de80ec0892160695886c9 to your computer and use it in GitHub Desktop.
ECMAScript Proposal: Non-Module Namespaces

ECMAScript Proposal: Non-Module Namespaces

This is a first draft for a proposal for adding a new dimension to the current ECMAScript module architecture that would make it possible to fulfill platform dependent behaviours for common and non-common requirements without breaking the established compliance criteria. This is not yet a formal proposal.

In the default case of resolving an external dependency (which translates into bindings across two or more linked ES modules) the existing standards are technically sufficient and can only improve by increased adoption and natural progression. However, when it is necessary to either import from or respond to a specific platform, the current standards can be improved upon, which is the topic of several proposals, and is the main focus of this proposal.

Potential Use Cases

// Module-specific
import { url } from '#meta' // equivelant to import.meta.url

// Module- and Platform-specific
import { __filename } from '#node'; // undefined if not node

// Arbitrary Namespaces
import { asset } from './index.js#assets' // custom-implementation
// Platform-specific
import { permissions } from '#browser'; // undefined it not browser
import { tabs } from '#chrome'; // undefined if not chrome
import { process } from '#node'; // undefined if not node
import { platform, version } from '#platform'; // possible standard
// Module- and Platform-specific Imports
import { __filename, require } from '#node'; // undefined if not node

// Module- and Platform-specific Exports
export let helper;

if (require) {
  helper = require('./module.js');
}
// Asynchronous (non-blocking) Imports
import compiledModule from './uncompiled.xs#@compiler';

Rationale

One of the fundamental goals behind ES modules has been to make it possible to author modular code which is functionally platform independent in such a format that it can be loaded on any conforming platform. While the current state of things makes it possible to accomplish this at some level it is not without significant challenges.

In truth, even after all the amazing progess, the reality is that almost all projects to-date shy away from embracing true ES module centric workflows, and almost all frameworks and libraries are still built around custom loaders and only occassionally publish ES modules and those are often generated outputs.

The reality is that ES modules cannot be authored in complete platform independence, and somewhere in any dependency graph some module will often break when statically imported on an unsupported platform.

In order for the adoption of ES modules to gain traction, challanges for handling cross-platform dependencies and non-standard facility for environments not bound by browser-related restrictions should not be attainable only by proprietary mechanisms which historically and if the current state of things remain unchange will ultimately mean that ES modules will at least have two flavours (one of the is .mjs).

Such an unfortunate outcome can and must be averted.

It seems reasonable to enough to conclude that unless there is a standard way for ES modules to be able to declare such non-standard dependencies using a special form of the existing specifiers convention or, less favourably, by introducing new and possibly independent mechanisms and standards.

If platform features were just global things (humoring the notion that global is acceptable) then if a certain global.<feature> is not defined, then modules can respond accordingly. In an ES module world, this roughly translates to import { maybe } from '…'; with the one difference that this would resolve gracefully (ie maybe === undefined or otherwise resolved).

This is the main contribution of this proposal, where certain specifiers are categorically and explicitly handled independently from actual module specifiers in such a way that they either resolve to platform-specific namespaces or at worst resolve to undefined.

This goal is to make use of the fact that hashes are a well-established aspect of URL specifications, which means that by extension their use as specifiers (both relative or absolute) should maintain the existing conformance criteria across all applicable standards and specifications.

Pseudo Specifiers

In order for a module specifier to conform to the current standards it basically needs to follow the rules that are needed to yield a valid and absolute URL, whereby relative specifiers would first need to be resolved relative the valid and absolute URL of module in which it has been requested.

Once a specifier is resolved and assuming that it points to a valid module file that is accessible by the platform, linking can take place. This works for static resources that are actual ES modules (with imports and/or exports) or valid ES files intended for side-effects (without imports and/or exports).

However, if what is being requested is not a static resource, or if it is simply another type of namespace (implements a key-value mapping for some known set of string keys). Such namespaces, which include both standard and non-standard namespaces, can be declared using "Pseudo Specifiers" which would not only conform to the existing standards and even benefit from how resolutions currently behave.

If such specifiers were to be restricted to an auxilary URL hash, it would be possible to use specifiers like import {…} from '#nodejs'; and instead of the current behaviour of resolving this as a link to an actual module, instead, the platform would either resolve this to a known namespace object the determination of which is beyond the scope of this proposal, or to an instance of a new ArbitraryNamespace which would follow a standard that would be further elaborted upon in this or subsequent proposals.

Non-Module Namespaces

The fundamental criteria for namespaces that are not a Module Namespace is that they do not prescribe the resolution or linking of any external dependencies (by default) and allow each platform to decide on how to handle them, or to simply return a namespace for which any key will simply return undefined.

Such namespaces would not need to follow the existing rules for static module namespaces, and are technically more inline with the various proposed and/or proprietary notions of dynamic or dynamically instantiated modules.

Platform-Specific "Optional" (Non-Module) Namespace

One use of such namespaces could make it possible for a module to modify it's behaviour across platforms, where it would utilize a given api only if import {api} from '#browser' returns the expected binding (ie api !== undefined).

Module-Specific "Meta" (Non-Module) Namespace

One use of such namespaces can further compliment the existing import.meta work is the introduction of meta-module namespaces, allowing a module to form true bindings for import {name} from '#meta'; which can follow the existing rules or go beyond that and make it possible to update the value of the value (considering that meta modules are local to their respective module).

Divergance

Implicit Validity

One divergance from the current standards which does not affect conforming implementations is that unlike specifiers that do not include hashes, specifiers with hashes would never throw, regardless of they are handled. Even in cases when the intent is to reference an external resource, like import {name} from 'module.src#@compile'; which can theoretically pre-compile into an ES module by a supporting platform, or import {name} from './module.js#123'; which can imply skipping cache and importing a fresh instance of the same module. In any of those cases, as long as the specifier includes a hash, any errors should be handled gracefully or through secondary channels so that such dependencies do not prevent a timely and successful conclusion of an otherwise valid static dependency graph.

Asynchronous Bindings

Another divergance from the current standards which may or may not be extended to existing bindings is the potential to introduce asynchronous bindings, which would be equal to undefined, however, at the same time, when used in an asynchronous manner like async () => await <bound name> it behaves just like a promise, and would as such perpetually (or immediately) resolve to some defined value as the "final" value of the binding reflected across the graph, or simply result in a rejection only when used asynchronously (to allow appropriate handling) and then the value would either remain undefined (or something with more explicit indication).

Such bindings are not being proposed at this point, they are simply used to illustrate how non-module namespace bindings can be expected to function such that they do not block or prevent a timely and successful conclusion of an otherwise valid static dependency graph.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment