Skip to content

Initial implementation. #1

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

Merged
merged 2 commits into from
Mar 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: Node.js test and Build
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't seem to actually run...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It will run once the workflow hits main. It's always the same issue when we bootstrap a project.
I linked a green build of the same commit in my sjrd/* repo: https://github.com/sjrd/vite-plugin-scalajs/actions/runs/4376164277


on:
push:
branches: [ main ]
pull_request:
branches: [ main ]

jobs:
build:
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
node-version: ['16']
java: ['8']

runs-on: ${{ matrix.os }}

steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- name: Set up JDK ${{ matrix.java }}
uses: actions/setup-java@v3
with:
distribution: 'adopt'
java-version: ${{ matrix.java }}
cache: 'sbt'

- name: Cache dependencies
uses: actions/cache@v3
with:
path: |
**/node_modules
key: ${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}

- name: Install dependencies
run: npm install
- name: Run sbt once in the test project to make sure sbt is downloaded
run: sbt projects
working-directory: ./test/testproject
- name: Perform unit test
run: npm test
- name: Build
run: npm run build
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/node_modules/
/dist/
target/
116 changes: 115 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,118 @@

A [Vite](https://vitejs.dev/) plugin for [Scala.js](https://www.scala-js.org/).

Work in progress.
## Usage

We assume that you have an existing Vite and Scala.js sbt project.
If not, [follow the accompanying tutorial](https://github.com/scala-js/scala-js-website/pull/590).

Install the plugin as a development dependency:

```shell
$ npm install -D @scala-js/vite-plugin-scalajs
```

Tell Vite to use the plugin in `vite.config.js`:

```javascript
import { defineConfig } from "vite";
import scalaJSPlugin from "@scala-js/vite-plugin-scalajs";

export default defineConfig({
plugins: [scalaJSPlugin()],
});
```

Finally, import the Scala.js output from a `.js` or `.ts` file with

```javascript
import 'scalajs:main.js';
```

which will execute the main method of the Scala.js application.

The sbt project must at least be configured to use ES modules.
For the best feedback loop with Vite, we recommend to emit small modules for application code.
If your application lives in the `my.app` package, configure the sbt project with the following settings:

```scala
scalaJSLinkerConfig ~= {
_.withModuleKind(ModuleKind.ESModule)
.withModuleSplitStyle(
ModuleSplitStyle.SmallModulesFor(List("my.app")))
},
```

## Configuration

The plugin supports the following configuration options:

```javascript
export default defineConfig({
plugins: [
scalaJSPlugin({
// path to the directory containing the sbt build
// default: '.'
cwd: '.',

// sbt project ID from within the sbt build to get fast/fullLinkJS from
// default: the root project of the sbt build
projectID: 'client',

// URI prefix of imports that this plugin catches (without the trailing ':')
// default: 'scalajs' (so the plugin recognizes URIs starting with 'scalajs:')
uriPrefix: 'scalajs',
}),
],
});
```

## Importing `@JSExportTopLevel` Scala.js members

`@JSExportTopLevel("foo")` members in the Scala.js code are exported from the modules that Scala.js generates.
They can be imported in `.js` and `.ts` files with the usual JavaScript `import` syntax.

For example, given the following Scala.js definition:

```scala
import scala.scalajs.js
import scala.scalajs.js.annotation._

@JSExportTopLevel("ScalaJSLib")
class ScalaJSLib extends js.Object {
def square(x: Double): Double = x * x
}
```

we can import and use it as

```javascript
import { ScalaJSLib } from 'scalajs:main.js';

const lib = new ScalaJSLib();
console.log(lib.square(5)); // 25
```

### Exports in other modules

By default, `@JSExportTopLevel("Foo")` exports `Foo` from the `main` module, which is why we import from `scalajs:main.js`.
We can also split the Scala.js exports into several modules.
For example,

```scala
import scala.scalajs.js
import scala.scalajs.js.annotation._

@JSExportTopLevel("ScalaJSLib", "library")
class ScalaJSLib extends js.Object {
def square(x: Double): Double = x * x
}
```

can be imported with

```javascript
import { ScalaJSLib } from 'scalajs:library.js';
```

The Scala.js documentation contains [more information about module splitting](https://www.scala-js.org/doc/project/module.html).
80 changes: 80 additions & 0 deletions index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { spawn, SpawnOptions } from "child_process";
import type { Plugin as VitePlugin } from "vite";

// Utility to invoke a given sbt task and fetch its output
function printSbtTask(task: string, cwd?: string): Promise<string> {
const args = ["--batch", "-no-colors", "-Dsbt.supershell=false", `print ${task}`];
const options: SpawnOptions = {
cwd: cwd,
stdio: ['ignore', 'pipe', 'inherit'],
};
const child = process.platform === 'win32'
? spawn("sbt.bat", args.map(x => `"${x}"`), {shell: true, ...options})
: spawn("sbt", args, options);

let fullOutput: string = '';

child.stdout!.setEncoding('utf-8');
child.stdout!.on('data', data => {
fullOutput += data;
process.stdout.write(data); // tee on my own stdout
});

return new Promise((resolve, reject) => {
child.on('error', err => {
reject(new Error(`sbt invocation for Scala.js compilation could not start. Is it installed?\n${err}`));
});
child.on('close', code => {
if (code !== 0)
reject(new Error(`sbt invocation for Scala.js compilation failed with exit code ${code}.`));
else
resolve(fullOutput.trimEnd().split('\n').at(-1)!);
});
});
}

export interface ScalaJSPluginOptions {
cwd?: string,
projectID?: string,
uriPrefix?: string,
}

export default function scalaJSPlugin(options: ScalaJSPluginOptions = {}): VitePlugin {
const { cwd, projectID, uriPrefix } = options;

const fullURIPrefix = uriPrefix ? (uriPrefix + ':') : 'scalajs:';

let isDev: boolean | undefined = undefined;
let scalaJSOutputDir: string | undefined = undefined;

return {
name: "scalajs:sbt-scalajs-plugin",

// Vite-specific
configResolved(resolvedConfig) {
isDev = resolvedConfig.mode === 'development';
},

// standard Rollup
async buildStart(options) {
if (isDev === undefined)
throw new Error("configResolved must be called before buildStart");

const task = isDev ? "fastLinkJSOutput" : "fullLinkJSOutput";
const projectTask = projectID ? `${projectID}/${task}` : task;
scalaJSOutputDir = await printSbtTask(projectTask, cwd);
},

// standard Rollup
resolveId(source, importer, options) {
if (scalaJSOutputDir === undefined)
throw new Error("buildStart must be called before resolveId");

if (!source.startsWith(fullURIPrefix))
return null;
const path = source.substring(fullURIPrefix.length);

return `${scalaJSOutputDir}/${path}`;
},
};
}
Loading