├── .gitignore ├── .npmignore ├── .travis.yml ├── Gruntfile.coffee ├── LICENSE.md ├── README.md ├── appveyor.yml ├── bin └── csonc ├── package.json ├── spec ├── cson-spec.coffee ├── csonc-spec.coffee └── fixtures │ ├── duplicate-keys.cson │ ├── empty-line.cson │ ├── empty-line.json │ ├── empty.cson │ ├── empty.json │ ├── invalid.cson │ ├── multi-comment.cson │ ├── sample.cson │ ├── sample.json │ ├── single-comment.cson │ ├── syntax-error.cson │ └── syntax-error.json └── src ├── cson.coffee └── csonc.coffee /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | *.js 3 | .DS_Store 4 | npm-debug.log 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .npmignore 2 | *.coffee 3 | script/ 4 | .DS_Store 5 | npm-debug.log 6 | .travis.yml 7 | appveyor.yml 8 | spec/ 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | notifications: 4 | email: 5 | on_success: never 6 | on_failure: change 7 | 8 | node_js: 9 | - 6 10 | -------------------------------------------------------------------------------- /Gruntfile.coffee: -------------------------------------------------------------------------------- 1 | module.exports = (grunt) -> 2 | grunt.initConfig 3 | pkg: grunt.file.readJSON('package.json') 4 | 5 | coffee: 6 | glob_to_multiple: 7 | expand: true 8 | cwd: 'src' 9 | src: ['*.coffee'] 10 | dest: 'lib' 11 | ext: '.js' 12 | 13 | coffeelint: 14 | options: 15 | no_empty_param_list: 16 | level: 'error' 17 | max_line_length: 18 | level: 'ignore' 19 | 20 | src: ['src/*.coffee'] 21 | test: ['spec/*.coffee'] 22 | 23 | shell: 24 | test: 25 | command: 'jasmine-focused --captureExceptions --coffee spec/' 26 | options: 27 | stdout: true 28 | stderr: true 29 | failOnError: true 30 | 31 | grunt.loadNpmTasks('grunt-contrib-coffee') 32 | grunt.loadNpmTasks('grunt-shell') 33 | grunt.loadNpmTasks('grunt-coffeelint') 34 | 35 | grunt.registerTask 'clean', -> require('rimraf').sync('lib') 36 | grunt.registerTask('lint', ['coffeelint:src', 'coffeelint:test']) 37 | grunt.registerTask('default', ['coffeelint', 'coffee']) 38 | grunt.registerTask('test', ['default', 'coffeelint:test', 'shell:test']) 39 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 GitHub Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ##### Atom and all repositories under Atom will be archived on December 15, 2022. Learn more in our [official announcement](https://github.blog/2022-06-08-sunsetting-atom/) 2 | # season - CSON Node module 3 | [![macOS Build Status](https://travis-ci.org/atom/season.svg?branch=master)](https://travis-ci.org/atom/season) [![Windows Build Status](https://ci.appveyor.com/api/projects/status/v3bth3ooq5q8k8lx/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/season) [![Dependency Status](https://david-dm.org/atom/season.svg)](https://david-dm.org/atom/season) 4 | 5 | Read and write CSON/JSON files seamlessly. 6 | 7 | ## Installing 8 | 9 | ```sh 10 | npm install season 11 | ``` 12 | 13 | ## Building 14 | * Clone the repository 15 | * Run `npm install` 16 | * Run `grunt` to compile the CoffeeScript code 17 | * Run `grunt test` to run the specs 18 | 19 | ## Compiling CSON to JSON 20 | 21 | This module comes with a `csonc` executable that allows you to compile a CSON 22 | file to JSON. 23 | 24 | To use: 25 | 26 | ```sh 27 | npm install -g season 28 | echo "this: 'is cson'" > file.cson 29 | csonc file.cson --output file.json 30 | cat file.json 31 | { 32 | "this": "is cson" 33 | } 34 | ``` 35 | 36 | ## Docs 37 | 38 | ```coffeescript 39 | CSON = require 'season' 40 | ``` 41 | 42 | ### CSON.setCacheDir(cacheDirectory) 43 | 44 | Set the cache directory to use for storing compiled CSON files. 45 | 46 | `cacheDirectory` - Root directory path for storing compiled CSON. 47 | 48 | ### CSON.stringify(object) 49 | 50 | Convert the object to a CSON string. 51 | 52 | `object` - The object to convert to CSON. 53 | 54 | Returns the CSON string representation of the given object. 55 | 56 | ### CSON.readFile(objectPath, callback) 57 | 58 | Read the CSON or JSON object at the given path and return it to the callback 59 | once it is read and parsed. 60 | 61 | `objectPath` - The string path to a JSON or CSON object file. 62 | 63 | `callback` - The function to call with the error or object once the path 64 | is read and parsed. 65 | 66 | ### CSON.readFileSync(objectPath) 67 | 68 | Synchronous version of `CSON.readFile(objectPath, callback)`. 69 | 70 | Returns the object read from the path or throws an error if reading fails. 71 | 72 | ### CSON.writeFile(objectPath, object, callback) 73 | 74 | Write the object to the given path as either JSON or CSON depending on the 75 | path's extension. 76 | 77 | `objectPath` - The string path to a JSON or CSON object file. 78 | 79 | `object` - The object to convert to a string and write to the path. 80 | 81 | `callback` - The function to with an error object on failures. 82 | 83 | ### CSON.writeFileSync(objectPath, object) 84 | 85 | Synchronous version of `CSON.writeFile(objectPath, object, callback)` 86 | 87 | ### CSON.isObjectPath(objectPath) 88 | 89 | Is the given path a valid object path? 90 | 91 | Returns `true` if the path has a `.json` or `.cson` file extension, `false` 92 | otherwise. 93 | 94 | ### CSON.resolve(objectPath) 95 | 96 | Resolve the path to an existent file that has a `.json` or `.cson` extension. 97 | 98 | `objectPath` - The string path to a JSON or CSON object file with or without 99 | an extension. 100 | 101 | Returns the path to an existent CSON or JSON file or `null` if none found. 102 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | nodejs_version: "6" 3 | 4 | platform: 5 | - x64 6 | - x86 7 | 8 | install: 9 | - ps: Install-Product node $env:nodejs_version 10 | - npm install 11 | 12 | test_script: 13 | - node -e "console.log(`${process.version} ${process.arch} ${process.platform}`)" 14 | - npm --version 15 | - npm test 16 | 17 | build: off 18 | -------------------------------------------------------------------------------- /bin/csonc: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('../lib/csonc')(process.argv.slice(2)) 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "season", 3 | "version": "6.0.2", 4 | "description": "CSON utilities", 5 | "licenses": [ 6 | { 7 | "type": "MIT", 8 | "url": "http://github.com/atom/season/raw/master/LICENSE.md" 9 | } 10 | ], 11 | "main": "./lib/cson.js", 12 | "bin": { 13 | "csonc": "./bin/csonc" 14 | }, 15 | "scripts": { 16 | "prepublish": "grunt clean lint coffee", 17 | "test": "grunt test" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/atom/season.git" 22 | }, 23 | "bugs": { 24 | "url": "https://github.com/atom/season/issues" 25 | }, 26 | "homepage": "http://atom.github.io/season", 27 | "keywords": [ 28 | "cson", 29 | "json", 30 | "CoffeeScript" 31 | ], 32 | "dependencies": { 33 | "cson-parser": "^1.3.0", 34 | "fs-plus": "^3.0.0", 35 | "yargs": "^3.23.0" 36 | }, 37 | "devDependencies": { 38 | "jasmine-focused": "1.x", 39 | "grunt-contrib-coffee": "~0.9.0", 40 | "grunt-cli": "~0.1.8", 41 | "grunt": "~0.4.1", 42 | "grunt-shell": "~0.2.2", 43 | "grunt-coffeelint": "0.0.6", 44 | "temp": "~0.5.0", 45 | "rimraf": "~2.1.4" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /spec/cson-spec.coffee: -------------------------------------------------------------------------------- 1 | path = require 'path' 2 | fs = require 'fs-plus' 3 | temp = require 'temp' 4 | CSON = require '../lib/cson' 5 | parser = require 'cson-parser' 6 | 7 | readFile = (filePath, callback) -> 8 | done = jasmine.createSpy('readFile callback') 9 | expect(CSON.readFile(filePath, done)).toBeUndefined() 10 | waitsFor -> done.callCount is 1 11 | runs -> callback(done.argsForCall[0]...) 12 | 13 | describe "CSON", -> 14 | beforeEach -> 15 | CSON.setCacheDir(null) 16 | CSON.resetCacheStats() 17 | 18 | describe ".stringify(object)", -> 19 | describe "when the object is undefined", -> 20 | it "returns undefined", -> 21 | expect(CSON.stringify(undefined)).toBe undefined 22 | 23 | describe "when the object is a function", -> 24 | it "returns undefined", -> 25 | expect(CSON.stringify(-> 'function')).toBe undefined 26 | 27 | describe "when the object contains a function", -> 28 | it "it gets filtered away, when not providing a visitor function", -> 29 | expect(CSON.stringify(a: -> 'function')).toBe '{}' 30 | 31 | describe "when formatting an undefined key", -> 32 | it "does not include the key in the formatted CSON", -> 33 | expect(CSON.stringify(b: 1, c: undefined)).toBe 'b: 1' 34 | 35 | describe "when formatting a string", -> 36 | it "returns formatted CSON", -> 37 | expect(CSON.stringify(a: 'b')).toBe 'a: "b"' 38 | 39 | it "doesn't escape single quotes", -> 40 | expect(CSON.stringify(a: "'b'")).toBe '''a: "'b'"''' 41 | 42 | it "escapes double quotes", -> 43 | expect(CSON.stringify(a: '"b"')).toBe '''a: "\\"b\\""''' 44 | 45 | it "turns strings with newlines into triple-apostrophe strings", -> 46 | expect(CSON.stringify("a\nb")).toBe """''' 47 | a 48 | b 49 | '''""" 50 | 51 | it "escapes triple-apostrophes in triple-apostrophe strings", -> 52 | expect(CSON.stringify("a\n'''")).toBe """''' 53 | a 54 | \\\''' 55 | '''""" 56 | 57 | describe "when formatting a boolean", -> 58 | it "returns formatted CSON", -> 59 | expect(CSON.stringify(true)).toBe 'true' 60 | expect(CSON.stringify(false)).toBe 'false' 61 | expect(CSON.stringify(a: true)).toBe 'a: true' 62 | expect(CSON.stringify(a: false)).toBe 'a: false' 63 | 64 | describe "when formatting a number", -> 65 | it "returns formatted CSON", -> 66 | expect(CSON.stringify(54321.012345)).toBe '54321.012345' 67 | expect(CSON.stringify(a: 14)).toBe 'a: 14' 68 | expect(CSON.stringify(a: 1.23)).toBe 'a: 1.23' 69 | 70 | describe "when formatting null", -> 71 | it "returns formatted CSON", -> 72 | expect(CSON.stringify(null)).toBe 'null' 73 | expect(CSON.stringify(a: null)).toBe 'a: null' 74 | 75 | describe "when formatting an array", -> 76 | describe "when the array is empty", -> 77 | it "puts the array on a single line", -> 78 | expect(CSON.stringify([])).toBe "[]" 79 | 80 | it "returns formatted CSON", -> 81 | expect(CSON.stringify(a: ['b'])).toBe ''' 82 | a: [ 83 | "b" 84 | ] 85 | ''' 86 | expect(CSON.stringify(a: ['b', 4])).toBe ''' 87 | a: [ 88 | "b" 89 | 4 90 | ] 91 | ''' 92 | 93 | describe "when the array has an undefined value", -> 94 | it "formats the undefined value as null", -> 95 | expect(CSON.stringify(['a', undefined, 'b'])).toBe '''[ 96 | "a" 97 | null 98 | "b" 99 | ]''' 100 | 101 | describe "when the array contains an object", -> 102 | it "wraps the object in {}", -> 103 | expect(CSON.stringify([{a:'b', a1: 'b1'}, {c: 'd'}])).toBe '''[ 104 | { 105 | a: "b" 106 | a1: "b1" 107 | } 108 | { 109 | c: "d" 110 | } 111 | ]''' 112 | 113 | describe "when formatting an object", -> 114 | describe "when the object is empty", -> 115 | it "returns {}", -> 116 | expect(CSON.stringify({})).toBe "{}" 117 | 118 | it "returns formatted CSON", -> 119 | expect(CSON.stringify(a: {b: 'c'})).toBe ''' 120 | a: 121 | b: "c" 122 | ''' 123 | expect(CSON.stringify(a:{})).toBe 'a: {}' 124 | expect(CSON.stringify(a:[])).toBe 'a: []' 125 | 126 | it "escapes object keys", -> 127 | expect(CSON.stringify('\\t': 3)).toBe '"\\\\t": 3' 128 | 129 | describe "when converting back to an object", -> 130 | it "produces the original object", -> 131 | object = 132 | a: true 133 | b: 20 134 | c: 135 | d: ['a', 'b'] 136 | e: 137 | f: true 138 | 139 | cson = CSON.stringify(object) 140 | CSONParser = require 'cson-parser' 141 | evaledObject = CSONParser.parse(cson) 142 | expect(evaledObject).toEqual object 143 | 144 | describe '.parse', -> 145 | it 'returns the javascript value', -> 146 | expect(CSON.parse 'a: "b"').toEqual a: 'b' 147 | 148 | describe ".isObjectPath(objectPath)", -> 149 | it "returns true if the path has an object extension", -> 150 | expect(CSON.isObjectPath('/test2.json')).toBe true 151 | expect(CSON.isObjectPath('/a/b.cson')).toBe true 152 | expect(CSON.isObjectPath()).toBe false 153 | expect(CSON.isObjectPath(null)).toBe false 154 | expect(CSON.isObjectPath('')).toBe false 155 | expect(CSON.isObjectPath('a/b/c.txt')).toBe false 156 | 157 | describe ".resolve(objectPath)", -> 158 | it "returns the path to the object file", -> 159 | objectDir = temp.mkdirSync('season-object-dir-') 160 | file1 = path.join(objectDir, 'file1.json') 161 | file2 = path.join(objectDir, 'file2.cson') 162 | file3 = path.join(objectDir, 'file3.json') 163 | folder1 = path.join(objectDir, 'folder1.json') 164 | fs.mkdirSync(folder1) 165 | fs.writeFileSync(file1, '{}') 166 | fs.writeFileSync(file2, '{}') 167 | fs.writeFileSync(file3, '{}') 168 | 169 | expect(CSON.resolve(file1)).toBe file1 170 | expect(CSON.resolve(file2)).toBe file2 171 | expect(CSON.resolve(file3)).toBe file3 172 | expect(CSON.resolve(path.join(objectDir, 'file4'))).toBe null 173 | expect(CSON.resolve(folder1)).toBe null 174 | expect(CSON.resolve()).toBe null 175 | expect(CSON.resolve(null)).toBe null 176 | expect(CSON.resolve('')).toBe null 177 | 178 | describe ".writeFile(objectPath, object, callback)", -> 179 | object = 180 | a: 1 181 | b: 2 182 | 183 | describe "when called with a .json path", -> 184 | it "writes the object and calls back", -> 185 | jsonPath = path.join(temp.mkdirSync('season-object-dir-'), 'file1.json') 186 | callback = jasmine.createSpy('callback') 187 | CSON.writeFile(jsonPath, object, callback) 188 | 189 | waitsFor -> 190 | callback.callCount is 1 191 | 192 | runs -> 193 | expect(CSON.readFileSync(jsonPath)).toEqual object 194 | 195 | describe "when called with a .cson path", -> 196 | csonPath = path.join(temp.mkdirSync('season-object-dir-'), 'file1.cson') 197 | 198 | it "writes the object and calls back", -> 199 | callback = jasmine.createSpy('callback') 200 | CSON.writeFile(csonPath, object, callback) 201 | 202 | waitsFor -> 203 | callback.callCount is 1 204 | 205 | runs -> 206 | expect(CSON.readFileSync(csonPath)).toEqual object 207 | 208 | describe "caching", -> 209 | describe "synchronous reads", -> 210 | it "caches the contents of the compiled CSON files", -> 211 | samplePath = path.join(__dirname, 'fixtures', 'sample.cson') 212 | cacheDir = temp.mkdirSync('cache-dir') 213 | CSON.setCacheDir(cacheDir) 214 | CSON.resetCacheStats() 215 | CSONParser = require 'cson-parser' 216 | spyOn(CSONParser, 'parse').andCallThrough() 217 | 218 | expect(CSON.getCacheHits()).toBe 0 219 | expect(CSON.getCacheMisses()).toBe 0 220 | 221 | expect(CSON.readFileSync(samplePath)).toEqual {a: 1, b: c: true} 222 | expect(CSONParser.parse.callCount).toBe 1 223 | expect(CSON.getCacheHits()).toBe 0 224 | expect(CSON.getCacheMisses()).toBe 1 225 | 226 | CSONParser.parse.reset() 227 | expect(CSON.readFileSync(samplePath)).toEqual {a: 1, b: c: true} 228 | expect(CSONParser.parse.callCount).toBe 0 229 | expect(CSON.getCacheHits()).toBe 1 230 | expect(CSON.getCacheMisses()).toBe 1 231 | 232 | describe "asynchronous reads", -> 233 | it "caches the contents of the compiled CSON files", -> 234 | samplePath = path.join(__dirname, 'fixtures', 'sample.cson') 235 | cacheDir = temp.mkdirSync('cache-dir') 236 | CSON.setCacheDir(cacheDir) 237 | CSON.resetCacheStats() 238 | CSONParser = require 'cson-parser' 239 | spyOn(CSONParser, 'parse').andCallThrough() 240 | 241 | expect(CSON.getCacheHits()).toBe 0 242 | expect(CSON.getCacheMisses()).toBe 0 243 | 244 | sample = null 245 | CSON.readFile samplePath, (error, object) -> sample = object 246 | waitsFor -> sample? 247 | runs -> 248 | expect(sample).toEqual {a: 1, b: c: true} 249 | expect(CSONParser.parse.callCount).toBe 1 250 | expect(CSON.getCacheHits()).toBe 0 251 | expect(CSON.getCacheMisses()).toBe 1 252 | 253 | CSONParser.parse.reset() 254 | sample = null 255 | CSON.readFile samplePath, (error, object) -> sample = object 256 | waitsFor -> sample? 257 | runs -> 258 | expect(CSONParser.parse.callCount).toBe 0 259 | expect(CSON.getCacheHits()).toBe 1 260 | expect(CSON.getCacheMisses()).toBe 1 261 | 262 | describe "readFileSync", -> 263 | it "returns null for files that are all whitespace", -> 264 | expect(CSON.readFileSync(path.join(__dirname, 'fixtures', 'empty.cson'))).toBeNull() 265 | expect(CSON.readFileSync(path.join(__dirname, 'fixtures', 'empty.json'))).toBeNull() 266 | expect(CSON.readFileSync(path.join(__dirname, 'fixtures', 'empty-line.cson'))).toBeNull() 267 | expect(CSON.readFileSync(path.join(__dirname, 'fixtures', 'empty-line.json'))).toBeNull() 268 | 269 | it "throws errors for invalid .cson files", -> 270 | errorPath = path.join(__dirname, 'fixtures', 'syntax-error.cson') 271 | parseError = null 272 | 273 | try 274 | CSON.readFileSync(errorPath) 275 | catch error 276 | parseError = error 277 | 278 | expect(parseError.path).toBe errorPath 279 | expect(parseError.filename).toBe errorPath 280 | expect(parseError.location.first_line).toBe 0 281 | expect(parseError.location.first_column).toBe 3 282 | 283 | it "throws errors for invalid .json files", -> 284 | errorPath = path.join(__dirname, 'fixtures', 'syntax-error.json') 285 | parseError = null 286 | 287 | try 288 | CSON.readFileSync(errorPath) 289 | catch error 290 | parseError = error 291 | 292 | expect(parseError.path).toBe errorPath 293 | expect(parseError.filename).toBe errorPath 294 | 295 | it "does not increment the cache stats when .json files are read", -> 296 | expect(CSON.getCacheHits()).toBe 0 297 | expect(CSON.getCacheMisses()).toBe 0 298 | CSON.readFileSync(path.join(__dirname, 'fixtures', 'sample.json')) 299 | expect(CSON.getCacheHits()).toBe 0 300 | expect(CSON.getCacheMisses()).toBe 0 301 | 302 | describe "when the allowDuplicateKeys option is set to false", -> 303 | it "throws errors if objects contain duplicate keys", -> 304 | expect(-> 305 | CSON.readFileSync(path.join(__dirname, 'fixtures', 'duplicate-keys.cson'), allowDuplicateKeys: false) 306 | ).toThrow("Duplicate key 'foo'") 307 | 308 | expect(CSON.readFileSync(path.join(__dirname, 'fixtures', 'sample.cson'), allowDuplicateKeys: false)).toEqual({ 309 | a: 1, b: {c: true} 310 | }) 311 | 312 | expect(CSON.readFileSync(path.join(__dirname, 'fixtures', 'duplicate-keys.cson'))).toEqual({ 313 | foo: 3, bar: 2 314 | }) 315 | 316 | describe "readFile", -> 317 | it "calls back with null for files that are all whitespace", -> 318 | callback = (error, content) -> 319 | expect(error).toBeNull() 320 | expect(content).toBeNull() 321 | 322 | readFile(path.join(__dirname, 'fixtures', 'empty.cson'), callback) 323 | readFile(path.join(__dirname, 'fixtures', 'empty.json'), callback) 324 | readFile(path.join(__dirname, 'fixtures', 'empty-line.cson'), callback) 325 | readFile(path.join(__dirname, 'fixtures', 'empty-line.json'), callback) 326 | 327 | it "calls back with an error for files that do no exist", -> 328 | callback = (error, content) -> 329 | expect(error).not.toBeNull() 330 | expect(content).toBeUndefined() 331 | 332 | readFile(path.join(__dirname, 'fixtures', 'this-file-does-not-exist.cson'), callback) 333 | readFile(path.join(__dirname, 'fixtures', 'this-file-does-not-exist.json'), callback) 334 | 335 | it "calls back with null for files that are all comments", -> 336 | callback = (error, content) -> 337 | expect(error).toBeNull() 338 | expect(content).toBeNull() 339 | 340 | readFile(path.join(__dirname, 'fixtures', 'single-comment.cson'), callback) 341 | readFile(path.join(__dirname, 'fixtures', 'multi-comment.cson'), callback) 342 | 343 | it "calls back with an error for invalid files", -> 344 | done = false 345 | 346 | callback = (error, content) -> 347 | done = true 348 | expect(error).not.toBeNull() 349 | expect(error.path).toEqual path.join(__dirname, 'fixtures', 'invalid.cson') 350 | expect(error.message).toContain path.join(__dirname, 'fixtures', 'invalid.cson') 351 | expect(content).toBeUndefined() 352 | 353 | readFile(path.join(__dirname, 'fixtures', 'invalid.cson'), callback) 354 | 355 | waitsFor -> done 356 | 357 | it "calls back with location information for .cson files with syntax errors", -> 358 | done = false 359 | errorPath = path.join(__dirname, 'fixtures', 'syntax-error.cson') 360 | 361 | callback = (parseError, content) -> 362 | done = true 363 | expect(parseError.path).toBe errorPath 364 | expect(parseError.filename).toBe errorPath 365 | expect(parseError.location.first_line).toBe 0 366 | expect(parseError.location.first_column).toBe 3 367 | 368 | readFile(errorPath, callback) 369 | 370 | waitsFor -> done 371 | 372 | it "calls back with path information for .json files with syntax errors", -> 373 | done = false 374 | errorPath = path.join(__dirname, 'fixtures', 'syntax-error.json') 375 | 376 | callback = (parseError, content) -> 377 | done = true 378 | expect(parseError.path).toBe errorPath 379 | expect(parseError.filename).toBe errorPath 380 | 381 | readFile(errorPath, callback) 382 | 383 | waitsFor -> done 384 | 385 | describe "when the allowDuplicateKeys option is set to false", -> 386 | it "calls back with an error if objects contain duplicate keys", -> 387 | fixturePath = path.join(__dirname, 'fixtures', 'duplicate-keys.cson') 388 | done = false 389 | 390 | runs -> 391 | CSON.readFile fixturePath, {allowDuplicateKeys: false}, (err, content) -> 392 | expect(err.message).toContain("Duplicate key 'foo'") 393 | expect(content).toBeUndefined() 394 | done = true 395 | 396 | waitsFor -> done 397 | 398 | runs -> 399 | done = false 400 | CSON.readFile fixturePath, (err, content) -> 401 | expect(content).toEqual({ 402 | foo: 3, 403 | bar: 2 404 | }) 405 | done = true 406 | 407 | waitsFor -> done 408 | 409 | describe "when an error is thrown by the callback", -> 410 | uncaughtListeners = null 411 | 412 | beforeEach -> 413 | uncaughtListeners = process.listeners('uncaughtException') 414 | process.removeAllListeners('uncaughtException') 415 | 416 | afterEach -> 417 | for listener in uncaughtListeners 418 | process.on('uncaughtException', listener) 419 | 420 | it "only calls the callback once when it throws an error", -> 421 | called = 0 422 | callback = -> 423 | called++ 424 | throw new Error('called') 425 | 426 | uncaughtHandler = jasmine.createSpy('uncaughtHandler') 427 | process.once('uncaughtException', uncaughtHandler) 428 | 429 | CSON.readFile(path.join(__dirname, 'fixtures', 'sample.cson'), callback) 430 | 431 | waitsFor -> 432 | called > 0 433 | 434 | runs -> 435 | expect(called).toBe 1 436 | expect(uncaughtHandler.callCount).toBe 1 437 | 438 | describe "when options are provided for the underlying fs call", -> 439 | 440 | it "passes options to the readFileSync call", -> 441 | spyOn(fs, 'readFileSync').andReturn "{}" 442 | spyOn(parser, 'parse').andCallThrough() 443 | 444 | CSON.readFileSync("/foo/blarg.cson", {encoding: 'cuneiform', allowDuplicateKeys: false}) 445 | 446 | expect(fs.readFileSync).toHaveBeenCalledWith "/foo/blarg.cson", {encoding: 'cuneiform'} 447 | expect(parser.parse.calls[0].args[0]).toEqual "{}" 448 | expect(typeof parser.parse.calls[0].args[1]).toEqual "function" 449 | 450 | it "passes options to the readFile call", -> 451 | called = 0 452 | callback = -> called++ 453 | 454 | spyOn(parser, 'parse').andCallThrough() 455 | spyOn(fs, 'readFile').andCallFake (filePath, fsOptions, callback) -> 456 | expect(filePath).toEqual "/bar/blarg.cson" 457 | expect(fsOptions).toEqual {encoding: 'cuneiform'} 458 | 459 | callback(null, "{}") 460 | 461 | cb = jasmine.createSpy 'callback' 462 | CSON.readFile("/bar/blarg.cson", {encoding: 'cuneiform', allowDuplicateKeys: false}, cb) 463 | 464 | expect(fs.readFile).toHaveBeenCalled() 465 | expect(parser.parse.calls[0].args[0]).toEqual "{}" 466 | expect(typeof parser.parse.calls[0].args[1]).toEqual "function" 467 | expect(cb).toHaveBeenCalledWith null, {} 468 | 469 | it "passes options to the writeFileSync call", -> 470 | spyOn(fs, 'writeFileSync').andCallFake (filePath, payload, fileOptions) -> 471 | expect(filePath).toEqual "/stuff/wat.cson" 472 | expect(fileOptions).toEqual {mode: 0o755} 473 | 474 | CSON.writeFileSync("/stuff/wat.cson", {data: 'yep'}, {mode: 0o755}) 475 | 476 | expect(fs.writeFileSync).toHaveBeenCalled() 477 | expect(fs.writeFileSync.calls[0].args[2]).toEqual {mode: 0o755} 478 | 479 | it "passes options to the writeFile call", -> 480 | spyOn(fs, 'writeFile').andCallFake (filePath, payload, fileOptions, callback) -> 481 | expect(filePath).toEqual "/eh/stuff.cson" 482 | expect(fileOptions).toEqual {flag: 'x'} 483 | callback(null) 484 | 485 | cb = jasmine.createSpy 'callback' 486 | CSON.writeFile("/eh/stuff.cson", {}, {flag: 'x'}, cb) 487 | 488 | expect(fs.writeFile).toHaveBeenCalled() 489 | expect(cb).toHaveBeenCalledWith null 490 | -------------------------------------------------------------------------------- /spec/csonc-spec.coffee: -------------------------------------------------------------------------------- 1 | path = require 'path' 2 | fs = require 'fs-plus' 3 | temp = require 'temp' 4 | csonc = require '../lib/csonc' 5 | 6 | describe "CSON compilation to JSON", -> 7 | [compileDir, inputFile, outputFile] = [] 8 | 9 | beforeEach -> 10 | compileDir = temp.mkdirSync('season-compile-dir-') 11 | inputFile = path.join(compileDir, 'input.cson') 12 | outputFile = path.join(compileDir, 'input.json') 13 | spyOn(process, 'exit') 14 | spyOn(console, 'error') 15 | 16 | it "writes the output file to the input file's directory with the same base name and .json extension", -> 17 | fs.writeFileSync(inputFile, 'deadmau: 5') 18 | csonc([inputFile, '--output', outputFile]) 19 | expect(fs.readFileSync(outputFile, {encoding: 'utf8'})).toBe '{\n "deadmau": 5\n}\n' 20 | 21 | describe "when a valid CSON file is specified", -> 22 | it "converts the file to JSON and writes it out", -> 23 | fs.writeFileSync(inputFile, 'a: 3') 24 | csonc([inputFile, '--output', outputFile]) 25 | expect(fs.readFileSync(outputFile, {encoding: 'utf8'})).toBe '{\n "a": 3\n}\n' 26 | 27 | describe "when an input CSON file is invalid CoffeeScript", -> 28 | it "logs an error and exits", -> 29 | fs.writeFileSync(inputFile, '<->') 30 | csonc([inputFile, '--output', outputFile]) 31 | expect(process.exit.mostRecentCall.args[0]).toBe 1 32 | expect(console.error.mostRecentCall.args[0].length).toBeGreaterThan 0 33 | 34 | describe "when the input CSON file does not exist", -> 35 | it "logs an error and exits", -> 36 | csonc([inputFile]) 37 | expect(process.exit.mostRecentCall.args[0]).toBe 1 38 | expect(console.error.mostRecentCall.args[0].length).toBeGreaterThan 0 39 | 40 | describe "when the root option is set", -> 41 | describe "when the input CSON file contains a root object", -> 42 | it "converts the file to JSON and writes it out", -> 43 | fs.writeFileSync(inputFile, 'a: 3') 44 | csonc(['--root', inputFile, '--output', outputFile]) 45 | expect(fs.readFileSync(outputFile, {encoding: 'utf8'})).toBe '{\n "a": 3\n}\n' 46 | 47 | describe "when the input CSON file contains a root array", -> 48 | it "logs and error and exits", -> 49 | fs.writeFileSync(inputFile, '[1,2,3]') 50 | csonc(['--root', inputFile, '--output', outputFile]) 51 | expect(process.exit.mostRecentCall.args[0]).toBe 1 52 | expect(console.error.mostRecentCall.args[0].length).toBeGreaterThan 0 53 | -------------------------------------------------------------------------------- /spec/fixtures/duplicate-keys.cson: -------------------------------------------------------------------------------- 1 | foo: 1 2 | bar: 2 3 | foo: 3 4 | -------------------------------------------------------------------------------- /spec/fixtures/empty-line.cson: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /spec/fixtures/empty-line.json: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /spec/fixtures/empty.cson: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atom/season/6d55bbe52f64d2e1dd4326703b4c20bc342d9006/spec/fixtures/empty.cson -------------------------------------------------------------------------------- /spec/fixtures/empty.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atom/season/6d55bbe52f64d2e1dd4326703b4c20bc342d9006/spec/fixtures/empty.json -------------------------------------------------------------------------------- /spec/fixtures/invalid.cson: -------------------------------------------------------------------------------- 1 | foo: -> 2 | -------------------------------------------------------------------------------- /spec/fixtures/multi-comment.cson: -------------------------------------------------------------------------------- 1 | # foo 2 | # bar 3 | 4 | # foo 5 | -------------------------------------------------------------------------------- /spec/fixtures/sample.cson: -------------------------------------------------------------------------------- 1 | 'a': 1 2 | 'b': 3 | 'c': true 4 | -------------------------------------------------------------------------------- /spec/fixtures/sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "foo": "bar" 3 | } 4 | -------------------------------------------------------------------------------- /spec/fixtures/single-comment.cson: -------------------------------------------------------------------------------- 1 | # foo 2 | -------------------------------------------------------------------------------- /spec/fixtures/syntax-error.cson: -------------------------------------------------------------------------------- 1 | foo': [] 2 | -------------------------------------------------------------------------------- /spec/fixtures/syntax-error.json: -------------------------------------------------------------------------------- 1 | foo': [] 2 | -------------------------------------------------------------------------------- /src/cson.coffee: -------------------------------------------------------------------------------- 1 | crypto = require 'crypto' 2 | path = require 'path' 3 | 4 | fs = require 'fs-plus' 5 | CSON = null # defer until used 6 | 7 | csonCache = null 8 | 9 | stats = 10 | hits: 0 11 | misses: 0 12 | 13 | getCachePath = (cson) -> 14 | digest = crypto.createHash('sha1').update(cson, 'utf8').digest('hex') 15 | path.join(csonCache, "#{digest}.json") 16 | 17 | writeCacheFileSync = (cachePath, object) -> 18 | try 19 | fs.writeFileSync(cachePath, JSON.stringify(object)) 20 | 21 | writeCacheFile = (cachePath, object) -> 22 | fs.writeFile cachePath, JSON.stringify(object), -> 23 | 24 | parseObject = (objectPath, contents, options) -> 25 | if path.extname(objectPath) is '.cson' 26 | CSON ?= require 'cson-parser' 27 | try 28 | parsed = CSON.parse(contents, detectDuplicateKeys if options?.allowDuplicateKeys is false) 29 | stats.misses++ 30 | return parsed 31 | catch error 32 | if isAllCommentsAndWhitespace(contents) 33 | return null 34 | else 35 | throw error 36 | else 37 | JSON.parse(contents) 38 | 39 | parseCacheContents = (contents) -> 40 | parsed = JSON.parse(contents) 41 | stats.hits++ 42 | parsed 43 | 44 | parseContentsSync = (objectPath, cachePath, contents, options) -> 45 | try 46 | object = parseObject(objectPath, contents, options) 47 | catch parseError 48 | parseError.path ?= objectPath 49 | parseError.filename ?= objectPath 50 | throw parseError 51 | 52 | writeCacheFileSync(cachePath, object) if cachePath 53 | object 54 | 55 | isAllCommentsAndWhitespace = (contents) -> 56 | lines = contents.split('\n') 57 | while lines.length > 0 58 | line = lines[0].trim() 59 | if line.length is 0 or line[0] is '#' 60 | lines.shift() 61 | else 62 | return false 63 | true 64 | 65 | parseContents = (objectPath, cachePath, contents, options, callback) -> 66 | try 67 | object = parseObject(objectPath, contents, options) 68 | catch parseError 69 | parseError.path = objectPath 70 | parseError.filename ?= objectPath 71 | parseError.message = "#{objectPath}: #{parseError.message}" 72 | callback?(parseError) 73 | return 74 | 75 | writeCacheFile(cachePath, object) if cachePath 76 | callback?(null, object) 77 | return 78 | 79 | module.exports = 80 | setCacheDir: (cacheDirectory) -> csonCache = cacheDirectory 81 | 82 | isObjectPath: (objectPath) -> 83 | return false unless objectPath 84 | 85 | extension = path.extname(objectPath) 86 | extension is '.cson' or extension is '.json' 87 | 88 | resolve: (objectPath='') -> 89 | return null unless objectPath 90 | 91 | return objectPath if @isObjectPath(objectPath) and fs.isFileSync(objectPath) 92 | 93 | jsonPath = "#{objectPath}.json" 94 | return jsonPath if fs.isFileSync(jsonPath) 95 | 96 | csonPath = "#{objectPath}.cson" 97 | return csonPath if fs.isFileSync(csonPath) 98 | 99 | null 100 | 101 | readFileSync: (objectPath, options = {}) -> 102 | parseOptions = 103 | allowDuplicateKeys: options.allowDuplicateKeys 104 | delete options.allowDuplicateKeys 105 | 106 | fsOptions = Object.assign({encoding: 'utf8'}, options) 107 | 108 | contents = fs.readFileSync(objectPath, fsOptions) 109 | return null if contents.trim().length is 0 110 | if csonCache and path.extname(objectPath) is '.cson' 111 | cachePath = getCachePath(contents) 112 | if fs.isFileSync(cachePath) 113 | try 114 | return parseCacheContents(fs.readFileSync(cachePath, 'utf8')) 115 | 116 | parseContentsSync(objectPath, cachePath, contents, parseOptions) 117 | 118 | readFile: (objectPath, options, callback) -> 119 | if arguments.length < 3 120 | callback = options 121 | options = {} 122 | 123 | parseOptions = 124 | allowDuplicateKeys: options.allowDuplicateKeys 125 | delete options.allowDuplicateKeys 126 | 127 | fsOptions = Object.assign({encoding: 'utf8'}, options) 128 | 129 | fs.readFile objectPath, fsOptions, (error, contents) => 130 | return callback?(error) if error? 131 | return callback?(null, null) if contents.trim().length is 0 132 | 133 | if csonCache and path.extname(objectPath) is '.cson' 134 | cachePath = getCachePath(contents) 135 | fs.stat cachePath, (error, stat) -> 136 | if stat?.isFile() 137 | fs.readFile cachePath, 'utf8', (error, cached) -> 138 | try 139 | parsed = parseCacheContents(cached) 140 | catch error 141 | try 142 | parseContents(objectPath, cachePath, contents, parseOptions, callback) 143 | return 144 | callback?(null, parsed) 145 | else 146 | parseContents(objectPath, cachePath, contents, parseOptions, callback) 147 | else 148 | parseContents(objectPath, null, contents, parseOptions, callback) 149 | 150 | writeFile: (objectPath, object, options, callback) -> 151 | if arguments.length < 4 152 | callback = options 153 | options = {} 154 | callback ?= -> 155 | 156 | try 157 | contents = @stringifyPath(objectPath, object) 158 | catch error 159 | callback(error) 160 | return 161 | 162 | fs.writeFile(objectPath, "#{contents}\n", options, callback) 163 | 164 | writeFileSync: (objectPath, object, options = undefined) -> 165 | fs.writeFileSync(objectPath, "#{@stringifyPath(objectPath, object)}\n", options) 166 | 167 | stringifyPath: (objectPath, object, visitor, space) -> 168 | if path.extname(objectPath) is '.cson' 169 | @stringify(object, visitor, space) 170 | else 171 | JSON.stringify(object, undefined, 2) 172 | 173 | stringify: (object, visitor, space = 2) -> 174 | CSON ?= require 'cson-parser' 175 | CSON.stringify(object, visitor, space) 176 | 177 | parse: (str, reviver) -> 178 | CSON ?= require 'cson-parser' 179 | CSON.parse(str, reviver) 180 | 181 | getCacheHits: -> stats.hits 182 | 183 | getCacheMisses: -> stats.misses 184 | 185 | resetCacheStats: -> 186 | stats = 187 | hits: 0 188 | misses: 0 189 | 190 | detectDuplicateKeys = (key, value) -> 191 | if this.hasOwnProperty(key) and this[key] isnt value 192 | throw new Error("Duplicate key '#{key}'") 193 | else 194 | value 195 | -------------------------------------------------------------------------------- /src/csonc.coffee: -------------------------------------------------------------------------------- 1 | fs = require 'fs' 2 | path = require 'path' 3 | yargs = require 'yargs' 4 | CSON = require 'cson-parser' 5 | 6 | module.exports = (argv=[]) -> 7 | 8 | options = yargs(argv) 9 | options.usage """ 10 | Usage: csonc [options] cson_file --output json_file 11 | csonc [options] < cson_file [> json_file] 12 | 13 | Compiles CSON to JSON. 14 | 15 | If no input file is specified then the CSON is read from standard in. 16 | 17 | If no output file is specified then the JSON is written to standard out. 18 | """ 19 | options.alias('h', 'help').describe('help', 'Print this help message') 20 | options.alias('r', 'root').boolean('root').describe('root', 'Require that the input file contain an object at the root').default('root', false) 21 | options.alias('o', 'output').string('output').describe('output', 'File path to write the JSON output to') 22 | options.alias('v', 'version').describe('version', 'Print the version') 23 | 24 | {argv} = options 25 | [inputFile] = argv._ 26 | inputFile = path.resolve(inputFile) if inputFile 27 | 28 | if argv.version 29 | {version} = require '../package.json' 30 | console.log(version) 31 | return 32 | 33 | if argv.help 34 | options.showHelp() 35 | return 36 | 37 | parseData = (data) -> 38 | try 39 | object = CSON.parse(data) 40 | 41 | if argv.r and (!_.isObject(object) or _.isArray(object)) 42 | console.error("CSON data does not contain a root object") 43 | process.exit(1) 44 | return 45 | catch error 46 | console.error("Parsing data failed: #{error.message}") 47 | process.exit(1) 48 | 49 | json = JSON.stringify(object, undefined, 2) + "\n" 50 | if argv.output 51 | outputFile = path.resolve(argv.output) 52 | try 53 | fs.writeFileSync(outputFile, json) 54 | catch error 55 | console.error("Writing #{outputFile} failed: #{error.code ? error}") 56 | else 57 | process.stdout.write(json) 58 | 59 | if inputFile 60 | try 61 | parseData(fs.readFileSync(inputFile, 'utf8')) 62 | catch error 63 | console.error("Reading #{inputFile} failed: #{error.code ? error}") 64 | process.exit(1) 65 | else 66 | process.stdin.resume() 67 | process.stdin.setEncoding('utf8') 68 | data = '' 69 | process.stdin.on 'data', (chunk) -> data += chunk.toString() 70 | process.stdin.on 'end', -> parseData(data) 71 | --------------------------------------------------------------------------------