diff --git a/TODO b/TODO index 262b2f1668..2a4026da27 100644 --- a/TODO +++ b/TODO @@ -6,7 +6,7 @@ - [ ] csv-to-pg should use inqurirer - [ ] get this PR from launchql-gen https://github.com/launchql/launchql-gen/pull/19 - [ ] Docker in workflow needs extensions directories - +- [ ] LaunchQL class for the project structures — control files, modules, paths, etc. Good next steps: diff --git a/__fixtures__/sqitch/broken/launchql.json b/__fixtures__/sqitch/broken/launchql.json index e69de29bb2..5dd168dc4a 100644 --- a/__fixtures__/sqitch/broken/launchql.json +++ b/__fixtures__/sqitch/broken/launchql.json @@ -0,0 +1,5 @@ +{ + "packages": [ + "packages/*" + ] + } \ No newline at end of file diff --git a/__fixtures__/sqitch/launchql/launchql.json b/__fixtures__/sqitch/launchql/launchql.json index e69de29bb2..0fe5ad2887 100644 --- a/__fixtures__/sqitch/launchql/launchql.json +++ b/__fixtures__/sqitch/launchql/launchql.json @@ -0,0 +1,5 @@ +{ + "packages": [ + "packages/*" + ] +} \ No newline at end of file diff --git a/__fixtures__/sqitch/publish/launchql.json b/__fixtures__/sqitch/publish/launchql.json new file mode 100644 index 0000000000..0fe5ad2887 --- /dev/null +++ b/__fixtures__/sqitch/publish/launchql.json @@ -0,0 +1,5 @@ +{ + "packages": [ + "packages/*" + ] +} \ No newline at end of file diff --git a/__fixtures__/sqitch/publish/skitch.json b/__fixtures__/sqitch/publish/skitch.json deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/package.json b/package.json index 91f3426377..cc809ca0f0 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,9 @@ "workspaces": [ "packages/*" ], + "engines": { + "node": ">=18.17.0" + }, "scripts": { "clean": "lerna run clean", "build": "lerna run build --stream", diff --git a/packages/cli/package.json b/packages/cli/package.json index d89d3717e8..0ead2cd831 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -37,13 +37,14 @@ "ts-node": "^10.9.2" }, "dependencies": { - "@launchql/migrate": "^2.0.0", "@launchql/explorer": "^2.0.0", + "@launchql/migrate": "^2.0.0", "@launchql/server": "^2.0.0", + "@launchql/templatizer": "^2.0.0", "@launchql/types": "^2.0.0", "chalk": "^4.1.0", "deepmerge": "^4.3.1", - "inquirerer": "^1.9.1", + "inquirerer": "^2.0.2", "js-yaml": "^4.1.0", "minimist": "^1.2.8", "shelljs": "^0.9.2" diff --git a/packages/cli/src/commands.ts b/packages/cli/src/commands.ts index 4f94434f94..46fad8eb15 100644 --- a/packages/cli/src/commands.ts +++ b/packages/cli/src/commands.ts @@ -9,6 +9,8 @@ import server from './commands/server'; import explorer from './commands/explorer'; import verify from './commands/verify'; import revert from './commands/revert'; +import init from './commands/init'; +import extension from './commands/extension'; export const commands = async (argv: Partial, prompter: Inquirerer, options: CLIOptions) => { if (argv.version || argv.v) { @@ -24,6 +26,17 @@ export const commands = async (argv: Partial, prompter: Inquirerer, process.exit(0); } + await prompter.prompt(argv, [ + { + type: 'text', + name: 'cwd', + message: 'Working directory', + required: false, + default: process.cwd(), + useDefault: true + } + ]); + switch (command) { case 'deploy': await deploy(newArgv, prompter, options); @@ -34,12 +47,19 @@ export const commands = async (argv: Partial, prompter: Inquirerer, case 'revert': await revert(newArgv, prompter, options); break; + case 'init': + await init(newArgv, prompter, options); + break; case 'server': await server(newArgv, prompter, options); break; case 'explorer': await explorer(newArgv, prompter, options); break; + case 'extension': + await extension(newArgv, prompter, options); + break; + break; default: console.error(`Unknown command: ${command}`); console.log(usageText); diff --git a/packages/cli/src/commands/deploy.ts b/packages/cli/src/commands/deploy.ts index c6dbb543be..b51ba6d0c2 100644 --- a/packages/cli/src/commands/deploy.ts +++ b/packages/cli/src/commands/deploy.ts @@ -11,15 +11,6 @@ export default async ( _options: CLIOptions ) => { const questions: Question[] = [ - // @ts-ignore - { - type: 'text', - name: 'dir', - message: chalk.cyan('Working directory'), - required: false, - default: false, - useDefault: true - }, { type: 'text', name: 'database', @@ -34,16 +25,16 @@ export default async ( } ]; - let { database, yes, recursive, createdb, dir } = await prompter.prompt(argv, questions); + let { database, yes, recursive, createdb, cwd } = await prompter.prompt(argv, questions); if (!yes) { console.log(chalk.gray('Operation cancelled.')); return; } - if (!dir) { - dir = process.cwd(); - console.log(chalk.gray(`Using current directory: ${dir}`)); + if (!cwd) { + cwd = process.cwd(); + console.log(chalk.gray(`Using current directory: ${cwd}`)); } if (createdb) { @@ -52,7 +43,7 @@ export default async ( } if (recursive) { - const modules = await listModules(dir); + const modules = await listModules(cwd); const mods = Object.keys(modules); if (!mods.length) { @@ -72,7 +63,7 @@ export default async ( ]); console.log(chalk.green(`Deploying project ${chalk.bold(project)} to database ${chalk.bold(database)}...`)); - await deploy(project, database, dir); + await deploy(project, database, cwd); console.log(chalk.green('Deployment complete.')); } else { console.log(chalk.green(`Running ${chalk.bold(`sqitch deploy db:pg:${database}`)}...`)); diff --git a/packages/cli/src/commands/extension.ts b/packages/cli/src/commands/extension.ts new file mode 100644 index 0000000000..8a66d17332 --- /dev/null +++ b/packages/cli/src/commands/extension.ts @@ -0,0 +1,40 @@ +import { CLIOptions, Inquirerer, OptionValue, Question } from 'inquirerer'; +import { ParsedArgs } from 'minimist'; +import { LaunchQLProject } from '@launchql/migrate'; + +export default async ( + argv: Partial, + prompter: Inquirerer, + _options: CLIOptions +) => { + const { cwd = process.cwd() } = argv; + + const project = new LaunchQLProject(cwd); + await project.init(); + + if (!project.isInModule()) { + throw new Error('You must run this command inside a LaunchQL module.'); + } + + const info = project.getModuleInfo(); + const installed = project.getRequiredModules(); + const available = project.getAvailableModules(); + const filtered = available.filter(name => name !== info.extname); + + const questions: Question[] = [ + { + name: 'modules', + message: 'Which modules does this one depend on?', + type: 'checkbox', + options: filtered, + default: installed + } + ]; + + const answers = await prompter.prompt(argv, questions); + const selected = (answers.modules as OptionValue[]) + .filter(opt => opt.selected) + .map(opt => opt.name); + + project.setModuleDependencies(selected); +}; diff --git a/packages/cli/src/commands/init/index.ts b/packages/cli/src/commands/init/index.ts new file mode 100644 index 0000000000..bb0de6665b --- /dev/null +++ b/packages/cli/src/commands/init/index.ts @@ -0,0 +1,23 @@ +import { CLIOptions, Inquirerer } from 'inquirerer'; +import runWorkspaceSetup from './workspace'; +import runModuleSetup from './module'; + +export default async ( + argv: Partial>, + prompter: Inquirerer, + _options: CLIOptions +) => { + return handlePromptFlow(argv, prompter); +}; + +async function handlePromptFlow(argv: Partial>, prompter: Inquirerer) { + const { workspace } = argv; + + switch (workspace) { + case true: + return runWorkspaceSetup(argv, prompter); + case false: + default: + return runModuleSetup(argv, prompter); + } +} diff --git a/packages/cli/src/commands/init/module.ts b/packages/cli/src/commands/init/module.ts new file mode 100644 index 0000000000..b17cddce90 --- /dev/null +++ b/packages/cli/src/commands/init/module.ts @@ -0,0 +1,154 @@ +import { Inquirerer, Question } from 'inquirerer'; +import chalk from 'chalk'; +import path from 'path'; +import fs, { writeFileSync, mkdirSync } from 'fs'; +import glob from 'glob'; +import { + writeRenderedTemplates, + moduleTemplate +} from '@launchql/templatizer'; +import { getAvailableExtensions, getExtensionInfo, getWorkspacePath, listModules, makePlan, sluggify, writeExtensionControlFile, writeExtensionMakefile } from '@launchql/migrate'; +import { exec } from 'shelljs'; + +function isInsideAllowedDirs(cwd: string, allowedDirs: string[]): boolean { + const resolvedCwd = path.resolve(cwd); + return allowedDirs.some(dir => resolvedCwd.startsWith(dir)); +} + +function loadAllowedDirs(workspacePath: string): string[] { + const configPath = path.join(workspacePath, 'launchql.json'); + if (!fs.existsSync(configPath)) { + throw new Error(`Missing launchql.json at workspace root: ${configPath}`); + } + + const config = JSON.parse(fs.readFileSync(configPath, 'utf8')); + const globs: string[] = config?.packages ?? []; + + const dirs = globs.flatMap(pattern => glob.sync(path.join(workspacePath, pattern))); + return dirs.map(dir => path.resolve(dir)); +} + +export default async function runModuleSetup(argv: Partial>, prompter: Inquirerer) { + const username = exec('git config --global user.name', { silent: true }).trim(); + const email = exec('git config --global user.email', { silent: true }).trim(); + const { cwd = process.cwd() } = argv; + + const workspacePath = await getWorkspacePath(cwd); + const isAtWorkspaceRoot = path.resolve(workspacePath) === path.resolve(cwd); + + if (!isAtWorkspaceRoot) { + const allowedDirs = loadAllowedDirs(workspacePath); + if (!isInsideAllowedDirs(cwd, allowedDirs)) { + console.error(chalk.red(`Error: You must be inside one of the workspace packages: ${allowedDirs.join(', ')}`)); + process.exit(1); + } + } + + + const moduleMap = await listModules(workspacePath); + const availExtensions = await getAvailableExtensions(moduleMap) + + + const moduleQuestions: Question[] = [ + // { + // name: 'USERFULLNAME', + // message: 'Enter author full name', + // required: true, + // default: username, + // useDefault: true, + // type: 'text', + // }, + // { + // name: 'USEREMAIL', + // message: 'Enter author email', + // required: true, + // default: email, + // useDefault: true, + // type: 'text', + // }, + { + name: 'MODULENAME', + message: 'Enter the module name', + required: true, + type: 'text', + }, + // { + // name: 'REPONAME', + // message: 'Enter the repository name', + // required: true, + // type: 'text', + // }, + // { + // name: 'USERNAME', + // message: 'Enter your GitHub username', + // required: true, + // type: 'text', + // }, + // { + // name: 'ACCESS', + // message: 'Module access?', + // required: true, + // type: 'autocomplete', + // options: ['public', 'restricted'], + // }, + { + name: 'extensions', + message: 'which extensions?', + options: availExtensions, + type: 'checkbox', + // default: ['plpgsql'], + // default: [{ + // name: 'plpgsql', + // value: 'plpgsql' + // }], + required: true + }, + ]; + + const answers = await prompter.prompt(argv, moduleQuestions); + const modName = sluggify(answers.MODULENAME); + let targetPath: string; + + if (isAtWorkspaceRoot) { + const packagesDir = path.join(cwd, 'packages'); + mkdirSync(packagesDir, { recursive: true }); + targetPath = path.join(packagesDir, modName); + mkdirSync(targetPath, { recursive: true }); + console.log(chalk.green(`Created module in workspace packages/: ${targetPath}`)); + } else { + targetPath = path.join(cwd, modName); + mkdirSync(targetPath, { recursive: true }); + console.log(chalk.green(`Created module: ${targetPath}`)); + } + + const cur = process.cwd(); + process.chdir(targetPath); + + writeRenderedTemplates(moduleTemplate, targetPath, { ...argv, ...answers }); + + const cmd = ['sqitch', 'init', modName, '--engine', 'pg'].join(' '); + exec(cmd.trim()); + + const plan = `%syntax-version=1.0.0 +%project=${modName} +%uri=${modName}`; + + writeFileSync(`${targetPath}/sqitch.plan`, plan); + + const info = await getExtensionInfo(targetPath); + + await writeExtensionMakefile( + info.Makefile, + modName, + '0.0.1' + ); + await writeExtensionControlFile( + info.controlFile, + modName, + answers.extensions.map((a:any)=>a.name), + '0.0.1' + ); + + process.chdir(cur); + return { ...argv, ...answers }; +} \ No newline at end of file diff --git a/packages/cli/src/commands/init/workspace.ts b/packages/cli/src/commands/init/workspace.ts new file mode 100644 index 0000000000..36a1f5e6c8 --- /dev/null +++ b/packages/cli/src/commands/init/workspace.ts @@ -0,0 +1,30 @@ +import { Inquirerer, Question } from 'inquirerer'; +import chalk from 'chalk'; +import path from 'path'; +import { mkdirSync } from 'fs'; +import { + writeRenderedTemplates, + workspaceTemplate +} from '@launchql/templatizer'; +import { sluggify } from '@launchql/migrate'; + +export default async function runWorkspaceSetup(argv: Partial>, prompter: Inquirerer) { + const workspaceQuestions: Question[] = [ + { + name: 'name', + message: 'Enter workspace name', + required: true, + type: 'text', + } + ]; + + const answers = await prompter.prompt(argv, workspaceQuestions); + const { cwd } = argv; + const targetPath = path.join(cwd!, sluggify(answers.name)); + + mkdirSync(targetPath, { recursive: true }); + console.log(chalk.green(`Created workspace directory: ${targetPath}`)); + + writeRenderedTemplates(workspaceTemplate, targetPath, { ...argv, ...answers }); + return { ...argv, ...answers, cwd: targetPath }; +} \ No newline at end of file diff --git a/packages/cli/src/commands/plan.ts b/packages/cli/src/commands/plan.ts new file mode 100644 index 0000000000..dfdc412a01 --- /dev/null +++ b/packages/cli/src/commands/plan.ts @@ -0,0 +1,35 @@ +import { CLIOptions, Inquirerer, Question } from 'inquirerer'; +import { getExtensionInfo, getModulePath, getWorkspacePath, makePlan } from '@launchql/migrate'; +import chalk from 'chalk'; + +export default async ( + argv: Partial>, + prompter: Inquirerer, + _options: CLIOptions +) => { + const questions: Question[] = [ + + ]; + + let { cwd } = await prompter.prompt(argv, questions); + + if (!cwd) { + cwd = process.cwd(); + console.log(chalk.gray(`Using current directory: ${cwd}`)); + } + + const workspacePath = await getWorkspacePath(cwd); + const modulePath = await getModulePath(cwd); + const info = await getExtensionInfo(modulePath); + + // apparently this is literally super dumb and simple? + const plan = await makePlan(workspacePath, modulePath, { + name: info.extname, + projects: true, + uri: info.extname + }); + + console.log(plan); + + return argv; +}; diff --git a/packages/cli/src/commands/revert.ts b/packages/cli/src/commands/revert.ts index 72e1e684b0..c514351975 100644 --- a/packages/cli/src/commands/revert.ts +++ b/packages/cli/src/commands/revert.ts @@ -12,7 +12,7 @@ export default async ( // @ts-ignore { type: 'text', - name: 'dir', + name: 'cwd', message: chalk.cyan('Working directory'), required: false, default: false, @@ -32,20 +32,20 @@ export default async ( } ]; - let { database, yes, recursive, dir } = await prompter.prompt(argv, questions); + let { database, yes, recursive, cwd } = await prompter.prompt(argv, questions); if (!yes) { console.log(chalk.gray('Operation cancelled.')); return; } - if (!dir) { - dir = process.cwd(); - console.log(chalk.gray(`Using current directory: ${dir}`)); + if (!cwd) { + cwd = process.cwd(); + console.log(chalk.gray(`Using current directory: ${cwd}`)); } if (recursive) { - const modules = await listModules(dir); + const modules = await listModules(cwd); const mods = Object.keys(modules); if (!mods.length) { @@ -65,7 +65,7 @@ export default async ( ]); console.log(chalk.green(`Reverting project ${chalk.bold(project)} on database ${chalk.bold(database)}...`)); - await revert(project, database, dir); + await revert(project, database, cwd); console.log(chalk.green('Revert complete.')); } else { console.log(chalk.green(`Running ${chalk.bold(`sqitch revert db:pg:${database} -y`)}...`)); diff --git a/packages/cli/src/commands/verify.ts b/packages/cli/src/commands/verify.ts index 29f2df93c9..1716276b53 100644 --- a/packages/cli/src/commands/verify.ts +++ b/packages/cli/src/commands/verify.ts @@ -12,7 +12,7 @@ export default async ( // @ts-ignore { type: 'text', - name: 'dir', + name: 'cwd', message: chalk.cyan('Working directory'), required: false, default: false, @@ -26,15 +26,15 @@ export default async ( } ]; - let { database, recursive, dir } = await prompter.prompt(argv, questions); + let { database, recursive, cwd } = await prompter.prompt(argv, questions); - if (!dir) { - dir = process.cwd(); - console.log(chalk.gray(`Using current directory: ${dir}`)); + if (!cwd) { + cwd = process.cwd(); + console.log(chalk.gray(`Using current directory: ${cwd}`)); } if (recursive) { - const modules = await listModules(dir); + const modules = await listModules(cwd); const mods = Object.keys(modules); if (!mods.length) { @@ -54,7 +54,7 @@ export default async ( ]); console.log(chalk.green(`Verifying project ${chalk.bold(project)} on database ${chalk.bold(database)}...`)); - await verify(project, database, dir); + await verify(project, database, cwd); console.log(chalk.green('Verify complete.')); } else { console.log(chalk.green(`Running ${chalk.bold(`sqitch verify db:pg:${database}`)}...`)); diff --git a/packages/codegen/src/index.ts b/packages/codegen/src/index.ts index c851a2b1e0..d1aed35b84 100644 --- a/packages/codegen/src/index.ts +++ b/packages/codegen/src/index.ts @@ -1,6 +1,5 @@ import { promises as fs } from 'fs'; import path from 'path'; -import rimraf from 'rimraf'; import { generateCodeTree } from './codegen/codegen'; import getIntrospectionRows, { GetIntrospectionRowsOptions } from './introspect'; @@ -21,7 +20,7 @@ import { DatabaseObject } from './types'; try { // Clean the output directory - await rimraf(outputDirectory); + await fs.rm(outputDirectory, { recursive: true, force: true }); console.log(`Cleaned output directory: ${outputDirectory}`); // Fetch introspection rows @@ -75,4 +74,4 @@ const writeGeneratedFiles = async ( console.error(`Failed to write files to ${outputDir}:`, error); throw error; } -}; +}; \ No newline at end of file diff --git a/packages/migrate/__tests__/class.launchql.meta.test.ts b/packages/migrate/__tests__/class.launchql.meta.test.ts new file mode 100644 index 0000000000..279728662e --- /dev/null +++ b/packages/migrate/__tests__/class.launchql.meta.test.ts @@ -0,0 +1,33 @@ +import fs from 'fs'; +import os from 'os'; +import path from 'path'; +import { LaunchQLProject } from '../src/class/launchql'; + +const fixture = (name: string) => + path.resolve(__dirname, '../../..', '__fixtures__', 'sqitch', name); + +it('writes module metadata files without modifying fixture', async () => { + const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'launchql-test-')); + const src = path.join(fixture('launchql'), 'packages', 'secrets'); + const dst = path.join(tempRoot, 'secrets'); + + fs.cpSync(src, dst, { recursive: true }); // copy module to temp dir + + const project = new LaunchQLProject(dst); + await project.init(); + + expect(() => + project.setModuleDependencies(['plpgsql', 'uuid-ossp', 'airpage', 'launchql', 'cosmology']) + ).not.toThrow(); + + const controlFile = fs.readFileSync( + path.join(dst, `${project.getModuleName()}.control`), + 'utf8' + ); + const makefile = fs.readFileSync(path.join(dst, 'Makefile'), 'utf8'); + + expect(controlFile).toContain('requires = \'plpgsql,uuid-ossp,airpage,launchql,cosmology\''); + expect(makefile).toContain('EXTENSION = secrets'); + + fs.rmSync(tempRoot, { recursive: true, force: true }); // cleanup +}); diff --git a/packages/migrate/__tests__/class.launchql.test.ts b/packages/migrate/__tests__/class.launchql.test.ts new file mode 100644 index 0000000000..693e6c4022 --- /dev/null +++ b/packages/migrate/__tests__/class.launchql.test.ts @@ -0,0 +1,162 @@ +import path from 'path'; +import { LaunchQLProject, ProjectContext } from '../src/class/launchql'; + +const fixture = (name: string) => + path.resolve(__dirname, '../../..', '__fixtures__', 'sqitch', name); + +describe('LaunchQLProject', () => { + it('detects workspace root context correctly', async () => { + const cwd = fixture('launchql'); + const project = new LaunchQLProject(cwd); + await project.init(); + + expect(project.getContext()).toBe(ProjectContext.Workspace); + expect(project.isInWorkspace()).toBe(true); + expect(project.isInModule()).toBe(false); + }); + + it('detects module inside workspace correctly', async () => { + const cwd = path.join(fixture('launchql'), 'packages', 'secrets'); + const project = new LaunchQLProject(cwd); + await project.init(); + + expect(project.getContext()).toBe(ProjectContext.ModuleInsideWorkspace); + expect(project.isInWorkspace()).toBe(false); + expect(project.isInModule()).toBe(true); + }); + + it('detects standalone module context', async () => { + const cwd = path.join(fixture('resolve'), 'basic'); + const project = new LaunchQLProject(cwd); + await project.init(); + + expect(project.getContext()).toBe(ProjectContext.Module); + expect(project.isInModule()).toBe(true); + expect(project.isInWorkspace()).toBe(false); + }); + + it('returns modules within workspace', async () => { + const cwd = fixture('launchql'); + const project = new LaunchQLProject(cwd); + await project.init(); + + const modules = await project.getModules(); + expect(Array.isArray(modules)).toBe(true); + expect(modules.length).toBeGreaterThan(0); + modules.forEach(mod => { + expect(mod.isInModule()).toBe(true); + }); + }); + + it('resolves module name from sqitch plan', async () => { + const cwd = path.join(fixture('launchql'), 'packages', 'secrets'); + const project = new LaunchQLProject(cwd); + await project.init(); + + const name = project.getModuleName(); + expect(typeof name).toBe('string'); + expect(name.length).toBeGreaterThan(0); + }); + + it('resolves module info with version and paths', async () => { + const cwd = path.join(fixture('launchql'), 'packages', 'secrets'); + const project = new LaunchQLProject(cwd); + await project.init(); + + const info = project.getModuleInfo(); + expect(info.extname).toBeTruthy(); + expect(info.version).toMatch(/\d+\.\d+\.\d+/); + expect(info.controlFile).toContain('.control'); + expect(info.Makefile).toContain('Makefile'); + }); + + it('gets required modules from control file', async () => { + const cwd = path.join(fixture('launchql'), 'packages', 'secrets'); + const project = new LaunchQLProject(cwd); + await project.init(); + + const deps = project.getRequiredModules(); + expect(Array.isArray(deps)).toBe(true); + expect(deps).toContain('plpgsql'); + }); + + it('gets latest change and version for a workspace module', async () => { + const cwd = fixture('launchql'); + const project = new LaunchQLProject(cwd); + await project.init(); + + const modules = project.getModuleMap(); + const modName = Object.keys(modules)[0]; + const { change, version } = project.getLatestChangeAndVersion(modName); + + expect(typeof change).toBe('string'); + expect(change.length).toBeGreaterThan(0); + expect(version).toMatch(/\d+\.\d+\.\d+/); + }); + + it('gets native and internal module dependencies', async () => { + const cwd = fixture('launchql'); + const project = new LaunchQLProject(cwd); + await project.init(); + + const modules = project.getModuleMap(); + const modName = Object.keys(modules)[0]; + + const { native, modules: deps } = project.getModuleDependencies(modName); + expect(native).toBeDefined(); + expect(deps).toBeDefined(); + }); + + it('clears internal caches correctly', async () => { + const cwd = path.join(fixture('launchql'), 'packages', 'secrets'); + const project = new LaunchQLProject(cwd); + await project.init(); + + const firstCall = project.getModuleInfo(); + project.clearCache(); + const secondCall = project.getModuleInfo(); + + expect(firstCall).not.toBe(secondCall); + }); + + it('throws on module info if not in module', async () => { + const cwd = fixture('launchql'); + const project = new LaunchQLProject(cwd); + await project.init(); + + expect(() => project.getModuleInfo()).toThrow('Not inside a module'); + }); + + it('gets latest change only from sqitch plan', async () => { + const cwd = fixture('launchql'); + const project = new LaunchQLProject(cwd); + await project.init(); + const modules = project.getModuleMap(); + const name = Object.keys(modules)[0]; + const change = project.getLatestChange(name); + expect(typeof change).toBe('string'); + expect(change.length).toBeGreaterThan(0); + }); + + it('gets dependency changes with versions for internal modules', async () => { + const cwd = fixture('launchql'); + const project = new LaunchQLProject(cwd); + await project.init(); + const name = Object.keys(project.getModuleMap())[0]; + const result = await project.getModuleDependencyChanges(name); + expect(result).toHaveProperty('native'); + expect(result).toHaveProperty('modules'); + expect(Array.isArray(result.modules)).toBe(true); + }); + + it('returns a list of available modules in workspace', async () => { + const cwd = fixture('launchql'); + const project = new LaunchQLProject(cwd); + await project.init(); + const modules = project.getAvailableModules(); + expect(Array.isArray(modules)).toBe(true); + expect(modules.length).toBeGreaterThan(0); + }); + + +}); diff --git a/packages/migrate/package.json b/packages/migrate/package.json index 764778dae2..773caf0d14 100644 --- a/packages/migrate/package.json +++ b/packages/migrate/package.json @@ -38,7 +38,6 @@ "case": "^1.6.3", "chalk": "^4.1.0", "glob": "^11.0.2", - "mkdirp": "^3.0.1", "pgsql-parser": "^13.16.0", "rimraf": "^6.0.1" } diff --git a/packages/migrate/src/class/launchql.ts b/packages/migrate/src/class/launchql.ts new file mode 100644 index 0000000000..2c937ba2b4 --- /dev/null +++ b/packages/migrate/src/class/launchql.ts @@ -0,0 +1,212 @@ +import fs from 'fs'; +import path from 'path'; +import * as glob from 'glob'; +import { walkUp } from '../utils'; + +import { + listModules, + latestChange, + latestChangeAndVersion, + getExtensionsAndModules, + getExtensionsAndModulesChanges, + ModuleMap +} from '../modules'; + +import { + getExtensionInfo, + writeExtensions, + getExtensionName, + getAvailableExtensions, + getInstalledExtensions, + ExtensionInfo, +} from '../extensions'; + +export enum ProjectContext { + Outside = 'outside', + Workspace = 'workspace-root', + Module = 'module', + ModuleInsideWorkspace = 'module-in-workspace', +} + +export class LaunchQLProject { + public readonly cwd: string; + public workspacePath?: string; + public modulePath?: string; + public config?: any; + public allowedDirs: string[] = []; + + private _moduleMap?: ModuleMap; + private _moduleInfo?: ExtensionInfo; + + constructor(cwd: string = process.cwd()) { + this.cwd = path.resolve(cwd); + } + + async init(): Promise { + this.workspacePath = await this.resolveLaunchqlPath(); + this.modulePath = await this.resolveSqitchPath(); + + if (this.workspacePath) { + this.config = this.loadConfig(); + this.allowedDirs = this.loadAllowedDirs(); + } + } + + private async resolveLaunchqlPath(): Promise { + try { + return await walkUp(this.cwd, 'launchql.json'); + } catch { + return undefined; + } + } + + private async resolveSqitchPath(): Promise { + try { + return await walkUp(this.cwd, 'sqitch.conf'); + } catch { + return undefined; + } + } + + private loadConfig(): any { + const configPath = path.join(this.workspacePath!, 'launchql.json'); + return JSON.parse(fs.readFileSync(configPath, 'utf8')); + } + + private loadAllowedDirs(): string[] { + const globs: string[] = this.config?.packages ?? []; + const dirs = globs.flatMap(pattern => + glob.sync(path.join(this.workspacePath!, pattern)) + ); + return dirs.map(dir => path.resolve(dir)); + } + + private ensureModule(): void { + if (!this.modulePath) throw new Error('Not inside a module'); + } + + getContext(): ProjectContext { + if (this.modulePath && this.workspacePath) { + const rel = path.relative(this.workspacePath, this.modulePath); + const nested = !rel.startsWith('..') && !path.isAbsolute(rel); + return nested ? ProjectContext.ModuleInsideWorkspace : ProjectContext.Module; + } + + if (this.modulePath) return ProjectContext.Module; + if (this.workspacePath) return ProjectContext.Workspace; + return ProjectContext.Outside; + } + + isInWorkspace(): boolean { + return this.getContext() === ProjectContext.Workspace; + } + + isInModule(): boolean { + return ( + this.getContext() === ProjectContext.Module || + this.getContext() === ProjectContext.ModuleInsideWorkspace + ); + } + + isInAllowedDir(): boolean { + return this.allowedDirs.some(dir => this.cwd.startsWith(dir)); + } + + getWorkspacePath(): string | undefined { + return this.workspacePath; + } + + getModulePath(): string | undefined { + return this.modulePath; + } + + clearCache() { + delete this._moduleInfo; + delete this._moduleMap; + } + + // ──────────────── Workspace-wide ──────────────── + + async getModules(): Promise { + if (!this.workspacePath || !this.config) return []; + + const dirs = this.loadAllowedDirs(); + const results: LaunchQLProject[] = []; + + for (const dir of dirs) { + const proj = new LaunchQLProject(dir); + await proj.init(); + if (proj.isInModule()) { + results.push(proj); + } + } + + return results; + } + + getModuleMap(): ModuleMap { + if (!this.workspacePath) return {}; + if (this._moduleMap) return this._moduleMap; + + this._moduleMap = listModules(this.workspacePath); + return this._moduleMap; + } + + getAvailableModules(): string[] { + const modules = this.getModuleMap(); + return getAvailableExtensions(modules); + } + + // ──────────────── Module-scoped ──────────────── + + getModuleInfo(): ExtensionInfo { + this.ensureModule(); + if (!this._moduleInfo) { + this._moduleInfo = getExtensionInfo(this.cwd); + } + return this._moduleInfo; + } + + getModuleName(): string { + this.ensureModule(); + return getExtensionName(this.cwd); + } + + getRequiredModules(): string[] { + this.ensureModule(); + const info = this.getModuleInfo(); + return getInstalledExtensions(info.controlFile); + } + + setModuleDependencies(modules: string[]): void { + this.ensureModule(); + writeExtensions(this.cwd, modules); + } + + // ──────────────── Dependency Analysis ──────────────── + + getLatestChange(moduleName: string): string { + const modules = this.getModuleMap(); + return latestChange(moduleName, modules, this.workspacePath!); + } + + getLatestChangeAndVersion(moduleName: string): { change: string; version: string } { + const modules = this.getModuleMap(); + return latestChangeAndVersion(moduleName, modules, this.workspacePath!); + } + + getModuleDependencies(moduleName: string): { native: string[]; modules: string[] } { + const modules = this.getModuleMap(); + const { native, sqitch } = getExtensionsAndModules(moduleName, modules); + return { native, modules: sqitch }; + } + + async getModuleDependencyChanges(moduleName: string): Promise<{ + native: string[]; + modules: { name: string; latest: string; version: string }[]; + }> { + const modules = this.getModuleMap(); + const { native, sqitch } = await getExtensionsAndModulesChanges(moduleName, modules, this.workspacePath!); + return { native, modules: sqitch }; + } +} diff --git a/packages/migrate/src/extensions.ts b/packages/migrate/src/extensions.ts index 18909fbfe4..7271859526 100644 --- a/packages/migrate/src/extensions.ts +++ b/packages/migrate/src/extensions.ts @@ -1,6 +1,7 @@ import { readFileSync, writeFileSync } from 'fs'; +import { ModuleMap } from './modules'; -interface ExtensionInfo { +export interface ExtensionInfo { extname: string; packageDir: string; version: string; @@ -13,7 +14,7 @@ interface ExtensionInfo { * Get the list of available extensions, including predefined core extensions. */ export const getAvailableExtensions = ( - modules: Record + modules: ModuleMap ): string[] => { const coreExtensions = [ 'address_standardizer', diff --git a/packages/migrate/src/index.ts b/packages/migrate/src/index.ts index 00d7a52dfa..2d450bcad9 100644 --- a/packages/migrate/src/index.ts +++ b/packages/migrate/src/index.ts @@ -9,4 +9,5 @@ export * from './transform'; export * from './utils'; export * from './deploy'; export * from './revert'; -export * from './verify'; \ No newline at end of file +export * from './verify'; +export * from './class/launchql'; \ No newline at end of file diff --git a/packages/migrate/src/modules.ts b/packages/migrate/src/modules.ts index 0dffca2b73..059ae77783 100644 --- a/packages/migrate/src/modules.ts +++ b/packages/migrate/src/modules.ts @@ -2,13 +2,13 @@ import { readFileSync } from 'fs'; import { sync as glob } from 'glob'; import { basename, dirname, relative } from 'path'; -interface Module { +export interface Module { path: string; requires: string[]; version: string; } -type ModuleMap = Record; +export type ModuleMap = Record; /** * Parse a .control file and extract its metadata. diff --git a/packages/migrate/src/package-utils.ts b/packages/migrate/src/package-utils.ts index dd8ed3e52f..5d0a43806e 100644 --- a/packages/migrate/src/package-utils.ts +++ b/packages/migrate/src/package-utils.ts @@ -1,6 +1,5 @@ import path from 'path'; -import mkdirp from 'mkdirp'; -import rimraf from 'rimraf'; +import { mkdirSync, rmSync } from 'fs'; import { sync as glob } from 'glob'; // import { init } from '@launchql/db-utils'; import Case from 'case'; @@ -38,7 +37,7 @@ export const preparePackage = async ({ }: PreparePackageOptions): Promise => { const curDir = process.cwd(); const sqitchDir = path.resolve(path.join(outdir, name)); - mkdirp.sync(sqitchDir); + mkdirSync(sqitchDir, { recursive: true }); process.chdir(sqitchDir); const plan = glob(path.join(sqitchDir, 'sqitch.plan')); @@ -51,9 +50,9 @@ export const preparePackage = async ({ // extensions // }); } else { - rimraf.sync(path.resolve(sqitchDir, 'deploy')); - rimraf.sync(path.resolve(sqitchDir, 'revert')); - rimraf.sync(path.resolve(sqitchDir, 'verify')); + rmSync(path.resolve(sqitchDir, 'deploy'), { recursive: true, force: true }); + rmSync(path.resolve(sqitchDir, 'revert'), { recursive: true, force: true }); + rmSync(path.resolve(sqitchDir, 'verify'), { recursive: true, force: true }); } process.chdir(curDir); @@ -82,4 +81,4 @@ export const makeReplacer = ({ schemas, name }: MakeReplacerOptions): ReplacerRe }; return { replacer, replace }; -}; +}; \ No newline at end of file diff --git a/packages/migrate/src/package.ts b/packages/migrate/src/package.ts index 16fba11c51..5a50252b83 100644 --- a/packages/migrate/src/package.ts +++ b/packages/migrate/src/package.ts @@ -1,8 +1,6 @@ -import { readFileSync, writeFileSync } from 'fs'; -import { sync as mkdirp } from 'mkdirp'; +import { readFileSync, writeFileSync, mkdirSync, rmSync } from 'fs'; import { relative } from 'path'; import { deparse, parse } from 'pgsql-parser'; -import { sync as rimraf } from 'rimraf'; import { getExtensionName } from './extensions'; import { resolve, resolveWithPlan } from './resolve'; @@ -115,8 +113,8 @@ export const writePackage = async ({ const outPath = extension ? `${packageDir}/sql` : `${packageDir}/out`; - rimraf(outPath); - mkdirp(outPath); + rmSync(outPath, { recursive: true, force: true }); + mkdirSync(outPath, { recursive: true }); if (extension) { // Update control file @@ -149,4 +147,4 @@ export const writePackage = async ({ const writePath = `${outPath}/${sqlFileName}`; writeFileSync(writePath, sql); console.log(`${relative(packageDir, writePath)} written`); -}; +}; \ No newline at end of file diff --git a/packages/migrate/src/paths.ts b/packages/migrate/src/paths.ts index b83f216820..fdff253634 100644 --- a/packages/migrate/src/paths.ts +++ b/packages/migrate/src/paths.ts @@ -12,9 +12,6 @@ const PROJECT_FILES = { * @returns A promise that resolves to the directory path containing `sqitch.conf`. */ export const sqitchPath = async (cwd: string = process.cwd()): Promise => { - if (process.env.SQITCH_PATH) { - return process.env.SQITCH_PATH; - } return walkUp(cwd, PROJECT_FILES.SQITCH); }; @@ -24,8 +21,31 @@ export const sqitchPath = async (cwd: string = process.cwd()): Promise = * @returns A promise that resolves to the directory path containing `launchql.json`. */ export const launchqlPath = async (cwd: string = process.cwd()): Promise => { - if (process.env.LAUNCHQL_PATH) { - return process.env.LAUNCHQL_PATH; - } return walkUp(cwd, PROJECT_FILES.LAUNCHQL); }; + +export const getWorkspacePath = async (cwd: string): Promise => { + let workspacePath: string; + + try { + workspacePath = await launchqlPath(cwd); + } catch (err) { + console.error('Error: You must be in a LaunchQL workspace. You can initialize one with the `--workspace` option.'); + process.exit(1); + } + + return workspacePath; +}; + +export const getModulePath = async (cwd: string): Promise => { + let pkgPath: string; + + try { + pkgPath = await sqitchPath(cwd); + } catch (err) { + console.error('Error: You must be in a LaunchQL module. You can initialize one with the `init` command.'); + process.exit(1); + } + + return pkgPath; +}; diff --git a/packages/migrate/src/sqitch.ts b/packages/migrate/src/sqitch.ts index ac4a56e75c..37a8fbabe1 100644 --- a/packages/migrate/src/sqitch.ts +++ b/packages/migrate/src/sqitch.ts @@ -1,6 +1,5 @@ import fs from 'fs'; import path from 'path'; -import mkdirp from 'mkdirp'; export interface SqitchRow { deploy: string; @@ -34,7 +33,7 @@ const writeDeploy = (row: SqitchRow, opts: WriteOptions): void => { const prefix = path.join(opts.outdir, opts.name, 'deploy'); const actualDir = path.resolve(prefix, dir); const actualFile = path.resolve(prefix, `${deploy}.sql`); - mkdirp.sync(actualDir); + fs.mkdirSync(actualDir, { recursive: true }); const content = `-- Deploy: ${deploy} to pg -- made with <3 @ launchql.com @@ -57,7 +56,7 @@ const writeVerify = (row: SqitchRow, opts: WriteOptions): void => { const prefix = path.join(opts.outdir, opts.name, 'verify'); const actualDir = path.resolve(prefix, dir); const actualFile = path.resolve(prefix, `${deploy}.sql`); - mkdirp.sync(actualDir); + fs.mkdirSync(actualDir, { recursive: true }); const content = opts.replacer(`-- Verify: ${deploy} on pg BEGIN; @@ -74,7 +73,7 @@ const writeRevert = (row: SqitchRow, opts: WriteOptions): void => { const prefix = path.join(opts.outdir, opts.name, 'revert'); const actualDir = path.resolve(prefix, dir); const actualFile = path.resolve(prefix, `${deploy}.sql`); - mkdirp.sync(actualDir); + fs.mkdirSync(actualDir, { recursive: true }); const content = `-- Revert: ${deploy} from pg BEGIN; @@ -87,7 +86,7 @@ COMMIT; export const writeSqitchPlan = (rows: SqitchRow[], opts: WriteOptions): void => { const dir = path.resolve(path.join(opts.outdir, opts.name)); - mkdirp.sync(dir); + fs.mkdirSync(dir, { recursive: true }); const date = (): string => '2017-08-11T08:11:51Z'; // stubbed timestamp @@ -114,4 +113,4 @@ ${rows `); fs.writeFileSync(path.join(dir, 'sqitch.plan'), plan); -}; +}; \ No newline at end of file diff --git a/packages/templatizer/package.json b/packages/templatizer/package.json index 3c46b16400..50b0a93370 100644 --- a/packages/templatizer/package.json +++ b/packages/templatizer/package.json @@ -1,6 +1,6 @@ { "name": "@launchql/templatizer", - "version": "0.0.1", + "version": "2.0.0", "author": "Dan Lynch ", "description": "make templates", "main": "index.js", @@ -32,7 +32,7 @@ "test:watch": "jest --watch" }, "devDependencies": { - "@types/node": "8.0.0", + "@types/node": "^20.12.7", "@types/glob": "^8.1.0", "@types/rimraf": "^4.0.5", "ts-node": "^10.9.2" @@ -41,7 +41,6 @@ "case": "^1.6.3", "chalk": "^4.1.0", "glob": "^11.0.2", - "mkdirp": "^3.0.1", "rimraf": "^6.0.1" } } \ No newline at end of file diff --git a/packages/templatizer/scripts/generate.d.ts b/packages/templatizer/scripts/generate.d.ts new file mode 100644 index 0000000000..cb0ff5c3b5 --- /dev/null +++ b/packages/templatizer/scripts/generate.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/packages/templatizer/scripts/generate.js b/packages/templatizer/scripts/generate.js new file mode 100644 index 0000000000..284ab722a0 --- /dev/null +++ b/packages/templatizer/scripts/generate.js @@ -0,0 +1,15 @@ +import { writeRenderedTemplates } from '../src/templatize/generateFromCompiled'; +import moduleTemplate from '../src/generated/module'; +import workspaceTemplate from '../src/generated/workspace'; +const vars = { + PACKAGE_IDENTIFIER: 'my-package', + ACCESS: 'public', + MODULEDESC: 'module description', + MODULENAME: 'my-module', + REPONAME: 'repository-name', + USEREMAIL: 'my@email.com', + USERFULLNAME: 'Dan Lynch', + USERNAME: 'pyramation' +}; +writeRenderedTemplates(moduleTemplate, './output-module', vars); +writeRenderedTemplates(workspaceTemplate, './output-workspace', vars); diff --git a/packages/templatizer/scripts/makeTemplates.d.ts b/packages/templatizer/scripts/makeTemplates.d.ts new file mode 100644 index 0000000000..cb0ff5c3b5 --- /dev/null +++ b/packages/templatizer/scripts/makeTemplates.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/packages/templatizer/scripts/makeTemplates.js b/packages/templatizer/scripts/makeTemplates.js new file mode 100644 index 0000000000..de369afd74 --- /dev/null +++ b/packages/templatizer/scripts/makeTemplates.js @@ -0,0 +1,10 @@ +import { resolve } from 'path'; +import { compileTemplatesToFunctions } from '../src/templatize/compileTemplatesToFunctions'; +import { writeCompiledTemplatesToFile } from '../src/templatize/writeCompiledTemplatesToFile'; +const workspaceDir = resolve(__dirname + '/../../../boilerplates/workspace'); +console.log(workspaceDir); +const compiled1 = compileTemplatesToFunctions(workspaceDir); +writeCompiledTemplatesToFile(workspaceDir, compiled1, './src/generated/workspace.ts'); +const packageDir = resolve(__dirname + '/../../../boilerplates/module'); +const compiled2 = compileTemplatesToFunctions(packageDir); +writeCompiledTemplatesToFile(packageDir, compiled2, './src/generated/module.ts'); diff --git a/packages/templatizer/src/index.ts b/packages/templatizer/src/index.ts new file mode 100644 index 0000000000..73250ca66c --- /dev/null +++ b/packages/templatizer/src/index.ts @@ -0,0 +1,9 @@ +import moduleTemplate from './generated/module'; +import workspaceTemplate from './generated/workspace'; +import { writeRenderedTemplates } from './templatize/generateFromCompiled'; + +export { + moduleTemplate, + workspaceTemplate, + writeRenderedTemplates +}; \ No newline at end of file diff --git a/packages/templatizer/src/templatize/generateFromCompiled.ts b/packages/templatizer/src/templatize/generateFromCompiled.ts index 970b9352e2..d074063010 100644 --- a/packages/templatizer/src/templatize/generateFromCompiled.ts +++ b/packages/templatizer/src/templatize/generateFromCompiled.ts @@ -1,6 +1,5 @@ -import fs from 'fs'; +import { mkdirSync, writeFileSync } from 'fs'; import path from 'path'; -import * as mkdirp from 'mkdirp'; type Func = (vars: Record) => { relPath: string, content: string }; @@ -12,7 +11,7 @@ export function writeRenderedTemplates( templates.forEach(tmpl => { const output = tmpl(vars); const outPath = path.join(outDir, output.relPath); - mkdirp.sync(path.dirname(outPath)); - fs.writeFileSync(outPath, output.content); + mkdirSync(path.dirname(outPath), { recursive: true }); + writeFileSync(outPath, output.content); }) -} +} \ No newline at end of file diff --git a/packages/templatizer/src/templatize/writeCompiledTemplatesToFile.ts b/packages/templatizer/src/templatize/writeCompiledTemplatesToFile.ts index 940b17ad77..5f7d6a1043 100644 --- a/packages/templatizer/src/templatize/writeCompiledTemplatesToFile.ts +++ b/packages/templatizer/src/templatize/writeCompiledTemplatesToFile.ts @@ -1,6 +1,5 @@ import fs from 'fs'; import path from 'path'; -import * as mkdirp from 'mkdirp'; import { CompiledTemplate } from './compileTemplatesToFunctions'; export function writeCompiledTemplatesToFile( @@ -9,7 +8,7 @@ export function writeCompiledTemplatesToFile( outFile: string ) { const dir = path.dirname(outFile); - mkdirp.sync(dir); + fs.mkdirSync(dir, { recursive: true }); const header = `// Auto-generated template module\n\n`; @@ -34,4 +33,4 @@ export function writeCompiledTemplatesToFile( const fullContent = header + exportBlock; fs.writeFileSync(outFile, fullContent); -} +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 6ac963cc73..50b42219d8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1715,11 +1715,6 @@ dependencies: undici-types "~5.26.4" -"@types/node@8.0.0": - version "8.0.0" - resolved "https://registry.yarnpkg.com/@types/node/-/node-8.0.0.tgz#acaa89247afddc7967e9902fd11761dadea1a555" - integrity sha512-j2tekvJCO7j22cs+LO6i0kRPhmQ9MXaPZ55TzOc1lzkN5b6BWqq4AFjl04s1oRRQ1v5rSe+KEvnLUSTonuls/A== - "@types/normalize-package-data@^2.4.0": version "2.4.4" resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz#56e2cc26c397c038fab0e3a917a12d5c5909e901" @@ -4913,10 +4908,10 @@ inquirer@^8.2.4: through "^2.3.6" wrap-ansi "^6.0.1" -inquirerer@^1.9.1: - version "1.9.1" - resolved "https://registry.yarnpkg.com/inquirerer/-/inquirerer-1.9.1.tgz#61da5a9a67cb80fa49a85063577bf459ab25802c" - integrity sha512-c7N3Yd9warVEpWdyX04dJUtYSad1qZFnNQYsKdqk0Av4qRg83lmxSnhWLn8Ok+UNzj87xXxo/ww0ReIL3ZO92g== +inquirerer@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/inquirerer/-/inquirerer-2.0.2.tgz#d7f09cab315641df2cefacd476688da43fea6abb" + integrity sha512-spvm1XnBvYtNcEcsmJvnZ6jnV/7XOxcbHOHnYKWZInSi2KibI4CsQ3mtBOiVjjEfb/Cdyj2kXD81gH5qyPSIsA== dependencies: chalk "^4.1.0" deepmerge "^4.3.1" @@ -6495,11 +6490,6 @@ mkdirp@^1.0.3, mkdirp@^1.0.4: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== -mkdirp@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-3.0.1.tgz#e44e4c5607fb279c168241713cc6e0fea9adcb50" - integrity sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg== - modify-values@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/modify-values/-/modify-values-1.0.1.tgz#b3939fa605546474e3e3e3c63d64bd43b4ee6022"