mdrefine 0.2.0-rc.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +577 -0
- package/CODE_OF_CONDUCT.md +30 -0
- package/CONTRIBUTING.md +189 -16
- package/README.md +100 -154
- package/SECURITY.md +15 -13
- package/config.schema.json +91 -0
- package/dist/cli/diff.d.ts +10 -0
- package/dist/cli/diff.js +247 -0
- package/dist/cli/diff.js.map +1 -0
- package/dist/cli/main.js +270 -150
- package/dist/cli/main.js.map +1 -1
- package/dist/config/load.js +67 -6
- package/dist/config/load.js.map +1 -1
- package/dist/config/problems.d.ts +7 -0
- package/dist/config/problems.js +208 -0
- package/dist/config/problems.js.map +1 -0
- package/dist/config/resolve.d.ts +42 -109
- package/dist/config/resolve.js +193 -52
- package/dist/config/resolve.js.map +1 -1
- package/dist/core/directives.d.ts +7 -0
- package/dist/core/directives.js +25 -13
- package/dist/core/directives.js.map +1 -1
- package/dist/core/engine.d.ts +42 -4
- package/dist/core/engine.js +518 -79
- package/dist/core/engine.js.map +1 -1
- package/dist/core/frozen.d.ts +6 -0
- package/dist/core/frozen.js +15 -0
- package/dist/core/frozen.js.map +1 -0
- package/dist/core/scope.d.ts +30 -0
- package/dist/core/scope.js +122 -0
- package/dist/core/scope.js.map +1 -0
- package/dist/core/types.d.ts +202 -23
- package/dist/core/types.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js.map +1 -1
- package/dist/node.d.ts +3 -0
- package/dist/node.js +4 -0
- package/dist/node.js.map +1 -0
- package/dist/rules/blocks.d.ts +34 -0
- package/dist/rules/blocks.js +550 -0
- package/dist/rules/blocks.js.map +1 -0
- package/dist/rules/content.d.ts +2 -0
- package/dist/rules/content.js +85 -0
- package/dist/rules/content.js.map +1 -0
- package/dist/rules/dialects.js +10 -6
- package/dist/rules/dialects.js.map +1 -1
- package/dist/rules/links.js +302 -6
- package/dist/rules/links.js.map +1 -1
- package/dist/rules/lists.d.ts +2 -0
- package/dist/rules/lists.js +924 -0
- package/dist/rules/lists.js.map +1 -0
- package/dist/rules/structure.js +127 -1
- package/dist/rules/structure.js.map +1 -1
- package/dist/rules/style.d.ts +8 -2
- package/dist/rules/style.js +1137 -165
- package/dist/rules/style.js.map +1 -1
- package/dist/syntax/block-identifier.d.ts +18 -0
- package/dist/syntax/block-identifier.js +24 -0
- package/dist/syntax/block-identifier.js.map +1 -0
- package/dist/syntax/obsidian.d.ts +29 -11
- package/dist/syntax/obsidian.js +9 -10
- package/dist/syntax/obsidian.js.map +1 -1
- package/dist/syntax/parse.d.ts +87 -3
- package/dist/syntax/parse.js +247 -19
- package/dist/syntax/parse.js.map +1 -1
- package/dist/workspace/access.d.ts +12 -3
- package/dist/workspace/access.js +33 -5
- package/dist/workspace/access.js.map +1 -1
- package/dist/workspace/discovery.d.ts +85 -0
- package/dist/workspace/discovery.js +155 -0
- package/dist/workspace/discovery.js.map +1 -0
- package/dist/workspace/encoding.d.ts +24 -0
- package/dist/workspace/encoding.js +113 -0
- package/dist/workspace/encoding.js.map +1 -0
- package/dist/workspace/files.d.ts +92 -6
- package/dist/workspace/files.js +715 -59
- package/dist/workspace/files.js.map +1 -1
- package/dist/workspace/git.d.ts +54 -0
- package/dist/workspace/git.js +198 -0
- package/dist/workspace/git.js.map +1 -0
- package/dist/workspace/index.d.ts +58 -3
- package/dist/workspace/index.js +136 -37
- package/dist/workspace/index.js.map +1 -1
- package/dist/workspace/selection.d.ts +2 -2
- package/dist/workspace/selection.js +3 -3
- package/dist/workspace/selection.js.map +1 -1
- package/dist/workspace/slug.js +8 -1
- package/dist/workspace/slug.js.map +1 -1
- package/dist/workspace/vault.js +3 -1
- package/dist/workspace/vault.js.map +1 -1
- package/docs/api.md +239 -0
- package/docs/benchmarks.md +140 -200
- package/docs/changelog.md +1 -0
- package/docs/cli.md +281 -89
- package/docs/configuration.md +193 -480
- package/docs/design.md +163 -284
- package/docs/dialects.md +169 -0
- package/docs/examples.md +151 -109
- package/docs/index.md +21 -37
- package/docs/integrations.md +144 -0
- package/docs/obsidian.md +91 -115
- package/docs/plugins.md +147 -212
- package/docs/quick-start.md +68 -74
- package/docs/rules.md +1237 -0
- package/docs/versions.md +56 -0
- package/examples/README.md +17 -14
- package/examples/automation/summarize-report.mjs +5 -2
- package/examples/library/lint-folder.mjs +5 -4
- package/examples/library/lint-workspace.mjs +32 -0
- package/examples/plugin/notes/release-checklist.md +2 -2
- package/examples/plugin/rules.mjs +17 -23
- package/examples/workspaces/obsidian-vault/after/mdtools.config.jsonc +2 -2
- package/examples/workspaces/obsidian-vault/before/mdtools.config.jsonc +2 -2
- package/examples/workspaces/repository-docs/after/README.md +2 -2
- package/examples/workspaces/repository-docs/after/mdtools.config.jsonc +0 -2
- package/examples/workspaces/repository-docs/before/README.md +2 -2
- package/examples/workspaces/repository-docs/before/docs/guide.md +7 -7
- package/examples/workspaces/repository-docs/before/docs/reference.md +3 -1
- package/examples/workspaces/repository-docs/before/mdtools.config.jsonc +0 -2
- package/package.json +38 -12
- package/docs/contributing.md +0 -218
- package/docs/releases/0.1.0-beta.1.md +0 -39
- package/docs/releases/0.1.0-beta.2.md +0 -83
- package/docs/releases/0.1.0-beta.3.md +0 -61
- package/docs/releases/0.1.0-beta.4.md +0 -50
- package/docs/releases/0.1.0-rc.1.md +0 -99
- package/docs/releases/0.2.0-rc.1.md +0 -141
- package/docs/releases/0.2.0-rc.2.md +0 -225
- package/docs/releases.md +0 -144
- package/docs/testing.md +0 -93
package/docs/api.md
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
# Library API
|
|
2
|
+
|
|
3
|
+
The package's main entry, `mdrefine`, lints and formats strings and never
|
|
4
|
+
touches the filesystem. A second entry, `mdrefine/node`, finds and reads files
|
|
5
|
+
the way the CLI does. Both are TypeScript-typed ES modules, and their exports
|
|
6
|
+
are part of the interface that [version numbers](versions.md) describe.
|
|
7
|
+
|
|
8
|
+
## Lint and format
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
import { createWorkspace, format, lint } from "mdrefine";
|
|
12
|
+
|
|
13
|
+
const files = {
|
|
14
|
+
"note.md": "A *short* note with [a link](target.md).\n",
|
|
15
|
+
"target.md": "# Target\n",
|
|
16
|
+
};
|
|
17
|
+
const options = {
|
|
18
|
+
path: "note.md",
|
|
19
|
+
config: { extends: ["recommended", "github"] },
|
|
20
|
+
workspace: createWorkspace(files, { dialect: "github" }),
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const diagnostics = lint(files["note.md"], options);
|
|
24
|
+
const result = format(files["note.md"], options);
|
|
25
|
+
// result: { output, changed, diagnostics }
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`lint(source, options)` returns the diagnostics, with the fields of the [JSON
|
|
29
|
+
report](cli.md#json-report). `format(source, options)` returns `output`, the
|
|
30
|
+
formatted text; `changed`, whether it differs; `diagnostics`, the findings that
|
|
31
|
+
remain in the output; and `scope` with [`ranges`](#part-of-a-document). The
|
|
32
|
+
edits of a rule that would change the document's meaning are left out and
|
|
33
|
+
reported as `engine/unsafe-format`, as the [safety
|
|
34
|
+
model](design.md#safety-model) describes, and a rule that fails is reported as
|
|
35
|
+
[`engine/rule-error`](plugins.md#rules). An invalid configuration, an unknown
|
|
36
|
+
rule, invalid rule options, or a plugin rule of the wrong shape throw an error.
|
|
37
|
+
|
|
38
|
+
| Option | Meaning |
|
|
39
|
+
| ---------------- | ------------------------------------------------------------------------------------------------------------------ |
|
|
40
|
+
| `path` | The document's path relative to the workspace root, with forward slashes. It selects overrides and resolves links. |
|
|
41
|
+
| `config` | The object a configuration file would hold. `plugins` there are not loaded; pass plugin objects instead. |
|
|
42
|
+
| `plugins` | [Plugin](plugins.md) objects. |
|
|
43
|
+
| `workspace` | The files that links can reach; see [workspaces](#workspaces). |
|
|
44
|
+
| `defaultDialect` | The dialect to assume when the configuration names none; see [the dialect](#the-dialect). |
|
|
45
|
+
| `ranges` | Limit the work to [part of a document](#part-of-a-document). |
|
|
46
|
+
|
|
47
|
+
Without `config`, the `recommended` preset applies. Positions count UTF-16 code
|
|
48
|
+
units in the text as given. A byte order mark at its start (U+FEFF, which some
|
|
49
|
+
editors write) is not part of what rules see, but it counts in the offsets that
|
|
50
|
+
`lint` and `format` report, so that their edits apply to the text as given, and
|
|
51
|
+
`format` keeps it in the output.
|
|
52
|
+
|
|
53
|
+
## The dialect
|
|
54
|
+
|
|
55
|
+
A document is read in the dialect that its configuration names, through
|
|
56
|
+
`dialect`, a preset, or a matching override. When none does, `lint`, `format`,
|
|
57
|
+
and `resolveConfig` assume `defaultDialect`, or `github` without it;
|
|
58
|
+
`defaultDialect` accepts a dialect or an alias and never overrides a dialect
|
|
59
|
+
that the configuration names. The library reads no files, so it cannot recognize
|
|
60
|
+
a vault by itself. The CLI passes `obsidian` for a document with a `.obsidian`
|
|
61
|
+
directory in its own directory or one above it, and gives the workspace that
|
|
62
|
+
vault's `strictLineBreaks` setting; a host for vaults should do the same:
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
// The host found `.obsidian/` at the root of the directory it read `files` from.
|
|
66
|
+
const options: ProcessOptions = { path: "Note.md", config, defaultDialect: "obsidian" };
|
|
67
|
+
const { dialect } = resolveConfig(options);
|
|
68
|
+
const workspace = createWorkspace(files, { dialect, strictLineBreaks });
|
|
69
|
+
const result = format(files["Note.md"], { ...options, workspace });
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`resolveConfig(options)` returns the settings in effect for a document:
|
|
73
|
+
`dialect`, `rules`, `ignore`, and `resolve`. It takes the `path`, `config`,
|
|
74
|
+
`plugins`, and `defaultDialect` of `lint` and `format`, so their options serve
|
|
75
|
+
as they are. The prereleases took these four as separate arguments; such a call
|
|
76
|
+
throws an error that says so.
|
|
77
|
+
|
|
78
|
+
## Workspaces
|
|
79
|
+
|
|
80
|
+
`createWorkspace(files, options)` builds the workspace that link checks, link
|
|
81
|
+
rewriting, and Obsidian wrapping need; without one, none of them happen. `files`
|
|
82
|
+
maps each path, relative to the workspace root with forward slashes, to one of:
|
|
83
|
+
|
|
84
|
+
- the text of a Markdown document, whose headings are link targets when its name
|
|
85
|
+
ends in `.md` or `.markdown`, or `.md` for Obsidian links;
|
|
86
|
+
- `null` for a file that is not Markdown, such as an image, so that links to it
|
|
87
|
+
resolve;
|
|
88
|
+
- a synchronous loader, `() => string`, which is called only when a link's
|
|
89
|
+
fragment needs the document. The workspace then keeps the document's anchors,
|
|
90
|
+
not its text. The anchors of Obsidian, of GitHub and CommonMark, and of
|
|
91
|
+
Forgejo and Gitea differ, so a loader is called once more when the same target
|
|
92
|
+
is checked for another of these groups. A loader must return the same text
|
|
93
|
+
each time, so create a new workspace when files change. A loader that throws
|
|
94
|
+
an error whose `code` is `EACCES`, `EPERM`, or `EBUSY`, or
|
|
95
|
+
`ERR_ENCODING_INVALID_ENCODED_DATA`, which a `TextDecoder` created with
|
|
96
|
+
`fatal: true` throws for a file that is not valid in its encoding, makes a
|
|
97
|
+
link to a heading in that file resolve as `unreadable`, while a link to the
|
|
98
|
+
file itself still resolves; any other error propagates.
|
|
99
|
+
|
|
100
|
+
| `WorkspaceOptions` | Meaning |
|
|
101
|
+
| ------------------ | --------------------------------------------------------------------------------------------------------------------------- |
|
|
102
|
+
| `dialect` | The dialect that link targets are parsed in; `github` when omitted. Give the dialect of the documents that link into them. |
|
|
103
|
+
| `strictLineBreaks` | `true` when the vault's "Strict line breaks" setting is verified on. Obsidian notes are wrapped only then. |
|
|
104
|
+
| `directories` | Existing directories that hold no files; the others are known from `files`. A link to a directory resolves as `directory`. |
|
|
105
|
+
| `unreadable` | Directories whose contents the host could not read. A link that may lead into one resolves as `unreadable`. |
|
|
106
|
+
| `vaults` | Directories below the root that are Obsidian vaults. An Obsidian link in one of their notes reaches only the vault's files. |
|
|
107
|
+
| `rootInVault` | When the root is a directory inside a vault: its path from the vault's root, such as `Projects/Notes`. |
|
|
108
|
+
| `lookup` | `(path, folded) => result`: what is at a path that `files` and `directories` do not hold; see below. |
|
|
109
|
+
|
|
110
|
+
`lookup` lets a host leave files out of `files` and still have links to them
|
|
111
|
+
checked. It is called once per path, with `folded` true for an Obsidian link,
|
|
112
|
+
which matches without regard to case, and returns
|
|
113
|
+
`{ type: "file", path, source }`, `{ type: "directory" }`,
|
|
114
|
+
`{ type: "outside", through }` for a path that leads out of the workspace root
|
|
115
|
+
through the link `through`, `{ type: "unreadable", at }`, or `undefined` when
|
|
116
|
+
nothing is there. An Obsidian search by note name does not call it.
|
|
117
|
+
|
|
118
|
+
The CLI passes the directories it skipped as `unreadable`, the vaults it found
|
|
119
|
+
as `vaults`, and `rootInVault` when it finds the vault above the root; [Obsidian
|
|
120
|
+
vaults](obsidian.md#vaults-inside-a-larger-workspace) describes the effects. A
|
|
121
|
+
rule asks the workspace where a link leads; [resolve links in a
|
|
122
|
+
rule](plugins.md#resolve-links-in-a-rule) describes the result. A host can also
|
|
123
|
+
implement the `Workspace` interface itself.
|
|
124
|
+
|
|
125
|
+
## Part of a document
|
|
126
|
+
|
|
127
|
+
An editor's "format selection", or an agent that changed one paragraph, can
|
|
128
|
+
limit `lint` and `format` to parts of a document with `ranges`: a list of
|
|
129
|
+
`{ start, end }` offsets into the source as given, with `end` exclusive. Each
|
|
130
|
+
range is widened to the whole blocks that it touches, such as a paragraph, a
|
|
131
|
+
heading, a table, or a code block, also inside lists and block quotes, where a
|
|
132
|
+
range inside one item selects only that item's paragraph. A selected block takes
|
|
133
|
+
in its whole lines and the line breaks around it, so that the blank lines next
|
|
134
|
+
to it can change. An empty range, a cursor position, selects the block on whose
|
|
135
|
+
lines it is; a range in a run of blank lines selects nothing.
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
const source = "Legacy *word* stays.\n\nChange *this paragraph*.\n";
|
|
139
|
+
const start = source.indexOf("Change");
|
|
140
|
+
const result = format(source, { config, ranges: [{ start, end: start }] });
|
|
141
|
+
// result.output: "Legacy *word* stays.\n\nChange _this paragraph_.\n"
|
|
142
|
+
// result.scope: [{ start: 21, end: 47 }]: the paragraph and its line breaks
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
The whole document is still parsed and checked, so link definitions, suppression
|
|
146
|
+
comments, and the semantic check work as without ranges. `lint` returns the
|
|
147
|
+
diagnostics that start in the selected parts. `format` applies only the edits
|
|
148
|
+
that lie wholly inside them and leaves the rest byte for byte as it was; an edit
|
|
149
|
+
that reaches outside is not applied, but its diagnostic is reported, and so is
|
|
150
|
+
every `engine/` diagnostic. `scope` gives the selected parts in the output's
|
|
151
|
+
positions, so it can be passed as `ranges` again. Without `ranges` the whole
|
|
152
|
+
document is processed, and an empty list processes nothing. A range outside the
|
|
153
|
+
source, with `start` after `end`, or with offsets that are not integers is an
|
|
154
|
+
error. The CLI has no option for ranges.
|
|
155
|
+
|
|
156
|
+
## Files on disk
|
|
157
|
+
|
|
158
|
+
The `mdrefine/node` entry exports `discoverWorkspace`, the CLI's own discovery:
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
import { lint } from "mdrefine";
|
|
162
|
+
import { discoverWorkspace } from "mdrefine/node";
|
|
163
|
+
|
|
164
|
+
const discovery = await discoverWorkspace({ root: "/path/to/documents", config });
|
|
165
|
+
for (const name of discovery.selected)
|
|
166
|
+
console.log(name, lint(discovery.read(name), discovery.options(name)));
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
It finds the files below `root` and selects documents as the CLI does: it honors
|
|
170
|
+
Git's ignore rules and the configuration's `ignore` and `resolve` settings,
|
|
171
|
+
leaves out nested repositories, symbolic links, and what the system does not let
|
|
172
|
+
it read, and recognizes each document's vault. It reads no configuration file
|
|
173
|
+
and writes nothing: pass the configuration as `config`, plugin objects as
|
|
174
|
+
`plugins`, and write results yourself.
|
|
175
|
+
|
|
176
|
+
- `inputs` lists files, directories, and patterns to select, absolute or
|
|
177
|
+
relative to `root`, not to the current directory, as the CLI [selects
|
|
178
|
+
them](cli.md#which-files-are-processed): a file is selected whatever its name,
|
|
179
|
+
and an ignored file or directory is an entry of `skipped` with the code
|
|
180
|
+
`IGNORED`. Omitted, every document is selected; an empty list selects none.
|
|
181
|
+
- `exclude` lists files and directories to leave out, as paths, not patterns.
|
|
182
|
+
- `documents` gives text for paths relative to `root`, such as an editor's
|
|
183
|
+
unsaved buffer. Those documents are selected, and the workspace reads that
|
|
184
|
+
text in place of a file, also where no file exists. One whose path ignore
|
|
185
|
+
rules leave out is not selected and is an entry of `skipped`, as a named file
|
|
186
|
+
is.
|
|
187
|
+
|
|
188
|
+
The result has `root`, the root's real path; `selected`, the documents as sorted
|
|
189
|
+
forward-slash paths relative to it; and `skipped`, with the entries of the JSON
|
|
190
|
+
report's `skipped` list. `read(name)` returns a selected document's text, read
|
|
191
|
+
when first needed and then kept, so one result is a snapshot for one run. An
|
|
192
|
+
error with the code `EACCES`, `EPERM`, or `EBUSY` means the file could not be
|
|
193
|
+
read, and one with the code `ERR_ENCODING_INVALID_ENCODED_DATA` that it is not
|
|
194
|
+
[UTF-8 text](cli.md#files-that-cannot-be-read-or-written), as its message says,
|
|
195
|
+
such as `Not UTF-8 text: notes/old.md (byte 0xE9 on line 1 is not valid UTF-8)`.
|
|
196
|
+
`options(name)` returns what the CLI passes to `lint` and `format` for the
|
|
197
|
+
document. Both throw for a name that is not selected. A root or input that
|
|
198
|
+
cannot be used is an error in the CLI's words, such as
|
|
199
|
+
`Input does not exist: notes/absent.md`.
|
|
200
|
+
|
|
201
|
+
## Other exports
|
|
202
|
+
|
|
203
|
+
- `parse(source, dialect, path, plugins)` returns a `Document`: `source`,
|
|
204
|
+
`path`, `dialect`, and `tree`, an
|
|
205
|
+
[mdast](https://github.com/syntax-tree/mdast) syntax tree. `document.source`
|
|
206
|
+
starts after any byte order mark, and `range(node)` gives a node's
|
|
207
|
+
`[start, end]` offsets into it. `textContent(node)` gives a node's text. An
|
|
208
|
+
alias such as `codeberg` reads the document in its dialect, which
|
|
209
|
+
`document.dialect` holds, and an unknown dialect is an error. A document whose
|
|
210
|
+
front matter would end inside a code block, an HTML block, or display math is
|
|
211
|
+
read without front matter, as the [safety model](design.md#safety-model)
|
|
212
|
+
describes. In the Obsidian dialect, the tree holds `wikiLink`,
|
|
213
|
+
`obsidianComment`, and `obsidianHighlight` nodes, typed as `WikiLink`,
|
|
214
|
+
`ObsidianComment`, and `ObsidianHighlight` and registered with mdast's node
|
|
215
|
+
types.
|
|
216
|
+
- `resolveConfig`, described above, and `validateConfig(value)`, which throws
|
|
217
|
+
when a value is not a valid configuration. `configSchema` is the
|
|
218
|
+
configuration's [JSON Schema](configuration.md#json-schema), which the package
|
|
219
|
+
also ships as `mdrefine/config.schema.json`. `presets` holds the built-in
|
|
220
|
+
presets.
|
|
221
|
+
- `dialects` lists the dialect names, `dialectAliases` maps aliases such as
|
|
222
|
+
`codeberg` to them, and `canonicalDialect(name)` resolves an alias.
|
|
223
|
+
- `builtInRules` holds the built-in rule objects, and `ruleRegistry(plugins)`
|
|
224
|
+
returns a new table that adds the rules of plugins under their IDs.
|
|
225
|
+
- `applyEdits(source, edits)` applies `{ start, end, text }` edits, which must
|
|
226
|
+
not overlap. `semanticFingerprint(document, workspace)` is the comparison that
|
|
227
|
+
`format` uses to refuse a change of meaning.
|
|
228
|
+
|
|
229
|
+
`presets`, `builtInRules`, `dialects`, `dialectAliases`, and `configSchema` are
|
|
230
|
+
shared by every caller in the process, so they are frozen and typed read-only;
|
|
231
|
+
copy what you want to change. Each export has a doc comment, and the TypeScript
|
|
232
|
+
types of these values are exported too, among them `Config`, `ProcessOptions`,
|
|
233
|
+
`FormatResult`, `Diagnostic`, `Plugin`, `Rule`, `JsonSchema`, `Workspace`,
|
|
234
|
+
`WorkspaceOptions`, and `LinkResolution`; `mdrefine/node` exports
|
|
235
|
+
`DiscoverOptions`, `WorkspaceDiscovery`, and `Skipped`. The type declarations
|
|
236
|
+
depend on `@types/mdast`, which the package installs.
|
|
237
|
+
|
|
238
|
+
Both entries are ES modules. A CommonJS module can load them with `require()`,
|
|
239
|
+
which the supported Node.js versions allow for ES modules.
|