version 0.1.3
std.module.format
Testing best way to support cjs and esm and how to structure project to enable that (see results.txt for more info)
(?<=(?:(?:import|require)\(|(?:import(?:\s|.)*from))\s*(?:\"|\'))(.*)(?=\"|\')
// test file for regex above
import("./module")
import('./module')
require("./module")
require('./module')
import * as Namespace from "./module"
import Namespace from "./module"
import { variableOne } from "./module"
import { variableOne, variableTwo } from "./module"
import {
variableOne,
variableTwo,
} from "./module"
import { variable as somethingElse } from "./module"
// code generated
const regex = /(?<=(?:(?:import|require)\(|(?:import(?:\s|.)*from))\s*(?:\"|\'))(.*)(?=\"|\')/gm;
// Alternative syntax using RegExp constructor
// const regex = new RegExp('(?<=(?:(?:import|require)\\(|(?:import(?:\\s|.)*from))\\s*(?:\\"|\\\'))(.*)(?=\\"|\\\')', 'gm')
const str = `import("./module")
import('./module')
require("./module")
require('./module')
import * as Namespace from "./module"
import Namespace from "./module"
import { variableOne } from "./module"
import { variableOne, variableTwo } from "./module"
import {
variableOne,
variableTwo,
} from "./module"
import { variable as somethingElse } from "./module"
`;
let m;
while ((m = regex.exec(str)) !== null) {
// This is necessary to avoid infinite loops with zero-width matches
if (m.index === regex.lastIndex) {
regex.lastIndex++;
}
// The result can be accessed through the `m`-variable.
m.forEach((match, groupIndex) => {
console.log(`Found match, group ${groupIndex}: ${match}`);
});
}
// import the entire object
import json from './example.json'
// import a root field as named exports - helps with tree-shaking!
import { field } from './example.json'
Use the Type-Only Imports and Export syntax to avoid potential problems like type-only imports being incorrectly bundled. for example:
import type { T } from 'only/types'
export type { T }
extends
importsNotUsedAsValues
preserveValueImports
jsxFactory
jsxFragmentFactory
["useDefineForClassFields": true]
["isolatedModules": true]
source, vitejs developer guide: vitejs.dev/guide/features.html#typescript-compiler-options
When CommonJS was the primary authoring format, the best practice was to export only one thing from a module using the module.exports = ... format. This aligned with the UNIX philosophy of "Do one thing well". The module would be consumed (const localName = require('the-module');) without having to know the internal structure.
Now, ESModules are the primary authoring format. They have numerous benefits, such as compile-time verification of exports, and standards-defined semantics. They have a similar mechanism known as "default exports", which allows for a consumer to import localName from 'the-module';. This is implicitly the same as import { default as localName } from 'the-module';
.
However, there are numerous reasons to avoid default exports, as documented by others before:
NOTE. https://humanwhocodes.com/blog/2019/01/stop-using-default-exports-javascript-module/
They add indirection by encouraging a developer to create local names for modules, increasing cognitive load and slowing down code comprehension: import TheListThing from 'not-a-list-thing';.
-
They thwart tools, such as IDEs, that can automatically rename and refactor code.
-
They promote typos and mistakes, as the imported member is completely up to the consuming developer to define.
-
They are ugly in CommonJS interop, as the default property must be manually specified by the consumer. This is often hidden by Babel's module interop.
-
They break re-exports due to name conflicts, forcing the developer to manually name each.
-
Using named exports helps prevent needing to rename symbols, which has myriad benefits. A few are:
IDE tools like "Find All References" and "Go To Definition" function Manual codebase searching ("grep", etc) is easier with a unique symbol
source: https://backstage.io/docs/architecture-decisions/adrs-adr004
We will make each exported symbol traceable through index files all the way down to the root of the package, src/index.ts. Each index file will only re-export from its own immediate directory children, and only index files will have re-exports. This gives a file tree similar to this:
index.ts
components/index.ts
/ComponentX/index.ts
/ComponentX.tsx
/SubComponentY.tsx
lib/index.ts
/UtilityX/index.ts
/UtilityX.ts
/helper.ts
To check whether for example SubComponentY is exported from the package, it should be possible to traverse the index files towards the root, starting at the adjacent one. If there is any index file that doesn't export the previous one, the symbol is not publicly exported. For example, if components/ComponentX/index.ts exports SubComponentY, but components/index.ts does not re-export ./ComponentX, one should be certain that SubComponentY is not exported outside the package. This rule would be broken if for example the root index.ts re-exports ./components/ComponentX
In addition, index files that are re-exporting other index files should always use wildcard form, that is:
// in components/index.ts
export * from './ComponentX';
Index files that are re-exporting symbols from non-index files should always enumerate all exports, that is:
// in components/ComponentX/index.ts
export { ComponentX } from './ComponentX';
export type { ComponentXProps } from './ComponentX';
Internal cross-directory imports are allowed from non-index modules to index modules, for example:
// in components/ComponentX/ComponentX.tsx
import { UtilityX } from '../../lib/UtilityX';
Imports that bypass an index file are discouraged, but may sometimes be necessary, for example:
// in components/ComponentX/ComponentX.tsx
import { helperFunc } from '../../lib/UtilityX/helper';
source https://devblogs.microsoft.com/typescript/announcing-typescript-4-5-beta/
For the last few years, Node.js has been working to support running ECMAScript modules (ESM). This has been a very difficult feature to support, since the foundation of the Node.js ecosystem is built on a different module system called CommonJS (CJS). Interoperating between the two brings large challenges, with many new features to juggle; however, support for ESM in Node.js is now largely implemented in Node.js 12 and later, and the dust has begun to settle.
That’s why TypeScript 4.5 brings two new module settings: node12 and nodenext.
{
"compilerOptions": {
"module": "nodenext",
}
}
These new modes bring a few high-level features which we’ll explore here.
type in package.json and New Extensions Node.js supports a new setting in package.json called type. "type" can be set to either "module" or "commonjs".
{
"name": "my-package",
"type": "module",
"//": "...",
"dependencies": {
}
}
Node.js supports two extensions to help with this: .mjs
and .cjs
. .mjs
files are always ES modules, and .cjs
files are always CommonJS modules, and there’s no way to override these.
In turn, TypeScript supports two new source file extensions: .mts and .cts
. When TypeScript emits these to JavaScript files, it will emit them to .mjs
and .cjs
respectively.
Furthermore, TypeScript also supports two new declaration file extensions: .d.mts and .d.cts. When TypeScript generates declaration files for .mts
and .cts
, their corresponding extensions will be .d.mts
and .d.cts
.
Using these extensions is entirely optional, but will often be useful even if you choose not to use them as part of your primary workflow.
source: TypeScript Deep Dive
source https://basarat.gitbook.io/typescript/main-1/defaultisbad
Consider you have a file foo.ts with the following contents:
Consider you have a file foo.ts
with the following contents:
You would import it (in bar.ts
) using ES6 syntax as follows:
There are a few maintainability concerns here:
-
If you refactor
Foo
infoo.ts
it will not rename it inbar.ts
. -
If you end up needing to export more stuff from
foo.ts
(which is what many of your files will have) then you have to juggle the import syntax.
For this reason I recommend simple exports + destructured import. E.g. foo.ts
:
import { Foo } from "./foo";
Below I also present a few more reasons.
Discoverability is very poor for default exports. You cannot explore a module with intellisense to see if it has a default export or not.
With export default you get nothing here (maybe it does export default / maybe it doesn't ¯\_(ツ)_/¯
):
import /\* here \*/ from 'something';
Without export default you get a nice intellisense here:
import { /\* here \*/ } from 'something';
Irrespective of if you know about the exports, you even autocomplete at this import {/*here*/} from "./foo";
cursor location. Gives your developers a bit of wrist relief.
With default
there is horrible experience for commonJS users who have to const {default} = require('module/foo');
instead of const {Foo} = require('module/foo')
. You will most likely want to rename the default
export to something else when you import it.
You don't get typos like one dev doing import Foo from "./foo";
and another doing import foo from "./foo";
Auto import quickfix works better. You use Foo
and auto import will write down import { Foo } from "./foo";
cause its a well-defined name exported from a module. Some tools out there will try to magic read and infer a name for a default export but magic is flaky.
Re-exporting is common for the root index
file in npm packages, and forces you to name the default export manually e.g. export { default as Foo } from "./foo";
(with default) vs. export * from "./foo"
(with named exports).
Default exports expose themselves badly named as default
in dynamic import
s e.g.
const HighCharts \= await import('https://code.highcharts.com/js/es-modules/masters/highcharts.src.js');
HighCharts.default.chart('container', { ... }); // Notice \`.default\`
Much nicer with named exports:
const {HighCharts} \= await import('https://code.highcharts.com/js/es-modules/masters/highcharts.src.js');
HighCharts.chart('container', { ... }); // Notice \`.default\`
esModuleInterop, https://www.typescriptlang.org/tsconfig#esModuleInterop
By default (with esModuleInterop false or not set) TypeScript treats CommonJS/AMD/UMD modules similar to ES6 modules. In doing this, there are two parts in particular which turned out to be flawed assumptions:
a namespace import like import * as moment from "moment"
acts the same as const moment = require("moment")
a default import like import moment from "moment"
acts the same as const moment = require("moment").default
This mis-match causes these two issues:
-
the ES6 modules spec states that a namespace import (import * as x) can only be an object, by having TypeScript treating it the same as = require("x") then TypeScript allowed for the import to be treated as a function and be callable. That’s not valid according to the spec.
-
while accurate to the ES6 modules spec, most libraries with CommonJS/AMD/UMD modules didn’t conform as strictly as TypeScript’s implementation.
The namespace import import * as fs from "fs"
only accounts for properties which are owned (basically properties set on the object and not via the prototype chain) on the imported object. If the module you’re importing defines its API using inherited properties, you need to use the default import form (import fs from "fs"
), or disable esModuleInterop.
Can be one statement for function / class e.g.
export default function foo() {
}
Can be one statement for non named / type annotated objects e.g.:
export default {
notAFunction: 'Yeah, I am not a function or a class',
soWhat: 'The export is now *removed* from the declaration'
};
But needs two statements otherwise:
// If you need to name it (here \`foo\`) for local use OR need to annotate type (here \`Foo\`)
notAFunction: 'Yeah, I am not a function or a class',
soWhat: 'The export is now \*removed\* from the declaration'
};
export default foo;
source, https://reactjs.org/docs/code-splitting.html#named-exports
React.lazy currently only supports default exports. If the module you want to import uses named exports, you can create an intermediate module that reexports it as the default. This ensures that tree shaking keeps working and that you don’t pull in unused components.
// ManyComponents.js
export const MyComponent = /* ... */;
export const MyUnusedComponent = /* ... */;
// MyComponent.js
export { MyComponent as default } from "./ManyComponents.js";
// MyApp.js
import React, { lazy } from 'react';
const MyComponent = lazy(() => import("./MyComponent.js"));
We will not be removing {}
from the rule defaults, as it is an unsafe type because it doesn't work how people think it works, and in most cases it allows weakly typed code.
This is the exact reason that the rule bans the {}
type.
It's a common misconception that the {}
type is the same as the {}
value.
But as the rule's message states: this is not the case!
The type {}
doesn't mean "any empty object", it means "any non-nullish value".
This is obviously a huge type safety hole!
For example - the following code is completely type-check valid, even though it might make no sense to be:
interface AAA {
aaa: {};
}
const x: AAA = { aaa: true };
It's also important to note that empty interfaces behave in exactly the same way as the {}
type!, which is why we have the no-empty-interface
lint rule.
Unfortunately, there's no type in TS that means "an empty object".
There are the following options for you:
You can use a type similar to this type.
type EmptyObject = Record<string, never>; // or {[k: string]: never}
const a: EmptyObject = { a: 1 }; // expect error
const b: EmptyObject = 1; // expect error
const c: EmptyObject = () => {}; // expect error
const d: EmptyObject = null; // expect error
const e: EmptyObject = undefined; // expect error
const f: EmptyObject = {}; // NO ERROR - as expected
This is technically safe in this instance, because under the hood the {}
type is passed into an intersection type (see the note at the end of this comment for why this is safe).
However, there is no way for us to statically analyse and know that this is a safe usage. To work around this, consider reconfiguring the lint rule to match your repository's coding style. You can use the following config to allow it:
{
"rules": {
"@typescript-eslint/ban-types": [
"error",
{
"extendDefaults": true,
"types": {
"{}": false
}
}
]
}
}
Consider using an eslint overrides config to limit the scope of this change to just react component files, to help ensure you're keeping your codebase as safe as possible.
As an aside - it's worth noting that {}
is a very weird anomaly in TypeScript, because there is just one case where it actually does mean something akin to "empty object"; in an intersection type.
type T1 = { a: 1 } & {};
const t11: T1 = { a: 1 };
const t12: T1 = true; // expected error
type T2 = true & {};
const t21: T2 = true;
const t22: T2 = false; // expected error
const t23: T2 = {}; // expected error
In this usage, the type essentially is a no-op, and means nothing at all.
In all other usages, including in the extends
clause of a generic type parameter, it means "anything non-nullish".
Originally posted by @bradzacher in typescript-eslint/typescript-eslint#2063 (comment)
/**
* Helper to avoid writing `Record<string, unknown>` everywhere you would usually use "object".
*
* @example (data: GenericObject) => void
* @example variables: GenericObject<string>
*
* @see https://github.com/typescript-eslint/typescript-eslint/issues/2063#issuecomment-632833366
*/
export type GenericObject<T = unknown> = Record<string, T>;
NOTE. Although the JavaScript specification speaks in terms of "scripts" versus "modules", in general this specification speaks in terms of classic scripts versus module scripts, since both of them use the script element.
The JavaScript specification defines a syntax for modules, as well as some host-agnostic parts of their processing model. This specification defines the rest of their processing model: how the module system is bootstrapped, via the [script](https://html.spec.whatwg.org/multipage/scripting.html#the-script-element)
element with [type](https://html.spec.whatwg.org/multipage/scripting.html#attr-script-type)
attribute set to "module
", and how modules are fetched, resolved, and executed. [JAVASCRIPT]
Although the JavaScript specification speaks in terms of "scripts" versus "modules", in general this specification speaks in terms of classic scripts versus module scripts, since both of them use the [script](https://html.spec.whatwg.org/multipage/scripting.html#the-script-element)
element.
modulePromise = [import(specifier)](https://tc39.es/ecma262/#sec-import-calls)
Returns a promise for the module namespace object for the module script identified by specifier. This allows dynamic importing of module scripts at runtime, instead of statically using the import
statement form. The specifier will be resolved relative to the active script's base URL.
The returned promise will be rejected if an invalid specifier is given, or if a failure is encountered while fetching or evaluating the resulting module graph.
This syntax can be used inside both classic and module scripts. It thus provides a bridge into the module-script world, from the classic-script world.
url = [import.meta](https://tc39.es/ecma262/#sec-meta-properties) .url
Returns the active module script's base URL.
This syntax can only be used inside module scripts.
A module map is a map keyed by tuples consisting of a URL record and a string. The URL record is the request URL at which the module was fetched, and the string indicates the type of the module (e.g. "javascript
"). The module map's values are either a module script, null (used to represent failed fetches), or a placeholder value "fetching
". Module maps are used to ensure that imported module scripts are only fetched, parsed, and evaluated once per [Document](https://html.spec.whatwg.org/multipage/dom.html#document)
or worker.
Since module maps are keyed by (URL, module type), the following code will create three separate entries in the module map, since it results in three different (URL, module type) tuples (all with "javascript
" type):
import "https://example.com/module.mjs";
import "https://example.com/module.mjs#map-buster";
import "https://example.com/module.mjs?debug=true";
That is, URL queries and fragments can be varied to create distinct entries in the module map; they are not ignored. Thus, three separate fetches and three separate module evaluations will be performed.
In contrast, the following code would only create a single entry in the module map, since after applying the URL parser to these inputs, the resulting URL records are equal:
import "https://example.com/module2.mjs";
import "https:example.com/module2.mjs";
import "https://///example.com\\module2.mjs";
import "https://example.com/foo/../module2.mjs";
So in this second example, only one fetch and one module evaluation will occur.
Note that this behavior is the same as how shared workers are keyed by their parsed constructor url.
Since module type is also part of the module map key, the following code will create two separate entries in the module map (the type is "javascript
" for the first, and "css
" for the second):
<script type=module>
import "https://example.com/module";
</script>
<script type=module>
import "https://example.com/module" assert { type: "css" };
</script>
This can result in two separate fetches and two separate module evaluations being performed. This is a willful violation of a constraint recommended (but not required) by the import assertions specification stating that each call to HostResolveImportedModule with the same (referencingScriptOrModule, moduleRequest.[[Specifier]]) pair must return the same Module Record. [JSIMPORTASSERTIONS]
In practice, due to the as-yet-unspecified memory cache (see issue #6110) the resource may only be fetched once in WebKit and Blink-based browsers. Additionally, as long as all module types are mutually exclusive, the module type check in fetch a single module script will fail for at least one of the imports, so at most one module evaluation will occur.
The purpose of including the type in the module map key is so that an import with the wrong type assertion does not prevent a different import of the same specifier but with the correct type from succeeding.
JavaScript module scripts are the default import type when importing from another JavaScript module; that is, when an import
statement lacks a type
import assertion the imported module script's type will be JavaScript. Attempting to import a JavaScript resource using an import
statement with a type
import assertion will fail:
<script type="module">
// All of the following will fail, assuming that the imported .mjs files are served with a
// JavaScript MIME type. JavaScript module scripts are the default and cannot be imported with
// any import type assertion.
import foo from "./foo.mjs" assert { type: "javascript" };
import foo2 from "./foo2.mjs" assert { type: "js" };
import foo3 from "./foo3.mjs" assert { type: "" };
await import("./foo4.mjs", { assert: { type: null } });
await import("./foo5.mjs", { assert: { type: undefined } });
</script>
To resolve a module specifier given a URL base URL and a string specifier, perform the following steps. It will return either a URL record or failure.
-
Apply the URL parser to specifier. If the result is not failure, return the result.
-
If specifier does not start with the character U+002F SOLIDUS (
/
), the two-character sequence U+002E FULL STOP, U+002F SOLIDUS (./
), or the three-character sequence U+002E FULL STOP, U+002E FULL STOP, U+002F SOLIDUS (../
), return failure.This restriction is in place so that in the future we can allow custom module loaders to give special meaning to "bare" import specifiers, like
import "jquery"
orimport "web/crypto"
. For now any such imports will fail, instead of being treated as relative URLs. -
Return the result of applying the URL parser to specifier with base URL.
The following are valid module specifiers according to the above algorithm:
https://example.com/apples.mjs
http:example.com\pears.js
(becomeshttp://example.com/pears.js
as step 1 parses with no base URL)//example.com/bananas
./strawberries.mjs.cgi
../lychees
/limes.jsx
data:text/javascript,export default 'grapes';
blob:https://whatwg.org/d0360e2f-caee-469f-9a2f-87d5b0456f6f
The following are valid module specifiers according to the above algorithm, but will invariably cause failures when they are fetched:
javascript:export default 'artichokes';
data:text/plain,export default 'kale';
about:legumes
wss://example.com/celery
The following are not valid module specifiers according to the above algorithm:
https://eggplant:b/c
pumpkins.js
.tomato
..zucchini.mjs
.\yam.es
[source, https://github.com/Microsoft/TypeScript/wiki/Using-the-Compiler-API#customizing-module-resolution]
You can override the standard way the compiler resolves modules by implementing optional method: CompilerHost.resolveModuleNames:
CompilerHost.resolveModuleNames(moduleNames: string[], containingFile: string): string[].
The method is given a list of module names in a file, and is expected to return an array of size moduleNames.length, each element of the array stores either:
an instance of ResolvedModule with non-empty property resolvedFileName - resolution for corresponding name from moduleNames array or undefined if module name cannot be resolved.
You can invoke the standard module resolution process via calling resolveModuleName:
resolveModuleName(moduleName: string, containingFile: string, options: CompilerOptions, moduleResolutionHost: ModuleResolutionHost): ResolvedModuleNameWithFallbackLocations.
This function returns an object that stores result of module resolution (value of resolvedModule property) as well as list of file names that were considered candidates before making current decision.
import * as ts from "typescript";
import * as path from "path";
function createCompilerHost(options: ts.CompilerOptions, moduleSearchLocations: string[]): ts.CompilerHost {
return {
getSourceFile,
getDefaultLibFileName: () => "lib.d.ts",
writeFile: (fileName, content) => ts.sys.writeFile(fileName, content),
getCurrentDirectory: () => ts.sys.getCurrentDirectory(),
getDirectories: path => ts.sys.getDirectories(path),
getCanonicalFileName: fileName =>
ts.sys.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase(),
getNewLine: () => ts.sys.newLine,
useCaseSensitiveFileNames: () => ts.sys.useCaseSensitiveFileNames,
fileExists,
readFile,
resolveModuleNames
};
function fileExists(fileName: string): boolean {
return ts.sys.fileExists(fileName);
}
function readFile(fileName: string): string | undefined {
return ts.sys.readFile(fileName);
}
function getSourceFile(fileName: string, languageVersion: ts.ScriptTarget, onError?: (message: string) => void) {
const sourceText = ts.sys.readFile(fileName);
return sourceText !== undefined
? ts.createSourceFile(fileName, sourceText, languageVersion)
: undefined;
}
function resolveModuleNames(
moduleNames: string[],
containingFile: string
): ts.ResolvedModule[] {
const resolvedModules: ts.ResolvedModule[] = [];
for (const moduleName of moduleNames) {
// try to use standard resolution
let result = ts.resolveModuleName(moduleName, containingFile, options, {
fileExists,
readFile
});
if (result.resolvedModule) {
resolvedModules.push(result.resolvedModule);
} else {
// check fallback locations, for simplicity assume that module at location
// should be represented by '.d.ts' file
for (const location of moduleSearchLocations) {
const modulePath = path.join(location, moduleName + ".d.ts");
if (fileExists(modulePath)) {
resolvedModules.push({ resolvedFileName: modulePath });
}
}
}
}
return resolvedModules;
}
}
function compile(sourceFiles: string[], moduleSearchLocations: string[]): void {
const options: ts.CompilerOptions = {
module: ts.ModuleKind.AMD,
target: ts.ScriptTarget.ES5
};
const host = createCompilerHost(options, moduleSearchLocations);
const program = ts.createProgram(sourceFiles, options, host);
/// do something with program...
}
source, https://www.npmjs.com/package/@typescript-eslint/parser
Default: undefined
This option allows you to provide a custom module resolution. The value should point to a JS file that default exports (export default
, or module.exports =
, or export =
) a file with the following interface:
interface ModuleResolver {
version: 1;
resolveModuleNames(
moduleNames: string[],
containingFile: string,
reusedNames: string[] | undefined,
redirectedReference: ts.ResolvedProjectReference | undefined,
options: ts.CompilerOptions,
): (ts.ResolvedModule | undefined)[];
}
Refer to the TypeScript Wiki for an example on how to write the resolveModuleNames function.
Note that if you pass custom programs via options.programs this option will not have any effect over them (you can simply add the custom resolution on them directly).
CC-SA-2.5