@tobiastengler/create-relay-app
Advanced tools
Comparing version 0.0.0-experimental-c4ec8b793 to 0.0.0-experimental-e6e505b7a
131
dist/bin.js
#!/usr/bin/env node | ||
import path from "path"; | ||
import path, { dirname } from "path"; | ||
import { TaskRunner } from "./TaskRunner.js"; | ||
@@ -7,25 +7,39 @@ import { AddGraphQlSchemaFileTask } from "./tasks/AddGraphQlSchemaFileTask.js"; | ||
import { AddRelayConfigurationTask } from "./tasks/AddRelayConfigurationTask.js"; | ||
import inquirer from "inquirer"; | ||
import { ToolChainOptions, LanguageOptions, PackageManagerOptions, } from "./types.js"; | ||
import { InstallNpmPackagesTask } from "./tasks/InstallNpmPackagesTask.js"; | ||
import { AddRelayPluginConfigurationTask } from "./tasks/AddRelayPluginConfigurationTask.js"; | ||
import { AddRelayEnvironmentTask } from "./tasks/AddRelayEnvironmentTask.js"; | ||
import { traverseUpToFindFile, getPackageManagerToUse } from "./helpers.js"; | ||
import { traverseUpToFindFile, hasUnsavedGitChanges, printError, } from "./helpers.js"; | ||
import { exit } from "process"; | ||
import { BABEL_RELAY_PACKAGE, PACKAGE_FILE, VITE_RELAY_PACKAGE, } from "./consts.js"; | ||
import { fileURLToPath } from "url"; | ||
import { getCliArguments } from "./cli.js"; | ||
const distDirectory = dirname(fileURLToPath(import.meta.url)); | ||
const ownPackageDirectory = path.join(distDirectory, ".."); | ||
const workingDirectory = process.cwd(); | ||
// FIND package.json FILE | ||
const packageJsonFile = await traverseUpToFindFile(workingDirectory, "package.json"); | ||
const packageJsonFile = await traverseUpToFindFile(workingDirectory, PACKAGE_FILE); | ||
if (!packageJsonFile) { | ||
// package.json file is missing. | ||
throw new Error("package.json file is missing"); | ||
printError(`Could not find a ${chalk.cyan.bold(PACKAGE_FILE)} in the ${chalk.cyan.bold(workingDirectory)} directory.`); | ||
exit(1); | ||
} | ||
const projectRootDirectory = path.dirname(packageJsonFile); | ||
// CHECK REPO FOR UNSAVED CHANGES | ||
// const hasUnsavedChanges = await hasUnsavedGitChanges(projectDir); | ||
// if (hasUnsavedChanges) { | ||
// throw new Error("Project has unsaved changes"); | ||
// } | ||
const settings = await readProjectSettings(); | ||
console.log(); | ||
const envArguments = { | ||
workingDirectory, | ||
ownPackageDirectory, | ||
packageJsonFile, | ||
projectRootDirectory, | ||
}; | ||
// 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); | ||
} | ||
} | ||
const dependencies = ["react-relay"]; | ||
const devDependencies = getRelayDevDependencies(settings.toolchain, settings.language); | ||
const devDependencies = getRelayDevDependencies(settings.toolChain, settings.useTypescript); | ||
const runner = new TaskRunner([ | ||
@@ -36,3 +50,3 @@ { | ||
.join(" ")}`, | ||
task: new InstallNpmPackagesTask(dependencies, settings.packageManager, projectRootDirectory), | ||
task: new InstallNpmPackagesTask(dependencies, false, settings), | ||
}, | ||
@@ -43,81 +57,48 @@ { | ||
.join(" ")}`, | ||
task: new InstallNpmPackagesTask(devDependencies, settings.packageManager, projectRootDirectory, true), | ||
task: new InstallNpmPackagesTask(devDependencies, true, settings), | ||
}, | ||
{ | ||
title: "Add Relay configuration to package.json", | ||
task: new AddRelayConfigurationTask(packageJsonFile, settings.schemaFilePath, settings.language), | ||
task: new AddRelayConfigurationTask(settings), | ||
}, | ||
{ | ||
title: "Add Relay plugin configuration", | ||
task: new AddRelayPluginConfigurationTask(projectRootDirectory, settings.toolchain, settings.language), | ||
task: new AddRelayPluginConfigurationTask(settings), | ||
}, | ||
{ | ||
title: "Add Relay environment", | ||
task: new AddRelayEnvironmentTask(), | ||
task: new AddRelayEnvironmentTask(settings), | ||
}, | ||
{ | ||
title: `Generate GraphQL schema file (${chalk.cyan.bold(settings.schemaFilePath)})`, | ||
task: new AddGraphQlSchemaFileTask(settings.schemaFilePath), | ||
task: new AddGraphQlSchemaFileTask(settings), | ||
}, | ||
]); | ||
await runner.run(); | ||
try { | ||
await runner.run(); | ||
} | ||
catch (_a) { | ||
console.log(); | ||
printError("Some of the tasks unexpectedly failed."); | ||
exit(1); | ||
} | ||
console.log(); | ||
console.log(chalk.italic.bold("### NEXT STEPS ###")); | ||
console.log(chalk.yellow.bold("Next steps:")); | ||
console.log(`1. Replace ${chalk.cyan.bold(settings.schemaFilePath)} with your own GraphQL schema file.`); | ||
console.log(`2. Replace the HOST variable in the RelayEnvironment.ts 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(); | ||
// todo: add integration tests | ||
async function readProjectSettings() { | ||
const defaultPackageManager = getPackageManagerToUse(); | ||
// todo: handle artifact directory | ||
// todo: handle error | ||
return await inquirer.prompt([ | ||
{ | ||
name: "toolchain", | ||
message: "Select the toolchain your project is using", | ||
type: "list", | ||
default: 0, | ||
choices: ToolChainOptions, | ||
}, | ||
{ | ||
name: "language", | ||
message: "Select the language of your project", | ||
type: "list", | ||
default: 0, | ||
choices: LanguageOptions, | ||
}, | ||
{ | ||
// todo: validate that it's inside project dir and ends in .graphql | ||
name: "schemaFilePath", | ||
message: "Select the path to your GraphQL schema file", | ||
type: "input", | ||
default: "./src/schema.graphql", | ||
validate: (input) => { | ||
if (!input.endsWith(".graphql")) { | ||
return `File needs to end in ${chalk.green(".graphql")}`; | ||
} | ||
return true; | ||
}, | ||
}, | ||
{ | ||
name: "packageManager", | ||
message: "Select the package manager you wish to use to install packages", | ||
type: "list", | ||
default: defaultPackageManager, | ||
choices: PackageManagerOptions, | ||
}, | ||
]); | ||
} | ||
export function getRelayDevDependencies(toolChain, language) { | ||
let relayDevDep = ["relay-compiler"]; | ||
if (toolChain === "Create-React-App") { | ||
relayDevDep = relayDevDep.concat(["babel-plugin-relay", "graphql"]); | ||
function getRelayDevDependencies(toolChain, useTypescript) { | ||
const relayDevDep = ["relay-compiler"]; | ||
if (useTypescript) { | ||
relayDevDep.push("@types/react-relay"); | ||
relayDevDep.push("@types/relay-runtime"); | ||
} | ||
else if (toolChain === "Vite") { | ||
relayDevDep.push("vite-plugin-relay"); | ||
if (toolChain === "cra" || toolChain === "vite") { | ||
relayDevDep.push(BABEL_RELAY_PACKAGE); | ||
} | ||
if (language === "Typescript") { | ||
relayDevDep = relayDevDep.concat(["@types/react-relay"]); | ||
if (toolChain === "vite") { | ||
relayDevDep.push(VITE_RELAY_PACKAGE); | ||
} | ||
return relayDevDep; | ||
} |
@@ -1,4 +0,49 @@ | ||
import { exec, execSync } from "child_process"; | ||
import { exec, execSync, spawn } from "child_process"; | ||
import path from "path"; | ||
import { promises as fs } from "fs"; | ||
import fs from "fs/promises"; | ||
import { NEXTJS_CONFIG_FILE, TS_CONFIG_FILE, TYPESCRIPT_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 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 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) { | ||
@@ -20,5 +65,57 @@ const isPartOfGitRepo = await new Promise((resolve) => { | ||
} | ||
export function getPackageManagerToUse() { | ||
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) { | ||
@@ -34,12 +131,19 @@ if (userAgent.startsWith("yarn")) { | ||
execSync("yarn --version", { stdio: "ignore" }); | ||
return "yarn"; | ||
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" }); | ||
return "pnpm"; | ||
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"; | ||
} | ||
catch (_b) { } | ||
return "npm"; | ||
} | ||
@@ -77,1 +181,18 @@ export async function traverseUpToFindFile(directory, filename) { | ||
} | ||
export async function searchFilesInDirectory(directory, pattern) { | ||
return new Promise((resolve) => { | ||
try { | ||
glob(pattern, { cwd: directory }, (error, matches) => { | ||
if (error || !matches || !matches.some((m) => !!m)) { | ||
resolve([]); | ||
} | ||
else { | ||
resolve(matches); | ||
} | ||
}); | ||
} | ||
catch (_a) { | ||
resolve([]); | ||
} | ||
}); | ||
} |
@@ -21,9 +21,12 @@ import ora from "ora"; | ||
let errorMsg; | ||
if (!!error && typeof error === "string") { | ||
errorMsg = error; | ||
if (!!error) { | ||
if (typeof error === "string") { | ||
errorMsg = error; | ||
} | ||
else if (error instanceof Error) { | ||
errorMsg = error.message; | ||
} | ||
} | ||
errorMsg !== null && errorMsg !== void 0 ? errorMsg : (errorMsg = "Unexpected error"); | ||
task.error(errorMsg); | ||
// throw error; | ||
break; | ||
} | ||
@@ -30,0 +33,0 @@ } |
@@ -1,2 +0,3 @@ | ||
import { existsSync, promises as fs } from "fs"; | ||
import { existsSync } from "fs"; | ||
import fs from "fs/promises"; | ||
import { EOL } from "os"; | ||
@@ -6,12 +7,12 @@ import { TaskBase } from "../TaskBase.js"; | ||
export class AddGraphQlSchemaFileTask extends TaskBase { | ||
constructor(schemaFilePath) { | ||
constructor(settings) { | ||
super(); | ||
this.schemaFilePath = schemaFilePath; | ||
this.settings = settings; | ||
} | ||
async run() { | ||
if (existsSync(this.schemaFilePath)) { | ||
if (existsSync(this.settings.schemaFilePath)) { | ||
this.skip("File exists"); | ||
} | ||
await fs.writeFile(this.schemaFilePath, schemaGraphQLContent, "utf-8"); | ||
await fs.writeFile(this.settings.schemaFilePath, schemaGraphQLContent, "utf-8"); | ||
} | ||
} |
import { TaskBase } from "../TaskBase.js"; | ||
import { promises as fs } from "fs"; | ||
import fs from "fs/promises"; | ||
import { getRelayCompilerLanguage } from "../helpers.js"; | ||
export class AddRelayConfigurationTask extends TaskBase { | ||
constructor(packageJsonFile, schemaFilePath, language) { | ||
constructor(settings) { | ||
super(); | ||
this.packageJsonFile = packageJsonFile; | ||
this.schemaFilePath = schemaFilePath; | ||
this.language = language; | ||
this.settings = settings; | ||
} | ||
async run() { | ||
var _a; | ||
const compilerLanguage = getCompilerLanguage(this.language); | ||
// todo: handle error | ||
const packageJsonContent = await fs.readFile(this.packageJsonFile, { | ||
const packageJsonContent = await fs.readFile(this.settings.packageJsonFile, { | ||
encoding: "utf-8", | ||
@@ -27,6 +25,5 @@ }); | ||
packageJson["relay"] = { | ||
// todo: this should probably be different for the Next.js project | ||
src: "./src", | ||
language: compilerLanguage, | ||
schema: this.schemaFilePath, | ||
src: this.settings.srcDirectory, | ||
language: getRelayCompilerLanguage(this.settings.useTypescript), | ||
schema: this.settings.schemaFilePath, | ||
exclude: [ | ||
@@ -41,14 +38,4 @@ "**/node_modules/**", | ||
// todo: handle error | ||
await fs.writeFile(this.packageJsonFile, serializedPackageJson, "utf-8"); | ||
await fs.writeFile(this.settings.packageJsonFile, serializedPackageJson, "utf-8"); | ||
} | ||
} | ||
function getCompilerLanguage(language) { | ||
switch (language) { | ||
case "Typescript": | ||
return "typescript"; | ||
case "Flow": | ||
return "flow"; | ||
default: | ||
return "javascript"; | ||
} | ||
} |
@@ -0,6 +1,77 @@ | ||
import traverse from "@babel/traverse"; | ||
import fs from "fs-extra"; | ||
import path from "path"; | ||
import { VITE_MAIN_FILE_NO_EXT, VITE_RELAY_ENV_FILE_NO_EXT, } from "../consts.js"; | ||
import { findFileInDirectory, insertNamedImport, parseAst, printAst, } from "../helpers.js"; | ||
import { TaskBase } from "../TaskBase.js"; | ||
import t from "@babel/types"; | ||
export class AddRelayEnvironmentTask extends TaskBase { | ||
run() { | ||
return Promise.resolve(); | ||
constructor(settings) { | ||
super(); | ||
this.settings = settings; | ||
} | ||
async run() { | ||
switch (this.settings.toolChain) { | ||
case "vite": | ||
await this.configureVite(); | ||
break; | ||
// todo: implement CRA and Next.js | ||
default: | ||
throw new Error("Unsupported toolchain"); | ||
} | ||
} | ||
async configureVite() { | ||
await this.addRelayEnvironmentFile(VITE_RELAY_ENV_FILE_NO_EXT); | ||
const relativeMainFilepath = VITE_MAIN_FILE_NO_EXT + (this.settings.useTypescript ? ".tsx" : ".jsx"); | ||
const searchDirectory = path.dirname(path.join(this.settings.projectRootDirectory, relativeMainFilepath)); | ||
const mainFilename = path.basename(relativeMainFilepath); | ||
const mainFilePath = await findFileInDirectory(searchDirectory, mainFilename); | ||
if (!mainFilePath) { | ||
throw new Error(`${relativeMainFilepath} not found`); | ||
} | ||
const mainCode = await fs.readFile(mainFilePath, "utf-8"); | ||
const ast = parseAst(mainCode); | ||
const RELAY_ENV_PROVIDER = "RelayEnvironmentProvider"; | ||
traverse.default(ast, { | ||
JSXElement: (path) => { | ||
const parent = path.parentPath.node; | ||
// Find ReactDOM.render(...) | ||
if (!t.isCallExpression(parent) || | ||
!t.isMemberExpression(parent.callee) || | ||
!t.isIdentifier(parent.callee.property) || | ||
parent.callee.property.name !== "render") { | ||
return; | ||
} | ||
const envId = insertNamedImport(path, "RelayEnvironment", "./RelayEnvironment"); | ||
const envProviderId = t.jsxIdentifier(insertNamedImport(path, RELAY_ENV_PROVIDER, "react-relay").name); | ||
if (t.isJSXIdentifier(path.node.openingElement.name) && | ||
path.node.openingElement.name.name === envProviderId.name) { | ||
// JSX has already been wrapped. | ||
return; | ||
} | ||
const test = t.jsxExpressionContainer(envId); | ||
// Wrap JSX inside render() into RelayEnvironmentProvider. | ||
path.replaceWith(t.jsxElement(t.jsxOpeningElement(envProviderId, [ | ||
t.jsxAttribute(t.jsxIdentifier("environment"), test), | ||
]), t.jsxClosingElement(envProviderId), [path.node])); | ||
path.skip(); | ||
}, | ||
}); | ||
const updatedCode = printAst(ast, mainCode); | ||
await fs.writeFile(mainFilePath, updatedCode, "utf-8"); | ||
} | ||
async addRelayEnvironmentFile(filepathNoExt) { | ||
const relativeRelayEnvFilepath = filepathNoExt + (this.settings.useTypescript ? ".ts" : ".js"); | ||
const relayEnvFilepath = path.join(this.settings.projectRootDirectory, relativeRelayEnvFilepath); | ||
let srcFile; | ||
if (this.settings.useTypescript) { | ||
srcFile = "./assets/env_ts"; | ||
} | ||
else { | ||
srcFile = "./assets/env"; | ||
} | ||
const srcFilepath = path.join(this.settings.ownPackageDirectory, srcFile); | ||
// todo: handle error | ||
await fs.copyFile(srcFilepath, relayEnvFilepath); | ||
} | ||
} |
@@ -1,15 +0,20 @@ | ||
import { findFileInDirectory } from "../helpers.js"; | ||
import { findFileInDirectory, getRelayCompilerLanguage, parseAst, printAst, } from "../helpers.js"; | ||
import { TaskBase } from "../TaskBase.js"; | ||
import fs from "fs/promises"; | ||
import traverse from "@babel/traverse"; | ||
import t from "@babel/types"; | ||
import { NEXTJS_CONFIG_FILE, VITE_CONFIG_FILE_NO_EXT } from "../consts.js"; | ||
export class AddRelayPluginConfigurationTask extends TaskBase { | ||
constructor(workingDirectory, toolChain, language) { | ||
constructor(settings) { | ||
super(); | ||
this.workingDirectory = workingDirectory; | ||
this.toolChain = toolChain; | ||
this.language = language; | ||
this.settings = settings; | ||
} | ||
async run() { | ||
switch (this.toolChain) { | ||
case "Vite": | ||
switch (this.settings.toolChain) { | ||
case "vite": | ||
await this.configureVite(); | ||
break; | ||
case "next": | ||
await this.configureNext(); | ||
break; | ||
default: | ||
@@ -19,10 +24,136 @@ throw new Error("Unsupported toolchain"); | ||
} | ||
async configureNext() { | ||
const configFilepath = await findFileInDirectory(this.settings.projectRootDirectory, NEXTJS_CONFIG_FILE); | ||
if (!configFilepath) { | ||
throw new Error(`${NEXTJS_CONFIG_FILE} not found`); | ||
} | ||
// todo: handle errors | ||
const configCode = await fs.readFile(configFilepath, "utf-8"); | ||
const ast = parseAst(configCode); | ||
traverse.default(ast, { | ||
AssignmentExpression: (path) => { | ||
const node = path.node; | ||
// We are looking for module.exports = ???. | ||
if (node.operator !== "=" || | ||
!t.isMemberExpression(node.left) || | ||
!t.isIdentifier(node.left.object) || | ||
!t.isIdentifier(node.left.property) || | ||
node.left.object.name !== "module" || | ||
node.left.property.name !== "exports") { | ||
throw new Error(`Expected to find a module.exports assignment that exports the Next.js configuration from ${NEXTJS_CONFIG_FILE}.`); | ||
} | ||
let objExp; | ||
// We are looking for the object expression | ||
// that was assigned to module.exports. | ||
if (t.isIdentifier(node.right)) { | ||
// The export is linked to a variable, | ||
// so we need to resolve the variable declaration. | ||
const binding = path.scope.getBinding(node.right.name); | ||
if (!binding || | ||
!t.isVariableDeclarator(binding.path.node) || | ||
!t.isObjectExpression(binding.path.node.init)) { | ||
throw new Error(`module.exports in ${NEXTJS_CONFIG_FILE} references a variable, which is not a valid object definition.`); | ||
} | ||
objExp = binding.path.node.init; | ||
} | ||
else if (t.isObjectExpression(node.right)) { | ||
objExp = node.right; | ||
} | ||
else { | ||
throw new Error(`Expected to find a module.exports assignment that exports the Next.js configuration from ${NEXTJS_CONFIG_FILE}.`); | ||
} | ||
// We are creating or getting the 'compiler' property. | ||
let compilerProperty = objExp.properties.find((p) => t.isObjectProperty(p) && | ||
t.isIdentifier(p.key) && | ||
p.key.name === "compiler"); | ||
if (!compilerProperty) { | ||
compilerProperty = t.objectProperty(t.identifier("compiler"), t.objectExpression([])); | ||
objExp.properties.push(compilerProperty); | ||
} | ||
if (!t.isObjectExpression(compilerProperty.value)) { | ||
throw new Error(`Could not create or get a "compiler" property on the Next.js configuration object in ${NEXTJS_CONFIG_FILE}.`); | ||
} | ||
const relayProperty = compilerProperty.value.properties.find((p) => t.isObjectProperty(p) && | ||
t.isIdentifier(p.key) && | ||
p.key.name === "relay"); | ||
if (!!relayProperty) { | ||
// A "relay" property already exists. | ||
return; | ||
} | ||
// Add the "relay" property to the "compiler" property object. | ||
compilerProperty.value.properties.push(t.objectProperty(t.identifier("relay"), t.objectExpression([ | ||
t.objectProperty(t.identifier("src"), t.stringLiteral(this.settings.srcDirectory)), | ||
t.objectProperty(t.identifier("language"), t.stringLiteral(getRelayCompilerLanguage(this.settings.useTypescript))), | ||
// todo: add artifact directory | ||
]))); | ||
}, | ||
}); | ||
const updatedConfigCode = printAst(ast, configCode); | ||
await fs.writeFile(configFilepath, updatedConfigCode, "utf-8"); | ||
} | ||
async configureVite() { | ||
const configFilename = "vite.config" + (this.language === "Typescript" ? ".ts" : ".js"); | ||
const configFilepath = await findFileInDirectory(this.workingDirectory, configFilename); | ||
const relayImportName = "relay"; | ||
const configFilename = VITE_CONFIG_FILE_NO_EXT + (this.settings.useTypescript ? ".ts" : ".js"); | ||
const configFilepath = await findFileInDirectory(this.settings.projectRootDirectory, configFilename); | ||
if (!configFilepath) { | ||
throw new Error(`Could not find ${configFilename} in ${this.workingDirectory}`); | ||
throw new Error(`${configFilename} not found`); | ||
} | ||
// todo: parse config and modify | ||
// todo: handle errors | ||
const configCode = await fs.readFile(configFilepath, "utf-8"); | ||
const ast = parseAst(configCode); | ||
traverse.default(ast, { | ||
Program: (path) => { | ||
const hasRelayImport = path | ||
.get("body") | ||
.some((s) => s.isImportDeclaration() && | ||
s.node.specifiers.some((sp) => t.isImportDefaultSpecifier(sp) && | ||
sp.local.name === relayImportName)); | ||
if (hasRelayImport) { | ||
// Import already exists. | ||
return; | ||
} | ||
const importDeclaration = t.importDeclaration([t.importDefaultSpecifier(t.identifier(relayImportName))], | ||
// todo: replace with VITE_RELAY_PACKAGE, | ||
// once it no longer has the explict version | ||
t.stringLiteral("vite-plugin-relay")); | ||
// Insert import at start of file. | ||
path.node.body.unshift(importDeclaration); | ||
}, | ||
ExportDefaultDeclaration: (path) => { | ||
const node = path.node; | ||
// Find export default defineConfig(???) | ||
if (!t.isCallExpression(node.declaration) || | ||
node.declaration.arguments.length < 1 || | ||
!t.isIdentifier(node.declaration.callee) || | ||
node.declaration.callee.name !== "defineConfig") { | ||
throw new Error(`Expected a export default defineConfig({}) in ${configFilename}.`); | ||
} | ||
const arg = node.declaration.arguments[0]; | ||
if (!t.isObjectExpression(arg)) { | ||
throw new Error(`Expected a export default defineConfig({}) in ${configFilename}.`); | ||
} | ||
// We are creating or getting the 'plugins' property. | ||
let pluginsProperty = arg.properties.find((p) => t.isObjectProperty(p) && | ||
t.isIdentifier(p.key) && | ||
p.key.name === "plugins"); | ||
if (!pluginsProperty) { | ||
pluginsProperty = t.objectProperty(t.identifier("plugins"), t.arrayExpression([])); | ||
arg.properties.push(pluginsProperty); | ||
} | ||
if (!t.isArrayExpression(pluginsProperty.value)) { | ||
throw new Error(`Could not create or get a "plugins" property on the Vite configuration object in ${configFilename}.`); | ||
} | ||
const vitePlugins = pluginsProperty.value.elements; | ||
if (vitePlugins.some((p) => t.isIdentifier(p) && p.name === relayImportName)) { | ||
// A "relay" entry already exists. | ||
return; | ||
} | ||
const relayPlugin = t.identifier(relayImportName); | ||
// Add the "relay" import to the "plugins". | ||
vitePlugins.push(relayPlugin); | ||
}, | ||
}); | ||
const updatedConfigCode = printAst(ast, configCode); | ||
await fs.writeFile(configFilepath, updatedConfigCode, "utf-8"); | ||
} | ||
} |
@@ -0,16 +1,16 @@ | ||
import { spawn } from "child_process"; | ||
import { TaskBase } from "../TaskBase.js"; | ||
export class InstallNpmPackagesTask extends TaskBase { | ||
constructor(packages, manager, workingDirectory, isDevDependency = false) { | ||
constructor(packages, isDevDependency, settings) { | ||
super(); | ||
this.packages = packages; | ||
this.manager = manager; | ||
this.workingDirectory = workingDirectory; | ||
this.isDevDependency = isDevDependency; | ||
this.settings = settings; | ||
} | ||
async run() { | ||
const command = this.manager; | ||
const useYarn = this.manager === "yarn"; | ||
const command = this.settings.packageManager; | ||
const useYarn = command === "yarn"; | ||
let args = []; | ||
if (useYarn) { | ||
args = ["add", "--exact", "--cwd", this.workingDirectory]; | ||
args = ["add", "--exact", "--cwd", this.settings.projectRootDirectory]; | ||
if (this.isDevDependency) { | ||
@@ -29,19 +29,18 @@ args.push("--dev"); | ||
} | ||
return Promise.resolve(); | ||
// return new Promise((resolve, reject) => { | ||
// const child = spawn(command, args, { | ||
// // stdio: "inherit", | ||
// cwd: this.workingDirectory, | ||
// env: process.env, | ||
// shell: true, | ||
// }); | ||
// child.on("close", (code) => { | ||
// if (code !== 0) { | ||
// reject({ command: `${command} ${args.join(" ")}` }); | ||
// return; | ||
// } | ||
// resolve(); | ||
// }); | ||
// }); | ||
return new Promise((resolve, reject) => { | ||
const child = spawn(command, args, { | ||
// stdio: "inherit", | ||
cwd: this.settings.projectRootDirectory, | ||
env: process.env, | ||
shell: true, | ||
}); | ||
child.on("close", (code) => { | ||
if (code !== 0) { | ||
reject({ command: `${command} ${args.join(" ")}` }); | ||
return; | ||
} | ||
resolve(); | ||
}); | ||
}); | ||
} | ||
} |
@@ -1,7 +0,2 @@ | ||
export const ToolChainOptions = [ | ||
"Create-React-App", | ||
"Next.js", | ||
"Vite", | ||
]; | ||
export const LanguageOptions = ["Typescript", "JavaScript", "Flow"]; | ||
export const ToolChainOptions = ["cra", "next", "vite"]; | ||
export const PackageManagerOptions = ["npm", "yarn", "pnpm"]; |
{ | ||
"name": "@tobiastengler/create-relay-app", | ||
"version": "0.0.0-experimental-c4ec8b793", | ||
"version": "0.0.0-experimental-e6e505b7a", | ||
"description": "Easy configuration of Relay for existing projects", | ||
"homepage": "https://github.com/tobias-tengler/create-relay-app#readme", | ||
"license": "MIT", | ||
"author": "tobias-tengler", | ||
"author": { | ||
"name": "Tobias Tengler", | ||
"url": "https://github.com/tobias-tengler", | ||
"email": "contact.tobiastengler@gmail.com" | ||
}, | ||
"repository": { | ||
"type": "git", | ||
"url": "git+https://github.com/tobias-tengler/create-relay-app.git" | ||
}, | ||
"bugs": { | ||
"url": "https://github.com/tobias-tengler/create-relay-app/issues" | ||
}, | ||
"keywords": [], | ||
"files": [ | ||
"dist" | ||
"./dist", | ||
"./assets" | ||
], | ||
@@ -17,5 +32,11 @@ "bin": "./dist/bin.js", | ||
"devDependencies": { | ||
"@types/babel__generator": "^7.6.4", | ||
"@types/babel__traverse": "^7.18.0", | ||
"@types/fs-extra": "^9.0.13", | ||
"@types/glob": "^7.2.0", | ||
"@types/inquirer": "^9.0.0", | ||
"@types/node": "^18.6.5", | ||
"@types/ora": "^3.2.0", | ||
"@types/prettier": "^2.7.0", | ||
"fs-extra": "^10.1.0", | ||
"tslib": "^2.4.0", | ||
@@ -25,6 +46,12 @@ "typescript": "^4.7.4" | ||
"dependencies": { | ||
"@babel/generator": "^7.18.12", | ||
"@babel/parser": "^7.18.11", | ||
"@babel/traverse": "^7.18.11", | ||
"chalk": "^5.0.1", | ||
"commander": "^9.4.0", | ||
"glob": "^8.0.3", | ||
"inquirer": "^9.1.0", | ||
"ora": "^6.1.2" | ||
"ora": "^6.1.2", | ||
"prettier": "^2.7.1" | ||
} | ||
} |
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
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 2 instances in 1 package
No README
QualityPackage does not have a README. This may indicate a failed publish or a low quality package.
Found 1 instance in 1 package
No bug tracker
MaintenancePackage does not have a linked bug tracker in package.json.
Found 1 instance in 1 package
No repository
Supply chain riskPackage does not have a linked source code repository. Without this field, a package will have no reference to the location of the source code use to generate the package.
Found 1 instance in 1 package
No website
QualityPackage does not have a website.
Found 1 instance in 1 package
43441
17
868
0
1
1
145
9
11
10
3
+ Added@babel/generator@^7.18.12
+ Added@babel/parser@^7.18.11
+ Added@babel/traverse@^7.18.11
+ Addedcommander@^9.4.0
+ Addedglob@^8.0.3
+ Addedprettier@^2.7.1
+ Added@babel/code-frame@7.26.2(transitive)
+ Added@babel/generator@7.26.5(transitive)
+ Added@babel/helper-string-parser@7.25.9(transitive)
+ Added@babel/helper-validator-identifier@7.25.9(transitive)
+ Added@babel/parser@7.26.5(transitive)
+ Added@babel/template@7.25.9(transitive)
+ Added@babel/traverse@7.26.5(transitive)
+ Added@babel/types@7.26.5(transitive)
+ Added@jridgewell/gen-mapping@0.3.8(transitive)
+ Added@jridgewell/resolve-uri@3.1.2(transitive)
+ Added@jridgewell/set-array@1.2.1(transitive)
+ Added@jridgewell/sourcemap-codec@1.5.0(transitive)
+ Added@jridgewell/trace-mapping@0.3.25(transitive)
+ Addedbalanced-match@1.0.2(transitive)
+ Addedbrace-expansion@2.0.1(transitive)
+ Addedcommander@9.5.0(transitive)
+ Addeddebug@4.4.0(transitive)
+ Addedfs.realpath@1.0.0(transitive)
+ Addedglob@8.1.0(transitive)
+ Addedglobals@11.12.0(transitive)
+ Addedinflight@1.0.6(transitive)
+ Addedjs-tokens@4.0.0(transitive)
+ Addedjsesc@3.1.0(transitive)
+ Addedminimatch@5.1.6(transitive)
+ Addedms@2.1.3(transitive)
+ Addedonce@1.4.0(transitive)
+ Addedpicocolors@1.1.1(transitive)
+ Addedprettier@2.8.8(transitive)
+ Addedwrappy@1.0.2(transitive)