Hints Syntax and Its Metadata

Table of Content:

Summary

This RFC suggests authoring syntax for specifying dynamic import hints and describes its metadata shape.

Motivation

Using hints allows component authors to specify a condition on which a dynamic import will be necessary. This is a way to describe the conditions required for this import to happen. An application framework can use these hints to optimize the loading of those resources (e.g.: pre-fetch at the time of resolving dependencies)

Hint Invariants

Detailed Design

Grammar

This section defines the grammar to be followed for expressing hints. The grammar establishes a contract between the component and the compiler to facilitate hint recognition by the compiler, gathering the metadata and providing that as part of the compilation result.

Hint Syntax

A hint is expressed as a comment inside a dynamic import expression. The hint syntax is either a line comment or a block comment with a key and value pair. The key and value will be separated by a colon(:) and delimited using double quotes(").

/* "<key>": "<value>" */

Hint comment e.g.:

/* "@salesforce/client/formFactor": "SMALL" */
/* "MY_CONDITION": "VALID" */
// "MY_OTHER_CONDITION": "NOT_VALID"

Dynamic import with a hint e.g.:

import formFactor from '@salesforce/client/formFactor';

export async function loadFoo() {
    if (formFactor === 'SMALL') {
        return import("lightning-foo-mobile" /* "@salesforce/client/formFactor": "SMALL" */);
    } else {
        return import("lightning-foo-desktop" /* "@salesforce/client/formFactor": "LARGE" */);
    }
}

Static Semantics

Hint Key:
Hint Value:
Multiple Hints:

When multiple hint comments are specified for a single dynamic import statement, only the first hint is considered. Example:

import("c-bar-mobile"
    /* "@salesforce/client/formFactor": "SMALL"*/
    /* "@salesforce/userPermission/ViewSetup": "true"*/
);

In the above example, only the formFactor hint is considered.

Semantics

The ModuleSpecifier of the import statement should be a string literal. The compiler cannot analyze data about a module name that will be discovered or learned at runtime. For example, in the following case no hint metadata will be gathered:

const bar = '/modules/my-module.js';
function foo() {
    import(
        bar
        /* "@salesforce/client/formFactor": "SMALL" */
    ).then(module => {
        success();
    });
}

If a hint does not follow the above grammar, metadata gathering for that specific hint will be skipped. However, if there are other hints in the same module that follow the grammar, metadata will be gathered.

Examples

Valid

// Single hint
import("c-bar-mobile"/* "@salesforce/client/formFactor": "SMALL"*/);
import("c-bar-mobile"/* "KEY.SUBKEY": "VALUE"*/);
import("c-bar-mobile"/* "KEY/SUBKEY1": "VALUE"*/);
import("c-bar-mobile"/* "KEY": "true"*/);
import("c-bar-mobile"/* "KEY": "1"*/);

Invalid

import("c-bar-mobile"/* "KEY SUBKEY": "SMALL"*/);
import("c-bar-mobile"/* @salesforce/client/formFactor = SMALL*/);

// Dynamic module name specifier
const bar = 'module-name';
import(bar/* "@salesforce/client/formFactor": "SMALL" */)

Separation of Concern

Dynamic imports hint metadata gathering happens during component compilation. An invariant of this process is that the metadata collection will not fail due to a bad hint. This could be due to a hint not following specified grammar.

Additional validation and post processing of the gathered metadata is out of the scope of this RFC. That logic will be the responsibility of the application framework that consumes the metadata.

Similarly, static analysis of dynamic import hints can be handled by linting module code. For example, on the salesforce platform, allowed hint keys might be restricted to only @salesforce scoped specifiers. The hint values will be validated based on the relevant value for the specifier. An ESLint rule will enforce this restriction via the eslint-plugin-lwc.

Reasons

The hint syntax in the form of a comment was selected for the following reasons:

Dynamic Import Metadata

The proposed metadata format will include dynamically imported component name and a hint object, which in-itself contains the original query and a hint value. A hint value will be an array of objects with raw hint query, expected hint condition, and hint resource information.

Proposed Shape

export interface SourceLocation {
    startLine: number;
    startColumn: number;
    endline: number;
    endColumn: number;
    file: string;
}

export interface DynamicImportHint {
    rawValue: string; // Leading and trailing spaces of hint phrase trimmed
    key: string;
    value: string;
    location: SourceLocation;
}

export interface DynamicImportMetadata {
    moduleName: string;
    moduleNameType: 'string' | 'unresolved';
    location: SourceLocation;
    hints: DynamicImportHint[];
}

Example:

{
    dynamicImports: [{
        moduleName: 'lightning-foo',
        location: {...}, // Location of the entire import statement
        hints: [{
            rawValue: '"@salesforce/client/formFactor": "SMALL"',
            key: '@salesforce/client/formFactor',
            location: {...}, // Location of the hint comment
            value: 'SMALL',
        }]
    }]
}

Alternate Syntax Considered

Hints as a query attached to the component name

The hint syntax is a parameterized query appended to the component name:

export async function example1() {
    loadedModule = await import("lightning-foo*?*@salesforce/client/formFactor=Small");
}

Pros:

Cons:

Hints via proprietary LWC function

The hint syntax is expressed via a proprietary lwc function, where the first parameter is a component name and the second parameter is a query object - hintImport(cmpName, queryObj). At compile time, the function will be transformed into a regular import without a string. Note, the hint is consumed by metadata parser only, which is traversed outside of the compilation sequence on the original source. Therefore, it is safe to have a ‘throw-away’ wrapper that will be eliminated at compiler time.

import formFactor from '@salesforce/client/formFactor';
import permName from '@salesforce/customPermission/PermName';

export async function example1() {
    loadedModule = await hintImport(
        "lightning-foo",
        {
            query: "{0} IS Small AND {1} NOT true",
            fields: [{
                resource: formFactor,
                value: "Small",
            }, {
                resource: permName,
                value: true
            }]
        }
    );
}

Pros:

Cons:

Future work

References

undefined