diff --git a/impl/core/pom.xml b/impl/core/pom.xml
index 0ee45fe9..13e8d364 100644
--- a/impl/core/pom.xml
+++ b/impl/core/pom.xml
@@ -24,5 +24,14 @@
de.huxhorn.sulky
de.huxhorn.sulky.ulid
+
+ org.graalvm.polyglot
+ polyglot
+
+
+ org.graalvm.polyglot
+ js
+ pom
+
diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/executors/RunScriptExecutor.java b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/RunScriptExecutor.java
new file mode 100644
index 00000000..ffb174e9
--- /dev/null
+++ b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/RunScriptExecutor.java
@@ -0,0 +1,259 @@
+/*
+ * Copyright 2020-Present The Serverless Workflow Specification Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.serverlessworkflow.impl.executors;
+
+import io.serverlessworkflow.api.types.RunScript;
+import io.serverlessworkflow.api.types.RunTaskConfiguration;
+import io.serverlessworkflow.api.types.Script;
+import io.serverlessworkflow.api.types.ScriptUnion;
+import io.serverlessworkflow.impl.TaskContext;
+import io.serverlessworkflow.impl.WorkflowApplication;
+import io.serverlessworkflow.impl.WorkflowContext;
+import io.serverlessworkflow.impl.WorkflowDefinition;
+import io.serverlessworkflow.impl.WorkflowError;
+import io.serverlessworkflow.impl.WorkflowException;
+import io.serverlessworkflow.impl.WorkflowModel;
+import io.serverlessworkflow.impl.WorkflowModelFactory;
+import io.serverlessworkflow.impl.WorkflowUtils;
+import io.serverlessworkflow.impl.expressions.ExpressionUtils;
+import io.serverlessworkflow.impl.resources.ResourceLoaderUtils;
+import java.io.ByteArrayOutputStream;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutorService;
+import org.graalvm.polyglot.Context;
+import org.graalvm.polyglot.PolyglotException;
+import org.graalvm.polyglot.Value;
+
+public class RunScriptExecutor implements RunnableTask {
+
+ enum ScriptLanguage {
+ JS("js"),
+ PYTHON("python");
+
+ private final String lang;
+
+ ScriptLanguage(String lang) {
+ this.lang = lang;
+ }
+
+ public String getLang() {
+ return lang;
+ }
+
+ public static boolean isSupported(String lang) {
+ for (ScriptLanguage l : ScriptLanguage.values()) {
+ if (l.getLang().equalsIgnoreCase(lang)) {
+ return true;
+ }
+ }
+ return false;
+ }
+ }
+
+ @FunctionalInterface
+ private interface ScriptExecutor {
+ WorkflowModel apply(WorkflowContext workflowContext, TaskContext taskContext);
+ }
+
+ private ScriptExecutor scriptExecutor;
+
+ @Override
+ public void init(RunScript taskConfiguration, WorkflowDefinition definition) {
+ ScriptUnion scriptUnion = taskConfiguration.getScript();
+ Script script = scriptUnion.get();
+ String language = script.getLanguage();
+ boolean isAwait = taskConfiguration.isAwait();
+
+ WorkflowApplication application = definition.application();
+ if (language == null || !ScriptLanguage.isSupported(language)) {
+ throw new IllegalArgumentException(
+ "Unsupported script language: "
+ + language
+ + ". Supported languages are: "
+ + Arrays.toString(
+ Arrays.stream(ScriptLanguage.values()).map(ScriptLanguage::getLang).toArray()));
+ }
+
+ String lowerLang = language.toLowerCase();
+
+ scriptExecutor =
+ (workflowContext, taskContext) -> {
+ String source;
+ if (scriptUnion.getInlineScript() != null) {
+ source = scriptUnion.getInlineScript().getCode();
+ } else if (scriptUnion.getExternalScript() == null) {
+ throw new WorkflowException(
+ WorkflowError.runtime(
+ taskContext, new IllegalStateException("No script source defined."))
+ .build());
+ } else {
+ source =
+ definition
+ .resourceLoader()
+ .load(
+ scriptUnion.getExternalScript().getSource(),
+ ResourceLoaderUtils::readString,
+ workflowContext,
+ taskContext,
+ taskContext.input());
+ }
+
+ Map envs = new HashMap<>();
+ if (script.getEnvironment() != null) {
+ for (Map.Entry entry :
+ script.getEnvironment().getAdditionalProperties().entrySet()) {
+ String value =
+ ExpressionUtils.isExpr(entry.getValue())
+ ? WorkflowUtils.buildStringResolver(
+ application,
+ entry.getValue().toString(),
+ taskContext.input().asJavaObject())
+ .apply(workflowContext, taskContext, taskContext.input())
+ : entry.getValue().toString();
+ envs.put(entry.getKey(), value);
+ }
+ }
+
+ Map args = new HashMap<>();
+ if (script.getArguments() != null) {
+ for (Map.Entry entry :
+ script.getArguments().getAdditionalProperties().entrySet()) {
+ String value =
+ ExpressionUtils.isExpr(entry.getValue())
+ ? WorkflowUtils.buildStringResolver(
+ application,
+ entry.getValue().toString(),
+ taskContext.input().asJavaObject())
+ .apply(workflowContext, taskContext, taskContext.input())
+ : entry.getValue().toString();
+ args.put(entry.getKey(), value);
+ }
+ }
+
+ ByteArrayOutputStream stderr = new ByteArrayOutputStream();
+ ByteArrayOutputStream stdout = new ByteArrayOutputStream();
+ try (Context context =
+ Context.newBuilder(lowerLang)
+ .err(stderr)
+ .out(stdout)
+ .environment(envs)
+ .useSystemExit(true)
+ .option(
+ "engine.WarnInterpreterOnly",
+ "false") // disabling it due to warnings in stderr
+ .build()) {
+
+ ExecutorService executorService = application.executorService();
+
+ args.forEach(
+ (arg, val) -> {
+ context.getBindings(lowerLang).putMember(arg, val);
+ });
+
+ // configure process.env for js environment variables
+ if (lowerLang.equalsIgnoreCase(ScriptLanguage.JS.lang)) {
+ configureProcessEnv(context, envs);
+ }
+
+ if (!isAwait) {
+ executorService.submit(
+ () -> {
+ context.eval(lowerLang, source);
+ });
+ return application.modelFactory().fromAny(taskContext.input());
+ }
+
+ context.eval(lowerLang, source);
+
+ WorkflowModelFactory modelFactory = application.modelFactory();
+
+ // GraalVM does not provide exit code, assuming 0 for successful execution
+ int statusCode = 0;
+
+ return switch (taskConfiguration.getReturn()) {
+ case ALL ->
+ modelFactory.fromAny(
+ new ProcessResult(statusCode, stdout.toString(), stderr.toString()));
+ case NONE -> modelFactory.fromNull();
+ case CODE -> modelFactory.from(statusCode);
+ case STDOUT -> modelFactory.from(stdout.toString().trim());
+ case STDERR -> modelFactory.from(stderr.toString().trim());
+ };
+ } catch (PolyglotException e) {
+ if (e.getExitStatus() != 0 || e.isSyntaxError()) {
+ throw new WorkflowException(WorkflowError.runtime(taskContext, e).build());
+ } else {
+ WorkflowModelFactory modelFactory = definition.application().modelFactory();
+ return switch (taskConfiguration.getReturn()) {
+ case ALL ->
+ modelFactory.fromAny(
+ new ProcessResult(
+ e.getExitStatus(), stdout.toString().trim(), buildStderr(e, stderr)));
+ case NONE -> modelFactory.fromNull();
+ case CODE -> modelFactory.from(e.getExitStatus());
+ case STDOUT -> modelFactory.from(stdout.toString().trim());
+ case STDERR -> modelFactory.from(buildStderr(e, stderr));
+ };
+ }
+ }
+ };
+ }
+
+ /**
+ * Gets the stderr message from the PolyglotException or the stderr stream.
+ *
+ * @param e the {@link PolyglotException} thrown during script execution
+ * @param stderr the stderr stream
+ * @return the stderr message
+ */
+ private String buildStderr(PolyglotException e, ByteArrayOutputStream stderr) {
+ String err = stderr.toString();
+ return err.isBlank() ? e.getMessage() : err.trim();
+ }
+
+ /**
+ * Configures the process.env object in the JavaScript context with the provided environment
+ * variables.
+ *
+ * @param context the GraalVM context
+ * @param envs the environment variables to set
+ */
+ private void configureProcessEnv(Context context, Map envs) {
+ String js = ScriptLanguage.JS.lang;
+ Value bindings = context.getBindings(js);
+ Value process = context.eval(js, "({ env: {} })");
+
+ for (var entry : envs.entrySet()) {
+ process.getMember("env").putMember(entry.getKey(), entry.getValue());
+ }
+ bindings.putMember("process", process);
+ }
+
+ @Override
+ public CompletableFuture apply(
+ WorkflowContext workflowContext, TaskContext taskContext, WorkflowModel input) {
+ return CompletableFuture.supplyAsync(
+ () -> this.scriptExecutor.apply(workflowContext, taskContext));
+ }
+
+ @Override
+ public boolean accept(Class extends RunTaskConfiguration> clazz) {
+ return RunScript.class.equals(clazz);
+ }
+}
diff --git a/impl/core/src/main/resources/META-INF/services/io.serverlessworkflow.impl.executors.RunnableTask b/impl/core/src/main/resources/META-INF/services/io.serverlessworkflow.impl.executors.RunnableTask
index 7d2be4f9..ea1bb37e 100644
--- a/impl/core/src/main/resources/META-INF/services/io.serverlessworkflow.impl.executors.RunnableTask
+++ b/impl/core/src/main/resources/META-INF/services/io.serverlessworkflow.impl.executors.RunnableTask
@@ -1,2 +1,3 @@
io.serverlessworkflow.impl.executors.RunWorkflowExecutor
-io.serverlessworkflow.impl.executors.RunShellExecutor
\ No newline at end of file
+io.serverlessworkflow.impl.executors.RunShellExecutor
+io.serverlessworkflow.impl.executors.RunScriptExecutor
\ No newline at end of file
diff --git a/impl/pom.xml b/impl/pom.xml
index 00d900f9..f75b4d6a 100644
--- a/impl/pom.xml
+++ b/impl/pom.xml
@@ -14,6 +14,7 @@
4.0.0
1.6.0
3.1.11
+ 23.1.1
@@ -92,6 +93,17 @@
serverlessworkflow-impl-openapi
${project.version}
+
+ org.graalvm.polyglot
+ polyglot
+ ${version.org.graalvm.plyglot}
+
+
+ org.graalvm.polyglot
+ js
+ ${version.org.graalvm.plyglot}
+ pom
+
net.thisptr
jackson-jq
diff --git a/impl/test/src/test/java/io/serverlessworkflow/impl/test/RunScriptJavaScriptTest.java b/impl/test/src/test/java/io/serverlessworkflow/impl/test/RunScriptJavaScriptTest.java
new file mode 100644
index 00000000..24832ebf
--- /dev/null
+++ b/impl/test/src/test/java/io/serverlessworkflow/impl/test/RunScriptJavaScriptTest.java
@@ -0,0 +1,186 @@
+/*
+ * Copyright 2020-Present The Serverless Workflow Specification Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.serverlessworkflow.impl.test;
+
+import io.serverlessworkflow.api.WorkflowReader;
+import io.serverlessworkflow.api.types.Workflow;
+import io.serverlessworkflow.impl.WorkflowApplication;
+import io.serverlessworkflow.impl.WorkflowModel;
+import io.serverlessworkflow.impl.executors.ProcessResult;
+import java.io.IOException;
+import java.util.Map;
+import okhttp3.mockwebserver.MockResponse;
+import okhttp3.mockwebserver.MockWebServer;
+import org.assertj.core.api.Assertions;
+import org.assertj.core.api.SoftAssertions;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+public class RunScriptJavaScriptTest {
+
+ private MockWebServer fileServer;
+
+ @BeforeEach
+ void setUp() throws IOException {
+ fileServer = new MockWebServer();
+ fileServer.start(8886);
+ }
+
+ @AfterEach
+ void tearDown() throws IOException {
+ fileServer.shutdown();
+ }
+
+ @Test
+ void testConsoleLog() throws IOException {
+ Workflow workflow =
+ WorkflowReader.readWorkflowFromClasspath("workflows-samples/run-script/console-log.yaml");
+ try (WorkflowApplication appl = WorkflowApplication.builder().build()) {
+ WorkflowModel model = appl.workflowDefinition(workflow).instance(Map.of()).start().join();
+
+ SoftAssertions.assertSoftly(
+ softly -> {
+ softly.assertThat(model.asText()).isPresent();
+ softly.assertThat(model.asText().get()).isEqualTo("hello from script");
+ });
+ }
+ }
+
+ @Test
+ void testConsoleLogWithArgs() throws IOException {
+ Workflow workflow =
+ WorkflowReader.readWorkflowFromClasspath(
+ "workflows-samples/run-script/console-log-args.yaml");
+ try (WorkflowApplication appl = WorkflowApplication.builder().build()) {
+ WorkflowModel model = appl.workflowDefinition(workflow).instance(Map.of()).start().join();
+
+ SoftAssertions.assertSoftly(
+ softly -> {
+ softly.assertThat(model.asText()).isPresent();
+ softly.assertThat(model.asText().get()).isEqualTo("Hello, world!");
+ });
+ }
+ }
+
+ @Test
+ void testConsoleLogWithEnvs() throws IOException {
+ Workflow workflow =
+ WorkflowReader.readWorkflowFromClasspath(
+ "workflows-samples/run-script/console-log-envs.yaml");
+ try (WorkflowApplication appl = WorkflowApplication.builder().build()) {
+ WorkflowModel model = appl.workflowDefinition(workflow).instance(Map.of()).start().join();
+
+ SoftAssertions.assertSoftly(
+ softly -> {
+ softly.assertThat(model.asText()).isPresent();
+ softly
+ .assertThat(model.asText().get())
+ .isEqualTo("Running JavaScript code using Serverless Workflow!");
+ });
+ }
+ }
+
+ @Test
+ void testConsoleLogWithExternalSource() throws IOException {
+ Workflow workflow =
+ WorkflowReader.readWorkflowFromClasspath(
+ "workflows-samples/run-script/console-log-external-source.yaml");
+
+ fileServer.enqueue(
+ new MockResponse()
+ .setBody(
+ """
+ console.log("hello from script");
+ """)
+ .setHeader("Content-Type", "application/javascript")
+ .setResponseCode(200));
+
+ try (WorkflowApplication appl = WorkflowApplication.builder().build()) {
+ WorkflowModel model = appl.workflowDefinition(workflow).instance(Map.of()).start().join();
+
+ SoftAssertions.assertSoftly(
+ softly -> {
+ softly.assertThat(model.asText()).isPresent();
+ softly.assertThat(model.asText().get()).isEqualTo("hello from script");
+ });
+ }
+ }
+
+ @Test
+ void testFunctionThrowingError() throws IOException {
+ Workflow workflow =
+ WorkflowReader.readWorkflowFromClasspath(
+ "workflows-samples/run-script/function-with-throw.yaml");
+ try (WorkflowApplication appl = WorkflowApplication.builder().build()) {
+ WorkflowModel model = appl.workflowDefinition(workflow).instance(Map.of()).start().join();
+
+ SoftAssertions.assertSoftly(
+ softly -> {
+ softly.assertThat(model.asText()).isPresent();
+ softly.assertThat(model.asText().get()).isEqualTo("Error: This is a test error");
+ });
+ }
+ }
+
+ @Test
+ void testFunctionThrowingErrorAndReturnAll() throws IOException {
+ Workflow workflow =
+ WorkflowReader.readWorkflowFromClasspath(
+ "workflows-samples/run-script/function-with-throw-all.yaml");
+ try (WorkflowApplication appl = WorkflowApplication.builder().build()) {
+ WorkflowModel model = appl.workflowDefinition(workflow).instance(Map.of()).start().join();
+
+ SoftAssertions.assertSoftly(
+ softly -> {
+ ProcessResult r = model.as(ProcessResult.class).orElseThrow();
+ softly.assertThat(r.stderr()).isEqualTo("Error: This is a test error");
+ softly.assertThat(r.stdout()).isEqualTo("logged before the 'throw' statement");
+ softly.assertThat(r.code()).isEqualTo(0);
+ });
+ }
+ }
+
+ @Test
+ void testFunctionWithSyntaxError() throws IOException {
+ Workflow workflow =
+ WorkflowReader.readWorkflowFromClasspath(
+ "workflows-samples/run-script/function-with-syntax-error.yaml");
+ try (WorkflowApplication appl = WorkflowApplication.builder().build()) {
+ Assertions.assertThatThrownBy(
+ () -> {
+ appl.workflowDefinition(workflow).instance(Map.of()).start().join();
+ })
+ .hasMessageContaining("SyntaxError");
+ }
+ }
+
+ @Test
+ void testConsoleLogNotAwaiting() throws IOException {
+ Workflow workflow =
+ WorkflowReader.readWorkflowFromClasspath(
+ "workflows-samples/run-script/console-log-not-awaiting.yaml");
+ try (WorkflowApplication appl = WorkflowApplication.builder().build()) {
+ Map input = Map.of("hello", "world");
+
+ WorkflowModel model = appl.workflowDefinition(workflow).instance(input).start().join();
+
+ Map output = model.asMap().orElseThrow();
+
+ Assertions.assertThat(output).isEqualTo(input);
+ }
+ }
+}
diff --git a/impl/test/src/test/resources/workflows-samples/run-script/console-log-args.yaml b/impl/test/src/test/resources/workflows-samples/run-script/console-log-args.yaml
new file mode 100644
index 00000000..c51681ea
--- /dev/null
+++ b/impl/test/src/test/resources/workflows-samples/run-script/console-log-args.yaml
@@ -0,0 +1,14 @@
+document:
+ dsl: '1.0.2'
+ namespace: test
+ name: run-script-example
+ version: '0.1.0'
+do:
+ - runScript:
+ run:
+ script:
+ language: js
+ arguments:
+ greetings: Hello, world!
+ code: >
+ console.log(greetings)
\ No newline at end of file
diff --git a/impl/test/src/test/resources/workflows-samples/run-script/console-log-envs.yaml b/impl/test/src/test/resources/workflows-samples/run-script/console-log-envs.yaml
new file mode 100644
index 00000000..36684b2b
--- /dev/null
+++ b/impl/test/src/test/resources/workflows-samples/run-script/console-log-envs.yaml
@@ -0,0 +1,14 @@
+document:
+ dsl: '1.0.2'
+ namespace: test
+ name: run-script-example
+ version: '0.1.0'
+do:
+ - runScript:
+ run:
+ script:
+ language: js
+ code: >
+ console.log(`Running ${process.env.LANGUAGE} code using Serverless Workflow!`);
+ environment:
+ LANGUAGE: JavaScript
\ No newline at end of file
diff --git a/impl/test/src/test/resources/workflows-samples/run-script/console-log-external-source.yaml b/impl/test/src/test/resources/workflows-samples/run-script/console-log-external-source.yaml
new file mode 100644
index 00000000..5ab9ba4d
--- /dev/null
+++ b/impl/test/src/test/resources/workflows-samples/run-script/console-log-external-source.yaml
@@ -0,0 +1,13 @@
+document:
+ dsl: '1.0.2'
+ namespace: test
+ name: run-script-example
+ version: '0.1.0'
+do:
+ - runScript:
+ run:
+ script:
+ language: js
+ source:
+ endpoint:
+ uri: http://localhost:8886/script.js
diff --git a/impl/test/src/test/resources/workflows-samples/run-script/console-log-not-awaiting.yaml b/impl/test/src/test/resources/workflows-samples/run-script/console-log-not-awaiting.yaml
new file mode 100644
index 00000000..c67adb64
--- /dev/null
+++ b/impl/test/src/test/resources/workflows-samples/run-script/console-log-not-awaiting.yaml
@@ -0,0 +1,13 @@
+document:
+ dsl: '1.0.2'
+ namespace: test
+ name: run-script-example
+ version: '0.1.0'
+do:
+ - runScript:
+ run:
+ script:
+ language: js
+ code: >
+ console.log("hello from script");
+ await: false
diff --git a/impl/test/src/test/resources/workflows-samples/run-script/console-log.yaml b/impl/test/src/test/resources/workflows-samples/run-script/console-log.yaml
new file mode 100644
index 00000000..79c5013a
--- /dev/null
+++ b/impl/test/src/test/resources/workflows-samples/run-script/console-log.yaml
@@ -0,0 +1,12 @@
+document:
+ dsl: '1.0.2'
+ namespace: test
+ name: run-script-example
+ version: '0.1.0'
+do:
+ - runScript:
+ run:
+ script:
+ language: js
+ code: >
+ console.log("hello from script");
diff --git a/impl/test/src/test/resources/workflows-samples/run-script/function-with-syntax-error.yaml b/impl/test/src/test/resources/workflows-samples/run-script/function-with-syntax-error.yaml
new file mode 100644
index 00000000..75bd074d
--- /dev/null
+++ b/impl/test/src/test/resources/workflows-samples/run-script/function-with-syntax-error.yaml
@@ -0,0 +1,19 @@
+document:
+ dsl: '1.0.2'
+ namespace: test
+ name: run-script-example
+ version: '0.1.0'
+do:
+ - runScript:
+ run:
+ script:
+ language: js
+ code: >
+ // there is no reserved word func in JavaScript, it should be function
+ func hello() {
+ console.log("hello from script");
+ throw new Error("This is a test error");
+ }
+
+ hello();
+ return: stderr
diff --git a/impl/test/src/test/resources/workflows-samples/run-script/function-with-throw-all.yaml b/impl/test/src/test/resources/workflows-samples/run-script/function-with-throw-all.yaml
new file mode 100644
index 00000000..c6e6cf3f
--- /dev/null
+++ b/impl/test/src/test/resources/workflows-samples/run-script/function-with-throw-all.yaml
@@ -0,0 +1,18 @@
+document:
+ dsl: '1.0.2'
+ namespace: test
+ name: run-script-example
+ version: '0.1.0'
+do:
+ - runScript:
+ run:
+ script:
+ language: js
+ code: >
+ function hello() {
+ console.log("logged before the 'throw' statement");
+ throw new Error("This is a test error");
+ }
+
+ hello();
+ return: all
diff --git a/impl/test/src/test/resources/workflows-samples/run-script/function-with-throw.yaml b/impl/test/src/test/resources/workflows-samples/run-script/function-with-throw.yaml
new file mode 100644
index 00000000..89236129
--- /dev/null
+++ b/impl/test/src/test/resources/workflows-samples/run-script/function-with-throw.yaml
@@ -0,0 +1,18 @@
+document:
+ dsl: '1.0.2'
+ namespace: test
+ name: run-script-example
+ version: '0.1.0'
+do:
+ - runScript:
+ run:
+ script:
+ language: js
+ code: >
+ function hello() {
+ console.log("hello from script");
+ throw new Error("This is a test error");
+ }
+
+ hello();
+ return: stderr
diff --git a/impl/test/src/test/resources/workflows-samples/run-script/script.js b/impl/test/src/test/resources/workflows-samples/run-script/script.js
new file mode 100644
index 00000000..f7b8bd8c
--- /dev/null
+++ b/impl/test/src/test/resources/workflows-samples/run-script/script.js
@@ -0,0 +1 @@
+console.log("hello from script");
\ No newline at end of file