├── npm-autoload.yaml ├── .npmignore ├── .gitignore ├── tsconfig.json ├── types └── yaml.d.ts ├── package.yaml ├── package.json ├── readme.md ├── src └── index.ts └── index.js /npm-autoload.yaml: -------------------------------------------------------------------------------- 1 | - ./index 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | tsconfig.json 3 | .package.*~ 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | .package.*~ 4 | index.d.ts 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": ".", 4 | "target": "es2015", 5 | "module": "commonjs", 6 | "strict": true, 7 | "declaration": true, 8 | "noImplicitAny": true, 9 | "alwaysStrict": true, 10 | "noUnusedLocals": true, 11 | "noUnusedParameters": true, 12 | "esModuleInterop": true, 13 | "experimentalDecorators": true, 14 | "emitDecoratorMetadata": true 15 | }, 16 | "include": [ 17 | "./src/**/*" 18 | ], 19 | "exclude": [ 20 | ] 21 | } -------------------------------------------------------------------------------- /types/yaml.d.ts: -------------------------------------------------------------------------------- 1 | import yaml from 'yaml'; 2 | 3 | declare module 'yaml' { 4 | namespace ast { 5 | type PathKey = string|number; 6 | interface Collection { 7 | getIn(path: PathKey[], keepScalar:true): any; 8 | getIn(path: PathKey[], keepScalar?:boolean): AstNode | undefined; 9 | setIn(path: PathKey[], value: any): void; 10 | deleteIn(path: PathKey[]): boolean; 11 | get(key:PathKey, keepScalar:true): any; 12 | get(key:PathKey, keepScalar?:boolean): AstNode | undefined; 13 | set(key:PathKey, value:any): void; 14 | delete(key:PathKey): boolean; 15 | } 16 | 17 | interface Document extends Collection { 18 | // Not strictly true but good enough for our purposes 19 | } 20 | 21 | interface MapBase extends Collection { 22 | 23 | } 24 | 25 | interface SeqBase extends Collection { 26 | 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /package.yaml: -------------------------------------------------------------------------------- 1 | name: package-yaml 2 | version: 1.0.0 3 | description: Reversible YAML parsing for package.json 4 | author: Danielle Church 5 | license: MIT 6 | keywords: 7 | - package.json 8 | - package.yaml 9 | - package.yml 10 | - yaml 11 | 12 | main: index.js 13 | types: index.d.ts 14 | scripts: 15 | build: tsc 16 | watch: tsc --watch 17 | prepare: tsc 18 | 19 | repository: 20 | type: git 21 | url: git+https://github.com/dmchurch/package-yaml.git 22 | bugs: 23 | url: https://github.com/dmchurch/package-yaml/issues 24 | homepage: https://github.com/dmchurch/package-yaml#readme 25 | 26 | dependencies: 27 | deep-diff: ^1.0.2 28 | yaml: ^1.7.0 29 | reflect-metadata: ^0.1.13 30 | npmlog: ^4.1.2 31 | osenv: ^0.1.5 32 | pkg-dir: ^4.2.0 33 | npm-autoloader: ^1.0.0 34 | 35 | devDependencies: 36 | "@types/deep-diff": ^1.0.0 37 | "@types/node": ^12.7.8 38 | "@types/yaml": ^1.0.2 39 | typescript: ^3.6.3 40 | "@types/npmlog": ^4.1.2 41 | "@types/osenv": ^0.1.0 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "package-yaml", 3 | "version": "1.0.0", 4 | "description": "Reversible YAML parsing for package.json", 5 | "author": "Danielle Church ", 6 | "license": "MIT", 7 | "keywords": [ 8 | "package.json", 9 | "package.yaml", 10 | "package.yml", 11 | "yaml" 12 | ], 13 | "main": "index.js", 14 | "scripts": { 15 | "build": "tsc", 16 | "watch": "tsc --watch", 17 | "prepare": "tsc" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/dmchurch/package-yaml.git" 22 | }, 23 | "bugs": { 24 | "url": "https://github.com/dmchurch/package-yaml/issues" 25 | }, 26 | "homepage": "https://github.com/dmchurch/package-yaml#readme", 27 | "dependencies": { 28 | "deep-diff": "^1.0.2", 29 | "npm-autoloader": "^1.0.0", 30 | "npmlog": "^4.1.2", 31 | "osenv": "^0.1.5", 32 | "pkg-dir": "^4.2.0", 33 | "reflect-metadata": "^0.1.13", 34 | "yaml": "^1.7.0" 35 | }, 36 | "devDependencies": { 37 | "@types/deep-diff": "^1.0.0", 38 | "@types/node": "^12.7.8", 39 | "@types/npmlog": "^4.1.2", 40 | "@types/osenv": "^0.1.0", 41 | "@types/yaml": "^1.0.2", 42 | "typescript": "^3.6.3" 43 | }, 44 | "types": "index.d.ts" 45 | } 46 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # package-yaml 2 | 3 | This builds on [@reggi](https://github.com/reggi)'s [`package-yml`](https://github.com/reggi/package-yml), which itself is on the shoulders of the work of [@saschagehlich](https://github.com/saschagehlich) and the [`npm-yaml`](https://github.com/saschagehlich/npm-yaml) project. In an effort to use `yaml` instead of `json` for npm's package file, while still retaining compatibility with legacy tools. 4 | 5 | ## Install 6 | 7 | The recommended installation is to install `npm-autoloader` globally, and add `package-yaml` as a devDependency of your project: 8 | 9 | ```bash 10 | # If npm-autoloader is not already installed: 11 | npm install npm-autoloader --global 12 | npm config set onload-script npm-autoloader --global 13 | 14 | # To install package-yaml for this project: 15 | npm install package-yaml --save-dev 16 | echo "- package-yaml" >> npm-autoload.json 17 | ``` 18 | 19 | Alternately, you can install package-yaml globally on its own: 20 | 21 | ```bash 22 | npm install package-yaml --global 23 | npm config set onload-script package-yaml 24 | ``` 25 | 26 | Or, less portably, manually in a package-local setting: 27 | 28 | ```bash 29 | npm install package-yaml --save-dev 30 | echo "onload-script=$PWD/node_modules/package-yaml" >> .npmrc 31 | ``` 32 | 33 | ## Why 34 | 35 | It's easier to read and write `yaml` over `json`. 36 | 37 | ## How 38 | 39 | Every time you run an `npm` command `package-yaml` will check for a `yaml` file. If one exists it will update the existing `json` file with the contents. When the `npm` process exists the contents from the `json` will update the `yaml` file. 40 | 41 | --- 42 | 43 | Copyright (c) 2014 Thomas Reggi, 2019 Danielle Church 44 | 45 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 46 | 47 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 48 | 49 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 50 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /// 4 | 5 | import yaml from 'yaml'; 6 | import fs from 'fs'; 7 | import path from 'path'; 8 | import { diff, Diff, applyChange } from 'deep-diff'; 9 | import log from 'npmlog'; 10 | import 'reflect-metadata'; 11 | import osenv from 'osenv'; 12 | import { calledFromNPM, getNPM, NPMExtensionCommand, npmExit } from 'npm-autoloader'; 13 | import pkgDir from 'pkg-dir'; 14 | 15 | log.heading = 'package-yaml'; 16 | if (!!process.env.DEBUG_PACKAGE_YAML) { 17 | log.level = 'verbose'; 18 | } 19 | 20 | type Constructor = 21 | T extends (undefined|null) ? never : 22 | T extends string ? StringConstructor : 23 | T extends number ? NumberConstructor : 24 | T extends boolean ? BooleanConstructor : 25 | T extends Function ? FunctionConstructor : 26 | T extends symbol ? SymbolConstructor : 27 | T extends bigint ? BigIntConstructor : 28 | new(...args:any[]) => T; 29 | type Instance = 30 | T extends SymbolConstructor ? Symbol : 31 | T extends BigIntConstructor ? BigInt : 32 | T extends new(...args:any)=>any ? InstanceType : 33 | never; 34 | 35 | type PickTypedPropNames = NonNullable<{[k in keyof T]: T[k] extends U ? k : never}[keyof T]>; 36 | type PickTypedProps = Pick>; 37 | type SimplePropNames = PickTypedPropNames; 38 | type SimpleProps = PickTypedProps; 39 | 40 | const propertyClassesKey = Symbol("propertyClasses"); 41 | 42 | function property(target: object, propertyKey: string) { 43 | const propClasses: {[p:string]:Constructor} = Reflect.getOwnMetadata(propertyClassesKey, target.constructor) || {}; 44 | const propClass:Constructor = Reflect.getMetadata('design:type', target, propertyKey); 45 | propClasses[propertyKey] = propClass; 46 | Reflect.defineMetadata(propertyClassesKey, propClasses, target.constructor); 47 | } 48 | 49 | function getPropClasses(target:Constructor | T): {[k in keyof T]: Constructor} { 50 | const classTarget:Constructor = typeof target === "object" ? target.constructor as Constructor : target 51 | const classProperties = Reflect.getOwnMetadata(propertyClassesKey, classTarget) || {}; 52 | return classProperties; 53 | } 54 | 55 | function getProps(target:Constructor | T): P[] { 56 | return Object.keys(getPropClasses(target)) as P[]; 57 | } 58 | 59 | function getPropClass(target:Constructor | T, prop:P): Constructor { 60 | return getPropClasses(target)[prop]; 61 | } 62 | 63 | function isPropClass>(target: Constructor, prop: keyof T, cls: U): prop is PickTypedPropNames> { 64 | const propClass:Constructor = getPropClass(target, prop); 65 | return (propClass === cls); 66 | } 67 | 68 | enum ConflictResolution { 69 | ask = "ask", 70 | useJson = "use-json", 71 | useYaml = "use-yaml", 72 | useLatest = "use-latest", 73 | }; 74 | 75 | type Mutable = { 76 | -readonly [P in keyof T]: T[P]; 77 | } 78 | class Config { 79 | @property readonly debug: boolean = false; 80 | @property readonly writeBackups: boolean = true; 81 | @property readonly backupPath: string = ".%s~"; // %s - basename; %S - full path with % interpolations 82 | @property readonly timestampFuzz: number = 5; 83 | @property readonly conflicts: ConflictResolution = ConflictResolution.ask; 84 | @property readonly tryMerge: boolean = true; // Only functions when backups are being written 85 | 86 | @property readonly defaultExtension: "yaml" | "yml" = "yaml"; 87 | 88 | _lockedProps: {-readonly [k in keyof Config]?: boolean} = {}; 89 | 90 | constructor(loadConfigFiles:boolean = true) { 91 | if (!!process.env.DEBUG_PACKAGE_YAML) { 92 | this.updateAndLock({debug:true}) 93 | } 94 | if (process.env.PACKAGE_YAML_FORCE) { 95 | const confl = `use-${process.env.PACKAGE_YAML_FORCE}`; 96 | if (Config.isValid("conflicts",confl)) { 97 | this.updateAndLock({conflicts: confl}); 98 | } 99 | } 100 | if (loadConfigFiles) { 101 | this.loadSystemConfig(); 102 | } 103 | } 104 | 105 | loadSystemConfig():void { 106 | for (let globalPath of ['/etc','/usr/local/etc']) { 107 | // FIXME: this won't work on Windows 108 | this.loadConfigFile(path.join(globalPath, "package-yaml.json")); 109 | this.loadConfigFile(path.join(globalPath, "package-yaml.yaml")); 110 | } 111 | const home = osenv.home(); 112 | this.loadConfigFile(path.join(home, ".package-yaml.json")); 113 | this.loadConfigFile(path.join(home, ".package-yaml.yaml")); 114 | } 115 | 116 | static isValid

(prop:P, value:any): value is Config[P] { 117 | log.verbose("Config.isValid","checking %s: %s", prop, value); 118 | if (prop === "conflicts") { 119 | log.verbose("Config.isValid","ovcfr: %o; includes: %s",Object.values(ConflictResolution),Object.values(ConflictResolution).includes(value as ConflictResolution)); 120 | return typeof value === 'string' && (Object.values(ConflictResolution).includes(value as ConflictResolution)); 121 | } else if (prop === "defaultExtension") { 122 | return value === 'yaml' || value === 'yml'; 123 | } else if (isPropClass(Config, prop, String)) { 124 | return typeof value === 'string'; 125 | } else if (isPropClass(Config, prop, Boolean)) { 126 | return true; // anything can be a Boolean if you just believe 127 | } else if (isPropClass(Config, prop, Number)) { 128 | return !isNaN(Number(value)); 129 | } 130 | return false; 131 | } 132 | 133 | validate(values: any): Partial> { 134 | const valid:Mutable>> = {}; 135 | const propNames = getProps(Config); 136 | 137 | for (const prop of propNames) { 138 | const val:any = values[prop]; 139 | if (this._lockedProps[prop] || !(prop in values) || !Config.isValid(prop, val)) { 140 | continue; 141 | } 142 | if (isPropClass(Config, prop, String)) { 143 | valid[prop] = String(values[prop]) as any; // We've already validated these 144 | } else if (isPropClass(Config, prop, Boolean)) { 145 | valid[prop] = !!values[prop]; 146 | } else if (isPropClass(Config, prop, Number)) { 147 | valid[prop] = Number(values[prop]); 148 | } 149 | } 150 | return valid; 151 | } 152 | update(values: Partial>):Partial> { 153 | const valid = this.validate(values); 154 | Object.assign(this, valid); 155 | if ('debug' in valid) { 156 | log.level = valid.debug ? 'verbose' : 'info'; 157 | } 158 | return valid; 159 | } 160 | 161 | lock(props: SimplePropNames[]):void { 162 | for (let prop of props) { 163 | if (prop in this) { 164 | this._lockedProps[prop as SimplePropNames] = true; 165 | } 166 | } 167 | } 168 | 169 | updateAndLock(values: Partial>):Partial> { 170 | const updated = this.update(values); 171 | this.lock(Object.keys(updated) as SimplePropNames[]); 172 | return updated; 173 | } 174 | 175 | loadConfigFile(path:string, rootElement?:string):Partial>|null { 176 | let configData:string; 177 | let configParsed; 178 | try { 179 | if (!fs.existsSync(path)) { 180 | return null; 181 | } 182 | configData = fs.readFileSync(path, {encoding: "utf8"}); 183 | } catch (e) { 184 | log.error("loadConfig", "Error loading config file %s: %s", path, e); 185 | return null; 186 | } 187 | try { 188 | // YAML parsing *should* work for JSON files without issue 189 | configParsed = yaml.parse(configData); 190 | } catch (yamlError) { 191 | // try using JSON as a backup 192 | try { 193 | configParsed = JSON.parse(configData) 194 | } catch (jsonError) { 195 | const error = path.endsWith(".json") ? jsonError : yamlError; 196 | log.error("loadConfig", "Error parsing YAML/JSON config file %s: %s", path, error); 197 | return null; 198 | } 199 | } 200 | if (rootElement) { 201 | if (!configParsed || typeof configParsed !== "object" || !configParsed[rootElement]) { 202 | // Acceptable, just like if the file didn't exist 203 | return null; 204 | } 205 | configParsed = configParsed[rootElement]; 206 | } 207 | if (!configParsed || typeof configParsed !== "object") { 208 | if (rootElement) { 209 | log.error("loadConfig", "Invalid configuration stanza %s in %s (should be an object)", rootElement, path); 210 | } else { 211 | log.error("loadConfig", "Invalid configuration file %s (should be a JSON/YAML object)", path); 212 | } 213 | return null; 214 | } 215 | return this.update(configParsed); 216 | } 217 | }; 218 | 219 | function loadAndParse(path:string, parser:(data:string)=>T, inhibitErrors?:false): T; 220 | function loadAndParse(path:string, parser:(data:string)=>T, inhibitErrors?:true): T | null; 221 | function loadAndParse(path:string, parser:(data:string)=>T, inhibitErrors=false): T | null { 222 | try { 223 | const data = fs.readFileSync(path, {encoding:"utf8"}); 224 | return parser(data); 225 | } catch (e) { 226 | if (inhibitErrors) { 227 | return null; 228 | } 229 | throw e; 230 | } 231 | } 232 | 233 | export class Project { 234 | readonly projectDir: string; 235 | yamlExtension: string | null; 236 | 237 | readonly config = new Config(); 238 | 239 | get jsonName() { 240 | return "package.json"; 241 | } 242 | 243 | get yamlName() { 244 | return `package.${this.yamlExtension || this.config.defaultExtension}`; 245 | } 246 | 247 | projectPath(localPath: string): string { 248 | return path.join(this.projectDir, localPath); 249 | } 250 | get jsonPath() { 251 | return this.projectPath(this.jsonName); 252 | } 253 | 254 | get yamlPath() { 255 | return this.projectPath(this.yamlName); 256 | } 257 | 258 | readonly jsonExists:boolean; 259 | readonly yamlExists:boolean; 260 | 261 | yamlModified:boolean = false; 262 | jsonModified:boolean = false; 263 | 264 | private _jsonContents?:object; 265 | private _yamlDocument?:yaml.ast.Document; 266 | 267 | get jsonContents():object { 268 | if (this._jsonContents) return this._jsonContents; 269 | if (this.jsonExists) { 270 | try { 271 | return this._jsonContents = loadAndParse(this.jsonPath, JSON.parse); 272 | } catch (e) { 273 | log.error("loadJson", "Cannot load or parse %s: %s", this.jsonPath, e); 274 | throw e; 275 | } 276 | } else { 277 | return this._jsonContents = {}; 278 | } 279 | } 280 | set jsonContents(value:object) { 281 | if (diff(this._jsonContents, value)) { 282 | this.jsonModified = true; 283 | } 284 | this._jsonContents = value; 285 | } 286 | 287 | get yamlDocument():yaml.ast.Document { 288 | if (this._yamlDocument) return this._yamlDocument; 289 | if (this.yamlExists) { 290 | try { 291 | return this._yamlDocument = loadAndParse(this.yamlPath, yaml.parseDocument); 292 | } catch (e) { 293 | log.error("loadYaml", "Cannot load or parse %s: %s", this.yamlPath, e); 294 | throw e; 295 | } 296 | } else { 297 | return this._yamlDocument = new yaml.Document(); 298 | } 299 | } 300 | set yamlDocument(value:yaml.ast.Document) { 301 | if (this._yamlDocument !== value) { 302 | this.yamlModified = true; 303 | } 304 | this._yamlDocument = value; 305 | } 306 | 307 | get yamlContents():object { 308 | return this.yamlDocument.toJSON(); 309 | } 310 | 311 | backupPath(filename:string): string { 312 | const fullPath = this.projectPath(filename).replace(/\//g, '%'); 313 | const backupPath = this.config.backupPath 314 | .replace("%s", filename) 315 | .replace("%S", fullPath); 316 | return path.resolve(this.projectDir, backupPath); 317 | } 318 | 319 | constructor(projectDir: string) { 320 | this.projectDir = projectDir; 321 | this.yamlExtension = 322 | fs.existsSync(this.projectPath('package.yaml')) ? 'yaml' : 323 | fs.existsSync(this.projectPath('package.yml')) ? 'yml' : 324 | null; 325 | this.config.loadConfigFile(this.projectPath("package-yaml.json")); 326 | this.config.loadConfigFile(this.projectPath("package-yaml.yaml")); 327 | this.config.loadConfigFile(this.jsonPath, "package-yaml"); 328 | this.config.loadConfigFile(this.yamlPath, "package-yaml"); 329 | this.jsonExists = fs.existsSync(this.jsonPath); 330 | this.yamlExists = fs.existsSync(this.yamlPath); 331 | } 332 | 333 | writeBackups():boolean { 334 | let success = true; 335 | if (!this.config.writeBackups) return success; 336 | try { 337 | fs.writeFileSync(this.backupPath(this.jsonName),JSON.stringify(this.jsonContents, null, 4)); 338 | } catch (e) { 339 | success = false; 340 | log.warn("writeBackups", "Error writing backup package.json file at %s: %s", this.backupPath(this.jsonName), e); 341 | } 342 | try { 343 | fs.writeFileSync(this.backupPath(this.yamlName),this.yamlDocument.toString()); 344 | } catch (e) { 345 | success = false; 346 | log.warn("writeBackups", "Error writing backup %s file at %s: %s", this.yamlName, this.backupPath(this.yamlName), e); 347 | } 348 | return success; 349 | } 350 | 351 | writePackageFiles():boolean { 352 | let success = true; 353 | if (this.yamlModified) { 354 | try { 355 | fs.writeFileSync(this.yamlPath, this.yamlDocument.toString()); 356 | this.yamlModified = false; 357 | } catch (e) { 358 | success = false; 359 | log.error("writePackageFiles", "Error writing %s: %s", this.yamlPath, e); 360 | } 361 | } 362 | if (this.jsonModified) { 363 | try { 364 | fs.writeFileSync(this.jsonPath, JSON.stringify(this.jsonContents, null, 4)); 365 | this.jsonModified = false; 366 | } catch (e) { 367 | success = false; 368 | log.error("writePackageFiles", "Error writing %s: %s", this.jsonPath, e); 369 | } 370 | } 371 | return success; 372 | } 373 | 374 | patchYaml(diff: Diff[] | null | undefined): yaml.ast.Document { 375 | if (diff) { 376 | this.yamlDocument = patchYamlDocument(this.yamlDocument, diff); 377 | this.yamlModified = true; 378 | } 379 | return this.yamlDocument; 380 | } 381 | 382 | patchJson(diff: Diff[] | null | undefined): any { 383 | if (diff) { 384 | this.jsonContents = patchObject(this.jsonContents, diff); 385 | this.jsonModified = true; 386 | } 387 | return this.jsonContents; 388 | } 389 | 390 | sync(conflictStrategy?:ConflictResolution):boolean | ConflictResolution.ask { 391 | conflictStrategy = conflictStrategy || this.config.conflicts; 392 | if (!diff(this.jsonContents, this.yamlContents)) { 393 | log.verbose("sync", "Package files already in sync, writing backups"); 394 | this.writeBackups(); 395 | return true; 396 | } 397 | log.verbose("sync", "Package files out of sync. Trying to resolve..."); 398 | if (!this.yamlExists) { 399 | log.verbose("sync", `${this.yamlName} does not exist, creating from package.json`); 400 | conflictStrategy = ConflictResolution.useJson; 401 | } else if (!this.jsonExists) { 402 | log.verbose("sync", `package.json does not exist, using ${this.yamlName}`); 403 | conflictStrategy = ConflictResolution.useYaml; 404 | } else if (this.config.writeBackups) { 405 | log.verbose("sync", "Attempting to read backups..."); 406 | const jsonBackup = loadAndParse(this.backupPath(this.jsonName), JSON.parse, true) || this.jsonContents; 407 | const yamlBackup = loadAndParse(this.backupPath(this.yamlName), yaml.parse, true) || this.yamlContents; 408 | if (!diff(this.jsonContents, yamlBackup)) { 409 | log.verbose("sync", "package.yaml has changed, applying to package.json"); 410 | conflictStrategy = ConflictResolution.useYaml; 411 | } else if (!diff(this.yamlContents, jsonBackup)) { 412 | log.verbose("sync", "package.json has changed, applying to package.yaml"); 413 | conflictStrategy = ConflictResolution.useJson; 414 | } else if (!diff(jsonBackup, yamlBackup) && this.config.tryMerge) { 415 | log.verbose("sync", "Both json and yaml have changed, attempting merge"); 416 | const jsonDiff = diff(jsonBackup, this.jsonContents); 417 | const yamlDiff = diff(yamlBackup, this.yamlContents); 418 | const patchedJson = yamlDiff ? patchObject(JSON.parse(JSON.stringify(this.jsonContents)), yamlDiff) : this.jsonContents; 419 | const patchedYaml = jsonDiff ? patchObject(this.yamlContents, jsonDiff) : this.yamlContents; 420 | if (!diff(patchedJson, patchedYaml)) { 421 | log.verbose("sync", "Merge successful, continuing") 422 | this.patchYaml(jsonDiff); 423 | conflictStrategy = ConflictResolution.useYaml; 424 | } else { 425 | log.verbose("sync", "Merge unsuccessful, reverting to default resolution (%s)", conflictStrategy); 426 | } 427 | } else { 428 | log.verbose("sync", "Backup(s) out of sync, reverting to default resolution (%s)", conflictStrategy); 429 | } 430 | } 431 | 432 | if (conflictStrategy == ConflictResolution.useLatest) { 433 | // We know that both yaml and json must exist, otherwise we wouldn't still be 434 | // set to useLatest 435 | log.verbose("sync", "Checking timestamps..."); 436 | const jsonTime = fs.statSync(this.jsonPath).mtimeMs / 1000.0; 437 | const yamlTime = fs.statSync(this.yamlPath).mtimeMs / 1000.0; 438 | if (Math.abs(yamlTime - jsonTime) <= this.config.timestampFuzz) { 439 | log.verbose("sync", "Timestamp difference %ss <= fuzz factor %ss, reverting to ask", Math.abs(jsonTime - yamlTime), this.config.timestampFuzz); 440 | conflictStrategy = ConflictResolution.ask; 441 | } else if (yamlTime > jsonTime) { 442 | log.verbose("sync", "%s %ss newer than package.json, overwriting", this.yamlName, yamlTime - jsonTime); 443 | conflictStrategy = ConflictResolution.useYaml; 444 | } else { 445 | log.verbose("sync", "package.json %ss newer than %s, overwriting", jsonTime - yamlTime, this.yamlName); 446 | conflictStrategy = ConflictResolution.useJson; 447 | } 448 | } 449 | 450 | if (conflictStrategy == ConflictResolution.ask) { 451 | log.verbose("sync", "Cannot sync, returning ask") 452 | return ConflictResolution.ask; 453 | } 454 | 455 | if (conflictStrategy == ConflictResolution.useJson) { 456 | log.verbose("sync", "Patching %s with changes from package.json", this.yamlName); 457 | this.patchYaml(diff(this.yamlContents, this.jsonContents)); 458 | } else if (conflictStrategy == ConflictResolution.useYaml) { 459 | log.verbose("sync", "Patching package.json with changes from %s", this.yamlName); 460 | this.patchJson(diff(this.jsonContents, this.yamlContents)); 461 | } 462 | 463 | this.writeBackups(); 464 | return this.writePackageFiles(); 465 | } 466 | } 467 | 468 | function patchObject(jsonContents: any, packageDiff: Diff[]): any { 469 | for (let diffEntry of packageDiff) { 470 | applyChange(jsonContents,null,diffEntry); 471 | } 472 | return jsonContents; 473 | } 474 | 475 | function patchYamlDocument(yamlDoc: yaml.ast.Document, packageDiff: Diff[]):yaml.ast.Document { 476 | for (const diffEntry of packageDiff) { 477 | const editPath = (diffEntry.path||[]).concat(diffEntry.kind == 'A' ? diffEntry.index: []); 478 | const editItem = (diffEntry.kind == 'A') ? diffEntry.item : diffEntry; 479 | if (editItem.kind == 'E' || editItem.kind == 'N') { 480 | yamlDoc.setIn(editPath, typeof editItem.rhs == 'undefined' ? undefined : yaml.createNode(editItem.rhs)); 481 | } else if (editItem.kind == 'D') { 482 | yamlDoc.deleteIn(editPath); 483 | } 484 | } 485 | return yamlDoc; 486 | } 487 | 488 | 489 | 490 | class PackageYamlCmd extends NPMExtensionCommand { 491 | execute(args:string[]):any { 492 | log.verbose("PackageYamlCommand", "called with args: %j", args); 493 | const project = new Project(this.npm.config.localPrefix); 494 | if (args[0] && args[0].startsWith('use-')) { 495 | project.config.updateAndLock({conflicts:args[0] as ConflictResolution}); 496 | } 497 | const syncResult = project.sync(); 498 | if (syncResult === 'ask') { 499 | console.error("Could not sync package.yaml and package.json. Try executing one of:\n" 500 | +" npm package-yaml use-yaml\n" 501 | +" npm package-yaml use-json"); 502 | } 503 | } 504 | 505 | usage = "npm package-yaml use-yaml\n" 506 | + "npm package-yaml use-json"; 507 | } 508 | 509 | function syncPackageYaml(projectDir: string):boolean { 510 | log.verbose("syncPackageYaml", "loading, projectDir: %s", projectDir); 511 | try { 512 | const syncResult = new Project(projectDir).sync(); 513 | if (syncResult !== true) { 514 | return false; // let the caller tell the client what to do 515 | } 516 | process.on('exit', function() { 517 | new Project(projectDir).sync(ConflictResolution.useJson); 518 | }); 519 | return true; 520 | } catch (e) { 521 | log.error("syncPackageYaml", "Unexpected error: %s", e); 522 | return false; 523 | } 524 | } 525 | 526 | export function _npm_autoload(npm: NPM.Static, command:string) { 527 | log.verbose("_npm_autoloader","called via npm-autoloader"); 528 | npm.commands['package-yaml'] = new PackageYamlCmd(npm); 529 | 530 | if (command == "package-yaml") { 531 | log.verbose("_npm_autoloader","not automatically syncing because of package-yaml command"); 532 | return; 533 | } 534 | if (!syncPackageYaml(npm.config.localPrefix)) { 535 | console.error("Could not sync package.yaml and package.json, aborting. Try executing one of:\n" 536 | +" npm package-yaml use-yaml\n" 537 | +" npm package-yaml use-json\n" 538 | +"and then try this command again."); 539 | npmExit(1); 540 | } 541 | } 542 | 543 | if (calledFromNPM(module)) { 544 | log.verbose("(main)", "called via onload-script"); 545 | const npm = getNPM(module); 546 | if (!syncPackageYaml(npm.config.localPrefix)) { 547 | let cmdline = "[args...]"; 548 | if (process.argv.slice(2).every(arg=>/^[a-zA-Z0-9_.,\/-]+$/.test(arg))) { 549 | cmdline = process.argv.slice(2).join(" "); 550 | } 551 | console.error("Could not sync package.yaml and package.json. Try executing one of:\n" 552 | +` PACKAGE_YAML_FORCE=yaml npm ${cmdline}\n` 553 | +` PACKAGE_YAML_FORCE=json npm ${cmdline}\n` 554 | +"and then try this command again."); 555 | npmExit(1); 556 | } 557 | } else if (!module.parent) { 558 | log.verbose("(main)","called directly from command line"); 559 | const dir = pkgDir.sync(); 560 | if (dir) { 561 | syncPackageYaml(dir); 562 | } else { 563 | log.verbose("(main)","Cannot find project dir, aborting"); 564 | } 565 | } else { 566 | log.verbose("(main)","not main module"); 567 | } 568 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | "use strict"; 3 | /// 4 | var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { 5 | var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; 6 | if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); 7 | else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; 8 | return c > 3 && r && Object.defineProperty(target, key, r), r; 9 | }; 10 | var __metadata = (this && this.__metadata) || function (k, v) { 11 | if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); 12 | }; 13 | var __importDefault = (this && this.__importDefault) || function (mod) { 14 | return (mod && mod.__esModule) ? mod : { "default": mod }; 15 | }; 16 | Object.defineProperty(exports, "__esModule", { value: true }); 17 | const yaml_1 = __importDefault(require("yaml")); 18 | const fs_1 = __importDefault(require("fs")); 19 | const path_1 = __importDefault(require("path")); 20 | const deep_diff_1 = require("deep-diff"); 21 | const npmlog_1 = __importDefault(require("npmlog")); 22 | require("reflect-metadata"); 23 | const osenv_1 = __importDefault(require("osenv")); 24 | const npm_autoloader_1 = require("npm-autoloader"); 25 | const pkg_dir_1 = __importDefault(require("pkg-dir")); 26 | npmlog_1.default.heading = 'package-yaml'; 27 | if (!!process.env.DEBUG_PACKAGE_YAML) { 28 | npmlog_1.default.level = 'verbose'; 29 | } 30 | const propertyClassesKey = Symbol("propertyClasses"); 31 | function property(target, propertyKey) { 32 | const propClasses = Reflect.getOwnMetadata(propertyClassesKey, target.constructor) || {}; 33 | const propClass = Reflect.getMetadata('design:type', target, propertyKey); 34 | propClasses[propertyKey] = propClass; 35 | Reflect.defineMetadata(propertyClassesKey, propClasses, target.constructor); 36 | } 37 | function getPropClasses(target) { 38 | const classTarget = typeof target === "object" ? target.constructor : target; 39 | const classProperties = Reflect.getOwnMetadata(propertyClassesKey, classTarget) || {}; 40 | return classProperties; 41 | } 42 | function getProps(target) { 43 | return Object.keys(getPropClasses(target)); 44 | } 45 | function getPropClass(target, prop) { 46 | return getPropClasses(target)[prop]; 47 | } 48 | function isPropClass(target, prop, cls) { 49 | const propClass = getPropClass(target, prop); 50 | return (propClass === cls); 51 | } 52 | var ConflictResolution; 53 | (function (ConflictResolution) { 54 | ConflictResolution["ask"] = "ask"; 55 | ConflictResolution["useJson"] = "use-json"; 56 | ConflictResolution["useYaml"] = "use-yaml"; 57 | ConflictResolution["useLatest"] = "use-latest"; 58 | })(ConflictResolution || (ConflictResolution = {})); 59 | ; 60 | class Config { 61 | constructor(loadConfigFiles = true) { 62 | this.debug = false; 63 | this.writeBackups = true; 64 | this.backupPath = ".%s~"; // %s - basename; %S - full path with % interpolations 65 | this.timestampFuzz = 5; 66 | this.conflicts = ConflictResolution.ask; 67 | this.tryMerge = true; // Only functions when backups are being written 68 | this.defaultExtension = "yaml"; 69 | this._lockedProps = {}; 70 | if (!!process.env.DEBUG_PACKAGE_YAML) { 71 | this.updateAndLock({ debug: true }); 72 | } 73 | if (process.env.PACKAGE_YAML_FORCE) { 74 | const confl = `use-${process.env.PACKAGE_YAML_FORCE}`; 75 | if (Config.isValid("conflicts", confl)) { 76 | this.updateAndLock({ conflicts: confl }); 77 | } 78 | } 79 | if (loadConfigFiles) { 80 | this.loadSystemConfig(); 81 | } 82 | } 83 | loadSystemConfig() { 84 | for (let globalPath of ['/etc', '/usr/local/etc']) { 85 | // FIXME: this won't work on Windows 86 | this.loadConfigFile(path_1.default.join(globalPath, "package-yaml.json")); 87 | this.loadConfigFile(path_1.default.join(globalPath, "package-yaml.yaml")); 88 | } 89 | const home = osenv_1.default.home(); 90 | this.loadConfigFile(path_1.default.join(home, ".package-yaml.json")); 91 | this.loadConfigFile(path_1.default.join(home, ".package-yaml.yaml")); 92 | } 93 | static isValid(prop, value) { 94 | npmlog_1.default.verbose("Config.isValid", "checking %s: %s", prop, value); 95 | if (prop === "conflicts") { 96 | npmlog_1.default.verbose("Config.isValid", "ovcfr: %o; includes: %s", Object.values(ConflictResolution), Object.values(ConflictResolution).includes(value)); 97 | return typeof value === 'string' && (Object.values(ConflictResolution).includes(value)); 98 | } 99 | else if (prop === "defaultExtension") { 100 | return value === 'yaml' || value === 'yml'; 101 | } 102 | else if (isPropClass(Config, prop, String)) { 103 | return typeof value === 'string'; 104 | } 105 | else if (isPropClass(Config, prop, Boolean)) { 106 | return true; // anything can be a Boolean if you just believe 107 | } 108 | else if (isPropClass(Config, prop, Number)) { 109 | return !isNaN(Number(value)); 110 | } 111 | return false; 112 | } 113 | validate(values) { 114 | const valid = {}; 115 | const propNames = getProps(Config); 116 | for (const prop of propNames) { 117 | const val = values[prop]; 118 | if (this._lockedProps[prop] || !(prop in values) || !Config.isValid(prop, val)) { 119 | continue; 120 | } 121 | if (isPropClass(Config, prop, String)) { 122 | valid[prop] = String(values[prop]); // We've already validated these 123 | } 124 | else if (isPropClass(Config, prop, Boolean)) { 125 | valid[prop] = !!values[prop]; 126 | } 127 | else if (isPropClass(Config, prop, Number)) { 128 | valid[prop] = Number(values[prop]); 129 | } 130 | } 131 | return valid; 132 | } 133 | update(values) { 134 | const valid = this.validate(values); 135 | Object.assign(this, valid); 136 | if ('debug' in valid) { 137 | npmlog_1.default.level = valid.debug ? 'verbose' : 'info'; 138 | } 139 | return valid; 140 | } 141 | lock(props) { 142 | for (let prop of props) { 143 | if (prop in this) { 144 | this._lockedProps[prop] = true; 145 | } 146 | } 147 | } 148 | updateAndLock(values) { 149 | const updated = this.update(values); 150 | this.lock(Object.keys(updated)); 151 | return updated; 152 | } 153 | loadConfigFile(path, rootElement) { 154 | let configData; 155 | let configParsed; 156 | try { 157 | if (!fs_1.default.existsSync(path)) { 158 | return null; 159 | } 160 | configData = fs_1.default.readFileSync(path, { encoding: "utf8" }); 161 | } 162 | catch (e) { 163 | npmlog_1.default.error("loadConfig", "Error loading config file %s: %s", path, e); 164 | return null; 165 | } 166 | try { 167 | // YAML parsing *should* work for JSON files without issue 168 | configParsed = yaml_1.default.parse(configData); 169 | } 170 | catch (yamlError) { 171 | // try using JSON as a backup 172 | try { 173 | configParsed = JSON.parse(configData); 174 | } 175 | catch (jsonError) { 176 | const error = path.endsWith(".json") ? jsonError : yamlError; 177 | npmlog_1.default.error("loadConfig", "Error parsing YAML/JSON config file %s: %s", path, error); 178 | return null; 179 | } 180 | } 181 | if (rootElement) { 182 | if (!configParsed || typeof configParsed !== "object" || !configParsed[rootElement]) { 183 | // Acceptable, just like if the file didn't exist 184 | return null; 185 | } 186 | configParsed = configParsed[rootElement]; 187 | } 188 | if (!configParsed || typeof configParsed !== "object") { 189 | if (rootElement) { 190 | npmlog_1.default.error("loadConfig", "Invalid configuration stanza %s in %s (should be an object)", rootElement, path); 191 | } 192 | else { 193 | npmlog_1.default.error("loadConfig", "Invalid configuration file %s (should be a JSON/YAML object)", path); 194 | } 195 | return null; 196 | } 197 | return this.update(configParsed); 198 | } 199 | } 200 | __decorate([ 201 | property, 202 | __metadata("design:type", Boolean) 203 | ], Config.prototype, "debug", void 0); 204 | __decorate([ 205 | property, 206 | __metadata("design:type", Boolean) 207 | ], Config.prototype, "writeBackups", void 0); 208 | __decorate([ 209 | property, 210 | __metadata("design:type", String) 211 | ], Config.prototype, "backupPath", void 0); 212 | __decorate([ 213 | property, 214 | __metadata("design:type", Number) 215 | ], Config.prototype, "timestampFuzz", void 0); 216 | __decorate([ 217 | property, 218 | __metadata("design:type", String) 219 | ], Config.prototype, "conflicts", void 0); 220 | __decorate([ 221 | property, 222 | __metadata("design:type", Boolean) 223 | ], Config.prototype, "tryMerge", void 0); 224 | __decorate([ 225 | property, 226 | __metadata("design:type", String) 227 | ], Config.prototype, "defaultExtension", void 0); 228 | ; 229 | function loadAndParse(path, parser, inhibitErrors = false) { 230 | try { 231 | const data = fs_1.default.readFileSync(path, { encoding: "utf8" }); 232 | return parser(data); 233 | } 234 | catch (e) { 235 | if (inhibitErrors) { 236 | return null; 237 | } 238 | throw e; 239 | } 240 | } 241 | class Project { 242 | constructor(projectDir) { 243 | this.config = new Config(); 244 | this.yamlModified = false; 245 | this.jsonModified = false; 246 | this.projectDir = projectDir; 247 | this.yamlExtension = 248 | fs_1.default.existsSync(this.projectPath('package.yaml')) ? 'yaml' : 249 | fs_1.default.existsSync(this.projectPath('package.yml')) ? 'yml' : 250 | null; 251 | this.config.loadConfigFile(this.projectPath("package-yaml.json")); 252 | this.config.loadConfigFile(this.projectPath("package-yaml.yaml")); 253 | this.config.loadConfigFile(this.jsonPath, "package-yaml"); 254 | this.config.loadConfigFile(this.yamlPath, "package-yaml"); 255 | this.jsonExists = fs_1.default.existsSync(this.jsonPath); 256 | this.yamlExists = fs_1.default.existsSync(this.yamlPath); 257 | } 258 | get jsonName() { 259 | return "package.json"; 260 | } 261 | get yamlName() { 262 | return `package.${this.yamlExtension || this.config.defaultExtension}`; 263 | } 264 | projectPath(localPath) { 265 | return path_1.default.join(this.projectDir, localPath); 266 | } 267 | get jsonPath() { 268 | return this.projectPath(this.jsonName); 269 | } 270 | get yamlPath() { 271 | return this.projectPath(this.yamlName); 272 | } 273 | get jsonContents() { 274 | if (this._jsonContents) 275 | return this._jsonContents; 276 | if (this.jsonExists) { 277 | try { 278 | return this._jsonContents = loadAndParse(this.jsonPath, JSON.parse); 279 | } 280 | catch (e) { 281 | npmlog_1.default.error("loadJson", "Cannot load or parse %s: %s", this.jsonPath, e); 282 | throw e; 283 | } 284 | } 285 | else { 286 | return this._jsonContents = {}; 287 | } 288 | } 289 | set jsonContents(value) { 290 | if (deep_diff_1.diff(this._jsonContents, value)) { 291 | this.jsonModified = true; 292 | } 293 | this._jsonContents = value; 294 | } 295 | get yamlDocument() { 296 | if (this._yamlDocument) 297 | return this._yamlDocument; 298 | if (this.yamlExists) { 299 | try { 300 | return this._yamlDocument = loadAndParse(this.yamlPath, yaml_1.default.parseDocument); 301 | } 302 | catch (e) { 303 | npmlog_1.default.error("loadYaml", "Cannot load or parse %s: %s", this.yamlPath, e); 304 | throw e; 305 | } 306 | } 307 | else { 308 | return this._yamlDocument = new yaml_1.default.Document(); 309 | } 310 | } 311 | set yamlDocument(value) { 312 | if (this._yamlDocument !== value) { 313 | this.yamlModified = true; 314 | } 315 | this._yamlDocument = value; 316 | } 317 | get yamlContents() { 318 | return this.yamlDocument.toJSON(); 319 | } 320 | backupPath(filename) { 321 | const fullPath = this.projectPath(filename).replace(/\//g, '%'); 322 | const backupPath = this.config.backupPath 323 | .replace("%s", filename) 324 | .replace("%S", fullPath); 325 | return path_1.default.resolve(this.projectDir, backupPath); 326 | } 327 | writeBackups() { 328 | let success = true; 329 | if (!this.config.writeBackups) 330 | return success; 331 | try { 332 | fs_1.default.writeFileSync(this.backupPath(this.jsonName), JSON.stringify(this.jsonContents, null, 4)); 333 | } 334 | catch (e) { 335 | success = false; 336 | npmlog_1.default.warn("writeBackups", "Error writing backup package.json file at %s: %s", this.backupPath(this.jsonName), e); 337 | } 338 | try { 339 | fs_1.default.writeFileSync(this.backupPath(this.yamlName), this.yamlDocument.toString()); 340 | } 341 | catch (e) { 342 | success = false; 343 | npmlog_1.default.warn("writeBackups", "Error writing backup %s file at %s: %s", this.yamlName, this.backupPath(this.yamlName), e); 344 | } 345 | return success; 346 | } 347 | writePackageFiles() { 348 | let success = true; 349 | if (this.yamlModified) { 350 | try { 351 | fs_1.default.writeFileSync(this.yamlPath, this.yamlDocument.toString()); 352 | this.yamlModified = false; 353 | } 354 | catch (e) { 355 | success = false; 356 | npmlog_1.default.error("writePackageFiles", "Error writing %s: %s", this.yamlPath, e); 357 | } 358 | } 359 | if (this.jsonModified) { 360 | try { 361 | fs_1.default.writeFileSync(this.jsonPath, JSON.stringify(this.jsonContents, null, 4)); 362 | this.jsonModified = false; 363 | } 364 | catch (e) { 365 | success = false; 366 | npmlog_1.default.error("writePackageFiles", "Error writing %s: %s", this.jsonPath, e); 367 | } 368 | } 369 | return success; 370 | } 371 | patchYaml(diff) { 372 | if (diff) { 373 | this.yamlDocument = patchYamlDocument(this.yamlDocument, diff); 374 | this.yamlModified = true; 375 | } 376 | return this.yamlDocument; 377 | } 378 | patchJson(diff) { 379 | if (diff) { 380 | this.jsonContents = patchObject(this.jsonContents, diff); 381 | this.jsonModified = true; 382 | } 383 | return this.jsonContents; 384 | } 385 | sync(conflictStrategy) { 386 | conflictStrategy = conflictStrategy || this.config.conflicts; 387 | if (!deep_diff_1.diff(this.jsonContents, this.yamlContents)) { 388 | npmlog_1.default.verbose("sync", "Package files already in sync, writing backups"); 389 | this.writeBackups(); 390 | return true; 391 | } 392 | npmlog_1.default.verbose("sync", "Package files out of sync. Trying to resolve..."); 393 | if (!this.yamlExists) { 394 | npmlog_1.default.verbose("sync", `${this.yamlName} does not exist, creating from package.json`); 395 | conflictStrategy = ConflictResolution.useJson; 396 | } 397 | else if (!this.jsonExists) { 398 | npmlog_1.default.verbose("sync", `package.json does not exist, using ${this.yamlName}`); 399 | conflictStrategy = ConflictResolution.useYaml; 400 | } 401 | else if (this.config.writeBackups) { 402 | npmlog_1.default.verbose("sync", "Attempting to read backups..."); 403 | const jsonBackup = loadAndParse(this.backupPath(this.jsonName), JSON.parse, true) || this.jsonContents; 404 | const yamlBackup = loadAndParse(this.backupPath(this.yamlName), yaml_1.default.parse, true) || this.yamlContents; 405 | if (!deep_diff_1.diff(this.jsonContents, yamlBackup)) { 406 | npmlog_1.default.verbose("sync", "package.yaml has changed, applying to package.json"); 407 | conflictStrategy = ConflictResolution.useYaml; 408 | } 409 | else if (!deep_diff_1.diff(this.yamlContents, jsonBackup)) { 410 | npmlog_1.default.verbose("sync", "package.json has changed, applying to package.yaml"); 411 | conflictStrategy = ConflictResolution.useJson; 412 | } 413 | else if (!deep_diff_1.diff(jsonBackup, yamlBackup) && this.config.tryMerge) { 414 | npmlog_1.default.verbose("sync", "Both json and yaml have changed, attempting merge"); 415 | const jsonDiff = deep_diff_1.diff(jsonBackup, this.jsonContents); 416 | const yamlDiff = deep_diff_1.diff(yamlBackup, this.yamlContents); 417 | const patchedJson = yamlDiff ? patchObject(JSON.parse(JSON.stringify(this.jsonContents)), yamlDiff) : this.jsonContents; 418 | const patchedYaml = jsonDiff ? patchObject(this.yamlContents, jsonDiff) : this.yamlContents; 419 | if (!deep_diff_1.diff(patchedJson, patchedYaml)) { 420 | npmlog_1.default.verbose("sync", "Merge successful, continuing"); 421 | this.patchYaml(jsonDiff); 422 | conflictStrategy = ConflictResolution.useYaml; 423 | } 424 | else { 425 | npmlog_1.default.verbose("sync", "Merge unsuccessful, reverting to default resolution (%s)", conflictStrategy); 426 | } 427 | } 428 | else { 429 | npmlog_1.default.verbose("sync", "Backup(s) out of sync, reverting to default resolution (%s)", conflictStrategy); 430 | } 431 | } 432 | if (conflictStrategy == ConflictResolution.useLatest) { 433 | // We know that both yaml and json must exist, otherwise we wouldn't still be 434 | // set to useLatest 435 | npmlog_1.default.verbose("sync", "Checking timestamps..."); 436 | const jsonTime = fs_1.default.statSync(this.jsonPath).mtimeMs / 1000.0; 437 | const yamlTime = fs_1.default.statSync(this.yamlPath).mtimeMs / 1000.0; 438 | if (Math.abs(yamlTime - jsonTime) <= this.config.timestampFuzz) { 439 | npmlog_1.default.verbose("sync", "Timestamp difference %ss <= fuzz factor %ss, reverting to ask", Math.abs(jsonTime - yamlTime), this.config.timestampFuzz); 440 | conflictStrategy = ConflictResolution.ask; 441 | } 442 | else if (yamlTime > jsonTime) { 443 | npmlog_1.default.verbose("sync", "%s %ss newer than package.json, overwriting", this.yamlName, yamlTime - jsonTime); 444 | conflictStrategy = ConflictResolution.useYaml; 445 | } 446 | else { 447 | npmlog_1.default.verbose("sync", "package.json %ss newer than %s, overwriting", jsonTime - yamlTime, this.yamlName); 448 | conflictStrategy = ConflictResolution.useJson; 449 | } 450 | } 451 | if (conflictStrategy == ConflictResolution.ask) { 452 | npmlog_1.default.verbose("sync", "Cannot sync, returning ask"); 453 | return ConflictResolution.ask; 454 | } 455 | if (conflictStrategy == ConflictResolution.useJson) { 456 | npmlog_1.default.verbose("sync", "Patching %s with changes from package.json", this.yamlName); 457 | this.patchYaml(deep_diff_1.diff(this.yamlContents, this.jsonContents)); 458 | } 459 | else if (conflictStrategy == ConflictResolution.useYaml) { 460 | npmlog_1.default.verbose("sync", "Patching package.json with changes from %s", this.yamlName); 461 | this.patchJson(deep_diff_1.diff(this.jsonContents, this.yamlContents)); 462 | } 463 | this.writeBackups(); 464 | return this.writePackageFiles(); 465 | } 466 | } 467 | exports.Project = Project; 468 | function patchObject(jsonContents, packageDiff) { 469 | for (let diffEntry of packageDiff) { 470 | deep_diff_1.applyChange(jsonContents, null, diffEntry); 471 | } 472 | return jsonContents; 473 | } 474 | function patchYamlDocument(yamlDoc, packageDiff) { 475 | for (const diffEntry of packageDiff) { 476 | const editPath = (diffEntry.path || []).concat(diffEntry.kind == 'A' ? diffEntry.index : []); 477 | const editItem = (diffEntry.kind == 'A') ? diffEntry.item : diffEntry; 478 | if (editItem.kind == 'E' || editItem.kind == 'N') { 479 | yamlDoc.setIn(editPath, typeof editItem.rhs == 'undefined' ? undefined : yaml_1.default.createNode(editItem.rhs)); 480 | } 481 | else if (editItem.kind == 'D') { 482 | yamlDoc.deleteIn(editPath); 483 | } 484 | } 485 | return yamlDoc; 486 | } 487 | class PackageYamlCmd extends npm_autoloader_1.NPMExtensionCommand { 488 | constructor() { 489 | super(...arguments); 490 | this.usage = "npm package-yaml use-yaml\n" 491 | + "npm package-yaml use-json"; 492 | } 493 | execute(args) { 494 | npmlog_1.default.verbose("PackageYamlCommand", "called with args: %j", args); 495 | const project = new Project(this.npm.config.localPrefix); 496 | if (args[0] && args[0].startsWith('use-')) { 497 | project.config.updateAndLock({ conflicts: args[0] }); 498 | } 499 | const syncResult = project.sync(); 500 | if (syncResult === 'ask') { 501 | console.error("Could not sync package.yaml and package.json. Try executing one of:\n" 502 | + " npm package-yaml use-yaml\n" 503 | + " npm package-yaml use-json"); 504 | } 505 | } 506 | } 507 | function syncPackageYaml(projectDir) { 508 | npmlog_1.default.verbose("syncPackageYaml", "loading, projectDir: %s", projectDir); 509 | try { 510 | const syncResult = new Project(projectDir).sync(); 511 | if (syncResult !== true) { 512 | return false; // let the caller tell the client what to do 513 | } 514 | process.on('exit', function () { 515 | new Project(projectDir).sync(ConflictResolution.useJson); 516 | }); 517 | return true; 518 | } 519 | catch (e) { 520 | npmlog_1.default.error("syncPackageYaml", "Unexpected error: %s", e); 521 | return false; 522 | } 523 | } 524 | function _npm_autoload(npm, command) { 525 | npmlog_1.default.verbose("_npm_autoloader", "called via npm-autoloader"); 526 | npm.commands['package-yaml'] = new PackageYamlCmd(npm); 527 | if (command == "package-yaml") { 528 | npmlog_1.default.verbose("_npm_autoloader", "not automatically syncing because of package-yaml command"); 529 | return; 530 | } 531 | if (!syncPackageYaml(npm.config.localPrefix)) { 532 | console.error("Could not sync package.yaml and package.json, aborting. Try executing one of:\n" 533 | + " npm package-yaml use-yaml\n" 534 | + " npm package-yaml use-json\n" 535 | + "and then try this command again."); 536 | npm_autoloader_1.npmExit(1); 537 | } 538 | } 539 | exports._npm_autoload = _npm_autoload; 540 | if (npm_autoloader_1.calledFromNPM(module)) { 541 | npmlog_1.default.verbose("(main)", "called via onload-script"); 542 | const npm = npm_autoloader_1.getNPM(module); 543 | if (!syncPackageYaml(npm.config.localPrefix)) { 544 | let cmdline = "[args...]"; 545 | if (process.argv.slice(2).every(arg => /^[a-zA-Z0-9_.,\/-]+$/.test(arg))) { 546 | cmdline = process.argv.slice(2).join(" "); 547 | } 548 | console.error("Could not sync package.yaml and package.json. Try executing one of:\n" 549 | + ` PACKAGE_YAML_FORCE=yaml npm ${cmdline}\n` 550 | + ` PACKAGE_YAML_FORCE=json npm ${cmdline}\n` 551 | + "and then try this command again."); 552 | npm_autoloader_1.npmExit(1); 553 | } 554 | } 555 | else if (!module.parent) { 556 | npmlog_1.default.verbose("(main)", "called directly from command line"); 557 | const dir = pkg_dir_1.default.sync(); 558 | if (dir) { 559 | syncPackageYaml(dir); 560 | } 561 | else { 562 | npmlog_1.default.verbose("(main)", "Cannot find project dir, aborting"); 563 | } 564 | } 565 | else { 566 | npmlog_1.default.verbose("(main)", "not main module"); 567 | } 568 | --------------------------------------------------------------------------------