Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(cli): Scaffold errors if file name exists #88

Merged
merged 5 commits into from Nov 12, 2019

Conversation

stevelikesmusic
Copy link
Contributor

Fixes: #17

This prevents scaffold from overwriting files if they exist. The option to force an overwrite is also now built in.

Also, ports scaffold to oclif.

@stevelikesmusic stevelikesmusic added cli Relates to `zapier-platform-cli` enhancement labels Oct 24, 2019
Copy link
Contributor

@xavdid xavdid left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

some stylistic things to keep it in line with our newer CLI code. otherwise looks decent though. thanks for grabbing!

packages/cli/src/oclif/commands/scaffold.js Outdated Show resolved Hide resolved
packages/cli/src/oclif/commands/scaffold.js Outdated Show resolved Hide resolved
packages/cli/src/oclif/commands/scaffold.js Outdated Show resolved Hide resolved
packages/cli/src/oclif/commands/scaffold.js Outdated Show resolved Hide resolved
packages/cli/src/oclif/commands/scaffold.js Outdated Show resolved Hide resolved
packages/cli/src/oclif/commands/scaffold.js Outdated Show resolved Hide resolved
packages/cli/src/oclif/commands/scaffold.js Show resolved Hide resolved
packages/cli/src/oclif/commands/scaffold.js Outdated Show resolved Hide resolved
packages/cli/src/oclif/commands/scaffold.js Outdated Show resolved Hide resolved
packages/cli/src/oclif/commands/scaffold.js Outdated Show resolved Hide resolved
Copy link
Member

@eliangcs eliangcs left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did a sanity test and it works! I didn't look closely because I believe @xavdid already did.

@stevelikesmusic
Copy link
Contributor Author

@xavdid I think it's ready for another pass. Thanks!

Comment on lines +26 to +53
function createTemplateContext(type, name) {
const contextKey = snakeCase(name);

// where will we create/required the new file?
const destMap = {
resource: `resources/${contextKey}`,
trigger: `triggers/${contextKey}`,
search: `searches/${contextKey}`,
create: `creates/${contextKey}`
};

return {
CAMEL: camelCase(name),
KEY: contextKey,
NOUN: _.capitalize(name),
LOWER_NOUN: name.toLowerCase(),
INPUT_FIELDS: '',
TYPE: type,
TYPE_PLURAL: type === 'search' ? `${type}es` : `${type}s`,
// resources need an extra line for tests to "just run"
MAYBE_RESOURCE: type === 'resource' ? 'list.' : '',
defaultDest: destMap[type]
};
}

function getTemplatePath(type) {
return path.join(__dirname, '../../..', `scaffold/${type}.template.js`);
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I liked the feel of pulling these functions out of the class. They're not exactly utils worthy of their own file, but could be class methods if we really wanted.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, either way! I think it's reasonable to bundle them all together. Typically we could test them, though these are simple enough we probably won't bother.

@@ -0,0 +1,13 @@
function splitFileFromPath(filePath, extension = 'js') {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function suits our purpose well for scaffold. I left out considering the extension being passed in for now (ex. src/triggers/foo.js since we'd also need to account for foo.test.js)

Comment on lines 118 to 144
// this is very dumb and will definitely break, it inserts lines of code
// we should look at jscodeshift or friends to do this instead

// insert Resource = require() line at top
const varName = `${templateContext.CAMEL}${camelCase(type)}`;
const importerLine = `const ${varName} = require('./${dest}');`;
lines.splice(0, 0, importerLine);

// insert '[Resource.key]: Resource,' after 'resources:' line
const injectAfter = `${typeMap[type]}: {`;
const injectorLine = `[${varName}.key]: ${varName},`;
const linesDefIndex = lines.findIndex(line => line.endsWith(injectAfter));

if (linesDefIndex === -1) {
this.stopSpinner(false);
return this.error(
[
`\n${colors.bold(
`Oops, we could not reliably rewrite your ${entry}.`
)} Please add:`,
` * \`${importerLine}\` to the top`,
` * \`${injectAfter} ${injectorLine} },\` in your app definition`
].join('\n')
);
}

lines.splice(linesDefIndex + 1, 0, ' ' + injectorLine);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I almost think this section could be its own function so it's even easier to swap in an AST approach eventually

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i agree!

Copy link
Contributor

@xavdid xavdid left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks awesome! thanks for revising.

Comment on lines +26 to +53
function createTemplateContext(type, name) {
const contextKey = snakeCase(name);

// where will we create/required the new file?
const destMap = {
resource: `resources/${contextKey}`,
trigger: `triggers/${contextKey}`,
search: `searches/${contextKey}`,
create: `creates/${contextKey}`
};

return {
CAMEL: camelCase(name),
KEY: contextKey,
NOUN: _.capitalize(name),
LOWER_NOUN: name.toLowerCase(),
INPUT_FIELDS: '',
TYPE: type,
TYPE_PLURAL: type === 'search' ? `${type}es` : `${type}s`,
// resources need an extra line for tests to "just run"
MAYBE_RESOURCE: type === 'resource' ? 'list.' : '',
defaultDest: destMap[type]
};
}

function getTemplatePath(type) {
return path.join(__dirname, '../../..', `scaffold/${type}.template.js`);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, either way! I think it's reasonable to bundle them all together. Typically we could test them, though these are simple enough we probably won't bother.

if (preventOverwrite && fileExistsSync(destPath)) {
const [filename, location] = splitFileFromPath(destPath);

return this.error(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor, but you don't need to return here. This'll throw as normal (I think; if you're seeing different behavior, feel free to ignore)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Force of habit to always return something—and also needing to read up on oclif's API 😅

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

haha yeah. this is a good starting point, plus the TS reference.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I'd like it better if it was this.throwError.

If I'm scanning the code and see this.error I don't automatically read that we break out of that block. Whereas return this.error() is crystal clear to me.

Comment on lines 118 to 144
// this is very dumb and will definitely break, it inserts lines of code
// we should look at jscodeshift or friends to do this instead

// insert Resource = require() line at top
const varName = `${templateContext.CAMEL}${camelCase(type)}`;
const importerLine = `const ${varName} = require('./${dest}');`;
lines.splice(0, 0, importerLine);

// insert '[Resource.key]: Resource,' after 'resources:' line
const injectAfter = `${typeMap[type]}: {`;
const injectorLine = `[${varName}.key]: ${varName},`;
const linesDefIndex = lines.findIndex(line => line.endsWith(injectAfter));

if (linesDefIndex === -1) {
this.stopSpinner(false);
return this.error(
[
`\n${colors.bold(
`Oops, we could not reliably rewrite your ${entry}.`
)} Please add:`,
` * \`${importerLine}\` to the top`,
` * \`${injectAfter} ${injectorLine} },\` in your app definition`
].join('\n')
);
}

lines.splice(linesDefIndex + 1, 0, ' ' + injectorLine);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i agree!

@@ -0,0 +1,13 @@
function splitFileFromPath(filePath, extension = 'js') {
const destParts = filePath.split('/');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚨 I'd use path.separator here, for cross-platform compatibility. Same below.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call!

.concat(`.${extension}`)
.join('');
const dirPath = destParts.join('/');
return [filename, dirPath];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels a little backwards to me, I think i'd expect [dirPath, filename] since that's the way it would be written out: src/triggers/foo.js. nbd either way though.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a fair point. For some reason I was thinking we were more likely to want the file name vs the dir.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that's a fair assumption. honestly, could return { filename, dirPath } and then we can always grab exactly what we want.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about returning an object, but I also like the idea of returning a tuple to make naming the values whatever you want easier—in the case where you call this function twice in the same scope.

const { dirPath: dirA, filename: fileA,  } = split(pathA)
const { dirPath: dirB, filename: fileB,  } = split(pathB)

// vs

const [dirA, fileA] = split(pathA)
const [dirB, fileB] = split(pathB)

And if we wanted just filename

const [,fileName] = split(path)

In the end, it's a minor tradeoff either way—not sure if this function will even get used outside of scaffold.

@xavdid
Copy link
Contributor

xavdid commented Oct 29, 2019 via email

@xavdid
Copy link
Contributor

xavdid commented Nov 12, 2019

@stevelikesmusic you planning on merging this?

@stevelikesmusic
Copy link
Contributor Author

@xavdid yikes just saw this languishing. Merging now.

@stevelikesmusic stevelikesmusic merged commit 6b0fded into master Nov 12, 2019
@stevelikesmusic stevelikesmusic deleted the fix/17-scaffold-warn-overwrite branch November 12, 2019 19:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
cli Relates to `zapier-platform-cli` enhancement
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Scaffold commands shouldn't overwrite existing files
3 participants