-
Notifications
You must be signed in to change notification settings - Fork 190
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
Conversation
There was a problem hiding this 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!
There was a problem hiding this 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.
@xavdid I think it's ready for another pass. Thanks! |
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`); | ||
} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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') { |
There was a problem hiding this comment.
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
)
// 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); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i agree!
There was a problem hiding this 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.
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`); | ||
} |
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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 😅
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
// 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); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i agree!
packages/cli/src/utils/string.js
Outdated
@@ -0,0 +1,13 @@ | |||
function splitFileFromPath(filePath, extension = 'js') { | |||
const destParts = filePath.split('/'); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good call!
packages/cli/src/utils/string.js
Outdated
.concat(`.${extension}`) | ||
.join(''); | ||
const dirPath = destParts.join('/'); | ||
return [filename, dirPath]; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
.
I can see that. I think I'd be fine to use our own method that calls
this.error if we want! No strong feelings.
On Mon, Oct 28, 2019 at 9:35 PM Steve Gardner ***@***.***> wrote:
***@***.**** commented on this pull request.
------------------------------
In packages/cli/src/oclif/commands/scaffold.js
<#88 (comment)>:
> +}
+
+function getTemplatePath(type) {
+ return path.join(__dirname, '../../..', `scaffold/${type}.template.js`);
+}
+
+class ScaffoldCommand extends BaseCommand {
+ async writeTemplateFile(type, templateContext, dest) {
+ const templatePath = getTemplatePath(type);
+ const destPath = path.join(process.cwd(), `${dest}.js`);
+ const preventOverwrite = !this.flags.force;
+
+ if (preventOverwrite && fileExistsSync(destPath)) {
+ const [filename, location] = splitFileFromPath(destPath);
+
+ return this.error(
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.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#88?email_source=notifications&email_token=AAJMYP4O2MLCRZYY2VFPKJ3QQ6OOLA5CNFSM4JEPZNFKYY3PNVWWK3TUL52HS4DFWFIHK3DMKJSXC5LFON2FEZLWNFSXPKTDN5WW2ZLOORPWSZGOCJPXWEI#discussion_r339871768>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AAJMYP3ULGXLPNLVEIGQKYLQQ6OOLANCNFSM4JEPZNFA>
.
--
~David Brownman
Platform Engineer | Zapier <https://zapier.com>
c: 303.819.0850
tw: @xavdid
|
@stevelikesmusic you planning on merging this? |
@xavdid yikes just saw this languishing. Merging now. |
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.