Skip to content

Commit

Permalink
Add support for multiple subprojects
Browse files Browse the repository at this point in the history
  • Loading branch information
v6ak committed Oct 21, 2023
1 parent 2f94cf3 commit cfd4924
Show file tree
Hide file tree
Showing 2 changed files with 157 additions and 14 deletions.
83 changes: 69 additions & 14 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ 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}`];
function printSbtTasks(tasks: Array<string>, cwd?: string): Promise<Array<string>> {
const args = ["--batch", "-no-colors", "-Dsbt.supershell=false", ...tasks.map(task => `print ${task}`)];
const options: SpawnOptions = {
cwd: cwd,
stdio: ['ignore', 'pipe', 'inherit'],
Expand All @@ -28,24 +28,68 @@ function printSbtTask(task: string, cwd?: string): Promise<string> {
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)!);
resolve(fullOutput.trimEnd().split('\n').slice(-tasks.length));
});
});
}

export interface Subproject {
projectID: string | null,
uriPrefix: string,
}

export interface ScalaJSPluginOptions {
cwd?: string,
projectID?: string,
uriPrefix?: string,
subprojects?: Array<Subproject>,
}

export default function scalaJSPlugin(options: ScalaJSPluginOptions = {}): VitePlugin {
const { cwd, projectID, uriPrefix } = options;
function extractSubprojects(options: ScalaJSPluginOptions): Array<Subproject> {
if (options.subprojects) {
if (options.projectID || options.uriPrefix) {
throw new Error("If you specify subprojects, you cannot specify projectID / uriPrefix")
}
return options.subprojects;
} else {
return [
{
projectID: options.projectID || null,
uriPrefix: options.uriPrefix || 'scalajs',
}
];
}
}

function mapBy<T, K>(a: Array<T>, f: ((item: T) => K), itemName: string): Map<K, T> {
const out = new Map<K, T>();
a.forEach((item) => {
const key: K = f(item);
if (out.has(key)) {
throw Error("Duplicate " + itemName + " " + key + ".");
} else {
out.set(key, item);
}
});
return out;
}

function zip<T, U>(a: Array<T>, b: Array<U>): Array<[T, U]> {
if (a.length != b.length) {
throw new Error("length mismatch: " + a.length + " ~= " + b.length)
}
return a.map((item, i) => [item, b[i]]);
}

const fullURIPrefix = uriPrefix ? (uriPrefix + ':') : 'scalajs:';
export default function scalaJSPlugin(options: ScalaJSPluginOptions = {}): VitePlugin {
const { cwd } = options;
const subprojects = extractSubprojects(options);
// This also checks for duplicates
const spByProjectID = mapBy(subprojects, (p) => p.projectID, "projectID")
const spByUriPrefix = mapBy(subprojects, (p) => p.uriPrefix, "uriPrefix")

let isDev: boolean | undefined = undefined;
let scalaJSOutputDir: string | undefined = undefined;
let scalaJSOutputDirs: Map<string, string> | undefined = undefined;

return {
name: "scalajs:sbt-scalajs-plugin",
Expand All @@ -61,20 +105,31 @@ export default function scalaJSPlugin(options: ScalaJSPluginOptions = {}): ViteP
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);
const projectTasks = subprojects.map( p =>
p.projectID ? `${p.projectID}/${task}` : task
);
const scalaJSOutputDirsArray = await printSbtTasks(projectTasks, cwd);
scalaJSOutputDirs = new Map(zip(
subprojects.map(p => p.uriPrefix),
scalaJSOutputDirsArray
))
},

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

if (!source.startsWith(fullURIPrefix))
const colonPos = source.indexOf(':');
if (colonPos == -1) {
return null;
}
const subprojectUriPrefix = source.substr(0, colonPos);
const outDir = scalaJSOutputDirs.get(subprojectUriPrefix)
if (outDir == null)
return null;
const path = source.substring(fullURIPrefix.length);
const path = source.substring(subprojectUriPrefix.length + 1);

return `${scalaJSOutputDir}/${path}`;
return `${outDir}/${path}`;
},
};
}
88 changes: 88 additions & 0 deletions test/plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,23 @@ function normalizeSlashes(path: string | null): string | null {
return path === null ? null : path.replace(/\\/g, '/');
}

function testBothModes(
testFunction: (d: string, func: () => Promise<void>, options: TestOptions) => void,
description: string,
f: (mode: string, suffix: string) => Promise<void>,
testOptions: TestOptions,
) {
testFunction ||= it
const MODES = [["production", MODE_PRODUCTION, "opt"], ["development", MODE_DEVELOPMENT, "fastopt"]]
MODES.forEach( ([modeName, mode, suffix]) => {
testFunction(
description + "(" + modeName + ")",
async () => await f(mode, suffix),
testOptions,
)
})
}

const MODE_DEVELOPMENT = 'development';
const MODE_PRODUCTION = 'production';

Expand Down Expand Up @@ -102,6 +119,77 @@ describe("scalaJSPlugin", () => {
.toBeNull();
}, testOptions);

testBothModes(it, "works with a project with subprojects", async (mode, suffix) => {
const [plugin, fakePluginContext] = setup({
subprojects: [
{
projectID: "otherProject",
uriPrefix: "foo",
},
{
projectID: null,
uriPrefix: "bar",
},
]
});

await plugin.configResolved.call(undefined, { mode: mode });
await plugin.buildStart.call(fakePluginContext, {});

expect(normalizeSlashes(await plugin.resolveId.call(fakePluginContext, 'foo:main.js')))
.toContain('/testproject/other-project/target/scala-3.2.2/otherproject-' + suffix + '/main.js');
expect(normalizeSlashes(await plugin.resolveId.call(fakePluginContext, 'bar:main.js')))
.toContain('/testproject/target/scala-3.2.2/testproject-' + suffix + '/main.js');

expect(await plugin.resolveId.call(fakePluginContext, 'scalajs/main.js'))
.toBeNull();
}, testOptions);

it.fails("with duplicate projectID", async () => {
setup({
subprojects: [
{
projectID: "otherProject",
uriPrefix: "foo",
},
{
projectID: "otherProject",
uriPrefix: "bar",
},
]
});
});

it.fails("with duplicate uriPrefix", async () => {
setup({
subprojects: [
{
projectID: "otherProject",
uriPrefix: "foo",
},
{
projectID: null,
uriPrefix: "foo",
},
]
});
});

it.fails("when both projectID and subproojects are specified", async () => {
setup({
projectID: "xxx",
subprojects: []
});
});

it.fails("when both uriPrefix and subproojects are specified", async () => {
setup({
uriPrefix: "xxx",
subprojects: []
});
});


it("does not work with a project that does not link", async () => {
const [plugin, fakePluginContext] = setup({
projectID: "invalidProject",
Expand Down

0 comments on commit cfd4924

Please sign in to comment.