|
|
@@ -0,0 +1,538 @@
|
|
|
+#!/usr/bin/env node
|
|
|
+
|
|
|
+import fs from "node:fs/promises"
|
|
|
+import path from "node:path"
|
|
|
+import process from "node:process"
|
|
|
+import { spawn } from "node:child_process"
|
|
|
+import { fileURLToPath } from "node:url"
|
|
|
+import { parseModule } from "meriyah"
|
|
|
+import { resolve as resolveDependencies } from "resolve-dependencies"
|
|
|
+import {
|
|
|
+ expandTilde,
|
|
|
+ fileExists,
|
|
|
+ readDirectoryRecursively,
|
|
|
+} from "../util/file-system.js"
|
|
|
+
|
|
|
+const modulePath = fileURLToPath(import.meta.url)
|
|
|
+const buildDir = path.dirname(modulePath)
|
|
|
+const rootDir = path.resolve(buildDir, "../..")
|
|
|
+const defaultInputPath = path.join(rootDir, "src/index.js")
|
|
|
+const runnerPath = path.join(rootDir, "src/run.js")
|
|
|
+const patchPath = path.join(rootDir, "src/build/nexe-python-compat.cjs")
|
|
|
+const pluginPath = path.join(rootDir, "src/build/nexe-entrypoint-compat.cjs")
|
|
|
+const defaultOutputPath = path.join(rootDir, "dist/rhedyn")
|
|
|
+const defaultTempPath = path.join(rootDir, ".nexe")
|
|
|
+const moduleFileExtensions = new Set([".js", ".mjs"])
|
|
|
+const stageRootDirName = "modules"
|
|
|
+
|
|
|
+function hasOption(args, optionNames) {
|
|
|
+ return args.some(arg =>
|
|
|
+ optionNames.includes(arg) ||
|
|
|
+ optionNames.some(optionName => arg.startsWith(`${optionName}=`)))
|
|
|
+}
|
|
|
+
|
|
|
+function getOptionValue(args, optionNames) {
|
|
|
+ for (let index = 0; index < args.length; index += 1) {
|
|
|
+ const arg = args[index]
|
|
|
+ for (const optionName of optionNames) {
|
|
|
+ if (arg === optionName) {
|
|
|
+ return args[index + 1]
|
|
|
+ }
|
|
|
+ if (arg.startsWith(`${optionName}=`)) {
|
|
|
+ return arg.slice(optionName.length + 1)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return null
|
|
|
+}
|
|
|
+
|
|
|
+function resolveArgPath(pathArg, cwd) {
|
|
|
+ return path.resolve(cwd, expandTilde(pathArg))
|
|
|
+}
|
|
|
+
|
|
|
+function validateOutputName(name) {
|
|
|
+ if (!name) {
|
|
|
+ throw new Error("Missing value for --name.")
|
|
|
+ }
|
|
|
+
|
|
|
+ const trimmedName = name.trim()
|
|
|
+ if (!trimmedName) {
|
|
|
+ throw new Error("Missing value for --name.")
|
|
|
+ }
|
|
|
+
|
|
|
+ if (
|
|
|
+ trimmedName === "." ||
|
|
|
+ trimmedName === ".." ||
|
|
|
+ trimmedName.includes("/") ||
|
|
|
+ trimmedName.includes("\\")
|
|
|
+ ) {
|
|
|
+ throw new Error(`Invalid --name value: ${name}`)
|
|
|
+ }
|
|
|
+
|
|
|
+ return trimmedName
|
|
|
+}
|
|
|
+
|
|
|
+export function toImportSpecifier(fromDir, targetPath) {
|
|
|
+ const relativePath = path.relative(fromDir, targetPath).replaceAll(path.sep, "/")
|
|
|
+ if (relativePath.startsWith(".")) {
|
|
|
+ return relativePath
|
|
|
+ }
|
|
|
+ return `./${relativePath}`
|
|
|
+}
|
|
|
+
|
|
|
+function isStageableModuleFile(filePath) {
|
|
|
+ return (
|
|
|
+ typeof filePath === "string" &&
|
|
|
+ path.isAbsolute(filePath) &&
|
|
|
+ !filePath.includes(`${path.sep}node_modules${path.sep}`) &&
|
|
|
+ moduleFileExtensions.has(path.extname(filePath))
|
|
|
+ )
|
|
|
+}
|
|
|
+
|
|
|
+export function getStagedModuleRelativePath(filePath) {
|
|
|
+ const resolvedPath = path.resolve(filePath)
|
|
|
+ const { root } = path.parse(resolvedPath)
|
|
|
+ const rootToken = root === path.sep
|
|
|
+ ? "_root"
|
|
|
+ : root.replaceAll(/[\\/:]+/g, "_").replace(/^_+|_+$/g, "") || "root"
|
|
|
+ const relativeFromRoot = path.relative(root, resolvedPath)
|
|
|
+ const stagedRelativePath = path.join(rootToken, relativeFromRoot)
|
|
|
+
|
|
|
+ return `${stagedRelativePath.slice(0, -path.extname(stagedRelativePath).length)}.mjs`
|
|
|
+}
|
|
|
+
|
|
|
+function getStagedModulePath(stagingRoot, filePath) {
|
|
|
+ return path.join(stagingRoot, getStagedModuleRelativePath(filePath))
|
|
|
+}
|
|
|
+
|
|
|
+function collectSpecifierNodes(node, specifierNodes = []) {
|
|
|
+ if (!node || typeof node !== "object") {
|
|
|
+ return specifierNodes
|
|
|
+ }
|
|
|
+
|
|
|
+ if (
|
|
|
+ (
|
|
|
+ node.type === "ImportDeclaration" ||
|
|
|
+ node.type === "ExportAllDeclaration" ||
|
|
|
+ node.type === "ExportNamedDeclaration"
|
|
|
+ ) &&
|
|
|
+ node.source &&
|
|
|
+ typeof node.source.value === "string"
|
|
|
+ ) {
|
|
|
+ specifierNodes.push(node.source)
|
|
|
+ }
|
|
|
+
|
|
|
+ if (
|
|
|
+ node.type === "ImportExpression" &&
|
|
|
+ node.source &&
|
|
|
+ node.source.type === "Literal" &&
|
|
|
+ typeof node.source.value === "string"
|
|
|
+ ) {
|
|
|
+ specifierNodes.push(node.source)
|
|
|
+ }
|
|
|
+
|
|
|
+ for (const value of Object.values(node)) {
|
|
|
+ if (!value || typeof value !== "object") {
|
|
|
+ continue
|
|
|
+ }
|
|
|
+
|
|
|
+ if (Array.isArray(value)) {
|
|
|
+ for (const item of value) {
|
|
|
+ collectSpecifierNodes(item, specifierNodes)
|
|
|
+ }
|
|
|
+ continue
|
|
|
+ }
|
|
|
+
|
|
|
+ collectSpecifierNodes(value, specifierNodes)
|
|
|
+ }
|
|
|
+
|
|
|
+ return specifierNodes
|
|
|
+}
|
|
|
+
|
|
|
+function applyTextReplacements(source, replacements) {
|
|
|
+ return replacements
|
|
|
+ .sort((left, right) => right.start - left.start)
|
|
|
+ .reduce(
|
|
|
+ (updatedSource, { start, end, text }) =>
|
|
|
+ `${updatedSource.slice(0, start)}${text}${updatedSource.slice(end)}`,
|
|
|
+ source,
|
|
|
+ )
|
|
|
+}
|
|
|
+
|
|
|
+function resolveDependencyRecord(fileRecord, specifier) {
|
|
|
+ if (!fileRecord?.deps) {
|
|
|
+ return null
|
|
|
+ }
|
|
|
+
|
|
|
+ return Object.prototype.hasOwnProperty.call(fileRecord.deps, specifier)
|
|
|
+ ? fileRecord.deps[specifier]
|
|
|
+ : null
|
|
|
+}
|
|
|
+
|
|
|
+function resolveStageableDependencyPath(filePath, specifier, stagedPathsByOriginalPath, fileRecord) {
|
|
|
+ const dependencyRecord = resolveDependencyRecord(fileRecord, specifier)
|
|
|
+ if (dependencyRecord?.absPath) {
|
|
|
+ return dependencyRecord.absPath
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!specifier.startsWith(".") && !path.isAbsolute(specifier)) {
|
|
|
+ return null
|
|
|
+ }
|
|
|
+
|
|
|
+ const candidatePath = path.resolve(path.dirname(filePath), specifier)
|
|
|
+ return stagedPathsByOriginalPath.has(candidatePath) ? candidatePath : null
|
|
|
+}
|
|
|
+
|
|
|
+async function resolveLocalDependencyPath(filePath, specifier, fileRecord) {
|
|
|
+ const dependencyRecord = resolveDependencyRecord(fileRecord, specifier)
|
|
|
+ if (isStageableModuleFile(dependencyRecord?.absPath)) {
|
|
|
+ return dependencyRecord.absPath
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!specifier.startsWith(".") && !path.isAbsolute(specifier)) {
|
|
|
+ return null
|
|
|
+ }
|
|
|
+
|
|
|
+ const resolvedPath = path.resolve(path.dirname(filePath), specifier)
|
|
|
+ return await fileExists(resolvedPath) ? resolvedPath : null
|
|
|
+}
|
|
|
+
|
|
|
+async function collectLocalDependencyPaths({ source, filePath, fileRecord }) {
|
|
|
+ const ast = parseModule(source, { next: true, ranges: true })
|
|
|
+ const dependencyPaths = await Promise.all(
|
|
|
+ collectSpecifierNodes(ast).map(specifierNode =>
|
|
|
+ resolveLocalDependencyPath(filePath, specifierNode.value, fileRecord)),
|
|
|
+ )
|
|
|
+
|
|
|
+ return dependencyPaths.filter(dependencyPath => isStageableModuleFile(dependencyPath))
|
|
|
+}
|
|
|
+
|
|
|
+export function rewriteModuleSpecifiers({
|
|
|
+ source,
|
|
|
+ filePath,
|
|
|
+ currentStagedPath,
|
|
|
+ fileRecord,
|
|
|
+ stagedPathsByOriginalPath,
|
|
|
+}) {
|
|
|
+ const ast = parseModule(source, { next: true, ranges: true })
|
|
|
+ const replacements = collectSpecifierNodes(ast).flatMap(specifierNode => {
|
|
|
+ const specifier = specifierNode.value
|
|
|
+ const resolvedDependencyPath = resolveStageableDependencyPath(
|
|
|
+ filePath,
|
|
|
+ specifier,
|
|
|
+ stagedPathsByOriginalPath,
|
|
|
+ fileRecord,
|
|
|
+ )
|
|
|
+
|
|
|
+ if (!resolvedDependencyPath) {
|
|
|
+ return []
|
|
|
+ }
|
|
|
+
|
|
|
+ const stagedDependencyPath = stagedPathsByOriginalPath.get(resolvedDependencyPath)
|
|
|
+ if (!stagedDependencyPath) {
|
|
|
+ return []
|
|
|
+ }
|
|
|
+
|
|
|
+ const nextSpecifier = toImportSpecifier(
|
|
|
+ path.dirname(currentStagedPath),
|
|
|
+ stagedDependencyPath,
|
|
|
+ )
|
|
|
+
|
|
|
+ return [
|
|
|
+ {
|
|
|
+ start: specifierNode.start,
|
|
|
+ end: specifierNode.end,
|
|
|
+ text: JSON.stringify(nextSpecifier),
|
|
|
+ },
|
|
|
+ ]
|
|
|
+ })
|
|
|
+
|
|
|
+ return applyTextReplacements(source, replacements)
|
|
|
+}
|
|
|
+
|
|
|
+export function parseBuildArgs(argv, cwd = process.cwd()) {
|
|
|
+ const forwardedArgs = []
|
|
|
+ let configPath = null
|
|
|
+ let outputName = null
|
|
|
+
|
|
|
+ for (let index = 0; index < argv.length; index += 1) {
|
|
|
+ const arg = argv[index]
|
|
|
+
|
|
|
+ if (arg === "-c" || arg === "--config") {
|
|
|
+ const pathArg = argv[index + 1]
|
|
|
+ if (!pathArg) {
|
|
|
+ throw new Error("Missing value for --config.")
|
|
|
+ }
|
|
|
+ configPath = resolveArgPath(pathArg, cwd)
|
|
|
+ index += 1
|
|
|
+ continue
|
|
|
+ }
|
|
|
+
|
|
|
+ if (arg.startsWith("--config=")) {
|
|
|
+ configPath = resolveArgPath(arg.slice("--config=".length), cwd)
|
|
|
+ continue
|
|
|
+ }
|
|
|
+
|
|
|
+ if (arg === "--name") {
|
|
|
+ outputName = validateOutputName(argv[index + 1])
|
|
|
+ index += 1
|
|
|
+ continue
|
|
|
+ }
|
|
|
+
|
|
|
+ if (arg.startsWith("--name=")) {
|
|
|
+ outputName = validateOutputName(arg.slice("--name=".length))
|
|
|
+ continue
|
|
|
+ }
|
|
|
+
|
|
|
+ forwardedArgs.push(arg)
|
|
|
+ }
|
|
|
+
|
|
|
+ return { configPath, forwardedArgs, outputName }
|
|
|
+}
|
|
|
+
|
|
|
+export function createConfiguredEntrypointSource({
|
|
|
+ entryDir,
|
|
|
+ stagedConfigPath,
|
|
|
+ stagedRunnerPath,
|
|
|
+}) {
|
|
|
+ return [
|
|
|
+ "(async () => {",
|
|
|
+ " const configModule = await import(" + JSON.stringify(toImportSpecifier(entryDir, stagedConfigPath)) + ")",
|
|
|
+ " const { runWithConfig } = await import(" + JSON.stringify(toImportSpecifier(entryDir, stagedRunnerPath)) + ")",
|
|
|
+ "",
|
|
|
+ " const config = configModule.default || configModule",
|
|
|
+ " await runWithConfig(config)",
|
|
|
+ "})().catch(err => {",
|
|
|
+ " console.error(err)",
|
|
|
+ " process.exit(1)",
|
|
|
+ "})",
|
|
|
+ ].join("\n")
|
|
|
+}
|
|
|
+
|
|
|
+async function resolveModuleGraph(entryPaths) {
|
|
|
+ const { files } = await resolveDependencies({
|
|
|
+ cwd: rootDir,
|
|
|
+ entries: entryPaths,
|
|
|
+ expand: "all",
|
|
|
+ loadContent: true,
|
|
|
+ })
|
|
|
+
|
|
|
+ const localModulePaths = new Set()
|
|
|
+ const queue = entryPaths
|
|
|
+ .map(entryPath => path.resolve(entryPath))
|
|
|
+ .filter(entryPath => isStageableModuleFile(entryPath))
|
|
|
+
|
|
|
+ while (queue.length > 0) {
|
|
|
+ const filePath = queue.pop()
|
|
|
+ if (!filePath || localModulePaths.has(filePath)) {
|
|
|
+ continue
|
|
|
+ }
|
|
|
+
|
|
|
+ localModulePaths.add(filePath)
|
|
|
+
|
|
|
+ const fileRecord = files[filePath]
|
|
|
+ const source = fileRecord?.contents ?? await fs.readFile(filePath, "utf8")
|
|
|
+ const dependencyPaths = await collectLocalDependencyPaths({
|
|
|
+ source,
|
|
|
+ filePath,
|
|
|
+ fileRecord,
|
|
|
+ })
|
|
|
+
|
|
|
+ for (const dependencyPath of dependencyPaths) {
|
|
|
+ if (!localModulePaths.has(dependencyPath)) {
|
|
|
+ queue.push(dependencyPath)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return {
|
|
|
+ filesByPath: files,
|
|
|
+ localModulePaths: [...localModulePaths].sort(),
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function stageModuleGraph({ tempDir, configPath }) {
|
|
|
+ const stagingRoot = path.join(tempDir, stageRootDirName)
|
|
|
+ const { filesByPath, localModulePaths } = await resolveModuleGraph([
|
|
|
+ runnerPath,
|
|
|
+ configPath,
|
|
|
+ ])
|
|
|
+ const stagedPathsByOriginalPath = new Map(
|
|
|
+ localModulePaths.map(filePath => [filePath, getStagedModulePath(stagingRoot, filePath)]),
|
|
|
+ )
|
|
|
+
|
|
|
+ await Promise.all(localModulePaths.map(async filePath => {
|
|
|
+ const stagedPath = stagedPathsByOriginalPath.get(filePath)
|
|
|
+ const fileRecord = filesByPath[filePath]
|
|
|
+ const source = fileRecord?.contents ?? await fs.readFile(filePath, "utf8")
|
|
|
+ const stagedSource = rewriteModuleSpecifiers({
|
|
|
+ source,
|
|
|
+ filePath,
|
|
|
+ currentStagedPath: stagedPath,
|
|
|
+ fileRecord,
|
|
|
+ stagedPathsByOriginalPath,
|
|
|
+ })
|
|
|
+
|
|
|
+ await fs.mkdir(path.dirname(stagedPath), { recursive: true })
|
|
|
+ await fs.writeFile(stagedPath, stagedSource, "utf8")
|
|
|
+ }))
|
|
|
+
|
|
|
+ return {
|
|
|
+ stagedPathsByOriginalPath,
|
|
|
+ stagedModulePaths: [...stagedPathsByOriginalPath.values()].sort(),
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function createConfiguredEntrypoint(configPath) {
|
|
|
+ const tempDir = await fs.mkdtemp(path.join(rootDir, ".rhedyn-build-"))
|
|
|
+ const { stagedPathsByOriginalPath, stagedModulePaths } = await stageModuleGraph({
|
|
|
+ tempDir,
|
|
|
+ configPath,
|
|
|
+ })
|
|
|
+ const stagedConfigPath = stagedPathsByOriginalPath.get(path.resolve(configPath))
|
|
|
+ const stagedRunnerPath = stagedPathsByOriginalPath.get(runnerPath)
|
|
|
+
|
|
|
+ if (!stagedConfigPath || !stagedRunnerPath) {
|
|
|
+ throw new Error("Failed to stage the tool entrypoint module graph.")
|
|
|
+ }
|
|
|
+
|
|
|
+ const entryPath = path.join(tempDir, "entry.cjs")
|
|
|
+ const entrySource = createConfiguredEntrypointSource({
|
|
|
+ entryDir: tempDir,
|
|
|
+ stagedConfigPath,
|
|
|
+ stagedRunnerPath,
|
|
|
+ })
|
|
|
+
|
|
|
+ await fs.writeFile(entryPath, entrySource, "utf8")
|
|
|
+
|
|
|
+ return {
|
|
|
+ entryPath,
|
|
|
+ resourcePaths: stagedModulePaths,
|
|
|
+ cleanup: async () => {
|
|
|
+ await fs.rm(tempDir, { force: true, recursive: true })
|
|
|
+ },
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function resolveEntrypoint(configPath) {
|
|
|
+ if (!configPath) {
|
|
|
+ return {
|
|
|
+ entryPath: defaultInputPath,
|
|
|
+ resourcePaths: [],
|
|
|
+ cleanup: async () => {},
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!await fileExists(configPath)) {
|
|
|
+ throw new Error(`Config file not found: ${configPath}`)
|
|
|
+ }
|
|
|
+
|
|
|
+ return createConfiguredEntrypoint(configPath)
|
|
|
+}
|
|
|
+
|
|
|
+function getNexeCommand() {
|
|
|
+ return process.platform === "win32" ? "nexe.cmd" : "nexe"
|
|
|
+}
|
|
|
+
|
|
|
+function resolveOutputPath(outputName) {
|
|
|
+ return outputName ? path.join(rootDir, "dist", outputName) : defaultOutputPath
|
|
|
+}
|
|
|
+
|
|
|
+export function buildNexeArgs(entryPath, forwardedArgs, outputName = null) {
|
|
|
+ return buildNexeArgsWithResources(entryPath, forwardedArgs, [], outputName)
|
|
|
+}
|
|
|
+
|
|
|
+function buildNexeArgsWithResources(entryPath, forwardedArgs, resourcePaths, outputName) {
|
|
|
+ const args = ["-i", entryPath]
|
|
|
+
|
|
|
+ if (!hasOption(forwardedArgs, ["-o", "--output"])) {
|
|
|
+ args.push("-o", resolveOutputPath(outputName))
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!hasOption(forwardedArgs, ["--temp"])) {
|
|
|
+ args.push("--temp", defaultTempPath)
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!hasOption(forwardedArgs, ["--python"])) {
|
|
|
+ args.push("--python", "python3")
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!hasOption(forwardedArgs, ["--patch"])) {
|
|
|
+ args.push("--patch", patchPath)
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!hasOption(forwardedArgs, ["--plugin"])) {
|
|
|
+ args.push("--plugin", pluginPath)
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!hasOption(forwardedArgs, ["--build"])) {
|
|
|
+ args.push("--build")
|
|
|
+ }
|
|
|
+
|
|
|
+ for (const resourcePath of resourcePaths) {
|
|
|
+ args.push("--resource", resourcePath)
|
|
|
+ }
|
|
|
+
|
|
|
+ return [...args, ...forwardedArgs]
|
|
|
+}
|
|
|
+
|
|
|
+async function runNexe(args) {
|
|
|
+ await fs.mkdir(path.dirname(defaultOutputPath), { recursive: true })
|
|
|
+
|
|
|
+ await new Promise((resolve, reject) => {
|
|
|
+ const child = spawn(getNexeCommand(), args, {
|
|
|
+ cwd: rootDir,
|
|
|
+ env: process.env,
|
|
|
+ stdio: "inherit",
|
|
|
+ })
|
|
|
+
|
|
|
+ child.on("error", reject)
|
|
|
+ child.on("close", code => {
|
|
|
+ if (code === 0) {
|
|
|
+ resolve()
|
|
|
+ return
|
|
|
+ }
|
|
|
+ reject(new Error(`nexe exited with code ${code}`))
|
|
|
+ })
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+async function invalidatePatchedBinaryCache(args) {
|
|
|
+ const tempArg = getOptionValue(args, ["--temp"])
|
|
|
+ const tempDir = tempArg
|
|
|
+ ? path.resolve(rootDir, tempArg)
|
|
|
+ : defaultTempPath
|
|
|
+
|
|
|
+ if (!await fileExists(tempDir)) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ const tempFiles = await readDirectoryRecursively(tempDir)
|
|
|
+ const compiledNodeBinaries = tempFiles.filter(filePath =>
|
|
|
+ filePath.endsWith(`${path.sep}out${path.sep}Release${path.sep}node`) ||
|
|
|
+ filePath.endsWith(`${path.sep}Release${path.sep}node.exe`))
|
|
|
+
|
|
|
+ await Promise.all(compiledNodeBinaries.map(filePath =>
|
|
|
+ fs.rm(filePath, { force: true })))
|
|
|
+}
|
|
|
+
|
|
|
+export async function buildAsTool(argv = process.argv.slice(2)) {
|
|
|
+ const { configPath, forwardedArgs, outputName } = parseBuildArgs(argv)
|
|
|
+ const { entryPath, resourcePaths, cleanup } = await resolveEntrypoint(configPath)
|
|
|
+
|
|
|
+ try {
|
|
|
+ const nexeArgs = buildNexeArgsWithResources(entryPath, forwardedArgs, resourcePaths, outputName)
|
|
|
+ await invalidatePatchedBinaryCache(nexeArgs)
|
|
|
+ await runNexe(nexeArgs)
|
|
|
+ } finally {
|
|
|
+ await cleanup()
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+if (path.resolve(process.argv[1] || "") === modulePath) {
|
|
|
+ await buildAsTool().catch(err => {
|
|
|
+ console.error(err.message)
|
|
|
+ process.exit(1)
|
|
|
+ })
|
|
|
+}
|