Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ES Module import/export conventions to improve the ergonomics around including other script files or code blocks. #84

Open
wants to merge 5 commits into
base: master
Choose a base branch
from

Conversation

dljsjr
Copy link
Contributor

@dljsjr dljsjr commented Feb 5, 2025

Note

This change includes the commit from #83

Overview

This PR adds improvements to the ergonomics around importing/requiring scripts.

To provide a rough summary: it adds a series of script transforms that enables emulating ES Module import/export semantics. And it retains backwards compatibility with the existing way of doing things where all scripts get treated as a function body.

Motivation

I wanted to be able to work on Datacore scripts in VS Code, with full IDE support. It was easy enough to emit type declaration files and set up a tsconfig file, but the custom dc.require semantics were obviously not supported.

Description of Changes

New Setting: scriptRoots

The first two commits (after the Jest fix commit) introduces a new Setting for the plug-in, scriptRoots. It's a Set<string> that is serialized as an array string[]. It lets the user configure as many additional roots as they'd like; each root added to this set will be used to attempt to resolve dc.require paths until one succeeds.

The first commit adds support for serializing Sets to Arrays. The second commit builds the new setting on top of this.

There is custom UI for the setting:

image

The setting is backed by a FuzzyFolderSearchSugggest class for ease-of-use.

ES Module import/export semantics

The final commit is the implementation of the import/export mode. An outline of the major stuff:

Script Definitions refactored in to an interface

The code + language payload loaded from the script cache has been refactored in to an interface. It also now includes a reference to the TFile that the source code came from.

Script Evaluation Context Helpers

The creation of the script context object for the Function evaluator is now handled by a utility function.

I added this as an exported free function that takes the Local API instance as an argument to avoid having to add this function to the Public API available on the dc object.

Script Cache

Datastore

  • If the datastore fails to find an object for the given link path, it will enter a heuristic where it will assume it may have been passed a filename without an extension. This is to accomodate ES Module paths, which don't utilize file extensions. It will non-recursively enumerate the Files in the Folder for the module path, looking for a file that matches the module name or a file + extension that matches the module name.

Script Transpiling/Sucrase Behavior

  • The imports transform for all script types has been enabled. This transform converts ES Module import statements to require statements. It also transforms items modified by the export keyword to properties on an exports object that is injected via the Function context/environment.
  • An additional transformation pass is introduced, with two jobs:
    • It identifies relative paths, and resolves them relative to the file containing the script that is requiring the script being processed.
    • It converts all bare require's to an await dc.require form.

Loading Views from Datacore Blocks

Views can now export default their render target instead of return'ing it.

Backwards Compatibility

  • The Public/Local API has not been modified; only some underlying behavior on the datastore and script cache has changed. If none of these new features are utilized, the plugin should behave the exact same as it did before these changes.
  • dc.require is still the only way to include another script; these changes introduce transforms for ES Module imports/CommonJS requires that make them converge on dc.require.
  • Because of the above, everything still goes through the script cache and datastore.
  • The return form is given precedent over the export form for generically required scripts.
  • Likewise, the return form is given precedent over the export default form for render targets.

To Do

  • Update Documentation
  • New Tests

- Add explicit dev dependency on `ts-node`, which fixes an configuration
issue with TypeScript/`ts-jest` that would prevent tests from running.
- Update the DateTime JSON conversion tests:
  - Update the deserializer logic so that it applies the zone offset to the
    resulting DateTime object if there is one in the ISO String
  - Use the ISO strings for equality comparisons, since the `.equals()` method
    on the DateTime object does a field-wise deep comparison, and zones don't
    serialize/deserialize symmetrically.
Provides an overloaded implementation of `Plugin.saveData()` that serializes
a `Set<T>` in to an array `T[]`.

`loadData()` is not overloaded for conversion back to a `Set<T>`, since there's
ambiguity around whether something is intended to be an array or not.
Allows users to provide any amount of folders in the vault that can
be used for shortening the paths that need to be provided when `require`'ing
other script files or code block links.
@dljsjr dljsjr force-pushed the feat/improved-import-ergonomics branch from 2d8eb1d to 25ddd66 Compare February 5, 2025 06:50
@dljsjr
Copy link
Contributor Author

dljsjr commented Feb 5, 2025

Here's a little demo as well:

CleanShot.2025-02-05.at.00.48.07.mp4

This commit improves the ergonomics aroud importing/requiring scripts from
other files or blocks, while remaining backwards compatible with the existing
functionality in Datacore. The specifics of the changes are:

- Paths and wikilink-like strings passed to `dc.require()` will be checked
against all elements in the `scriptRoots` Setting if the path fails to resolve
against the vault root
- The `imports` transform is now enabled on the Sucrase `transform` operations. This
causes `import { foo } from "bar"` syntax to be transformed in to `const _foo = require("bar")` during
transpiling.
- The script transpilation pass will convert any `foo = require("bar")` statements in to the form
`foo = await dc.require("bar")` before evaluation. This covers both explicitly authored CommonJS require
syntax, as well as the output of the Sucrase transformation for ES Module imports.
- The script transpilation pass will resolve relative paths (e.g. `./foo`) against the script that called
`dc.require()`, allowing for relative imports to now be utilized. This happens as part of the same new
transform sa the one above that converts all `require` statements in to `dc.require` statements.
- The `imports` transform also allows for the use of the `export` keyword in scripts, which gets
transformed in to keys on an `exports` object.
  - The `exports` object is injected via the context used to construct the `Function` evaluator.
  - Scripts can now choose to either `return { ... }`, as is the current pattern, *or* they can use
    `export`.
  - Returned values take precedence over the exports object.
- Datacore code blocks that return a View to be rendered can now use `export default` by leveraging
the same effects introduced by the `imports` transform.
  - Similarly to scripts loaded with `dc.require()`, the `return function ...` form is still supported.
  - Also similarly to the above, the `return function ...` form is given precedence over the `export default` form.
@dljsjr dljsjr force-pushed the feat/improved-import-ergonomics branch from 25ddd66 to 23be4b0 Compare February 5, 2025 14:46
@blacksmithgu
Copy link
Owner

Very interesting - I didn't even know this was possible. I will review this soon.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants