import { ts, JSON5, type ArrayShape, CallExpressionShape, type ObjectShape, Plugin, Shape, type Source, SourceFileShape, StringShape, } from '@servicenow/sdk-build-core' export class JsonStringifyShape extends CallExpressionShape { constructor({ source, json }: { source: Source; json: NonNullable | ObjectShape | ArrayShape }) { super({ source, callee: 'JSON.stringify', args: [json] }) } getJson(): ObjectShape | ArrayShape { const arg = this.getArgument(0) return arg.isArray() ? arg.asArray() : arg.asObject() } override getValue(): string { return JSON.stringify(this.getJson().getValue()) } } export class JsonFileShape extends SourceFileShape { private readonly json: ObjectShape | ArrayShape constructor({ source, json, path, content, }: { source: Source; json: unknown; path: string | Shape; content: string | Shape }) { super({ source, path, content }) const shape = Shape.from(source, json) this.json = shape.isArray() ? shape.asArray() : shape.asObject('Expected JSON file content to be a JSON object or array') } getJson(): ObjectShape | ArrayShape { return this.json } } export const JsonPlugin = Plugin.create({ name: 'JsonPlugin', noTelemetry: true, shapes: [ { shape: SourceFileShape, fileTypes: ['json'], async toSubclass(file, { compiler, transform }) { const actualFile = compiler.getSourceFile(file.getPath()) if (!actualFile) { return { success: true, value: new JsonFileShape({ source: file, path: file.getPath(), content: file.getContent(), json: JSON5.parse(file.getContent()), }), } } // Get the root-level expression from the first statement // We need to check the direct child, not descendants, to avoid matching nested arrays/objects const firstStatement = actualFile.getStatements()[0] const rootExpression = firstStatement?.isKind(ts.SyntaxKind.ExpressionStatement) ? firstStatement.getExpression() : undefined const root = rootExpression?.isKind(ts.SyntaxKind.ObjectLiteralExpression) || rootExpression?.isKind(ts.SyntaxKind.ArrayLiteralExpression) ? rootExpression : undefined if (!root) { return { success: false } } const json = await transform.toShape(root) if (!json.success) { return { success: false } } return { success: true, value: new JsonFileShape({ source: file, json: json.value, path: file.getPath(), content: file.getContent(), }), } }, }, { shape: CallExpressionShape, fileTypes: ['fluent'], toSubclass(callExpression, { diagnostics }) { if (callExpression.getCallee() !== 'JSON.stringify') { return { success: false } } if (callExpression.getArguments().length > 1) { diagnostics.error( callExpression.getArgument(1), 'Additional arguments to JSON.stringify are not supported' ) return { success: false } } const arg = callExpression.getArgument(0) if (!arg.isObject() && !arg.isArray() && !arg.is(JsonFileShape)) { diagnostics.error( arg, 'Argument to JSON.stringify must be an object, array, or an import from a JSON file' ) return { success: false } } return { success: true, value: new JsonStringifyShape({ source: callExpression, json: arg.if(JsonFileShape)?.getJson() ?? arg, }), } }, }, { shape: StringShape, async commit(shape, target, { logger, transform, commit }) { const result = await transform.toShape(target) if (!result.success) { return { success: false } } const { value: targetShape } = result if (targetShape.is(JsonStringifyShape)) { await commit(shape, targetShape.getJson().getOriginalNode()) return { success: true } } if (!targetShape.isObject() && !targetShape.isArray() && !targetShape.is(JsonFileShape)) { return { success: false } } let json: ObjectShape | ArrayShape try { const parsed = Shape.from(shape, JSON5.parse(shape.getValue())) json = parsed.isArray() ? parsed.asArray() : parsed.asObject() } catch (e) { logger.info('Incoming string value could not be parsed as JSON and will overwrite the node') return { success: false } } await commit(json, (targetShape.if(JsonFileShape)?.getJson() ?? targetShape).getOriginalNode()) return { success: true } }, }, ], })