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

feat: task amanger #893

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
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
65 changes: 46 additions & 19 deletions sandpack-client/src/clients/node/client.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { invariant } from "outvariant";
import type { SandpackBundlerFiles } from "../..";
import { createError } from "../..";

import { tokenize, TokenType } from "./taskManager";

let counter = 0;

export function generateRandomId() {
Expand Down Expand Up @@ -45,13 +47,14 @@ export const fromBundlerFilesToFS = (
);
};

type Command = [string, string[], ShellCommandOptions];

/**
* Figure out which script it must run to start a server
*/
export const findStartScriptPackageJson = (
packageJson: string
): [string, string[], ShellCommandOptions] => {
export const findStartScriptPackageJson = (packageJson: string): Command[] => {
let scripts: Record<string, string> = {};
// TODO: support postinstall
const possibleKeys = ["dev", "start"];

try {
Expand All @@ -70,24 +73,48 @@ export const findStartScriptPackageJson = (
for (let index = 0; index < possibleKeys.length; index++) {
if (possibleKeys[index] in scripts) {
const script = possibleKeys[index];

const candidate = scripts[script];

const env = candidate
.match(/(\w+=\w+;)*\w+=\w+/g)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
?.reduce<any>((acc, curr) => {
const [key, value] = curr.split("=");
acc[key] = value;
return acc;
}, {});

const [command, ...args] = candidate
.replace(/(\w+=\w+;)*\w+=\w+/g, "")
.trim()
.split(" ");

return [command, args, { env }];
const listOfCommands: Command[] = [];
const commandTokens = tokenize(candidate);

let env = {};
let command = "";
let args: string[] = [];

commandTokens.forEach((token, tokenIndex) => {
const commandNotFoundYet = command === "";

if (token.type === TokenType.EnvVar) {
env = token.value;
}

if (token.type === TokenType.Command && commandNotFoundYet) {
command = token.value;
}

if (
token.type === TokenType.Argument ||
(!commandNotFoundYet && token.type === TokenType.Command)
) {
args.push(token.value);
}

if (
token.type === TokenType.AND ||
tokenIndex === commandTokens.length - 1
) {
const nodeboxCommand: Command = [command, args, { env }];
listOfCommands.push(nodeboxCommand);
Copy link
Member

Choose a reason for hiding this comment

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

This is a bit weird, to have a hardcoded dependency on AND in your data structure. But probably fine for testing


command = "";
args = [];
}

// TODO: support TokenType.OR, TokenType.PIPE
});

return listOfCommands;
}
}

Expand Down
64 changes: 47 additions & 17 deletions sandpack-client/src/clients/node/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import type {
FilesMap,
ShellProcess,
FSWatchEvent,
ShellInfo,
} from "@codesandbox/nodebox";
import type { ShellCommandOptions } from "@codesandbox/nodebox/build/modules/shell";

import type {
ClientOptions,
Expand Down Expand Up @@ -41,7 +41,6 @@ export class SandpackNode extends SandpackClient {
private emulatorIframe!: HTMLIFrameElement;
private emulator!: Nodebox;
private emulatorShellProcess: ShellProcess | undefined;
private emulatorCommand: [string, string[], ShellCommandOptions] | undefined;
private iframePreviewUrl: string | undefined;
private _modulesCache = new Map();
private messageChannelId = generateRandomId();
Expand Down Expand Up @@ -127,18 +126,10 @@ export class SandpackNode extends SandpackClient {
): Promise<{ id: string }> {
const packageJsonContent = readBuffer(files["/package.json"]);

this.emulatorCommand = findStartScriptPackageJson(packageJsonContent);
const emulatorCommand = findStartScriptPackageJson(packageJsonContent);
this.emulatorShellProcess = this.emulator.shell.create();

// Shell listeners
await this.emulatorShellProcess.on("exit", (exitCode) => {
this.dispatch({
type: "action",
action: "notification",
notificationType: "error",
title: createError(`Error: process.exit(${exitCode}) called.`),
});
});
let globalIndexScript = 0;

await this.emulatorShellProcess.on("progress", (data) => {
if (
Expand All @@ -148,10 +139,10 @@ export class SandpackNode extends SandpackClient {
this.dispatch({
type: "shell/progress",
data: {
...data,
state: "command_running",
command: [
this.emulatorCommand?.[0],
this.emulatorCommand?.[1].join(" "),
emulatorCommand?.[globalIndexScript][0],
emulatorCommand?.[globalIndexScript][1].join(" "),
].join(" "),
},
});
Expand All @@ -170,7 +161,46 @@ export class SandpackNode extends SandpackClient {
this.dispatch({ type: "stdout", payload: { data, type: "err" } });
});

return await this.emulatorShellProcess.runCommand(...this.emulatorCommand);
await this.emulatorShellProcess.on("exit", (exitCode) => {
if (globalIndexScript === emulatorCommand.length - 1) {
this.dispatch({
type: "action",
action: "notification",
notificationType: "error",
title: createError(`Error: process.exit(${exitCode}) called.`),
});
}
});

let shellId: ShellInfo;

for (
let indexScript = 0;
indexScript < emulatorCommand.length;
indexScript++
) {
globalIndexScript = indexScript;

shellId = await this.emulatorShellProcess.runCommand(
...emulatorCommand[indexScript]
);

await new Promise(async (resolve) => {
await this.emulatorShellProcess?.on("exit", async () => {
if (
this.emulatorShellProcess?.id &&
this.emulatorShellProcess?.state === "running"
) {
console.log(this.emulatorShellProcess);
await this.emulatorShellProcess.kill();
}

resolve(undefined);
});
});
}

return shellId;
}

private async createPreviewURLFromId(id: string): Promise<void> {
Expand Down Expand Up @@ -360,7 +390,7 @@ export class SandpackNode extends SandpackClient {
*/

public async restartShellProcess(): Promise<void> {
if (this.emulatorShellProcess && this.emulatorCommand) {
if (this.emulatorShellProcess) {
// 1. Set the loading state and clean the URL
this.dispatch({ type: "start", firstLoad: true });

Expand Down
124 changes: 124 additions & 0 deletions sandpack-client/src/clients/node/taskManager.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { tokenize } from "./taskManager";

describe(tokenize, () => {
it("parses environment variables", () => {
const input = tokenize("FOO=1 tsc -p");
const output = [
{ type: "EnvVar", value: { FOO: "1" } },
{ type: "Command", value: "tsc" },
{ type: "Argument", value: "-p" },
];

expect(input).toEqual(output);
});

it("parses multiples envs environment variables", () => {
const input = tokenize("FOO=1 BAZ=bla tsc -p");
const output = [
{ type: "EnvVar", value: { FOO: "1", BAZ: "bla" } },
{ type: "Command", value: "tsc" },
{ type: "Argument", value: "-p" },
];

expect(input).toEqual(output);
});

it("parses command and argument", () => {
const input = tokenize("tsc -p");
const output = [
{ type: "Command", value: "tsc" },
{ type: "Argument", value: "-p" },
];

expect(input).toEqual(output);
});

it("parses two commands", () => {
const input = tokenize("tsc && node");
const output = [
{ type: "Command", value: "tsc" },
{ type: "AND" },
{ type: "Command", value: "node" },
];

expect(input).toEqual(output);
});

it("parses two commands", () => {
const input = tokenize("tsc -p . && node index.js");
const output = [
{ type: "Command", value: "tsc" },
{ type: "Argument", value: "-p" },
{ type: "Command", value: "." },
{ type: "AND" },
{ type: "Command", value: "node" },
{ type: "Command", value: "index.js" },
];

expect(input).toEqual(output);
});

it("parses multiple arguments", () => {
const input = tokenize("tsc --foo -- --foo");
const output = [
{ type: "Command", value: "tsc" },
{ type: "Argument", value: "--foo" },
{ type: "Argument", value: "--" },
{ type: "Argument", value: "--foo" },
];

expect(input).toEqual(output);
});

it("parses pipe and string commands", () => {
const input = tokenize(`echo "Hello World" | wc -w`);
const output = [
{ type: "Command", value: "echo" },
{ type: "String", value: '"Hello World"' },
{ type: "PIPE" },
{ type: "Command", value: "wc" },
{ type: "Argument", value: "-w" },
];

expect(input).toEqual(output);
});

it("parses escaped characters", () => {
const input = tokenize(`echo "Hello | World" | wc -w`);
const output = [
{ type: "Command", value: "echo" },
{ type: "String", value: '"Hello | World"' },
{ type: "PIPE" },
{ type: "Command", value: "wc" },
{ type: "Argument", value: "-w" },
];

expect(input).toEqual(output);
});

it("parses escaped characters", () => {
const input = tokenize(`echo "Hello | World" | wc -w`);
const output = [
{ type: "Command", value: "echo" },
{ type: "String", value: '"Hello | World"' },
{ type: "PIPE" },
{ type: "Command", value: "wc" },
{ type: "Argument", value: "-w" },
];

expect(input).toEqual(output);
});

it("parses or", () => {
const input = tokenize(`echo "Hello | World" || wc -w`);
const output = [
{ type: "Command", value: "echo" },
{ type: "String", value: '"Hello | World"' },
{ type: "OR" },
{ type: "Command", value: "wc" },
{ type: "Argument", value: "-w" },
];

expect(input).toEqual(output);
});
});
Loading