diff --git a/.github/workflows/mikeals-workflow.yml b/.github/workflows/mikeals-workflow.yml index 1b88e24..d7787f0 100644 --- a/.github/workflows/mikeals-workflow.yml +++ b/.github/workflows/mikeals-workflow.yml @@ -6,7 +6,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node-version: [12.x, 14.x] + node-version: [12.x, 14.x, 16.x] steps: - uses: actions/checkout@v2 - name: Use Node.js ${{ matrix.node-version }} diff --git a/index.js b/index.js index eca32ef..3efa1cd 100644 --- a/index.js +++ b/index.js @@ -1,57 +1,102 @@ import json from 'fast-json-stable-stringify' +// @ts-ignore import isCircular from '@ipld/is-circular' import transform from 'lodash.transform' import { bytes, CID } from 'multiformats' import { base64 } from 'multiformats/bases/base64' -const _encode = (obj) => transform(obj, (result, value, key) => { - const cid = CID.asCID(value) - if (cid) { - result[key] = { '/': cid.toString() } - } else if (bytes.isBinary(value)) { - value = bytes.coerce(value) - result[key] = { '/': { bytes: base64.encode(value) } } - } else if (typeof value === 'object' && value !== null) { - result[key] = _encode(value) - } else { - result[key] = value - } -}) +/** + * @template {number} Code + * @template T + * @typedef {import('multiformats/codecs/interface').BlockCodec} BlockCodec + */ + +/** + * @template T + * @param {T} obj + * @returns {T} + */ +const transformEncode = (obj) => transform(obj, + /** + * @param {any} result + * @param {any} value + * @param {string} key + */ + (result, value, key) => { + const cid = CID.asCID(value) + if (cid) { + result[key] = { '/': cid.toString() } + } else if (bytes.isBinary(value)) { + value = bytes.coerce(value) + result[key] = { '/': { bytes: base64.encode(value) } } + } else if (typeof value === 'object' && value !== null) { + result[key] = transformEncode(value) + } else { + result[key] = value + } + }) -const encode = (obj) => { +/** + * @template T + * @param {T} obj + * @returns {Uint8Array} + */ +const _encode = (obj) => { if (typeof obj === 'object' && !bytes.isBinary(obj) && !CID.asCID(obj) && obj) { if (isCircular(obj, { asCID: true })) { throw new Error('Object contains circular references') } - obj = _encode(obj) + obj = transformEncode(obj) } return bytes.fromString(json(obj)) } -const _decode = (obj) => transform(obj, (result, value, key) => { - if (typeof value === 'object' && value !== null) { - if (value['/']) { - if (typeof value['/'] === 'string') { - result[key] = CID.parse(value['/']) - } else if (typeof value['/'] === 'object' && value['/'].bytes) { - result[key] = base64.decode(value['/'].bytes) +/** + * @param {object} obj + * @returns {any} + */ +const transformDecode = (obj) => transform(obj, + /** + * @param {any} result + * @param {any} value + * @param {string} key + * @returns {any} + */ + (result, value, key) => { + if (typeof value === 'object' && value !== null) { + if (value['/']) { + if (typeof value['/'] === 'string') { + result[key] = CID.parse(value['/']) + } else if (typeof value['/'] === 'object' && value['/'].bytes) { + result[key] = base64.decode(value['/'].bytes) + } else { + result[key] = transformDecode(value) + } } else { - result[key] = _decode(value) + result[key] = transformDecode(value) } } else { - result[key] = _decode(value) + result[key] = value } - } else { - result[key] = value - } -}) + }) -const decode = (buffer) => { - const obj = JSON.parse(bytes.toString(buffer)) - return _decode({ value: obj }).value +/** + * @template T + * @param {Uint8Array} data + * @returns {T} + */ +const _decode = (data) => { + const obj = JSON.parse(bytes.toString(data)) + return transformDecode({ value: obj }).value } -const name = 'dag-json' -const code = 0x0129 - -export { encode, decode, name, code } +/** + * @template T + * @type {BlockCodec<0x0129, T>} + */ +export const { name, code, decode, encode } = { + name: 'dag-json', + code: 0x0129, + encode: _encode, + decode: _decode +} diff --git a/package.json b/package.json index 5388c3f..c06ae2d 100644 --- a/package.json +++ b/package.json @@ -1,45 +1,67 @@ { "name": "@ipld/dag-json", "version": "0.0.0-dev", - "description": "JSON Directed Acrylic Graph for IPLD", + "description": "JSON Directed Acrylic Graph (DAG-JSON) for IPLD", + "main": "index.js", + "types": "./types/index.d.ts", "type": "module", - "directories": { - "test": "test" - }, "scripts": { - "lint": "standard", - "build": "npm_config_yes=true npx ipjs@latest build --tests", - "publish": "npm_config_yes=true npx ipjs@latest publish", - "test:cjs": "npm run build && mocha dist/cjs/node-test/test-*.js", + "lint": "standard *.js test/*.js", + "build": "npm run build:js && npm run build:types", + "build:js": "ipjs build --tests --main && npm run build:copy", + "build:copy": "cp -a tsconfig.json index.js dist/ && mkdir -p dist/test && cp test/*.js dist/test/", + "build:types": "npm run build:copy && cd dist && tsc --build", + "publish": "ipjs publish", + "test:cjs": "npm run build:js && mocha dist/cjs/node-test/test-*.js && npm run test:cjs:browser", "test:node": "hundreds mocha test/test-*.js", - "test:browser": "polendina --cleanup dist/cjs/node-test/test-*.js", - "test": "npm run lint && npm run test:node && npm run test:cjs && npm run test:browser", - "coverage": "c8 --reporter=html mocha test/test-*.js && npx st -d coverage -p 8080" + "test:cjs:browser": "polendina --page --worker --serviceworker --cleanup dist/cjs/node-test/test-*.js", + "test:ts": "npm run build:types && npm run test --prefix test/ts-use", + "test": "npm run lint && npm run test:node && npm run test:cjs && npm run test:ts", + "coverage": "c8 --reporter=html mocha test/test-*.js && npm_config_yes=true npx st -d coverage -p 8080" }, "exports": { "import": "./index.js" }, + "license": "(Apache-2.0 AND MIT)", "repository": { "type": "git", - "url": "git+https://github.com/mikeal/dag-json.git" + "url": "git+https://github.com/ipld/js-dag-json.git" }, - "keywords": [], - "author": "Mikeal Rogers (https://www.mikealrogers.com/)", - "license": "(Apache-2.0 AND MIT)", + "keywords": [ + "IPFS", + "IPLD" + ], "bugs": { - "url": "https://github.com/mikeal/dag-json/issues" - }, - "homepage": "https://github.com/mikeal/dag-json#readme", - "devDependencies": { - "hundreds": "0.0.8", - "mocha": "^8.1.1", - "polendina": "^1.1.0", - "standard": "^14.3.4" + "url": "https://github.com/ipld/js-dag-json/issues" }, + "homepage": "https://github.com/ipld/js-dag-json", "dependencies": { "@ipld/is-circular": "^2.0.0", + "@types/lodash.transform": "^4.6.6", "fast-json-stable-stringify": "^2.1.0", "lodash.transform": "^4.6.0", - "multiformats": "^4.0.0" - } + "multiformats": "^7.0.0" + }, + "devDependencies": { + "hundreds": "^0.0.9", + "ipjs": "^5.0.0", + "mocha": "^8.3.2", + "polendina": "^1.1.0", + "standard": "^16.0.3", + "typescript": "^4.2.4" + }, + "directories": { + "test": "test" + }, + "typesVersions": { + "*": { + "*": [ + "types/*" + ], + "types/*": [ + "types/*" + ] + } + }, + "author": "Mikeal Rogers (https://www.mikealrogers.com/)" } diff --git a/test/ts-use/.gitignore b/test/ts-use/.gitignore new file mode 100644 index 0000000..6881777 --- /dev/null +++ b/test/ts-use/.gitignore @@ -0,0 +1,3 @@ +node_modules +src/main.js +tsconfig.tsbuildinfo diff --git a/test/ts-use/package.json b/test/ts-use/package.json new file mode 100644 index 0000000..70c5de1 --- /dev/null +++ b/test/ts-use/package.json @@ -0,0 +1,11 @@ +{ + "name": "ts-use", + "private": true, + "dependencies": { + "@ipld/dag-json": "file:../../dist/", + "multiformats": "file:../../node_modules/multiformats" + }, + "scripts": { + "test": "npm install && npx -p typescript tsc && node src/main.js" + } +} diff --git a/test/ts-use/src/main.ts b/test/ts-use/src/main.ts new file mode 100644 index 0000000..4ac245f --- /dev/null +++ b/test/ts-use/src/main.ts @@ -0,0 +1,45 @@ +import { deepStrictEqual } from 'assert' + +import { BlockEncoder, BlockDecoder, BlockCodec } from 'multiformats/codecs/interface' +import * as dagJson from '@ipld/dag-json' + +const main = () => { + // make sure we have a full CodecFeature + useCodecFeature(dagJson) +} + +function useCodecFeature (codec: BlockCodec<297, any>) { + // use only as a BlockEncoder + useEncoder(codec) + + // use only as a BlockDecoder + useDecoder(codec) + + // use as a full BlockCodec which does both BlockEncoder & BlockDecoder + useBlockCodec(codec) +} + +function useEncoder (encoder: BlockEncoder) { + deepStrictEqual(encoder.code, 297) + deepStrictEqual(encoder.name, 'dag-json') + deepStrictEqual(Array.from(encoder.encode('blip')), [34, 98, 108, 105, 112, 34]) + console.log('[TS] ✓ { encoder: BlockEncoder }') +} + +function useDecoder (decoder: BlockDecoder) { + deepStrictEqual(decoder.code, 297) + deepStrictEqual(decoder.decode(Uint8Array.from([34, 98, 108, 105, 112, 34 ])), 'blip') + console.log('[TS] ✓ { decoder: BlockDecoder }') +} + +function useBlockCodec (blockCodec: BlockCodec) { + deepStrictEqual(blockCodec.code, 297) + deepStrictEqual(blockCodec.name, 'dag-json') + deepStrictEqual(Array.from(blockCodec.encode('blip')), [34, 98, 108, 105, 112, 34]) + deepStrictEqual(blockCodec.decode(Uint8Array.from([34, 98, 108, 105, 112, 34])), 'blip') + console.log('[TS] ✓ {}:BlockCodec') +} + +main() + +export default main diff --git a/test/ts-use/tsconfig.json b/test/ts-use/tsconfig.json new file mode 100644 index 0000000..ff14759 --- /dev/null +++ b/test/ts-use/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "strict": true, + "moduleResolution": "node", + "noImplicitAny": true, + "skipLibCheck": true, + "incremental": true + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..58e8c8d --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,37 @@ +{ + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "forceConsistentCasingInFileNames": true, + "noImplicitReturns": false, + "noImplicitAny": true, + "noImplicitThis": true, + "noFallthroughCasesInSwitch": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "strictFunctionTypes": false, + "strictNullChecks": true, + "strictPropertyInitialization": true, + "strictBindCallApply": true, + "strict": true, + "alwaysStrict": true, + "esModuleInterop": true, + "target": "ES2018", + "moduleResolution": "node", + "declaration": true, + "declarationMap": true, + "outDir": "types", + "skipLibCheck": true, + "stripInternal": true, + "resolveJsonModule": true, + "baseUrl": ".", + "emitDeclarationOnly": true + }, + "include": [ + "index.js" + ], + "exclude": [ + "node_modules" + ], + "compileOnSave": false +}