import { path as pathModule, CallExpressionShape, Plugin, type ProjectFile, type Record, type Source, StringShape, Shape, FileSystem, type Transform, taxonomySchemaDefault, } from '@servicenow/sdk-build-core' import { CallExpressionPlugin } from './call-expression-plugin' export class NowIncludeShape extends CallExpressionShape { private readonly includedText: string constructor({ source, path, includedText }: { source: Source; path: string; includedText: string }) { super({ source, callee: 'Now.include', args: [path] }) this.includedText = includedText } getPath(): string { return this.getArgument(0).asString().getValue() } override getValue(): string { return this.includedText } override toString(): StringShape { return Shape.from(this, this.getValue()).asString().withContentType('cdata') } override equals(other: unknown): boolean { if (typeof other === 'string') { return this.includedText === other } else if (other instanceof Shape) { return other.equals(this.includedText) } else { return super.equals(other) } } static async fromRecord(record: Record, text: string | Shape, transform: Transform): Promise { const scriptType = taxonomySchemaDefault[record.getTable()] const scriptIdentifier = scriptType?.startsWith('client-development') ? '.client' : scriptType?.startsWith('server-development') ? '.server' : '' return new NowIncludeShape({ source: record, path: `./${await transform.getUpdateName(record)}${scriptIdentifier}.js`, includedText: text instanceof Shape ? text.toString().getValue() : text, }) } } export const NowIncludePlugin = Plugin.create({ name: 'NowIncludePlugin', noTelemetry: true, shapes: [ { shape: CallExpressionShape, fileTypes: ['fluent'], toSubclass(callExpression, { diagnostics, project }) { if (callExpression.getCallee() !== 'Now.include') { return { success: false } } const arg = callExpression.getArgument(0) if (!arg.isString()) { diagnostics.error(arg, 'Now.include() must have a string argument') return { success: false } } const path = arg.getValue() if (!/^\.\.?\/.*$/.test(path.trim()) && path.includes('/')) { diagnostics.error(arg, 'Now.include() argument must be a relative path') return { success: false } } const absolutePath = pathModule.resolve(pathModule.dirname(callExpression.getOriginalFilePath()), path) if (!project.isInRootDir(absolutePath)) { diagnostics.error(arg, `Included file path is not within project: ${absolutePath}`) return { success: false } } let includedFile: ProjectFile try { includedFile = project.addFile(absolutePath, { resolveDependencies: false, excludeFromCompiler: true, }) } catch (e) { diagnostics.error(arg, `Failed to include file. Reason: ${e instanceof Error ? e.message : e}`) return { success: false } } return { success: true, value: new NowIncludeShape({ source: callExpression, path, includedText: includedFile.getContent(), }), } }, }, { shape: NowIncludeShape, async commit(shape, target, { transform, commit, project, fs }) { const targetResult = await transform.toShape(target) if (!targetResult.success) { return { success: false } } const targetDirPath = target.getSourceFile().getDirectoryPath() const targetPath = project.resolvePath(targetDirPath, shape.getPath()) const { value: targetShape } = targetResult if (targetShape.equals(shape.getValue())) { return { success: true } } if (targetShape.is(NowIncludeShape)) { // Never update the path of an existing Now.include() - just update the file content project .getFile(project.resolvePath(targetDirPath, targetShape.getPath())) .setContent(shape.getValue()) } else if (targetShape.is(CallExpressionShape) && targetShape.getCallee() === 'Now.include') { // TODO: This is hacky AF. When Now.include() is generated for the first time, it's // just written to the file using getCode() which doesn't generate any file at the // included path. Therefore, the shape is not parsed into a NowIncludeShape because // we check if the file exists and return success: false if it doesn't. So it just // comes back as a generic CallExpressionShape. await commit(shape, target, CallExpressionPlugin) if (FileSystem.existsSync(fs, targetPath)) { project .addFile(targetPath, { resolveDependencies: false, excludeFromCompiler: true }) .setContent(shape.getValue()) } else { project.addFile( { path: targetPath, content: shape.getValue() }, { resolveDependencies: false, excludeFromCompiler: true } ) } } else { // If we reach this point it's likely an inline string literal or some other value that // just needs to be replaced. target.replaceWithText(shape.toString().getCode()) } return { success: true } }, }, { shape: StringShape, async commit(shape, target, { transform, project }) { const targetResult = await transform.toShape(target) if (!targetResult.success) { return { success: false } } const targetShape = targetResult.value.ifIdentifier()?.resolve() ?? targetResult.value if (!targetShape.is(NowIncludeShape)) { return { success: false } } if (!shape.equals(targetShape.getValue())) { const targetDirPath = target.getSourceFile().getDirectoryPath() const file = project.getFile(project.resolvePath(targetDirPath, targetShape.getPath())) file.setContent(shape.getValue()) } return { success: true } }, }, ], })