diff --git a/.travis.yml b/.travis.yml index 4301e00..33c4e28 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,12 @@ ### Project specific config ### -language: generic +language: node_js +node_js: + - "8" env: + global: + - secure: "ixSEYjGpVSOPGu0t58ZWOZ/FudTrOa1vkV5MxHdkRMyDi2S25naE7cQukxaFwQk/Mn1bk+LJl6cyDT6wrd8BeNdWVOTy/+ZZB2mMa5oYGN1eGFG+p9JAshCLjfNmPj6gz6Ubo5iuKXkBjckvCFj2DQgvgBHRn2mP3+coGLn/9rfChSU0VbyTSx0gHp7+G4VOvs9sgG5tkxLuc1ItHJuDoeZlGW4R660LqY+HDRZRFBcJft2+zPMWOGbOMEMHnNBSOfyfaeAeHulJRd8TJ05/IAyGsoU8biXFxCAjveSSMT6IWAOkBdpaDVacDg0P7ClYvxup0eAImHlOXaoSaMUWLu4pvSk+6FTG2qauRT+t0igeWNuNdu5X4kmdL50vDvykLW88ln1pJ8hflEeH7sV8aTvStklXgSoi5PaCtpKLlDp3HXw9tTgV7iC+afVm2gpJUL7RB7yAInptUci+Bnq7FqVpGXUFlLbIP4VbB8sApI0ZaarbpQVMsP6N3N8pHtOEPMu0PepRIbqnf/cnRKxe1GdI0VU2JTMYgHq+OqFou4EuQKtNW+6552TiXY8Johl3vdJQWCbby88YfPkx1hRETAoVA/fUpkeO3lo4A0PIFzdQeG+8ZyOG4Seg3kvA9Lc8d3L5yEQtdVJR0vWWds7Va++NP1O3bx3eTGBQcBqN+K4=" + - secure: "VWt+R8fkwoezX+5WdB5Bmb09fIGvOCcwxdYhxdkMQlpWbPwlZoqw3OYY/9/aLGIAEzpYxSpKWvA+McMRmvmzQWRknWmhwwt2+MbQkfpy9k8rpoJmGQsBEWmcC7aQ/38fyDUgx5vxWuADyCVa0dzm6MqgL3E9VlhJaNCnJfsA+oadN6EG5S5sYRTQdBiarZ+GEqzcS50Nii/HuHUT881EXKPcMkSuefyzpzmEbVzwwECG2Huq1po0KErY1Ud4QgQyyI8JG/JaXIoAKloACfkL+oMbInfi0/4/hL4kOUWOOglNc1hwn+C1bTwsndyTaqgAdbIzLbid7acww/Mb0dufCnf4ddF1fQTCYNuaGLe/WuqszQHUMn1/2uxE9hjJY/Q0pAPXmsjV/ju3nEvRNZ6t2ABYSiXkN09Ro6R7DohR18Pdcj5b1Vb0kh8jDg/ljd368FQMN9pvw3Gj7qi3AEAyyDqLgdvm6ikyuQKiOgqwDwsbSCqgBAYZJgRQnCf7h2Wah1zY7djIBG+01mtLbPpcOh3D2mxSg0wmg4E9wMpJwYxsmHy5YvP+wTKinQWk1oPRgUtgEK4Z0m1mHsilcS0dZDDyLf6UxzCqvma/r7eJUV1XrsfSypen1yCyWYdT2SrB7m6tyXp4PjN2htKpemjiY57yHQxSOWWglDDeK6i7xy0=" matrix: - ATOM_CHANNEL=stable - ATOM_CHANNEL=beta @@ -9,13 +14,19 @@ env: os: - linux -### Generic setup follows ### -dist: trusty +before_script: + - commitlint-travis +after_success: + # Add apm to the PATH + - export PATH=${PATH}:${HOME}/atom/usr/bin/ + - npm run travis-deploy-once "npm run semantic-release" + +### Generic setup follows ### script: - curl -s -O https://raw.githubusercontent.com/atom/ci/master/build-package.sh - chmod u+x build-package.sh - - ./build-package.sh + - "./build-package.sh" notifications: email: @@ -25,17 +36,19 @@ notifications: branches: only: - master - - /^greenkeeper/.*$/ + - "/^greenkeeper/.*$/" git: depth: 10 sudo: false +dist: trusty + addons: apt: packages: - build-essential - - git - - libgnome-keyring-dev - fakeroot + - git + - libsecret-1-dev diff --git a/commitlint.config.js b/commitlint.config.js new file mode 100644 index 0000000..f37b7d2 --- /dev/null +++ b/commitlint.config.js @@ -0,0 +1,5 @@ +module.exports = { + extends: [ + '@commitlint/config-conventional', + ], +}; diff --git a/lib/init.coffee b/lib/init.coffee deleted file mode 100644 index 166e3c7..0000000 --- a/lib/init.coffee +++ /dev/null @@ -1,122 +0,0 @@ -{CompositeDisposable} = require('atom') -bundledPugLint = require('pug-lint') -path = require('path') -objectAssign = require('object-assign') -configFile = require('pug-lint/lib/config-file') -reqResolve = require('resolve') - -pugLints = new Map() - -resolvePath = (name, baseDir) -> - return new Promise (resolve, reject) -> - reqResolve name, { basedir: baseDir }, (err, res) -> - reject(err) if err? - resolve(res) - -getPugLint = (baseDir) -> - if pugLints.has(baseDir) - return Promise.resolve pugLints.get(baseDir) - - resolvePath('pug-lint', baseDir) - .then (pugLintPath) -> - pugLints.set(baseDir, require(pugLintPath)) - return Promise.resolve pugLints.get(baseDir) - .catch () -> - pugLints.set(baseDir, bundledPugLint) - return Promise.resolve pugLints.get(baseDir) - -module.exports = - config: - projectConfigFile: - type: 'string' - default: '' - description: 'Relative path from project to config file' - - onlyRunWhenConfig: - default: false - title: 'Run Pug-lint only if config is found' - description: 'Disable linter if there is no config file found for the linter.', - type: 'boolean' - - activate: -> - require('atom-package-deps').install('linter-pug') - - if atom.config.get('linter-pug.executablePath')? - atom.notifications.addWarning('Removing custom pug-lint path', { - detail: "linter-pug has moved to the Node.js API for pug-lint and " + - "will now use a project's local instance where possible, falling " + - "back to a bundled version of pug-lint where none is found." - }) - atom.config.unset('linter-pug.executablePath') - - @subscriptions = new CompositeDisposable - @subscriptions.add atom.config.observe 'linter-pug.executablePath', - (executablePath) => - @executablePath = executablePath - @subscriptions.add atom.config.observe 'linter-pug.projectConfigFile', - (projectConfigFile) => - @projectConfigFile = projectConfigFile - @subscriptions.add atom.config.observe 'linter-pug.onlyRunWhenConfig', - (onlyRunWhenConfig) => - @onlyRunWhenConfig = onlyRunWhenConfig - - getConfig: (filePath) -> - config = undefined - if path.isAbsolute(@projectConfigFile) - config = configFile.load(false, @projectConfigFile) - else - config = configFile.load(false, path.join(path.dirname(filePath), @projectConfigFile)) - if !config and @onlyRunWhenConfig - return undefined - - options = {} - newConfig = objectAssign(options, config) - - if !newConfig.configPath and config and config.configPath - newConfig.configPath = config.configPath - return newConfig - - provideLinter: -> - helpers = require('atom-linter') - provider = - name: 'pug-lint' - grammarScopes: ['source.jade', 'source.pug'] - scope: 'file' - lintOnFly: true - - lint: (textEditor) => - rules = [] - filePath = textEditor.getPath() - fileText = textEditor.getText() - projectConfig = @getConfig(filePath) - - # Use Atom's project root folder - projectDir = atom.project.relativizePath(filePath)[0] - if !projectDir? - # Fall back to the file directory - projectDir = path.dirname(filePath) - - if !fileText - return Promise.resolve([]) - - if !projectConfig || !projectConfig.configPath - if @onlyRunWhenConfig - atom.notifications.addError 'Pug-lint config not found' - return Promise.resolve([]) - - if(@onlyRunWhenConfig || projectConfig) - rules = projectConfig - - return new Promise (resolve) -> - getPugLint(projectDir).then (pugLint) -> - linter = new pugLint() - linter.configure rules - - results = linter.checkString fileText - - resolve results.map (res) -> { - type: res.name - filePath: filePath - range: helpers.generateRange textEditor, res.line - 1, res.column - 1 - text: res.msg - } diff --git a/lib/init.js b/lib/init.js new file mode 100644 index 0000000..ae21794 --- /dev/null +++ b/lib/init.js @@ -0,0 +1,182 @@ +'use babel'; + +// eslint-disable-next-line import/no-extraneous-dependencies, import/extensions +import { CompositeDisposable } from 'atom'; +import path from 'path'; + +// Dependencies +let bundledPugLint; +let objectAssign; +let configFile; +let helpers; +let resolve; + +const pugLints = new Map(); + +const resolvePath = (name, basedir) => + new Promise(((res, reject) => + resolve(name, { basedir }, (err, modulePath) => { + if (err != null) { + reject(err); + } + return res(modulePath); + }) + )); + +const getPugLint = async (baseDir) => { + if (pugLints.has(baseDir)) { + return pugLints.get(baseDir); + } + + try { + const pugLintPath = await resolvePath('pug-lint', baseDir); + // eslint-disable-next-line import/no-dynamic-require + pugLints.set(baseDir, require(pugLintPath)); + } catch (e) { + pugLints.set(baseDir, bundledPugLint); + } + + return pugLints.get(baseDir); +}; + +const loadDeps = () => { + if (!bundledPugLint) { + bundledPugLint = require('pug-lint'); + } + if (!objectAssign) { + objectAssign = require('object-assign'); + } + if (!configFile) { + configFile = require('pug-lint/lib/config-file'); + } + if (!helpers) { + helpers = require('atom-linter'); + } + if (!resolve) { + resolve = require('resolve'); + } +}; + +module.exports = { + activate() { + this.idleCallbacks = new Set(); + let depsCallbackID; + const installLinterJSHintDeps = () => { + this.idleCallbacks.delete(depsCallbackID); + loadDeps(); + }; + depsCallbackID = window.requestIdleCallback(installLinterJSHintDeps); + this.idleCallbacks.add(depsCallbackID); + + if (atom.config.get('linter-pug.executablePath')) { + atom.notifications.addWarning( + 'Removing custom pug-lint path', + { + detail: 'linter-pug has moved to the Node.js API for pug-lint and ' + + "will now use a project's local instance where possible, falling " + + 'back to a bundled version of pug-lint if none is found.', + }, + ); + atom.config.unset('linter-pug.executablePath'); + } + + this.subscriptions = new CompositeDisposable(); + this.subscriptions.add(atom.config.observe( + 'linter-pug.projectConfigFile', + (value) => { this.projectConfigFile = value; }, + )); + this.subscriptions.add(atom.config.observe( + 'linter-pug.onlyRunWhenConfig', + (value) => { this.onlyRunWhenConfig = value; }, + )); + }, + + deactivate() { + this.idleCallbacks.forEach(callbackID => window.cancelIdleCallback(callbackID)); + this.idleCallbacks.clear(); + this.subscriptions.dispose(); + }, + + getConfig(filePath) { + let config; + if (path.isAbsolute(this.projectConfigFile)) { + config = configFile.load(false, this.projectConfigFile); + } else { + config = configFile.load(false, path.join(path.dirname(filePath), this.projectConfigFile)); + } + if (!config && this.onlyRunWhenConfig) { + return undefined; + } + + const options = {}; + const newConfig = objectAssign(options, config); + + if (!newConfig.configPath && config && config.configPath) { + newConfig.configPath = config.configPath; + } + return newConfig; + }, + + provideLinter() { + return { + name: 'pug-lint', + grammarScopes: ['source.jade', 'source.pug'], + scope: 'file', + lintOnFly: true, + + lint: async (textEditor) => { + if (!atom.workspace.isTextEditor(textEditor)) { + // Somehow, called with an invalid TextEditor instance + return null; + } + + const filePath = textEditor.getPath(); + if (!filePath) { + // File somehow has no path + return null; + } + + const fileText = textEditor.getText(); + if (!fileText) { + // Nothing in the file + return null; + } + + // Load the dependencies if they aren't already + loadDeps(); + + const projectConfig = this.getConfig(filePath); + if (!projectConfig || !projectConfig.configPath) { + if (this.onlyRunWhenConfig) { + atom.notifications.addError('Pug-lint config not found'); + return null; + } + } + + let rules = []; + if (this.onlyRunWhenConfig || projectConfig) { + rules = projectConfig; + } + + // Use Atom's project root folder + let projectDir = atom.project.relativizePath(filePath)[0]; + if ((projectDir == null)) { + // Fall back to the file directory + projectDir = path.dirname(filePath); + } + + const linter = new (await getPugLint(projectDir))(); + linter.configure(rules); + const results = linter.checkString(fileText, filePath); + return results.map(res => ({ + severity: 'error', + location: { + file: res.filename, + position: helpers.generateRange(textEditor, res.line - 1, res.column - 1), + }, + excerpt: res.msg, + })); + }, + }; + }, +}; diff --git a/package.json b/package.json index 0720510..8ecdd89 100644 --- a/package.json +++ b/package.json @@ -9,28 +9,79 @@ }, "license": "MIT", "engines": { - "atom": ">=1.0.0 <2.0.0" + "atom": ">=1.7.0 <2.0.0" }, "readmeFilename": "README.md", "bugs": { "url": "https://github.com/AtomLinter/atom-linter-pug/issues" }, "homepage": "https://github.com/AtomLinter/atom-linter-pug", + "configSchema": { + "projectConfigFile": { + "type": "string", + "default": "", + "description": "Relative path from project to config file" + }, + "onlyRunWhenConfig": { + "default": false, + "title": "Run Pug-lint only if config is found", + "description": "Disable linter if there is no config file found for the linter.", + "type": "boolean" + } + }, "providedServices": { "linter": { "versions": { - "1.0.0": "provideLinter" + "2.0.0": "provideLinter" } } }, "dependencies": { "atom-linter": "^10.0.0", - "atom-package-deps": "^4.0.1", "object-assign": "^4.1.0", "pug-lint": "^2.1.9", "resolve": "^1.5.0" }, - "package-deps": [ - "linter" - ] + "devDependencies": { + "@commitlint/config-conventional": "^5.2.3", + "@commitlint/travis-cli": "^5.2.8", + "@semantic-release/apm-config": "^1.0.2", + "commitlint": "^5.2.8", + "eslint": "^4.14.0", + "eslint-config-airbnb-base": "^12.1.0", + "eslint-plugin-import": "^2.8.0", + "husky": "^0.14.3", + "jasmine-fix": "^1.3.1", + "semantic-release": "^12.1.0", + "travis-deploy-once": "^4.3.0" + }, + "scripts": { + "commitmsg": "commitlint -e $GIT_PARAMS", + "semantic-release": "semantic-release", + "travis-deploy-once": "travis-deploy-once" + }, + "release": { + "extends": "@semantic-release/apm-config" + }, + "eslintConfig": { + "rules": { + "global-require": "off", + "import/no-unresolved": [ + "error", + { + "ignore": [ + "atom" + ] + } + ] + }, + "extends": "airbnb-base", + "globals": { + "atom": true + }, + "env": { + "node": true, + "browser": true + } + } } diff --git a/spec/.eslintrc.js b/spec/.eslintrc.js new file mode 100644 index 0000000..3dc3525 --- /dev/null +++ b/spec/.eslintrc.js @@ -0,0 +1,14 @@ +module.exports = { + env: { + atomtest: true, + jasmine: true, + }, + rules: { + "import/no-extraneous-dependencies": [ + "error", + { + "devDependencies": true + } + ] + } +}; diff --git a/spec/linter-stylint-spec.coffee b/spec/linter-stylint-spec.coffee deleted file mode 100644 index d5344ac..0000000 --- a/spec/linter-stylint-spec.coffee +++ /dev/null @@ -1,57 +0,0 @@ -path = require 'path' - -goodPug = path.join(__dirname, 'fixtures', 'config', 'good.pug') -badPug = path.join(__dirname, 'fixtures', 'config', 'bad.pug') -noConfigRule = path.join(__dirname, 'fixtures', 'noConfig', 'badRule.pug') -noConfigSyntax = path.join(__dirname, 'fixtures', 'noConfig', 'badSyntax.pug') - -describe 'The pug-lint provider for Linter', -> - lint = require('../lib/init.coffee').provideLinter().lint - - beforeEach -> - atom.workspace.destroyActivePaneItem() - waitsForPromise -> - atom.packages.activatePackage 'linter-pug' - - - it 'should be in the packages list', -> - expect(atom.packages.isPackageLoaded('linter-pug')).toBe true - - it 'should be an active package', -> - expect(atom.packages.isPackageActive('linter-pug')).toBe true - - describe 'works with a configuration', -> - it 'finds nothing wrong with valid file', -> - waitsForPromise -> - atom.workspace.open(goodPug).then (editor) -> - lint(editor).then (messages) -> - expect(messages.length).toBe 0 - - it 'finds something wrong with invalid file', -> - errMsg = 'Attribute interpolation operators must not be used' - waitsForPromise -> - atom.workspace.open(badPug).then (editor) -> - lint(editor).then (messages) -> - expect(messages.length).toEqual 1 - expect(messages[0].html).not.toBeDefined() - expect(messages[0].text).toBe errMsg - expect(messages[0].filePath).toBe badPug - expect(messages[0].range).toEqual [[0, 13], [0, 20]] - - describe 'works without a configuration', -> - it 'finds nothing wrong with a "bad" file', -> - waitsForPromise -> - atom.workspace.open(noConfigRule).then (editor) -> - lint(editor).then (messages) -> - expect(messages.length).toBe 0 - - it 'finds syntax errors without a configuration', -> - errMsg = 'The end of the string reached with no closing bracket ) found.' - waitsForPromise -> - atom.workspace.open(noConfigSyntax).then (editor) -> - lint(editor).then (messages) -> - expect(messages.length).toEqual 1 - expect(messages[0].html).not.toBeDefined() - expect(messages[0].text).toBe errMsg - expect(messages[0].filePath).toBe noConfigSyntax - expect(messages[0].range).toEqual [[1, 0], [1, 0]] diff --git a/spec/linter-stylint-spec.js b/spec/linter-stylint-spec.js new file mode 100644 index 0000000..0b07eb0 --- /dev/null +++ b/spec/linter-stylint-spec.js @@ -0,0 +1,65 @@ +'use babel'; + +// eslint-disable-next-line no-unused-vars +import { it, fit, wait, beforeEach, afterEach } from 'jasmine-fix'; +import path from 'path'; + +const { lint } = require('../lib/init').provideLinter(); + +const goodPug = path.join(__dirname, 'fixtures', 'config', 'good.pug'); +const badPug = path.join(__dirname, 'fixtures', 'config', 'bad.pug'); +const noConfigRule = path.join(__dirname, 'fixtures', 'noConfig', 'badRule.pug'); +const noConfigSyntax = path.join(__dirname, 'fixtures', 'noConfig', 'badSyntax.pug'); + +describe('The pug-lint provider for Linter', () => { + beforeEach(async () => { + atom.workspace.destroyActivePaneItem(); + await atom.packages.activatePackage('linter-pug'); + }); + + it('should be in the packages list', () => + expect(atom.packages.isPackageLoaded('linter-pug')).toBe(true)); + + it('should be an active package', () => + expect(atom.packages.isPackageActive('linter-pug')).toBe(true)); + + describe('works with a configuration', () => { + it('finds nothing wrong with valid file', async () => { + const editor = await atom.workspace.open(goodPug); + const messages = await lint(editor); + expect(messages.length).toBe(0); + }); + + it('finds something wrong with invalid file', async () => { + const errMsg = 'Attribute interpolation operators must not be used'; + const editor = await atom.workspace.open(badPug); + const messages = await lint(editor); + + expect(messages.length).toEqual(1); + expect(messages[0].description).not.toBeDefined(); + expect(messages[0].excerpt).toBe(errMsg); + expect(messages[0].location.file).toBe(badPug); + expect(messages[0].location.position).toEqual([[0, 13], [0, 20]]); + }); + }); + + describe('works without a configuration', () => { + it('finds nothing wrong with a "bad" file', async () => { + const editor = await atom.workspace.open(noConfigRule); + const messages = await lint(editor); + expect(messages.length).toBe(0); + }); + + it('finds syntax errors without a configuration', async () => { + const errMsg = 'The end of the string reached with no closing bracket ) found.'; + const editor = await atom.workspace.open(noConfigSyntax); + const messages = await lint(editor); + + expect(messages.length).toEqual(1); + expect(messages[0].description).not.toBeDefined(); + expect(messages[0].excerpt).toBe(errMsg); + expect(messages[0].location.file).toBe(noConfigSyntax); + expect(messages[0].location.position).toEqual([[1, 0], [1, 0]]); + }); + }); +});