@tobiastengler/create-relay-app
Advanced tools
Comparing version 0.0.0-experimental-e6e505b7a to 0.0.0-experimental-ea7ea2081
134
dist/bin.js
#!/usr/bin/env node | ||
import path, { dirname } from "path"; | ||
import { TaskRunner } from "./TaskRunner.js"; | ||
import { AddGraphQlSchemaFileTask } from "./tasks/AddGraphQlSchemaFileTask.js"; | ||
import { GenerateGraphQlSchemaFileTask } from "./tasks/GenerateGraphQlSchemaFileTask.js"; | ||
import chalk from "chalk"; | ||
import { AddRelayConfigurationTask } from "./tasks/AddRelayConfigurationTask.js"; | ||
import { InstallNpmPackagesTask } from "./tasks/InstallNpmPackagesTask.js"; | ||
import { AddRelayPluginConfigurationTask } from "./tasks/AddRelayPluginConfigurationTask.js"; | ||
import { AddRelayEnvironmentTask } from "./tasks/AddRelayEnvironmentTask.js"; | ||
import { traverseUpToFindFile, hasUnsavedGitChanges, printError, } from "./helpers.js"; | ||
import { ConfigureRelayGraphqlTransformTask } from "./tasks/ConfigureRelayGraphqlTransformTask.js"; | ||
import { AddRelayEnvironmentProviderTask } from "./tasks/AddRelayEnvironmentProviderTask.js"; | ||
import { traverseUpToFindFile, printError, getRelayDevDependencies, getRelayCompilerLanguage, highlight, printInvalidArg, getToolchainSettings, prettifyRelativePath, } from "./helpers.js"; | ||
import { exit } from "process"; | ||
import { BABEL_RELAY_PACKAGE, PACKAGE_FILE, VITE_RELAY_PACKAGE, } from "./consts.js"; | ||
import { ARTIFACT_DIR_ARG, BABEL_RELAY_PACKAGE, PACKAGE_FILE, REACT_RELAY_PACKAGE, SCHEMA_FILE_ARG, SRC_DIR_ARG, } from "./consts.js"; | ||
import { fileURLToPath } from "url"; | ||
import { getCliArguments } from "./cli.js"; | ||
import { getCliArguments, promptForMissingCliArguments } from "./cli.js"; | ||
import { hasUnsavedGitChanges, isValidArtifactDirectory, isValidSchemaPath, isValidSrcDirectory, } from "./validation.js"; | ||
import { GenerateRelayEnvironmentTask } from "./tasks/GenerateRelayEnvironmentTask.js"; | ||
import { GenerateArtifactDirectoryTask } from "./tasks/GenerateArtifactDirectoryTask.js"; | ||
import { getProjectRelayEnvFilepath } from "./defaults.js"; | ||
// INIT ENVIRONMENT | ||
const distDirectory = dirname(fileURLToPath(import.meta.url)); | ||
@@ -20,3 +25,3 @@ const ownPackageDirectory = path.join(distDirectory, ".."); | ||
if (!packageJsonFile) { | ||
printError(`Could not find a ${chalk.cyan.bold(PACKAGE_FILE)} in the ${chalk.cyan.bold(workingDirectory)} directory.`); | ||
printError(`Could not find a ${highlight(PACKAGE_FILE)} in the ${highlight(workingDirectory)} directory.`); | ||
exit(1); | ||
@@ -31,20 +36,53 @@ } | ||
}; | ||
// todo: handle errors | ||
const cliArguments = await getCliArguments(envArguments); | ||
const settings = Object.assign(Object.assign(Object.assign({}, envArguments), cliArguments), { | ||
// todo: determine based on toolchain | ||
srcDirectory: "./src" }); | ||
if (!settings.ignoreGitChanges) { | ||
const hasUnsavedChanges = await hasUnsavedGitChanges(envArguments.projectRootDirectory); | ||
if (hasUnsavedChanges) { | ||
printError(`Please commit or discard all changes in the ${chalk.cyan.bold(envArguments.projectRootDirectory)} directory before continuing.`); | ||
exit(1); | ||
// GET ARGUMENTS | ||
let cliArgs; | ||
try { | ||
const partialCliArguments = await getCliArguments(envArguments); | ||
if (!partialCliArguments.ignoreGitChanges) { | ||
const hasUnsavedChanges = await hasUnsavedGitChanges(envArguments.projectRootDirectory); | ||
if (hasUnsavedChanges) { | ||
printError(`Please commit or discard all changes in the ${highlight(envArguments.projectRootDirectory)} directory before continuing.`); | ||
exit(1); | ||
} | ||
} | ||
cliArgs = await promptForMissingCliArguments(partialCliArguments, envArguments); | ||
} | ||
const dependencies = ["react-relay"]; | ||
const devDependencies = getRelayDevDependencies(settings.toolChain, settings.useTypescript); | ||
catch (error) { | ||
if (error instanceof Error) { | ||
printError(error.message); | ||
} | ||
else { | ||
printError("Unexpected error while parsing CLI arguments"); | ||
} | ||
exit(1); | ||
} | ||
// VALIDATE ARGUMENTS | ||
const schemaPathValid = isValidSchemaPath(cliArgs.schemaFile, envArguments.projectRootDirectory); | ||
if (schemaPathValid !== true) { | ||
printInvalidArg(SCHEMA_FILE_ARG, schemaPathValid, cliArgs.schemaFile); | ||
exit(1); | ||
} | ||
const srcDirValid = isValidSrcDirectory(cliArgs.src, envArguments.projectRootDirectory); | ||
if (srcDirValid !== true) { | ||
printInvalidArg(SRC_DIR_ARG, srcDirValid, cliArgs.src); | ||
exit(1); | ||
} | ||
const artifactDirValid = isValidArtifactDirectory(cliArgs.artifactDirectory, cliArgs.toolchain, envArguments.projectRootDirectory); | ||
if (artifactDirValid !== true) { | ||
printInvalidArg(ARTIFACT_DIR_ARG, artifactDirValid, cliArgs.artifactDirectory); | ||
exit(1); | ||
} | ||
const toolchainSettings = await getToolchainSettings(envArguments, cliArgs); | ||
const settings = Object.assign(Object.assign(Object.assign(Object.assign({}, envArguments), cliArgs), { compilerLanguage: getRelayCompilerLanguage(cliArgs.typescript), relayEnvFilepath: getProjectRelayEnvFilepath(envArguments, cliArgs) }), toolchainSettings); | ||
// EXECUTE TASKS | ||
const dependencies = [REACT_RELAY_PACKAGE]; | ||
const devDependencies = getRelayDevDependencies(settings.toolchain, settings.typescript); | ||
const relRelayEnvPath = prettifyRelativePath(settings.projectRootDirectory, settings.relayEnvFilepath); | ||
const relMainPath = prettifyRelativePath(settings.projectRootDirectory, settings.mainFilepath); | ||
const relConfigPath = prettifyRelativePath(settings.projectRootDirectory, settings.configFilepath); | ||
const relSchemaPath = prettifyRelativePath(settings.projectRootDirectory, settings.schemaFile); | ||
const runner = new TaskRunner([ | ||
{ | ||
title: `Add Relay dependencies: ${dependencies | ||
.map((d) => chalk.cyan.bold(d)) | ||
.map((d) => highlight(d)) | ||
.join(" ")}`, | ||
@@ -55,3 +93,3 @@ task: new InstallNpmPackagesTask(dependencies, false, settings), | ||
title: `Add Relay devDependencies: ${devDependencies | ||
.map((d) => chalk.cyan.bold(d)) | ||
.map((d) => highlight(d)) | ||
.join(" ")}`, | ||
@@ -61,17 +99,28 @@ task: new InstallNpmPackagesTask(devDependencies, true, settings), | ||
{ | ||
title: "Add Relay configuration to package.json", | ||
title: `Add Relay configuration to ${highlight(PACKAGE_FILE)}`, | ||
task: new AddRelayConfigurationTask(settings), | ||
}, | ||
{ | ||
title: "Add Relay plugin configuration", | ||
task: new AddRelayPluginConfigurationTask(settings), | ||
title: `Configure Relay transform in ${highlight(relConfigPath)}`, | ||
task: new ConfigureRelayGraphqlTransformTask(settings), | ||
when: !!settings.configFilepath, // todo: remove once cra is fully supported | ||
}, | ||
{ | ||
title: "Add Relay environment", | ||
task: new AddRelayEnvironmentTask(settings), | ||
title: `Generate Relay environment ${highlight(relRelayEnvPath)}`, | ||
task: new GenerateRelayEnvironmentTask(settings), | ||
}, | ||
{ | ||
title: `Generate GraphQL schema file (${chalk.cyan.bold(settings.schemaFilePath)})`, | ||
task: new AddGraphQlSchemaFileTask(settings), | ||
title: `Add RelayEnvironmentProvider to ${highlight(relMainPath)}`, | ||
task: new AddRelayEnvironmentProviderTask(settings), | ||
when: !!settings.configFilepath, // todo: remove once cra is fully supported | ||
}, | ||
{ | ||
title: `Generate GraphQL schema file ${highlight(relSchemaPath)}`, | ||
task: new GenerateGraphQlSchemaFileTask(settings), | ||
}, | ||
{ | ||
title: `Generate artifact directory ${highlight(settings.artifactDirectory)}`, | ||
task: new GenerateArtifactDirectoryTask(settings), | ||
when: !!settings.artifactDirectory, | ||
}, | ||
]); | ||
@@ -83,24 +132,17 @@ try { | ||
console.log(); | ||
printError("Some of the tasks unexpectedly failed."); | ||
printError("Some of the tasks failed unexpectedly."); | ||
exit(1); | ||
// todo: if tasks fail, display ways to resovle the tasks manually | ||
} | ||
// DISPLAY RESULT | ||
console.log(); | ||
console.log(chalk.yellow.bold("Next steps:")); | ||
console.log(`1. Replace ${chalk.cyan.bold(settings.schemaFilePath)} with your own GraphQL schema file.`); | ||
// todo: get correct path to file | ||
console.log(`2. Replace the value of the ${chalk.cyan.bold("HOST")} variable in the RelayEnvironment.ts file.`); | ||
console.log(); | ||
function getRelayDevDependencies(toolChain, useTypescript) { | ||
const relayDevDep = ["relay-compiler"]; | ||
if (useTypescript) { | ||
relayDevDep.push("@types/react-relay"); | ||
relayDevDep.push("@types/relay-runtime"); | ||
} | ||
if (toolChain === "cra" || toolChain === "vite") { | ||
relayDevDep.push(BABEL_RELAY_PACKAGE); | ||
} | ||
if (toolChain === "vite") { | ||
relayDevDep.push(VITE_RELAY_PACKAGE); | ||
} | ||
return relayDevDep; | ||
console.log(chalk.cyan.bold.underline("Next steps")); | ||
console.log(); | ||
console.log(`1. Replace ${highlight(relSchemaPath)} with your own GraphQL schema file.`); | ||
console.log(`2. Replace the value of the ${highlight("HOST")} variable in the ${highlight(relRelayEnvPath)} file.`); | ||
// todo: remove once cra is fully supported | ||
if (settings.toolchain === "cra") { | ||
console.log(`3. Setup ${highlight(BABEL_RELAY_PACKAGE)} to conform to your project. ${chalk.dim("(This will soon be automated!)")}`); | ||
} | ||
console.log(); |
164
dist/cli.js
@@ -1,9 +0,8 @@ | ||
import path from "path"; | ||
import { PACKAGE_FILE } from "./consts.js"; | ||
import { PackageManagerOptions, ToolChainOptions, } from "./types.js"; | ||
import fs from "fs/promises"; | ||
import { PackageManagerOptions, ToolchainOptions, } from "./types.js"; | ||
import { program } from "commander"; | ||
import chalk from "chalk"; | ||
import inquirer from "inquirer"; | ||
import { getProjectToolChain, doesProjectUseTypescript, getProjectPackageManager, } from "./helpers.js"; | ||
import { getPackageDetails, prettifyPath, } from "./helpers.js"; | ||
import { getDefaultCliArguments, getProjectSchemaFilepath, } from "./defaults.js"; | ||
import { isValidArtifactDirectory, isValidSchemaPath, isValidSrcDirectory, } from "./validation.js"; | ||
import { ARTIFACT_DIR_ARG, IGNORE_GIT_CHANGES_ARG, PACKAGE_MANAGER_ARG, SCHEMA_FILE_ARG, SRC_DIR_ARG, TOOLCHAIN_ARG, TYPESCRIPT_ARG, VERSION_ARG, YES_ARG, } from "./consts.js"; | ||
export async function getCliArguments(env) { | ||
@@ -14,55 +13,23 @@ const { name: PACKAGE_NAME, version: PACKAGE_VERSION, description: PACKAGE_DESCRIPTION, } = await getPackageDetails(env); | ||
.description(PACKAGE_DESCRIPTION) | ||
.version(PACKAGE_VERSION, "-v, --version"); | ||
// If you change argumemts, make to match up RawCliArguments as well. | ||
.version(PACKAGE_VERSION, `-v, ${VERSION_ARG}`); | ||
// If you change argumemts, make sure to match up RawCliArguments as well. | ||
program | ||
.option("-t, --toolchain <toolchain>", "the toolchain used to bundle / serve the project") | ||
.option("--typescript", "use Typescript") | ||
.option("-s, --schema-file <path>", "path to a GraphQL schema file, relative to the root directory") | ||
.option("-p, --package-manager <manager>") | ||
.option("--ignore-git-changes", "do not exit if the current directory has un-commited Git changes") | ||
.option("-y, --yes", `answer \"yes\" to any prompts`); | ||
.option(`-t, ${TOOLCHAIN_ARG} <toolchain>`, "the toolchain used to bundle / serve the project", parseToolChain) | ||
.option(TYPESCRIPT_ARG, "use Typescript") | ||
.option(`-f, ${SCHEMA_FILE_ARG} <path>`, "path to a GraphQL schema file, relative to the root directory") | ||
.option(`-s, ${SRC_DIR_ARG} <path>`, "path to the directory, where the Relay compiler will be run on") | ||
.option(`-a, ${ARTIFACT_DIR_ARG} <path>`, "path to the directory, where the Relay compiler will be run on") | ||
.option(`-p, ${PACKAGE_MANAGER_ARG} <manager>`, "the package manager to use for installing packages", parsePackageManager) | ||
.option(IGNORE_GIT_CHANGES_ARG, "do not exit if the current directory has un-commited Git changes") | ||
.option(`-y, ${YES_ARG}`, `answer \"yes\" to any prompts`); | ||
program.parse(); | ||
const rawCliArguments = program.opts(); | ||
const partialCliArguments = parseCliArguments(rawCliArguments); | ||
return await promptForMissingCliArguments(partialCliArguments, env); | ||
return program.opts(); | ||
} | ||
function parseCliArguments(args) { | ||
return { | ||
toolChain: tryParseToolChain(args.toolchain), | ||
packageManager: tryParsePackageManager(args.packageManager), | ||
schemaFilePath: args.schemaFile, | ||
useTypescript: args.typescript, | ||
skipPrompts: args.yes, | ||
ignoreGitChanges: args.ignoreGitChanges, | ||
}; | ||
} | ||
async function getDefaultCliArguments(existingArgs, env) { | ||
const packageManager = existingArgs.packageManager || | ||
(await getProjectPackageManager(env.projectRootDirectory)); | ||
const toolChain = existingArgs.toolChain || | ||
(await getProjectToolChain(env.projectRootDirectory)); | ||
const useTypescript = existingArgs.useTypescript || | ||
(await doesProjectUseTypescript(env.projectRootDirectory, packageManager)); | ||
// todo: use the src directory as base once configurable | ||
const schemaFilePath = existingArgs.schemaFilePath || "./schema.graphql"; | ||
const ignoreGitChanges = existingArgs.ignoreGitChanges || false; | ||
const skipPrompts = existingArgs.skipPrompts || false; | ||
return { | ||
packageManager, | ||
toolChain, | ||
useTypescript, | ||
schemaFilePath, | ||
ignoreGitChanges, | ||
skipPrompts, | ||
}; | ||
} | ||
async function promptForMissingCliArguments(existingArgs, env) { | ||
export async function promptForMissingCliArguments(existingArgs, env) { | ||
const defaults = await getDefaultCliArguments(existingArgs, env); | ||
const definedExistingArgs = Object.assign({}, existingArgs); | ||
Object.keys(definedExistingArgs).forEach((k) => definedExistingArgs[k] === undefined && delete definedExistingArgs[k]); | ||
// todo: find better way to skip prompts, but still show inquirer results | ||
if (existingArgs.skipPrompts) { | ||
return Object.assign(Object.assign({}, defaults), definedExistingArgs); | ||
// todo: find way to auto submit inquirer form instead, | ||
// so the default values are still outputted to the console. | ||
if (existingArgs.yes) { | ||
return buildFinalArgs(defaults, {}); | ||
} | ||
// todo: handle artifact directory | ||
// todo: maybe handle subscription or @defer / @stream setup | ||
@@ -72,34 +39,49 @@ // todo: handle error | ||
{ | ||
name: "toolChain", | ||
message: "Select the toolchain your project is using", | ||
name: "toolchain", | ||
message: "Select the toolchain your project was setup with", | ||
type: "list", | ||
default: defaults.toolChain, | ||
choices: ToolChainOptions, | ||
when: !existingArgs.toolChain, | ||
default: defaults.toolchain, | ||
choices: ToolchainOptions, | ||
when: !existingArgs.toolchain, | ||
}, | ||
{ | ||
name: "useTypescript", | ||
name: "typescript", | ||
message: "Does your project use Typescript", | ||
type: "confirm", | ||
default: defaults.useTypescript, | ||
when: !existingArgs.useTypescript, | ||
default: defaults.typescript, | ||
when: !existingArgs.typescript, | ||
}, | ||
{ | ||
name: "schemaFilePath", | ||
name: "src", | ||
message: "Select the root directory of your application code", | ||
type: "input", | ||
default: defaults.src, | ||
validate: (input) => isValidSrcDirectory(input, env.projectRootDirectory), | ||
when: !existingArgs.src, | ||
}, | ||
{ | ||
name: "schemaFile", | ||
message: "Select the path to your GraphQL schema file", | ||
type: "input", | ||
default: defaults.schemaFilePath, | ||
// todo: also have this validation for cli args | ||
// todo: validate that it's inside project dir | ||
validate: (input) => { | ||
if (!input.endsWith(".graphql")) { | ||
return `File needs to end in ${chalk.green(".graphql")}`; | ||
} | ||
return true; | ||
default: (answers) => { | ||
var _a, _b; | ||
return getProjectSchemaFilepath((_a = answers.toolchain) !== null && _a !== void 0 ? _a : defaults.toolchain, (_b = answers.src) !== null && _b !== void 0 ? _b : defaults.src); | ||
}, | ||
when: !existingArgs.schemaFilePath, | ||
validate: (input) => isValidSchemaPath(input, env.projectRootDirectory), | ||
when: !existingArgs.schemaFile, | ||
}, | ||
{ | ||
name: "artifactDirectory", | ||
message: "Select, if needed, a directory to place all Relay artifacts in", | ||
type: "input", | ||
default: defaults.artifactDirectory || undefined, | ||
validate: (input, answers) => { | ||
var _a; | ||
return isValidArtifactDirectory(input, (_a = answers === null || answers === void 0 ? void 0 : answers.toolchain) !== null && _a !== void 0 ? _a : defaults.toolchain, env.projectRootDirectory); | ||
}, | ||
when: !existingArgs.artifactDirectory, | ||
}, | ||
{ | ||
name: "packageManager", | ||
message: "Select the package manager you wish to use to install packages", | ||
message: "Select the package manager to install packages with", | ||
type: "list", | ||
@@ -112,25 +94,13 @@ default: defaults.packageManager, | ||
console.log(); | ||
return Object.assign(Object.assign({}, answers), definedExistingArgs); | ||
return buildFinalArgs(defaults, answers); | ||
} | ||
async function getPackageDetails(env) { | ||
const ownPackageJsonFile = path.join(env.ownPackageDirectory, PACKAGE_FILE); | ||
const packageJsonContent = await fs.readFile(ownPackageJsonFile, "utf8"); | ||
const packageJson = JSON.parse(packageJsonContent); | ||
const name = packageJson === null || packageJson === void 0 ? void 0 : packageJson.name; | ||
if (!name) { | ||
throw new Error(`Could not determine name in ${ownPackageJsonFile}`); | ||
} | ||
const version = packageJson === null || packageJson === void 0 ? void 0 : packageJson.version; | ||
if (!version) { | ||
throw new Error(`Could not determine version in ${ownPackageJsonFile}`); | ||
} | ||
const description = packageJson === null || packageJson === void 0 ? void 0 : packageJson.description; | ||
if (!description) { | ||
throw new Error(`Could not determine description in ${ownPackageJsonFile}`); | ||
} | ||
return { name, version, description }; | ||
function buildFinalArgs(defaults, answers) { | ||
const combined = Object.assign(Object.assign({}, defaults), answers); | ||
return Object.assign(Object.assign({}, combined), { src: prettifyPath(combined.src), schemaFile: prettifyPath(combined.schemaFile), artifactDirectory: combined.artifactDirectory | ||
? prettifyPath(combined.artifactDirectory) | ||
: "" }); | ||
} | ||
function tryParsePackageManager(rawInput) { | ||
function parsePackageManager(rawInput) { | ||
if (!rawInput) { | ||
return undefined; | ||
return null; | ||
} | ||
@@ -147,7 +117,7 @@ const input = getNormalizedCliString(rawInput); | ||
} | ||
throw invalidArgError("--package-manager", input, PackageManagerOptions); | ||
throw invalidArgError(PACKAGE_MANAGER_ARG, input, PackageManagerOptions); | ||
} | ||
function tryParseToolChain(rawInput) { | ||
function parseToolChain(rawInput) { | ||
if (!rawInput) { | ||
return undefined; | ||
return null; | ||
} | ||
@@ -164,3 +134,3 @@ const input = getNormalizedCliString(rawInput); | ||
} | ||
throw invalidArgError("--toolchain", input, ToolChainOptions); | ||
throw invalidArgError(TOOLCHAIN_ARG, input, ToolchainOptions); | ||
} | ||
@@ -167,0 +137,0 @@ function invalidArgError(arg, value, validValues) { |
export const TS_CONFIG_FILE = "tsconfig.json"; | ||
export const PACKAGE_FILE = "package.json"; | ||
export const NEXTJS_CONFIG_FILE = "next.config.js"; | ||
export const VITE_CONFIG_FILE_NO_EXT = "vite.config"; | ||
export const VITE_SRC_DIR = "src"; | ||
export const VITE_MAIN_FILE_NO_EXT = "./src/main"; | ||
export const VITE_RELAY_ENV_FILE_NO_EXT = "./src/RelayEnvironment"; | ||
export const TYPESCRIPT_PACKAGE = "typescript"; | ||
export const BABEL_RELAY_PACKAGE = "babel-plugin-relay"; | ||
export const REACT_RELAY_PACKAGE = "react-relay"; | ||
// todo: remove branch, once released | ||
export const VITE_RELAY_PACKAGE = "vite-plugin-relay@https://github.com/tobias-tengler/vite-plugin-relay#tte/fix-plugin"; | ||
// CLI ARGUMENTS | ||
export const TOOLCHAIN_ARG = "--toolchain"; | ||
export const TYPESCRIPT_ARG = "--typescript"; | ||
export const SCHEMA_FILE_ARG = "--schema-file"; | ||
export const SRC_DIR_ARG = "--src"; | ||
export const ARTIFACT_DIR_ARG = "--artifact-directory"; | ||
export const PACKAGE_MANAGER_ARG = "--package-manager"; | ||
export const IGNORE_GIT_CHANGES_ARG = "--ignore-git-changes"; | ||
export const YES_ARG = "--yes"; | ||
export const VERSION_ARG = "--version"; |
@@ -1,148 +0,15 @@ | ||
import { exec, execSync, spawn } from "child_process"; | ||
import path from "path"; | ||
import fs from "fs/promises"; | ||
import { NEXTJS_CONFIG_FILE, TS_CONFIG_FILE, TYPESCRIPT_PACKAGE, } from "./consts.js"; | ||
import { BABEL_RELAY_PACKAGE, PACKAGE_FILE, VITE_RELAY_PACKAGE, } from "./consts.js"; | ||
import glob from "glob"; | ||
import t from "@babel/types"; | ||
import { parse } from "@babel/parser"; | ||
import generate from "@babel/generator"; | ||
import { format } from "prettier"; | ||
import chalk from "chalk"; | ||
export function printInvalidArg(arg, validationMsg, value) { | ||
printError(`Invalid ${arg} specified: ${value} ${chalk.dim(validationMsg)}`); | ||
} | ||
export function printError(message) { | ||
console.log(chalk.red("✖") + " " + message); | ||
} | ||
export function insertNamedImport(path, importName, packageName) { | ||
const importIdentifier = t.identifier(importName); | ||
const program = path.findParent((p) => p.isProgram()); | ||
const existingImport = program.node.body.find((s) => t.isImportDeclaration(s) && | ||
s.source.value === packageName && | ||
s.specifiers.some((sp) => t.isImportSpecifier(sp) && sp.local.name === importName)); | ||
if (!!existingImport) { | ||
return importIdentifier; | ||
} | ||
const importDeclaration = t.importDeclaration([t.importSpecifier(t.cloneNode(importIdentifier), importIdentifier)], t.stringLiteral(packageName)); | ||
// Insert import at start of file. | ||
program.node.body.unshift(importDeclaration); | ||
return importIdentifier; | ||
export function highlight(message) { | ||
return chalk.cyan.bold(message); | ||
} | ||
export function parseAst(code) { | ||
return parse(code, { | ||
sourceType: "module", | ||
plugins: ["typescript", "jsx"], | ||
}); | ||
} | ||
export function printAst(ast, oldCode) { | ||
const newCode = generate.default(ast, { retainLines: true }, oldCode).code; | ||
return format(newCode, { | ||
bracketSameLine: false, | ||
parser: "babel-ts", | ||
}); | ||
} | ||
export function getRelayCompilerLanguage(useTypescript) { | ||
if (useTypescript) { | ||
return "typescript"; | ||
} | ||
else { | ||
return "javascript"; | ||
} | ||
} | ||
export async function hasUnsavedGitChanges(dir) { | ||
const isPartOfGitRepo = await new Promise((resolve) => { | ||
exec("git rev-parse --is-inside-work-tree", { cwd: dir }, (error) => { | ||
resolve(!error); | ||
}); | ||
}); | ||
if (!isPartOfGitRepo) { | ||
return false; | ||
} | ||
const hasUnsavedChanges = await new Promise((resolve) => { | ||
exec("git status --porcelain", { cwd: dir }, (error, stdout) => { | ||
resolve(!!error || !!stdout); | ||
}); | ||
}); | ||
return hasUnsavedChanges; | ||
} | ||
export async function getProjectToolChain(projectRootDirectory) { | ||
const nextjsConfigFile = await findFileInDirectory(projectRootDirectory, NEXTJS_CONFIG_FILE); | ||
if (!!nextjsConfigFile) { | ||
return "next"; | ||
} | ||
const viteConfigFiles = await searchFilesInDirectory(projectRootDirectory, "vite.config.*"); | ||
if (viteConfigFiles.some((f) => !!f)) { | ||
return "vite"; | ||
} | ||
return "cra"; | ||
} | ||
export async function doesProjectUseTypescript(projectRootDirectory, manager) { | ||
const tsconfigFile = await findFileInDirectory(projectRootDirectory, TS_CONFIG_FILE); | ||
if (!!tsconfigFile) { | ||
return true; | ||
} | ||
const typescriptInstalled = await isNpmPackageInstalled(manager, projectRootDirectory, TYPESCRIPT_PACKAGE); | ||
if (typescriptInstalled) { | ||
return true; | ||
} | ||
return false; | ||
} | ||
export async function isNpmPackageInstalled(manager, projectRootDirectory, packageName) { | ||
const command = manager; | ||
const useYarn = manager === "yarn"; | ||
let args = []; | ||
if (useYarn) { | ||
args = ["list", "--depth=0", "--pattern", packageName]; | ||
} | ||
else { | ||
args = ["ls", "--depth=0", packageName]; | ||
} | ||
return new Promise((resolve) => { | ||
const child = spawn(command, args, { | ||
// stdio: "inherit", | ||
cwd: projectRootDirectory, | ||
env: process.env, | ||
shell: true, | ||
}); | ||
child.stdout.on("data", (data) => { | ||
const stringData = data.toString(); | ||
if (stringData.includes(packageName)) { | ||
resolve(true); | ||
} | ||
}); | ||
child.on("close", () => { | ||
resolve(false); | ||
}); | ||
}); | ||
} | ||
export async function getProjectPackageManager(projectRootDirectory) { | ||
try { | ||
const userAgent = process.env.npm_config_user_agent; | ||
// If this script is being run by a specific manager, | ||
// we use this mananger. | ||
if (userAgent) { | ||
if (userAgent.startsWith("yarn")) { | ||
return "yarn"; | ||
} | ||
else if (userAgent.startsWith("pnpm")) { | ||
return "pnpm"; | ||
} | ||
} | ||
try { | ||
execSync("yarn --version", { stdio: "ignore" }); | ||
const hasLockfile = await findFileInDirectory(projectRootDirectory, "yarn.lock"); | ||
if (hasLockfile) { | ||
// Yarn is installed and the project contains a yarn.lock file. | ||
return "yarn"; | ||
} | ||
} | ||
catch (_a) { | ||
execSync("pnpm --version", { stdio: "ignore" }); | ||
const hasLockfile = await findFileInDirectory(projectRootDirectory, "pnpm-lock.yaml"); | ||
if (hasLockfile) { | ||
// pnpm is installed and the project contains a pnpm-lock.yml file. | ||
return "pnpm"; | ||
} | ||
} | ||
} | ||
catch (_b) { } | ||
return "npm"; | ||
} | ||
export async function traverseUpToFindFile(directory, filename) { | ||
@@ -196,1 +63,121 @@ let currentDirectory = directory; | ||
} | ||
export async function getPackageDetails(env) { | ||
const ownPackageJsonFile = path.join(env.ownPackageDirectory, PACKAGE_FILE); | ||
const packageJsonContent = await fs.readFile(ownPackageJsonFile, "utf8"); | ||
const packageJson = JSON.parse(packageJsonContent); | ||
const name = packageJson === null || packageJson === void 0 ? void 0 : packageJson.name; | ||
if (!name) { | ||
throw new Error(`Could not determine name in ${ownPackageJsonFile}`); | ||
} | ||
const version = packageJson === null || packageJson === void 0 ? void 0 : packageJson.version; | ||
if (!version) { | ||
throw new Error(`Could not determine version in ${ownPackageJsonFile}`); | ||
} | ||
const description = packageJson === null || packageJson === void 0 ? void 0 : packageJson.description; | ||
if (!description) { | ||
throw new Error(`Could not determine description in ${ownPackageJsonFile}`); | ||
} | ||
return { name, version, description }; | ||
} | ||
export function getRelayDevDependencies(toolchain, useTypescript) { | ||
const relayDevDep = ["relay-compiler"]; | ||
if (useTypescript) { | ||
relayDevDep.push("@types/react-relay"); | ||
relayDevDep.push("@types/relay-runtime"); | ||
} | ||
if (toolchain === "cra" || toolchain === "vite") { | ||
relayDevDep.push(BABEL_RELAY_PACKAGE); | ||
} | ||
if (toolchain === "vite") { | ||
relayDevDep.push(VITE_RELAY_PACKAGE); | ||
} | ||
return relayDevDep; | ||
} | ||
export function getSpecifiedProperties(obj) { | ||
const keys = Object.keys(obj); | ||
const newObj = {}; | ||
for (const key of keys) { | ||
if (obj[key] === null) { | ||
continue; | ||
} | ||
newObj[key] = obj[key]; | ||
} | ||
return newObj; | ||
} | ||
export function isSubDirectory(parent, dir) { | ||
const relative = path.relative(parent, dir); | ||
return !relative.startsWith("..") && !path.isAbsolute(relative); | ||
} | ||
export function prettifyRelativePath(parentPath, childPath) { | ||
const relativePath = path.relative(parentPath, childPath); | ||
return prettifyPath(relativePath); | ||
} | ||
export function prettifyPath(input) { | ||
let normalizedPath = normalizePath(input); | ||
if (!normalizedPath.startsWith("..") && !normalizedPath.startsWith("./")) { | ||
normalizedPath = "./" + normalizedPath; | ||
} | ||
return normalizedPath; | ||
} | ||
export function normalizePath(input) { | ||
return input.split(path.sep).join("/"); | ||
} | ||
export function removeExtension(filename) { | ||
return filename.substring(0, filename.lastIndexOf(".")) || filename; | ||
} | ||
export function getRelayCompilerLanguage(useTypescript) { | ||
if (useTypescript) { | ||
return "typescript"; | ||
} | ||
else { | ||
return "javascript"; | ||
} | ||
} | ||
export async function getToolchainSettings(env, args) { | ||
if (args.toolchain === "vite") { | ||
const configFilename = "vite.config" + (args.typescript ? ".ts" : ".js"); | ||
const configFilepath = await findFileInDirectory(env.projectRootDirectory, configFilename); | ||
if (!configFilepath) { | ||
throw new Error(`${configFilename} not found`); | ||
} | ||
const mainFilename = "main" + (args.typescript ? ".tsx" : ".jsx"); | ||
const searchDirectory = path.join(env.projectRootDirectory, "src"); | ||
const mainFilepath = await findFileInDirectory(searchDirectory, mainFilename); | ||
if (!mainFilepath) { | ||
throw new Error(`${mainFilename} not found`); | ||
} | ||
return { | ||
configFilepath, | ||
mainFilepath, | ||
}; | ||
} | ||
else if (args.toolchain === "next") { | ||
const configFilename = "next.config.js"; | ||
const configFilepath = await findFileInDirectory(env.projectRootDirectory, configFilename); | ||
if (!configFilepath) { | ||
throw new Error(`${configFilename} not found`); | ||
} | ||
const mainFilename = "_app" + (args.typescript ? ".tsx" : ".jsx"); | ||
const searchDirectory = path.join(env.projectRootDirectory, "pages"); | ||
const mainFilepath = await findFileInDirectory(searchDirectory, mainFilename); | ||
if (!mainFilepath) { | ||
throw new Error(`${mainFilename} not found`); | ||
} | ||
return { | ||
configFilepath, | ||
mainFilepath, | ||
}; | ||
} | ||
else { | ||
const mainFilename = "index" + (args.typescript ? ".tsx" : ".jsx"); | ||
const searchDirectory = path.join(env.projectRootDirectory, "src"); | ||
const mainFilepath = await findFileInDirectory(searchDirectory, mainFilename); | ||
if (!mainFilepath) { | ||
throw new Error(`${mainFilename} not found`); | ||
} | ||
return { | ||
configFilepath: "", | ||
mainFilepath, | ||
}; | ||
} | ||
} |
import chalk from "chalk"; | ||
export class TaskBase { | ||
setSpinner(spinner) { | ||
init(spinner) { | ||
this.spinner = spinner; | ||
@@ -8,3 +8,4 @@ } | ||
var _a; | ||
(_a = this.spinner) === null || _a === void 0 ? void 0 : _a.warn(this.spinner.text + " " + chalk.dim("[Skipped: " + message + "]")); | ||
const reason = message ? ": " + message : undefined; | ||
(_a = this.spinner) === null || _a === void 0 ? void 0 : _a.warn(this.spinner.text + " " + chalk.dim(`[Skipped${reason}]`)); | ||
throw new TaskSkippedError(); | ||
@@ -11,0 +12,0 @@ } |
@@ -8,5 +8,8 @@ import ora from "ora"; | ||
async run() { | ||
for (const { title, task } of this.taskDefs) { | ||
for (const { title, task, when } of this.taskDefs) { | ||
if (when === false) { | ||
continue; | ||
} | ||
const spinner = ora(title); | ||
task.setSpinner(spinner); | ||
task.init(spinner); | ||
try { | ||
@@ -13,0 +16,0 @@ spinner.start(); |
import { TaskBase } from "../TaskBase.js"; | ||
import fs from "fs/promises"; | ||
import { getRelayCompilerLanguage } from "../helpers.js"; | ||
export class AddRelayConfigurationTask extends TaskBase { | ||
@@ -24,6 +23,6 @@ constructor(settings) { | ||
// Add "relay" configuration section | ||
packageJson["relay"] = { | ||
src: this.settings.srcDirectory, | ||
language: getRelayCompilerLanguage(this.settings.useTypescript), | ||
schema: this.settings.schemaFilePath, | ||
const relayConfig = { | ||
src: this.settings.src, | ||
language: this.settings.compilerLanguage, | ||
schema: this.settings.schemaFile, | ||
exclude: [ | ||
@@ -35,2 +34,6 @@ "**/node_modules/**", | ||
}; | ||
if (this.settings.artifactDirectory) { | ||
relayConfig.artifactDirectory = this.settings.artifactDirectory; | ||
} | ||
packageJson["relay"] = relayConfig; | ||
} | ||
@@ -37,0 +40,0 @@ const serializedPackageJson = JSON.stringify(packageJson, null, 2); |
@@ -1,2 +0,2 @@ | ||
export const ToolChainOptions = ["cra", "next", "vite"]; | ||
export const ToolchainOptions = ["cra", "next", "vite"]; | ||
export const PackageManagerOptions = ["npm", "yarn", "pnpm"]; |
{ | ||
"name": "@tobiastengler/create-relay-app", | ||
"version": "0.0.0-experimental-e6e505b7a", | ||
"version": "0.0.0-experimental-ea7ea2081", | ||
"description": "Easy configuration of Relay for existing projects", | ||
@@ -40,3 +40,2 @@ "homepage": "https://github.com/tobias-tengler/create-relay-app#readme", | ||
"@types/prettier": "^2.7.0", | ||
"fs-extra": "^10.1.0", | ||
"tslib": "^2.4.0", | ||
@@ -51,2 +50,3 @@ "typescript": "^4.7.4" | ||
"commander": "^9.4.0", | ||
"fs-extra": "^10.1.0", | ||
"glob": "^8.0.3", | ||
@@ -53,0 +53,0 @@ "inquirer": "^9.1.0", |
@@ -12,2 +12,6 @@ <h1 align="center" style="font-size: 30px;">create-relay-app</h1> | ||
<p align="center"> | ||
<img src="./showcase.gif" alt="Showcase" /> | ||
</p> | ||
## Motivation | ||
@@ -30,3 +34,3 @@ | ||
```bash | ||
npx @tobiastengler/create-relay-app | ||
npx @tobiastengler/create-relay-app@experimental | ||
``` | ||
@@ -68,3 +72,3 @@ | ||
### -s, --schema-file <path> | ||
### -f, --schema-file <path> | ||
@@ -83,2 +87,30 @@ Specifies the location of the GraphQL schema file inside of your project directory. | ||
### -s, --src <path> | ||
Specifies the source directory of your project, where the Relay compiler will be run on. | ||
_Possible values_ | ||
A relative path to a directory inside the root directory of your project. | ||
_Example_ | ||
```bash | ||
--src ./src | ||
``` | ||
### -a, --artifact-directory <path> | ||
Specifies a directory, where all artifacts generated by the Relay compiler will be placed. | ||
_Possible values_ | ||
A relative path to a directory inside the root directory of your project. | ||
_Example_ | ||
```bash | ||
--artifact-directory ./src/__generated__ | ||
``` | ||
### -p, --package-manager <manager> | ||
@@ -118,7 +150,7 @@ | ||
If there is a `next.config.js` file in the root directory of your project, we assume it's a _Next.js_ project, unless specified otherwise. | ||
If the `next` package is installed, we assume it's a _Next.js_ project, unless specified otherwise. | ||
If there is a `vite.config.js` or `vite.config.ts` file in the root directory of your project, we assume it's a _Vite.js_ project, unless specified otherwise. | ||
If the `vite` package is installed, we assume it's a _Vite.js_ project, unless specified otherwise. | ||
If none of the files above are matched, we assume it's a _Create React App_ project, unless specified otherwise. | ||
Otherwise, we assume it's a _Create React App_ project, unless specified otherwise. | ||
@@ -135,10 +167,18 @@ ### --typescript | ||
- `yarn`, if `yarn` is installed and a `yarn.lock` file exists at the root of the project. | ||
- `pnpm`, if `pnpm` is installed and a `pnpm-lock.yml` file exists at the root of the project. | ||
- `yarn`, if `yarn` is installed **and** a `yarn.lock` file exists at the root of the project. | ||
- `pnpm`, if `pnpm` is installed **and** a `pnpm-lock.yml` file exists at the root of the project. | ||
- `npm` in all other cases. | ||
### --src | ||
If the [toolchain](#t---toolchain-lttoolchaingt) is `next`, `./pages`, otherwise `./src`, unless specified otherwise. | ||
### --schema-file | ||
`./schema.graphql`, unless specified otherwise. | ||
If the [toolchain](#t---toolchain-lttoolchaingt) is `next`, `./src/schema.graphql`, otherwise the value of [--src](#s---src-ltpathgt) joined with `schema.graphql`, unless specified otherwise. | ||
### --artifact-directory | ||
If the [toolchain](#t---toolchain-lttoolchaingt) is `next`, `./__generated__`, otherwise it's not specified, unless specified otherwise. | ||
### --yes | ||
@@ -145,0 +185,0 @@ |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
56606
10
22
1164
185
10
11
4
+ Addedfs-extra@^10.1.0
+ Addedfs-extra@10.1.0(transitive)
+ Addedgraceful-fs@4.2.11(transitive)
+ Addedjsonfile@6.1.0(transitive)
+ Addeduniversalify@2.0.1(transitive)