diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4d231267fb..30b8ce0bfe 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -42,7 +42,6 @@ jobs: expo-doctor, expo-codemod, image-utils, - json-file, pkcs12, plist, pwa, diff --git a/.github/workflows/test_windows.yml b/.github/workflows/test_windows.yml index d299117cc9..24b595615e 100644 --- a/.github/workflows/test_windows.yml +++ b/.github/workflows/test_windows.yml @@ -40,7 +40,6 @@ jobs: expo-cli, expo-codemod, image-utils, - json-file, pkcs12, plist, pwa, diff --git a/packages/json-file/.eslintignore b/packages/json-file/.eslintignore deleted file mode 100644 index dc84959d1d..0000000000 --- a/packages/json-file/.eslintignore +++ /dev/null @@ -1,2 +0,0 @@ -build/ - diff --git a/packages/json-file/LICENSE b/packages/json-file/LICENSE deleted file mode 100644 index ae378232a1..0000000000 --- a/packages/json-file/LICENSE +++ /dev/null @@ -1,22 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2015 650 Industries - -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: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -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. - diff --git a/packages/json-file/README.md b/packages/json-file/README.md index 18ad87d9e8..fe37f1bfb6 100644 --- a/packages/json-file/README.md +++ b/packages/json-file/README.md @@ -1,16 +1,3 @@ - -

-šŸ‘‹ Welcome to
@expo/json-file -

+# `@expo/json-file` -

A library for reading and writing JSON files.

- -

- - - - - -

- - +This package has [moved to the `expo/expo` repo](https://github.com/expo/expo/tree/main/packages/%40expo/json-file). diff --git a/packages/json-file/__tests__/JsonFile-test.ts b/packages/json-file/__tests__/JsonFile-test.ts deleted file mode 100644 index fa512fe35c..0000000000 --- a/packages/json-file/__tests__/JsonFile-test.ts +++ /dev/null @@ -1,138 +0,0 @@ -import fs from 'fs'; -import os from 'os'; -import path from 'path'; - -import JsonFile from '../src/JsonFile'; - -jasmine.DEFAULT_TIMEOUT_INTERVAL = 20 * 1000; - -const FIXTURES = path.join(os.tmpdir(), 'json-file-fixtures'); - -beforeAll(() => fs.promises.mkdir(FIXTURES, { recursive: true })); -afterAll(() => fs.promises.rmdir(FIXTURES, { recursive: true })); - -it(`is a class`, () => { - const file = new JsonFile(path.join(__dirname, '../package.json')); - expect(file instanceof JsonFile).toBe(true); -}); - -it(`has static functions`, () => { - expect(JsonFile.readAsync).toBeDefined(); - expect(JsonFile.writeAsync).toBeDefined(); -}); - -it(`reads JSON from a file`, async () => { - const file = new JsonFile(path.join(__dirname, '../package.json')); - const object = await file.readAsync(); - expect(object.version).toBeDefined(); -}); - -it(`reads JSON statically from a file`, async () => { - const object = await JsonFile.readAsync(path.join(__dirname, '../package.json')); - expect(object.version).toBeDefined(); -}); - -it(`reads JSON5 from a file`, async () => { - const file = new JsonFile(path.join(__dirname, 'files/test-json5.json'), { json5: true }); - const object = await file.readAsync(); - expect(object.itParsedProperly).toBe(42); -}); - -it(`has useful error messages for JSON parsing errors`, async () => { - await expect( - JsonFile.readAsync(path.join(__dirname, 'files/syntax-error.json')) - ).rejects.toThrowError(/Cause: SyntaxError: Unexpected string in JSON at position 602/); -}); - -it(`has useful error messages for JSON5 parsing errors`, async () => { - await expect( - JsonFile.readAsync(path.join(__dirname, 'files/syntax-error.json5'), { json5: true }) - ).rejects.toThrowError(/Cause: SyntaxError: JSON5: invalid character ',' at 4:15/); -}); - -const obj1 = { x: 1 }; - -it(`writes JSON to a file`, async () => { - const filename = path.join(FIXTURES, 'test.json'); - const file = new JsonFile(filename, { json5: true }); - await file.writeAsync(obj1); - expect(fs.existsSync(filename)).toBe(true); - await expect(file.readAsync()).resolves.toEqual(obj1); -}); - -it(`rewrite async`, async () => { - const filename = path.join(FIXTURES, 'test.json'); - const file = new JsonFile(filename, { json5: true }); - await file.writeAsync(obj1); - expect(fs.existsSync(filename)).toBe(true); - await expect(file.readAsync()).resolves.toEqual(obj1); - await expect(file.rewriteAsync()).resolves.toBeDefined(); - expect(fs.existsSync(filename)).toBe(true); - await expect(file.readAsync()).resolves.toEqual(obj1); -}); - -it(`changes an existing key in that file`, async () => { - const file = new JsonFile(path.join(FIXTURES, 'test.json'), { json5: true }); - await expect(file.setAsync('x', 2)).resolves.toBeDefined(); - await expect(file.readAsync()).resolves.toEqual({ x: 2 }); -}); - -it(`adds a new key to the file`, async () => { - const file = new JsonFile(path.join(FIXTURES, 'test.json'), { json5: true }); - await expect(file.setAsync('x', 2)).resolves.toBeDefined(); - await expect(file.readAsync()).resolves.toEqual({ x: 2 }); - await expect(file.setAsync('y', 3)).resolves.toBeDefined(); - await expect(file.readAsync()).resolves.toEqual({ x: 2, y: 3 }); -}); - -it(`deletes that same new key from the file`, async () => { - const file = new JsonFile(path.join(FIXTURES, 'test.json'), { json5: true }); - await expect(file.setAsync('x', 2)).resolves.toBeDefined(); - await expect(file.setAsync('y', 3)).resolves.toBeDefined(); - await expect(file.deleteKeyAsync('y')).resolves.toBeDefined(); - await expect(file.readAsync()).resolves.toEqual({ x: 2 }); -}); - -it(`deletes another key from the file`, async () => { - const file = new JsonFile(path.join(FIXTURES, 'test.json'), { json5: true }); - await expect(file.setAsync('x', 2)).resolves.toBeDefined(); - await expect(file.setAsync('y', 3)).resolves.toBeDefined(); - await expect(file.deleteKeyAsync('x')).resolves.toBeDefined(); - await expect(file.deleteKeyAsync('y')).resolves.toBeDefined(); - await expect(file.readAsync()).resolves.toEqual({}); -}); - -// This fails when i is high, around 200. However, no realistic use case would have the user -// constantly update a file that often -it('Multiple updates to the same file have no race conditions', async () => { - const file = new JsonFile(path.join(FIXTURES, 'atomic-test.json'), { json5: true }); - for (let i = 0; i < 50; i++) { - await file.writeAsync({}); - let baseObj = {}; - for (let j = 0; j < 20; j++) { - baseObj = { ...baseObj, [j]: j }; - await file.setAsync(String(j), j); - } - const json = await file.readAsync(); - expect(json).toEqual(baseObj); - } -}); - -it('Continuous updating!', async () => { - const file = new JsonFile(path.join(FIXTURES, 'test.json'), { json5: true }); - await file.writeAsync({ i: 0 }); - for (let i = 0; i < 20; i++) { - await file.writeAsync({ i }); - await expect(file.readAsync()).resolves.toEqual({ i }); - } -}); - -it('adds a new line at the eof', async () => { - const filename = path.join(FIXTURES, 'test.json'); - const file = new JsonFile(filename, { json5: true }); - await file.writeAsync(obj1); - expect(fs.existsSync(filename)).toBe(true); - const data = await fs.promises.readFile(filename, 'utf-8'); - const lastChar = data[data.length - 1]; - expect(lastChar).toEqual('\n'); -}); diff --git a/packages/json-file/__tests__/JsonFileError-test.ts b/packages/json-file/__tests__/JsonFileError-test.ts deleted file mode 100644 index 4fbb9c6dee..0000000000 --- a/packages/json-file/__tests__/JsonFileError-test.ts +++ /dev/null @@ -1,21 +0,0 @@ -import JsonFileError from '../src/JsonFileError'; - -describe('JsonFileError', () => { - it(`is an error`, () => { - const error = new JsonFileError('Example'); - expect(error instanceof Error).toBe(true); - expect(error instanceof JsonFileError).toBe(true); - }); - - it(`has a flag that says it's a JsonFileError`, () => { - const error = new JsonFileError('Example'); - expect(error.isJsonFileError).toBe(true); - }); - - it(`includes its cause`, () => { - const cause = new Error('Root cause'); - const error = new JsonFileError('Example', cause); - expect(error.cause).toBe(cause); - expect(error.message).toMatch(cause.message); - }); -}); diff --git a/packages/json-file/__tests__/files/syntax-error.json b/packages/json-file/__tests__/files/syntax-error.json deleted file mode 100644 index cbb816e844..0000000000 --- a/packages/json-file/__tests__/files/syntax-error.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "expo": { - "name": "test-project", - "description": "This project is really great.", - "slug": "test-project", - "privacy": "public", - "sdkVersion": "26.0.0", - "platforms": ["ios", "android"], - "version": "1.0.0", - "orientation": "portrait", - "icon": "./assets/icon.png", - "splash": { - "image": "./assets/splash.png", - "resizeMode": "contain", - "backgroundColor": "#ffffff" - }, - "updates": { - "fallbackToCacheTimeout": 0 - }, - "assetBundlePatterns": [ - "**/*" - ], - "ios": { - "supportsTablet": true, - "bundleIdentifier" "com.wow.testapp" - } - } -} diff --git a/packages/json-file/__tests__/files/syntax-error.json5 b/packages/json-file/__tests__/files/syntax-error.json5 deleted file mode 100644 index 11a4faf21d..0000000000 --- a/packages/json-file/__tests__/files/syntax-error.json5 +++ /dev/null @@ -1,6 +0,0 @@ -{ - expo: { - // The following line has an error - sdkVersion, "26.0.0", - }, -} diff --git a/packages/json-file/__tests__/files/test-json5.json b/packages/json-file/__tests__/files/test-json5.json deleted file mode 100644 index 0abee6caef..0000000000 --- a/packages/json-file/__tests__/files/test-json5.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - '3': 4, - // Another comment! - '5': { - '6': { - /* Comment comment comment! */ - '7': 8 - }, - '9': 10 - }, - '11': 12, - itParsedProperly: 42, - score: 5, - x: 'z' -} diff --git a/packages/json-file/__tests__/files/test.json b/packages/json-file/__tests__/files/test.json deleted file mode 100644 index c38dbfc173..0000000000 --- a/packages/json-file/__tests__/files/test.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "3": 4, - "5": { - "6": { - "7": 8 - }, - "9": 10 - }, - "11": 12, - "score": 5, - "x": "z" -} \ No newline at end of file diff --git a/packages/json-file/jest.config.js b/packages/json-file/jest.config.js deleted file mode 100644 index 706ff52839..0000000000 --- a/packages/json-file/jest.config.js +++ /dev/null @@ -1,8 +0,0 @@ -const path = require('path'); - -module.exports = { - preset: '../../jest/unit-test-config', - rootDir: path.resolve(__dirname), - displayName: require('./package').name, - testRunner: 'jest-jasmine2', -}; diff --git a/packages/json-file/package.json b/packages/json-file/package.json deleted file mode 100644 index ad26013e5d..0000000000 --- a/packages/json-file/package.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "name": "@expo/json-file", - "version": "8.2.37", - "description": "A module for reading, writing, and manipulating JSON files", - "main": "build/JsonFile.js", - "scripts": { - "watch": "tsc --watch --preserveWatchOutput", - "build": "tsc", - "prepare": "yarn run clean && yarn build", - "clean": "rimraf build ./tsconfig.tsbuildinfo", - "lint": "eslint .", - "test": "jest" - }, - "repository": { - "type": "git", - "url": "https://github.com/expo/expo-cli.git", - "directory": "packages/json-file" - }, - "keywords": [ - "json" - ], - "license": "MIT", - "bugs": { - "url": "https://github.com/expo/expo-cli/issues" - }, - "homepage": "https://github.com/expo/expo-cli/tree/main/packages/json-file#readme", - "files": [ - "build" - ], - "dependencies": { - "@babel/code-frame": "~7.10.4", - "json5": "^2.2.2", - "write-file-atomic": "^2.3.0" - }, - "devDependencies": { - "@types/babel__code-frame": "^7.0.1", - "@types/json5": "^2.2.0", - "@types/write-file-atomic": "^2.1.1" - }, - "publishConfig": { - "access": "public" - } -} diff --git a/packages/json-file/src/JsonFile.ts b/packages/json-file/src/JsonFile.ts deleted file mode 100644 index 651a9df127..0000000000 --- a/packages/json-file/src/JsonFile.ts +++ /dev/null @@ -1,352 +0,0 @@ -import { codeFrameColumns } from '@babel/code-frame'; -import fs from 'fs'; -import JSON5 from 'json5'; -import path from 'path'; -import { promisify } from 'util'; -import writeFileAtomic from 'write-file-atomic'; - -import JsonFileError, { EmptyJsonFileError } from './JsonFileError'; - -const writeFileAtomicAsync: ( - filename: string, - data: string | Buffer, - options: writeFileAtomic.Options -) => void = promisify(writeFileAtomic); - -export type JSONValue = boolean | number | string | null | JSONArray | JSONObject; -export interface JSONArray extends Array {} -export interface JSONObject { - [key: string]: JSONValue | undefined; -} - -type Defined = T extends undefined ? never : T; - -type Options = { - badJsonDefault?: TJSONObject; - jsonParseErrorDefault?: TJSONObject; - cantReadFileDefault?: TJSONObject; - ensureDir?: boolean; - default?: TJSONObject; - json5?: boolean; - space?: number; - addNewLineAtEOF?: boolean; -}; - -const DEFAULT_OPTIONS = { - badJsonDefault: undefined, - jsonParseErrorDefault: undefined, - cantReadFileDefault: undefined, - ensureDir: false, - default: undefined, - json5: false, - space: 2, - addNewLineAtEOF: true, -}; - -/** - * The JsonFile class represents the contents of json file. - * - * It's polymorphic on "JSONObject", which is a simple type representing - * and object with string keys and either objects or primitive types as values. - * @type {[type]} - */ -export default class JsonFile { - file: string; - options: Options; - - static read = read; - static readAsync = readAsync; - static parseJsonString = parseJsonString; - static writeAsync = writeAsync; - static getAsync = getAsync; - static setAsync = setAsync; - static mergeAsync = mergeAsync; - static deleteKeyAsync = deleteKeyAsync; - static deleteKeysAsync = deleteKeysAsync; - static rewriteAsync = rewriteAsync; - - constructor(file: string, options: Options = {}) { - this.file = file; - this.options = options; - } - - read(options?: Options): TJSONObject { - return read(this.file, this._getOptions(options)); - } - - async readAsync(options?: Options): Promise { - return readAsync(this.file, this._getOptions(options)); - } - - async writeAsync(object: TJSONObject, options?: Options) { - return writeAsync(this.file, object, this._getOptions(options)); - } - - parseJsonString(json: string, options?: Options): TJSONObject { - return parseJsonString(json, options); - } - - async getAsync( - key: K, - defaultValue: TDefault, - options?: Options - ): Promise | TDefault> { - return getAsync(this.file, key, defaultValue, this._getOptions(options)); - } - - async setAsync(key: string, value: unknown, options?: Options) { - return setAsync(this.file, key, value, this._getOptions(options)); - } - - async mergeAsync( - sources: Partial | Partial[], - options?: Options - ): Promise { - return mergeAsync(this.file, sources, this._getOptions(options)); - } - - async deleteKeyAsync(key: string, options?: Options) { - return deleteKeyAsync(this.file, key, this._getOptions(options)); - } - - async deleteKeysAsync(keys: string[], options?: Options) { - return deleteKeysAsync(this.file, keys, this._getOptions(options)); - } - - async rewriteAsync(options?: Options) { - return rewriteAsync(this.file, this._getOptions(options)); - } - - _getOptions(options?: Options): Options { - return { - ...this.options, - ...options, - }; - } -} - -function read( - file: string, - options?: Options -): TJSONObject { - let json; - try { - json = fs.readFileSync(file, 'utf8'); - } catch (error: any) { - assertEmptyJsonString(json, file); - const defaultValue = cantReadFileDefault(options); - if (defaultValue === undefined) { - throw new JsonFileError(`Can't read JSON file: ${file}`, error, error.code, file); - } else { - return defaultValue; - } - } - return parseJsonString(json, options, file); -} - -async function readAsync( - file: string, - options?: Options -): Promise { - let json; - try { - json = await fs.promises.readFile(file, 'utf8'); - } catch (error: any) { - assertEmptyJsonString(json, file); - const defaultValue = cantReadFileDefault(options); - if (defaultValue === undefined) { - throw new JsonFileError(`Can't read JSON file: ${file}`, error, error.code); - } else { - return defaultValue; - } - } - return parseJsonString(json, options); -} - -function parseJsonString( - json: string, - options?: Options, - fileName?: string -): TJSONObject { - assertEmptyJsonString(json, fileName); - try { - if (_getOption(options, 'json5')) { - return JSON5.parse(json); - } else { - return JSON.parse(json); - } - } catch (e: any) { - const defaultValue = jsonParseErrorDefault(options); - if (defaultValue === undefined) { - const location = locationFromSyntaxError(e, json); - if (location) { - const codeFrame = codeFrameColumns(json, { start: location }); - e.codeFrame = codeFrame; - e.message += `\n${codeFrame}`; - } - throw new JsonFileError(`Error parsing JSON: ${json}`, e, 'EJSONPARSE', fileName); - } else { - return defaultValue; - } - } -} - -async function getAsync( - file: string, - key: K, - defaultValue: DefaultValue, - options?: Options -): Promise { - const object = await readAsync(file, options); - if (key in object) { - return object[key]; - } - if (defaultValue === undefined) { - throw new JsonFileError(`No value at key path "${key}" in JSON object from: ${file}`); - } - return defaultValue; -} - -async function writeAsync( - file: string, - object: TJSONObject, - options?: Options -): Promise { - if (options?.ensureDir) { - await fs.promises.mkdir(path.dirname(file), { recursive: true }); - } - const space = _getOption(options, 'space'); - const json5 = _getOption(options, 'json5'); - const addNewLineAtEOF = _getOption(options, 'addNewLineAtEOF'); - let json; - try { - if (json5) { - json = JSON5.stringify(object, null, space); - } else { - json = JSON.stringify(object, null, space); - } - } catch (e: any) { - throw new JsonFileError(`Couldn't JSON.stringify object for file: ${file}`, e); - } - const data = addNewLineAtEOF ? `${json}\n` : json; - await writeFileAtomicAsync(file, data, {}); - return object; -} - -async function setAsync( - file: string, - key: string, - value: unknown, - options?: Options -): Promise { - // TODO: Consider implementing some kind of locking mechanism, but - // it's not critical for our use case, so we'll leave it out for now - const object = await readAsync(file, options); - return writeAsync(file, { ...object, [key]: value }, options); -} - -async function mergeAsync( - file: string, - sources: Partial | Partial[], - options?: Options -): Promise { - const object = await readAsync(file, options); - if (Array.isArray(sources)) { - Object.assign(object, ...sources); - } else { - Object.assign(object, sources); - } - return writeAsync(file, object, options); -} - -async function deleteKeyAsync( - file: string, - key: string, - options?: Options -): Promise { - return deleteKeysAsync(file, [key], options); -} - -async function deleteKeysAsync( - file: string, - keys: string[], - options?: Options -): Promise { - const object = await readAsync(file, options); - let didDelete = false; - - for (let i = 0; i < keys.length; i++) { - const key = keys[i]; - if (object.hasOwnProperty(key)) { - delete object[key]; - didDelete = true; - } - } - - if (didDelete) { - return writeAsync(file, object, options); - } - return object; -} - -async function rewriteAsync( - file: string, - options?: Options -): Promise { - const object = await readAsync(file, options); - return writeAsync(file, object, options); -} - -function jsonParseErrorDefault( - options: Options = {} -): TJSONObject | void { - if (options.jsonParseErrorDefault === undefined) { - return options.default; - } else { - return options.jsonParseErrorDefault; - } -} - -function cantReadFileDefault( - options: Options = {} -): TJSONObject | void { - if (options.cantReadFileDefault === undefined) { - return options.default; - } else { - return options.cantReadFileDefault; - } -} - -function _getOption>( - options: Options | undefined, - field: K -): Options[K] { - if (options) { - if (options[field] !== undefined) { - return options[field]; - } - } - return DEFAULT_OPTIONS[field]; -} - -function locationFromSyntaxError(error: any, sourceString: string) { - // JSON5 SyntaxError has lineNumber and columnNumber. - if ('lineNumber' in error && 'columnNumber' in error) { - return { line: error.lineNumber, column: error.columnNumber }; - } - // JSON SyntaxError only includes the index in the message. - const match = /at position (\d+)/.exec(error.message); - if (match) { - const index = parseInt(match[1], 10); - const lines = sourceString.slice(0, index + 1).split('\n'); - return { line: lines.length, column: lines[lines.length - 1].length }; - } - - return null; -} - -function assertEmptyJsonString(json?: string, file?: string) { - if (json?.trim() === '') { - throw new EmptyJsonFileError(file); - } -} diff --git a/packages/json-file/src/JsonFileError.ts b/packages/json-file/src/JsonFileError.ts deleted file mode 100644 index 1cc956bff3..0000000000 --- a/packages/json-file/src/JsonFileError.ts +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Note that instances of this class do NOT pass `instanceof JsonFileError`. - */ -export default class JsonFileError extends Error { - cause: Error | undefined; - code: string | undefined; - fileName: string | undefined; - isJsonFileError: true; - - constructor(message: string, cause?: Error, code?: string, fileName?: string) { - let fullMessage = message; - if (fileName) { - fullMessage += `\n${cause ? 'ā”œ' : 'ā””'}ā”€ File: ${fileName}`; - } - if (cause) { - fullMessage += `\nā””ā”€ Cause: ${cause.name}: ${cause.message}`; - } - super(fullMessage); - this.name = this.constructor.name; - this.cause = cause; - this.code = code; - this.fileName = fileName; - this.isJsonFileError = true; - } -} - -export class EmptyJsonFileError extends JsonFileError { - constructor(fileName?: string) { - super(`Cannot parse an empty JSON string`, undefined, 'EJSONEMPTY', fileName); - } -} diff --git a/packages/json-file/tsconfig.json b/packages/json-file/tsconfig.json deleted file mode 100644 index c2ad6364cc..0000000000 --- a/packages/json-file/tsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "../../tsconfig.base", - "include": ["src/**/*.ts"], - "compilerOptions": { - "outDir": "build", - "rootDir": "src" - }, - "exclude": ["**/__mocks__/*", "**/__tests__/*"] -}