-
Notifications
You must be signed in to change notification settings - Fork 362
feat: web-ext create simple version #771
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
Changes from all commits
68e6143
5229109
647c0a1
4cdc8d7
4349d8e
37e83c7
4e8e438
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
/* @flow */ | ||
import path from 'path'; | ||
import readline from 'readline'; | ||
import tty from 'tty'; | ||
|
||
import {fs} from 'mz'; | ||
import mkdirp from 'mkdirp'; | ||
import promisify from 'es6-promisify'; | ||
|
||
import {createLogger} from '../util/logger'; | ||
import {UsageError, isErrorWithCode} from '../errors'; | ||
|
||
const log = createLogger(__filename); | ||
const defaultAsyncMkdirp = promisify(mkdirp); | ||
|
||
export type CreateParams = {| | ||
dirPath: string, | ||
stdin?: stream$Readable, | ||
|}; | ||
|
||
export default async function create( | ||
{ | ||
dirPath, | ||
stdin = process.stdin, | ||
}: CreateParams | ||
): Promise<void> { | ||
const targetPath = path.join(process.cwd(), dirPath); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm thinking that instead of using This way, the command can provide much more control to the caller if needed, which should be helpful both in the unit tests and while using web-ext as a library. |
||
const name = path.basename(dirPath); | ||
log.info(name, targetPath); | ||
|
||
let userAbort = true; | ||
|
||
try { | ||
const stats = await fs.stat(targetPath); | ||
if (stats.isDirectory()) { | ||
if (stdin.isTTY && (stdin instanceof tty.ReadStream)) { | ||
stdin.setRawMode(true); | ||
readline.emitKeypressEvents(stdin); | ||
let userConfirmation = false; | ||
|
||
while (!userConfirmation) { | ||
log.info(`The ${targetPath} already exists. Are you sure you want ` + | ||
'to use this directory and overwrite existing files? Y/N'); | ||
|
||
const pressed = await new Promise((resolve) => { | ||
stdin.once('keypress', (str, key) => resolve(key)); | ||
}); | ||
|
||
if (pressed.name === 'n' || (pressed.ctrl && pressed.name === 'c')) { | ||
userConfirmation = true; | ||
break; | ||
} else if (pressed.name === 'y') { | ||
userConfirmation = true; | ||
userAbort = false; | ||
break; | ||
} | ||
} | ||
} else { | ||
throw new UsageError('Target dir already exist, overwrite is not ' + | ||
'allowed without user confirmation.'); | ||
} | ||
} | ||
|
||
if (userAbort) { | ||
log.info('User aborted the command.'); | ||
stdin.pause(); | ||
return; | ||
} | ||
|
||
return await createFiles(name, targetPath).then(() => { | ||
stdin.pause(); | ||
}); | ||
} catch (statErr) { | ||
if (!isErrorWithCode('ENOENT', statErr)) { | ||
throw statErr; | ||
} else { | ||
try { | ||
await defaultAsyncMkdirp(targetPath); | ||
await createFiles(name, targetPath); | ||
} catch (mkdirErr) { | ||
throw mkdirErr; | ||
} | ||
} | ||
} | ||
} | ||
|
||
async function createFiles(name, targetPath): Promise<void> { | ||
log.info('Creating manifest file'); | ||
const generatedManifest = await generateManifest(name); | ||
const json = JSON.stringify(generatedManifest, null, 2); | ||
try { | ||
log.info('Writing files'); | ||
await fs.writeFile(path.join(targetPath, 'manifest.json'), json, 'utf8'); | ||
await fs.open(path.join(targetPath, 'background.js'), 'w'); | ||
await fs.open(path.join(targetPath, 'content.js'), 'w'); | ||
} catch (error) { | ||
throw error; | ||
} | ||
return; | ||
} | ||
|
||
async function generateManifest(title) { | ||
return { | ||
manifest_version: 2, | ||
name: `${title} (name)`, | ||
description: `${title} (description)`, | ||
version: 0.1, | ||
default_locale: 'en', | ||
icons: { | ||
'48': 'icon.png', | ||
'96': 'icon@2x.png', | ||
}, | ||
browser_action: { | ||
default_title: `${title} (browserAction)`, | ||
default_icon: { | ||
'19': 'button/button-19.png', | ||
'38': 'button/button-38.png', | ||
}, | ||
}, | ||
background: { | ||
scripts: ['background.js'], | ||
page: '', | ||
}, | ||
content_scripts: [ | ||
{ | ||
exclude_matches: [], | ||
matches: [], | ||
js: ['content.js'], | ||
}, | ||
], | ||
permissions: [], | ||
}; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,147 @@ | ||
/* @flow */ | ||
import path from 'path'; | ||
import tty from 'tty'; | ||
|
||
import {fs} from 'mz'; | ||
import {describe, it, afterEach} from 'mocha'; | ||
import {assert} from 'chai'; | ||
import sinon from 'sinon'; | ||
import mockStdin from 'mock-stdin'; | ||
import rimraf from 'rimraf'; | ||
|
||
import create from '../../../src/cmd/create'; | ||
import {withTempDir} from '../../../src/util/temp-dir'; | ||
import {makeSureItFails} from '../helpers'; | ||
import {onlyInstancesOf, UsageError} from '../../../src/errors'; | ||
|
||
const homeDir = process.cwd(); | ||
|
||
describe('create', () => { | ||
|
||
afterEach(() => { | ||
process.chdir(homeDir); | ||
}); | ||
|
||
async function cleanTmpDir(dir) { | ||
rimraf(dir, (error) => { | ||
if (error) { | ||
throw error; | ||
} | ||
}); | ||
} | ||
|
||
it('creates files including manifest with correct name', () => withTempDir( | ||
(tmpDir) => { | ||
process.chdir(tmpDir.path()); | ||
const targetDir = path.join(tmpDir.path(), 'target'); | ||
const manifest = path.join(targetDir, 'manifest.json'); | ||
return create({dirPath: 'target'}) | ||
.then(() => { | ||
return fs.stat(path.join(targetDir, 'content.js')) | ||
.then((contentstat) => { | ||
assert.equal(contentstat.isDirectory(), false); | ||
return fs.stat(path.join(targetDir, 'background.js')) | ||
.then((bgstat) => { | ||
assert.equal(bgstat.isDirectory(), false); | ||
return fs.readFile(manifest, 'utf-8') | ||
.then((data) => { | ||
const parsed = JSON.parse(data); | ||
assert.equal(parsed.name, 'target (name)'); | ||
}) | ||
.then( | ||
() => cleanTmpDir(targetDir), | ||
() => cleanTmpDir(targetDir) | ||
); | ||
}); | ||
}); | ||
}); | ||
})); | ||
|
||
it('creates directory recursively when needed', () => withTempDir( | ||
(tmpDir) => { | ||
process.chdir(tmpDir.path()); | ||
const targetDir = path.join(tmpDir.path(), 'sub/target'); | ||
const manifest = path.join(targetDir, 'manifest.json'); | ||
return create({dirPath: 'sub/target'}) | ||
.then(() => { | ||
return fs.stat(path.join(targetDir)) | ||
.then((contentstat) => { | ||
assert.equal(contentstat.isDirectory(), true); | ||
return fs.readFile(manifest, 'utf-8') | ||
.then((data) => { | ||
const parsed = JSON.parse(data); | ||
assert.equal(parsed.name, 'target (name)'); | ||
}) | ||
.then( | ||
() => cleanTmpDir(targetDir), | ||
() => cleanTmpDir(targetDir) | ||
); | ||
}); | ||
}); | ||
})); | ||
|
||
it('does not overwrite existing directory if user aborts', () => withTempDir( | ||
(tmpDir) => { | ||
process.chdir(tmpDir.path()); | ||
const targetDir = path.join(tmpDir.path(), 'target'); | ||
const fakeStdin = new tty.ReadStream(); | ||
fs.mkdir('target'); | ||
setTimeout(() => { | ||
fakeStdin.emit('keypress', 'n', {name: 'n', ctrl: false}); | ||
}, 50); | ||
return create({dirPath: 'target', stdin: fakeStdin}) | ||
.then(() => { | ||
return fs.readFile(path.join(targetDir, 'manifest.json'), 'utf-8') | ||
.then(makeSureItFails()) | ||
.catch((error) => { | ||
assert.equal(error.code, 'ENOENT'); | ||
}) | ||
.then( | ||
() => cleanTmpDir(targetDir), | ||
() => cleanTmpDir(targetDir) | ||
); | ||
}); | ||
})); | ||
|
||
it('overwrites existing directory if user allows', () => withTempDir( | ||
(tmpDir) => { | ||
process.chdir(tmpDir.path()); | ||
const targetDir = path.join(tmpDir.path(), 'target'); | ||
const fakeStdin = new tty.ReadStream(); | ||
sinon.spy(fakeStdin, 'pause'); | ||
fs.mkdir('target'); | ||
setTimeout(() => { | ||
fakeStdin.emit('keypress', 'y', {name: 'y', ctrl: false}); | ||
}, 50); | ||
return create({dirPath: 'target', stdin: fakeStdin}) | ||
.then(() => { | ||
return fs.readFile(path.join(targetDir, 'manifest.json'), 'utf-8') | ||
.then((data) => { | ||
const manifest = JSON.parse(data); | ||
assert.equal(manifest.name, 'target (name)'); | ||
assert.ok(fakeStdin.pause.called); | ||
}) | ||
.then( | ||
() => cleanTmpDir(targetDir), | ||
() => cleanTmpDir(targetDir) | ||
); | ||
}); | ||
})); | ||
|
||
it('throws error when user cannot confirm overwriting', () => withTempDir( | ||
(tmpDir) => { | ||
process.chdir(tmpDir.path()); | ||
mockStdin.isTTY = false; | ||
fs.mkdir('target'); | ||
return create({dirPath: 'target', stdin: mockStdin}) | ||
.then(makeSureItFails()) | ||
.catch(onlyInstancesOf(UsageError, (error) => { | ||
assert.match(error.message, /without user confirmation/); | ||
})) | ||
.then( | ||
() => cleanTmpDir(path.join(tmpDir.path(), 'target')), | ||
() => cleanTmpDir(path.join(tmpDir.path(), 'target')) | ||
); | ||
})); | ||
|
||
}); |
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.
we should split these properties into two parameters and flow types like we do for the other commands:
SomethingParams
(e.g.CreateCmdParams
) and it should only contains what is supposed to be in the regular parameters of the command (e.g.dirPath
in this case)SomethingOptions
(e.g.CreateCmdOptions
) and it should only contains the optional injected dependencies (stdin?
in this case), which are used mostly to override the dependencies in the unit tests