diff --git a/.github/depdendabot.yaml b/.github/depdendabot.yaml new file mode 100644 index 0000000..3ce2ac6 --- /dev/null +++ b/.github/depdendabot.yaml @@ -0,0 +1,8 @@ +version: 2 +updates: + - package-ecosystem: 'gitsubmodule' + directory: '/' + - package-ecosystem: 'npm' + directory: '/' +schedule: + interval: 'monthly' diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 04d06c9..ca2b3b4 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -11,8 +11,6 @@ on: - main - features/* - release/* -env: - CI_PUBLISH: jobs: build: runs-on: ubuntu-latest @@ -20,18 +18,22 @@ jobs: contents: read steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v4 + with: + submodules: true - name: Set up Node.js uses: actions/setup-node@v2 with: - node-version: "18" + node-version: '18' - name: Enable Corepack run: | corepack enable - name: Install dependencies - run: yarn install --immutable + run: yarn install --no-immutable + env: + YARN_ENABLE_HARDENED_MODE: 0 - name: Build run: yarn run build:all @@ -47,7 +49,9 @@ jobs: id-token: write steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v4 + with: + submodules: true - name: Get commit version shell: bash @@ -57,7 +61,7 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v2 with: - node-version: "18" + node-version: '18' registry-url: 'https://npm.pkg.github.com' scope: '@radius-project' - name: Enable Corepack @@ -65,7 +69,9 @@ jobs: corepack enable - name: Install dependencies - run: yarn install --immutable + run: yarn install --no-immutable + env: + YARN_ENABLE_HARDENED_MODE: 0 - name: Publish if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index cfcde29..7e4d373 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -17,18 +17,22 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v4 + with: + submodules: true - name: Set up Node.js uses: actions/setup-node@v2 with: - node-version: "18" + node-version: '18' - name: Enable Corepack run: | corepack enable - name: Install dependencies - run: yarn install --immutable + run: yarn install --no-immutable + env: + YARN_ENABLE_HARDENED_MODE: 0 - name: Run tests run: yarn run test:all diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..f96ac88 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "bicep-types"] + path = bicep-types + url = https://github.com/Azure/bicep-types diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..6d11797 --- /dev/null +++ b/.prettierignore @@ -0,0 +1 @@ +bicep-types/ \ No newline at end of file diff --git a/.prettierrc b/.prettierrc index 8b8b7b9..fa51da2 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,6 +1,6 @@ { "trailingComma": "es5", - "tabWidth": 4, + "tabWidth": 2, "semi": false, "singleQuote": true -} \ No newline at end of file +} diff --git a/bicep-types b/bicep-types new file mode 160000 index 0000000..3676a8b --- /dev/null +++ b/bicep-types @@ -0,0 +1 @@ +Subproject commit 3676a8bf689e62780c64c79bdca42f1799958cd4 diff --git a/eslint.config.mjs b/eslint.config.mjs index 651517a..66520fb 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,10 +1,15 @@ // @ts-check -import eslint from "@eslint/js"; -import tseslint from "typescript-eslint"; +import eslint from '@eslint/js' +import tseslint from 'typescript-eslint' export default tseslint.config( { - ignores: ["**/jest.config.js", "**/eslint.config.mjs", "**/dist/**"], + ignores: [ + '**/jest.config.js', + '**/eslint.config.mjs', + '**/dist/**', + '**/bicep-types/**', + ], }, eslint.configs.recommended, tseslint.configs.strict, @@ -16,5 +21,5 @@ export default tseslint.config( tsconfigRootDir: import.meta.dirname, }, }, - }, -); + } +) diff --git a/jest.config.js b/jest.config.js index f3ded9c..30b2777 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,8 +1,8 @@ /** @type {import('ts-jest').JestConfigWithTsJest} **/ module.exports = { - projects: ["/packages/*"], - testEnvironment: "node", + projects: ['/packages/*'], + testEnvironment: 'node', transform: { - "^.+.tsx?$": ["ts-jest", {}], + '^.+.tsx?$': ['ts-jest', {}], }, -}; +} diff --git a/packages/manifest-to-bicep-extension/jest.config.js b/packages/manifest-to-bicep-extension/jest.config.js index 72e669f..628fa55 100644 --- a/packages/manifest-to-bicep-extension/jest.config.js +++ b/packages/manifest-to-bicep-extension/jest.config.js @@ -1,12 +1,12 @@ module.exports = { - displayName: "manifest-to-bicep-extension", - roots: [""], + displayName: 'manifest-to-bicep-extension', + roots: [''], transform: { - "^.+\\.tsx?$": "ts-jest", + '^.+\\.tsx?$': 'ts-jest', }, - testRegex: "(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$", - moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"], + testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$', + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], moduleNameMapper: { - "^src/(.*)": "/src/$1", + '^src/(.*)': '/src/$1', }, -}; +} diff --git a/packages/manifest-to-bicep-extension/package.json b/packages/manifest-to-bicep-extension/package.json index 88cd2f7..e62c454 100644 --- a/packages/manifest-to-bicep-extension/package.json +++ b/packages/manifest-to-bicep-extension/package.json @@ -16,11 +16,24 @@ "dist/" ], "main": "dist/src/index.js", + "dependencies": { + "bicep-types": "file:../../bicep-types/src/bicep-types", + "yaml": "^2.6.0", + "yargs": "^17.7.2" + }, + "devDependencies": { + "@types/node": "^22.7.5", + "@types/yargs": "^17.0.33" + }, + "bundledDependencies": [ + "bicep-types" + ], "scripts": { "build": "tsc", "watch": "tsc -w", "publish": "npm publish", "prepublishOnly": "yarn build", + "preinstall": "cd ../../bicep-types/src/bicep-types && npm ci && npm run build", "version": "npm pkg set version=${0}" } } diff --git a/packages/manifest-to-bicep-extension/src/converter.ts b/packages/manifest-to-bicep-extension/src/converter.ts new file mode 100644 index 0000000..7ab6b7e --- /dev/null +++ b/packages/manifest-to-bicep-extension/src/converter.ts @@ -0,0 +1,148 @@ +import { + buildIndex, + ObjectTypeProperty, + ObjectTypePropertyFlags, + ResourceFlags, + ScopeType, + TypeFactory, + TypeFile, + TypeReference, + writeIndexJson, + writeIndexMarkdown, + writeTypesJson, +} from 'bicep-types' +import { ResourceProvider, Schema } from './manifest' + +export function convert(manifest: ResourceProvider): { + typesContent: string + indexContent: string + documentationContent: string +} { + const factory = new TypeFactory() + + for (const resourceType of manifest.types) { + for (const apiVersion of resourceType.apiVersions) { + const qualifiedName = `${manifest.name}/${resourceType.name}@${apiVersion.name}` + + const propertyType = factory.addObjectType( + `${resourceType.name}Properties`, + schemaProperties(apiVersion.schema, factory) + ) + + const bodyType = factory.addObjectType(qualifiedName, { + name: { + type: factory.addStringType(), + flags: + ObjectTypePropertyFlags.Required | + ObjectTypePropertyFlags.Identifier, + description: 'The resource name.', + }, + location: { + type: factory.addStringType(), + flags: ObjectTypePropertyFlags.None, + description: 'The resource location.', + }, + properties: { + type: propertyType, + flags: ObjectTypePropertyFlags.Required, + description: 'The resource properties.', + }, + apiVersion: { + type: factory.addStringLiteralType(apiVersion.name), + flags: + ObjectTypePropertyFlags.ReadOnly | + ObjectTypePropertyFlags.DeployTimeConstant, + description: 'The API version.', + }, + type: { + type: factory.addStringLiteralType( + `${manifest.name}/${resourceType.name}` + ), + flags: + ObjectTypePropertyFlags.ReadOnly | + ObjectTypePropertyFlags.DeployTimeConstant, + description: 'The resource type.', + }, + id: { + type: factory.addStringType(), + flags: ObjectTypePropertyFlags.ReadOnly, + description: 'The resource id.', + }, + }) + factory.addResourceType( + qualifiedName, + ScopeType.Unknown, + undefined, + bodyType, + ResourceFlags.None, + {} + ) + } + } + + const typeFiles: TypeFile[] = [] + typeFiles.push({ + relativePath: 'types.json', + types: factory.types, + }) + + const indexContent = buildIndex(typeFiles, (log) => console.log(log), { + name: manifest.name.toLowerCase().replace('.', ''), + version: '0.0.1', + isSingleton: false, + }) + + return { + typesContent: writeTypesJson(factory.types), + indexContent: writeIndexJson(indexContent), + documentationContent: writeIndexMarkdown(indexContent), + } +} + +function schemaProperties( + parent: Schema, + factory: TypeFactory +): Record { + const results: Record = {} + for (const [key, value] of Object.entries(parent.properties ?? {})) { + results[key] = addSchemaProperty(parent, key, value, factory) + } + return results +} + +function addSchemaProperty( + parent: Schema, + key: string, + property: Schema, + factory: TypeFactory +): ObjectTypeProperty { + const propertyType = addSchema(property, key, factory) + + let flags = ObjectTypePropertyFlags.None + if (parent.required?.includes(key)) { + flags |= ObjectTypePropertyFlags.Required + } + if (property.readOnly === true) { + flags |= ObjectTypePropertyFlags.ReadOnly + } + + return { + description: property.description, + type: propertyType, + flags: flags, + } +} + +function addSchema( + schema: Schema, + name: string, + factory: TypeFactory +): TypeReference { + if (schema.type === 'string') { + return factory.addStringType() + } else if (schema.type === 'object') { + return factory.addObjectType(name, schemaProperties(schema, factory)) + } else { + throw new Error(`Unsupported schema type: ${schema.type}`) + } +} diff --git a/packages/manifest-to-bicep-extension/src/main.ts b/packages/manifest-to-bicep-extension/src/main.ts index 299d77f..402787d 100644 --- a/packages/manifest-to-bicep-extension/src/main.ts +++ b/packages/manifest-to-bicep-extension/src/main.ts @@ -1,2 +1,3 @@ -import { add } from './math'; -add(9, 3); \ No newline at end of file +// eslint-disable-next-line @typescript-eslint/no-require-imports +const program = require('./program') +program.run() diff --git a/packages/manifest-to-bicep-extension/src/manifest.ts b/packages/manifest-to-bicep-extension/src/manifest.ts new file mode 100644 index 0000000..bcb0398 --- /dev/null +++ b/packages/manifest-to-bicep-extension/src/manifest.ts @@ -0,0 +1,24 @@ +export interface ResourceProvider { + name: string + types: ResourceType[] +} + +export interface ResourceType { + name: string + defaultApiVersion?: string + apiVersions: APIVersion[] +} + +export interface APIVersion { + name: string + schema: Schema + capabilities?: string[] +} + +export interface Schema { + type: 'string' | 'object' + description?: string + properties?: Schema[] + required?: string[] + readOnly?: boolean +} diff --git a/packages/manifest-to-bicep-extension/src/math.ts b/packages/manifest-to-bicep-extension/src/math.ts index 8d9b8a2..a39d4ef 100644 --- a/packages/manifest-to-bicep-extension/src/math.ts +++ b/packages/manifest-to-bicep-extension/src/math.ts @@ -1,3 +1,3 @@ export function add(a: number, b: number): number { - return a + b; + return a + b } diff --git a/packages/manifest-to-bicep-extension/src/program.ts b/packages/manifest-to-bicep-extension/src/program.ts new file mode 100644 index 0000000..cf183be --- /dev/null +++ b/packages/manifest-to-bicep-extension/src/program.ts @@ -0,0 +1,60 @@ +import { parse } from 'yaml' +import yargs from 'yargs' +import { hideBin } from 'yargs/helpers' +import fs from 'node:fs' +import { convert } from './converter' + +async function generate(manifest: string, output: string) { + const data = fs.readFileSync(manifest, 'utf8') + const parsed = parse(data) + const converted = convert(parsed) + + fs.rmSync(`${output}/types.json`, { force: true }) + fs.rmSync(`${output}/index.json`, { force: true }) + fs.rmSync(`${output}/index.md`, { force: true }) + + console.log(`Writing types to ${output}/types.json`) + fs.writeFileSync(`${output}/types.json`, converted.typesContent, { + encoding: 'utf8', + }) + + console.log(`Writing index to ${output}/index.json`) + fs.writeFileSync(`${output}/index.json`, converted.indexContent, { + encoding: 'utf8', + }) + + console.log(`Writing documentation to ${output}/index.md`) + fs.writeFileSync(`${output}/index.md`, converted.documentationContent, { + encoding: 'utf8', + }) +} + +export async function run() { + return await yargs(hideBin(process.argv)) + .usage('Usage: $0 [options]') + .showHelpOnFail(true) + .demandCommand(1, 'Command name is required') + .command( + 'generate [options]', + 'Generate Bicep extension from Radius Resource Provider manifest.', + (yargs) => { + return yargs + .positional('manifest', { + demandOption: true, + type: 'string', + }) + .positional('output', { + demandOption: true, + type: 'string', + }) + }, + async (yargs) => { + try { + await generate(yargs.manifest, yargs.output) + } catch (err) { + console.error(err) + } + } + ) + .parseAsync() +} diff --git a/packages/manifest-to-bicep-extension/test/math.test.ts b/packages/manifest-to-bicep-extension/test/math.test.ts index e655600..3ea3744 100644 --- a/packages/manifest-to-bicep-extension/test/math.test.ts +++ b/packages/manifest-to-bicep-extension/test/math.test.ts @@ -1,8 +1,8 @@ -import { add } from "src/math"; -import { describe, it, expect } from "@jest/globals"; +import { add } from 'src/math' +import { describe, it, expect } from '@jest/globals' -describe("add", () => { - it("should add two numbers", () => { - expect(add(1, 2)).toBe(3); - }); -}); +describe('add', () => { + it('should add two numbers', () => { + expect(add(1, 2)).toBe(3) + }) +}) diff --git a/packages/manifest-to-bicep-extension/tsconfig.json b/packages/manifest-to-bicep-extension/tsconfig.json index 4932cf5..0a99e99 100644 --- a/packages/manifest-to-bicep-extension/tsconfig.json +++ b/packages/manifest-to-bicep-extension/tsconfig.json @@ -17,8 +17,9 @@ "target": "esnext", "outDir": "dist", "paths": { - "src/*": ["src/*"] + "src/*": ["src/*"], + "bicep-types": ["../../bicep-types/src/bicep-types"] } }, - "include": ["src/**/*", "test/**/*"], + "include": ["src/**/*", "test/**/*"] } diff --git a/yarn.lock b/yarn.lock index 113f608..9b877ec 100644 --- a/yarn.lock +++ b/yarn.lock @@ -874,6 +874,12 @@ __metadata: "@radius-project/manifest-to-bicep-extension@workspace:packages/manifest-to-bicep-extension": version: 0.0.0-use.local resolution: "@radius-project/manifest-to-bicep-extension@workspace:packages/manifest-to-bicep-extension" + dependencies: + "@types/node": "npm:^22.7.5" + "@types/yargs": "npm:^17.0.33" + bicep-types: "file:../../bicep-types/src/bicep-types" + yaml: "npm:^2.6.0" + yargs: "npm:^17.7.2" languageName: unknown linkType: soft @@ -991,7 +997,7 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:*": +"@types/node@npm:*, @types/node@npm:^22.7.5": version: 22.10.2 resolution: "@types/node@npm:22.10.2" dependencies: @@ -1014,7 +1020,7 @@ __metadata: languageName: node linkType: hard -"@types/yargs@npm:^17.0.8": +"@types/yargs@npm:^17.0.33, @types/yargs@npm:^17.0.8": version: 17.0.33 resolution: "@types/yargs@npm:17.0.33" dependencies: @@ -1359,6 +1365,13 @@ __metadata: languageName: unknown linkType: soft +"bicep-types@file:../../bicep-types/src/bicep-types::locator=%40radius-project%2Fmanifest-to-bicep-extension%40workspace%3Apackages%2Fmanifest-to-bicep-extension": + version: 0.0.0-placeholder + resolution: "bicep-types@file:../../bicep-types/src/bicep-types#../../bicep-types/src/bicep-types::hash=15c3ef&locator=%40radius-project%2Fmanifest-to-bicep-extension%40workspace%3Apackages%2Fmanifest-to-bicep-extension" + checksum: 10c0/6a38eccdd4b4ecfff20b63eb522af8ac4cd8d6f87bf8451b367e0ffac2742f2139364749c507c3eed52b426c7bb129f3a1cc745ce81cb0d1ff2199b338ccb8d2 + languageName: node + linkType: hard + "brace-expansion@npm:^1.1.7": version: 1.1.11 resolution: "brace-expansion@npm:1.1.11" @@ -4270,6 +4283,15 @@ __metadata: languageName: node linkType: hard +"yaml@npm:^2.6.0": + version: 2.6.1 + resolution: "yaml@npm:2.6.1" + bin: + yaml: bin.mjs + checksum: 10c0/aebf07f61c72b38c74d2b60c3a3ccf89ee4da45bcd94b2bfb7899ba07a5257625a7c9f717c65a6fc511563d48001e01deb1d9e55f0133f3e2edf86039c8c1be7 + languageName: node + linkType: hard + "yargs-parser@npm:^21.1.1": version: 21.1.1 resolution: "yargs-parser@npm:21.1.1" @@ -4277,7 +4299,7 @@ __metadata: languageName: node linkType: hard -"yargs@npm:^17.3.1": +"yargs@npm:^17.3.1, yargs@npm:^17.7.2": version: 17.7.2 resolution: "yargs@npm:17.7.2" dependencies: