diff --git a/repo-convention.d.ts b/index.d.ts similarity index 83% rename from repo-convention.d.ts rename to index.d.ts index 9d77775..409e0e4 100644 --- a/repo-convention.d.ts +++ b/index.d.ts @@ -32,4 +32,11 @@ declare module '@ridomin/repo-scripts' { * @returns {boolean} */ function checkIds(dtdlJson: any): boolean; + + /** + * @description Copy DTDL file to the /dtmi/com/model-1.json folder struct + * @param file + * @returns The root DTMI of the file, if successfull + */ + function addModel(file: string): string; } \ No newline at end of file diff --git a/index.js b/index.js new file mode 100644 index 0000000..42d0615 --- /dev/null +++ b/index.js @@ -0,0 +1 @@ +export { isDtmi, dtmiToPath, getDependencies, checkIds, checkDtmiPathFromFile } from './repo-convention.js' diff --git a/package-lock.json b/package-lock.json index ed7d715..31f36c6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@ridomin/repo-scripts", - "version": "0.0.9", + "version": "0.0.13", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -712,6 +712,16 @@ "@types/istanbul-lib-report": "*" } }, + "@types/jest": { + "version": "26.0.15", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-26.0.15.tgz", + "integrity": "sha512-s2VMReFXRg9XXxV+CW9e5Nz8fH2K1aEhwgjUqPPbQd7g95T0laAcvLv032EhFHIa5GHsZ8W7iJEQVaJq6k3Gog==", + "dev": true, + "requires": { + "jest-diff": "^26.0.0", + "pretty-format": "^26.0.0" + } + }, "@types/node": { "version": "14.11.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.11.2.tgz", @@ -4047,6 +4057,11 @@ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", "dev": true }, + "node-fetch": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", + "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==" + }, "node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", diff --git a/package.json b/package.json index a5cdf27..8560809 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,10 @@ { "name": "@ridomin/repo-scripts", - "version": "0.0.10", + "version": "0.0.14", "description": "", - "main": "repo-convention.js", - "type": "commonjs", + "main": "index.js", + "type": "module", + "types": "index.d.ts", "scripts": { "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js", "prepare": "standard && npm test" @@ -20,12 +21,14 @@ }, "homepage": "https://github.com/iotmodels/repo-scripts/#readme", "devDependencies": { + "@types/jest": "^26.0.15", "jest": "^26.4.2", "standard": "^14.3.4" }, "dependencies": { "jsonata": "^1.8.3", - "mkdirp": "^1.0.4" + "mkdirp": "^1.0.4", + "node-fetch": "^2.6.1" }, "standard": { "globals": [ diff --git a/repo-convention.js b/repo-convention.js index a25bcfa..f058844 100644 --- a/repo-convention.js +++ b/repo-convention.js @@ -1,21 +1,22 @@ -const fs = require('fs') -const path = require('path') -const jsonata = require('jsonata') +import { readFileSync, existsSync } from 'fs' +import { join, resolve, normalize } from 'path' +import jsonata from 'jsonata' +import fetch from 'node-fetch' -/** +export/** * @description Validates DTMI with RegEx from https://github.com/Azure/digital-twin-model-identifier#validation-regular-expressions * @param {string} dtmi */ const isDtmi = dtmi => RegExp('^dtmi:[A-Za-z](?:[A-Za-z0-9_]*[A-Za-z0-9])?(?::[A-Za-z](?:[A-Za-z0-9_]*[A-Za-z0-9])?)*;[1-9][0-9]{0,8}$').test(dtmi) -/** +export/** * @description Converts DTMI to /dtmi/com/example/device-1.json path. * @param {string} dtmi * @returns {string} */ const dtmiToPath = dtmi => isDtmi(dtmi) ? `/${dtmi.toLowerCase().replace(/:/g, '/').replace(';', '-')}.json` : null -/** +export/** * @description Returns external IDs in `extend` and `component` elements * @param {{ extends: any[]; contents: any[]; }} rootJson * @returns {Array} @@ -46,22 +47,22 @@ const getDependencies = rootJson => { return deps } -/** +export/** * @description Checks all dependencies are available * @param {Array} deps * @returns {boolean} */ const checkDependencies = dtmi => { let result = true - const fileName = path.join(__dirname, dtmiToPath(dtmi)) + const fileName = join(__dirname, dtmiToPath(dtmi) || '') console.log(`Validating dependencies for ${dtmi} from ${fileName}`) - const dtdlJson = JSON.parse(fs.readFileSync(fileName, 'utf-8')) + const dtdlJson = JSON.parse(readFileSync(fileName, 'utf-8')) const deps = getDependencies(dtdlJson) deps.forEach(d => { - const fileName = path.join(__dirname, dtmiToPath(d)) - if (fs.existsSync(fileName)) { + const fileName = join(__dirname, dtmiToPath(d) || '') + if (existsSync(fileName)) { console.log(`Dependency ${d} found`) - const model = JSON.parse(fs.readFileSync(fileName, 'utf-8')) + const model = JSON.parse(readFileSync(fileName, 'utf-8')) if (model['@id'] !== d) { console.error(`ERROR: LowerCase issue with dependent id ${d}. Was ${model['@id']}. Aborting`) result = result && true @@ -74,7 +75,7 @@ const checkDependencies = dtmi => { return result } -/** +export/** * @description Validates all internal IDs follow the namepspace set by the root id * @param {any} dtdlJson * @returns {boolean} @@ -104,18 +105,18 @@ const checkIds = dtdlJson => { } } -/** +export/** * @description Checks if the folder/name convention matches the DTMI * @param {string} file * @returns {boolean} */ const checkDtmiPathFromFile = file => { - const model = JSON.parse(fs.readFileSync(file, 'utf-8')) + const model = JSON.parse(readFileSync(file, 'utf-8')) const id = model['@id'] if (id) { - const expectedPath = path.join(process.cwd(), dtmiToPath(model['@id'])) - if (path.resolve(file) !== expectedPath) { - console.log(`ERROR: in current path ${path.normalize(file)}, expecting ${expectedPath}.`) + const expectedPath = join(process.cwd(), dtmiToPath(model['@id']) || '') + if (resolve(file) !== expectedPath) { + console.log(`ERROR: in current path ${normalize(file)}, expecting ${expectedPath}.`) return false } else { console.log(`FilePath ${file} for ${id} seems OK.`) @@ -126,4 +127,41 @@ const checkDtmiPathFromFile = file => { return false } } -module.exports = { dtmiToPath, isDtmi, checkIds, getDependencies, checkDependencies, checkDtmiPathFromFile } + +export /** +* @param {string} dtmi +* @param {string | undefined} [repo] +* @param {undefined} [expanded] +* @returns {Array} +*/ +const resolveDtmi = async (dtmi, repo, expanded) => { + const result = [] + if (!repo) repo = 'https://' + 'devicemodels.azure.com' + const url = `${repo}${dtmiToPath(dtmi)}` + + if (expanded) { + const xurl = url.replace('.json', '.expanded.json') + const expRaw = await (await fetch(xurl)).text() + const exp = JSON.parse(expRaw) + exp.foreach(doc => { + const id = doc['@id'] + result.push({ id, doc }) + }) + return result + } + + const respJson = await (await fetch(url)).json() + const id = respJson['@id'] + if (id === dtmi) { + result.push({ id, respJson }) + const deps = getDependencies(respJson) + for await (const d of deps) { + const json = await (await fetch(`${repo}${dtmiToPath(d)}`)).json() + result.push({ d, json }) + } + } else { + console.error('ERR. Case diff ', id) + } + // console.log(result) + return result +} diff --git a/test/repo-convention.test.js b/test/repo-convention.test.js index abddda7..be11ad6 100644 --- a/test/repo-convention.test.js +++ b/test/repo-convention.test.js @@ -1,35 +1,53 @@ -const rc = require('../repo-convention.js') -const td = require('./test-models') +import { isDtmi, dtmiToPath, getDependencies, checkIds, checkDtmiPathFromFile, resolveDtmi } from '../repo-convention.js' +import { noDepsJson, oneDepJson, twoDepsJson, globalId } from './test-models' test('is valid dtmi', () => { - expect(rc.isDtmi('dtmi:with::twosemicolons;1')).toBe(false) + expect(isDtmi('dtmi:with::twosemicolons;1')).toBe(false) }) test('invalid dtmi', () => { - expect(rc.dtmiToPath('')).toBe(null) - expect(rc.dtmiToPath('notadtmi')).toBe(null) - expect(rc.dtmiToPath('dtmi:notadtmi')).toBe(null) - expect(rc.dtmiToPath('dtmi:com:example:thermostat:1')).toBe(null) - expect(rc.dtmiToPath('dtmi:com:example-bad:thermostat;1')).toBe(null) + expect(dtmiToPath('')).toBe(null) + expect(dtmiToPath('notadtmi')).toBe(null) + expect(dtmiToPath('dtmi:notadtmi')).toBe(null) + expect(dtmiToPath('dtmi:com:example:thermostat:1')).toBe(null) + expect(dtmiToPath('dtmi:com:example-bad:thermostat;1')).toBe(null) }) test('dtmi to path', () => { - expect(rc.dtmiToPath('dtmi:com:example:Thermostat;1')).toBe('/dtmi/com/example/thermostat-1.json') - expect(rc.dtmiToPath('dtmi:com:Example:thermostat;1')).toBe('/dtmi/com/example/thermostat-1.json') + expect(dtmiToPath('dtmi:com:example:Thermostat;1')).toBe('/dtmi/com/example/thermostat-1.json') + expect(dtmiToPath('dtmi:com:Example:thermostat;1')).toBe('/dtmi/com/example/thermostat-1.json') }) test('get dependencies', () => { - expect(rc.getDependencies(td.noDepsJson)).toEqual([]) - expect(rc.getDependencies(td.oneDepJson)).toEqual(['dtmi:test:base;1']) - expect(rc.getDependencies(td.twoDepsJson)).toEqual(['dtmi:test:base;1', 'dtmi:test:onedep:comp1;1']) + expect(getDependencies(noDepsJson)).toEqual([]) + expect(getDependencies(oneDepJson)).toEqual(['dtmi:test:base;1']) + expect(getDependencies(twoDepsJson)).toEqual(['dtmi:test:base;1', 'dtmi:test:onedep:comp1;1']) }) test('check ids', () => { - expect(rc.checkIds(td.globalId)).toBe(false) - expect(rc.checkIds(td.oneDepJson)).toBe(true) + expect(checkIds(globalId)).toBe(false) + expect(checkIds(oneDepJson)).toBe(true) }) test('checkDtmiPathFromFile', () => { - expect(rc.checkDtmiPathFromFile('dtmi/azure/devicemanagement/deviceinformation-1.json')).toBe(true) - expect(rc.checkDtmiPathFromFile('test/badpath.json')).toBe(false) + expect(checkDtmiPathFromFile('dtmi/azure/devicemanagement/deviceinformation-1.json')).toBe(true) + expect(checkDtmiPathFromFile('test/badpath.json')).toBe(false) +}) + +test('resolveDtmi_noDeps', async () => { + const models = await resolveDtmi('dtmi:azure:DeviceManagement:DeviceInformation;1') + console.log(models.length) + expect(models.length).toBe(1) +}) + +test('resolveDtmi_Deps', async () => { + const models = await resolveDtmi('dtmi:com:example:TemperatureController;1') + console.log(models.length) + expect(models.length).toBe(3) +}) + +test('resolveDtmi_DepsExpanded', async () => { + const models = await resolveDtmi('dtmi:Espressif:SensorController;2', null, true) + console.log(models.length) + expect(models.length).toBe(6) }) diff --git a/test/test-models.js b/test/test-models.js index 4b870bc..8d82e20 100644 --- a/test/test-models.js +++ b/test/test-models.js @@ -1,4 +1,4 @@ -const noDepsJson = { +export const noDepsJson = { '@context': 'dtmi:dtdl:context;2', '@id': 'dtmi:test:onedep;1', '@type': 'Interface', @@ -6,7 +6,7 @@ const noDepsJson = { contents: [] } -const oneDepJson = { +export const oneDepJson = { '@context': 'dtmi:dtdl:context;2', '@id': 'dtmi:test:onedep;1', '@type': 'Interface', @@ -15,7 +15,7 @@ const oneDepJson = { contents: [] } -const globalId = { +export const globalId = { '@context': 'dtmi:dtdl:context;2', '@id': 'dtmi:test:twodeps;1', '@type': 'Interface', @@ -31,7 +31,7 @@ const globalId = { ] } -const twoDepsJson = { +export const twoDepsJson = { '@context': 'dtmi:dtdl:context;2', '@id': 'dtmi:test:twodeps;1', '@type': 'Interface', @@ -45,5 +45,3 @@ const twoDepsJson = { } ] } - -module.exports = { noDepsJson, oneDepJson, twoDepsJson, globalId } diff --git a/tools/add-model.js b/tools/add-model.js index 3c5a646..4cc07c4 100644 --- a/tools/add-model.js +++ b/tools/add-model.js @@ -1,31 +1,31 @@ -const fs = require('fs') -const path = require('path') -const mkdirp = require('mkdirp') -const { dtmiToPath, checkIds, checkDependencies } = require('../repo-convention.js') +import { readFileSync, existsSync, writeFileSync, unlinkSync } from 'fs' +import { join, dirname } from 'path' +import mkdirp from 'mkdirp' +import { dtmiToPath, checkIds, checkDependencies } from '../repo-convention.js' const createInterfaceFromFile = async file => { - const jsonDtdl = JSON.parse(fs.readFileSync(file, 'utf-8')) + const jsonDtdl = JSON.parse(readFileSync(file, 'utf-8')) await createInterfaceFromJson(jsonDtdl) } const createInterfaceFromJson = async jsonDtdl => { const dtmi = jsonDtdl['@id'] - const fileName = path.join(process.cwd(), dtmiToPath(dtmi)) - if (fs.existsSync(fileName)) { + const fileName = join(process.cwd(), dtmiToPath(dtmi) || '') + if (existsSync(fileName)) { console.log(`WARNING: ID ${dtmi} already exists at ${fileName} . Skipping `) } else { - await mkdirp(path.dirname(fileName)) - fs.writeFileSync(fileName, JSON.stringify(jsonDtdl, null, 2)) + await mkdirp(dirname(fileName)) + writeFileSync(fileName, JSON.stringify(jsonDtdl, null, 2)) console.log(`Model ${dtmi} added successfully to ${fileName}`) } } -/** +export/** * @description Adds a model to the repo. Validates ids, dependencies and set the right folder/file name * @param {string} file */ const addModel = async file => { - const rootJson = JSON.parse(fs.readFileSync(file, 'utf-8')) + const rootJson = JSON.parse(readFileSync(file, 'utf-8')) if (Array.isArray(rootJson)) { for await (const d of rootJson) { checkIds(d) @@ -45,7 +45,7 @@ const main = async () => { const id = await addModel(file) console.log('added', id) if (id && !checkDependencies(id)) { - fs.unlinkSync(path.join(process.cwd(), dtmiToPath(id))) + unlinkSync(join(process.cwd(), dtmiToPath(id) || '')) console.log('ERROR: Dont forget to include all the dependencies before submitting.') process.exit(1) } diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..0c3af5f --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,69 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig.json to read more about this file */ + + /* Basic Options */ + // "incremental": true, /* Enable incremental compilation */ + "target": "ES6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ + "module": "es2020", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ + // "lib": [], /* Specify library files to be included in the compilation. */ + "allowJs": true, /* Allow javascript files to be compiled. */ + "checkJs": true, /* Report errors in .js files. */ + // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ + "declaration": true, /* Generates corresponding '.d.ts' file. */ + // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ + // "sourceMap": true, /* Generates corresponding '.map' file. */ + // "outFile": "./", /* Concatenate and emit output to single file. */ + // "outDir": "./", /* Redirect output structure to the directory. */ + // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + // "composite": true, /* Enable project compilation */ + // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ + // "removeComments": true, /* Do not emit comments to output. */ + "noEmit": true, /* Do not emit outputs. */ + // "importHelpers": true, /* Import emit helpers from 'tslib'. */ + // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ + // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ + + /* Strict Type-Checking Options */ + "strict": true, /* Enable all strict type-checking options. */ + "noImplicitAny": false, /* Raise error on expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* Enable strict null checks. */ + // "strictFunctionTypes": true, /* Enable strict checking of function types. */ + // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ + // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ + // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ + // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ + + /* Additional Checks */ + // "noUnusedLocals": true, /* Report errors on unused locals. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ + // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + + /* Module Resolution Options */ + "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ + // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ + // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ + // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ + // "typeRoots": [], /* List of folders to include type definitions from. */ + // "types": [], /* Type declaration files to be included in compilation. */ + // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ + "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ + // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + + /* Source Map Options */ + // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ + // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ + + /* Experimental Options */ + // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ + // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + + /* Advanced Options */ + "skipLibCheck": true, /* Skip type checking of declaration files. */ + "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ + } +}