transform-document-node.ts 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  1. import { print } from 'graphql';
  2. /**
  3. * Transforms Storybook source code to replace inline DocumentNode objects
  4. * with their GraphQL SDL string representation.
  5. *
  6. * @param source - The source code string from Storybook containing inline DocumentNode objects
  7. * @param storyContext - The Storybook context containing args with DocumentNode instances
  8. * @returns Transformed source code with DocumentNodes displayed as SDL strings
  9. *
  10. * @example
  11. * // Input source:
  12. * // queryDocument={{ kind: 'Document', definitions: [...] }}
  13. * //
  14. * // Output:
  15. * // queryDocument={graphql`
  16. * // query Product($id: ID!) {
  17. * // product(id: $id) { ... }
  18. * // }
  19. * // `}
  20. */
  21. export function transformDocumentNodeInSource(source: string, storyContext: any): string {
  22. let transformedSource = source;
  23. try {
  24. // Get the actual DocumentNode instances from story args
  25. const args = storyContext?.args;
  26. if (!args) return source;
  27. // Find all prop assignments that could be DocumentNodes
  28. // Pattern: propName={{ ... }}
  29. const propPattern = /(\w+)=\{\{/g;
  30. let match;
  31. const replacements: Array<{ start: number; end: number; propName: string }> = [];
  32. while ((match = propPattern.exec(source)) !== null) {
  33. const propName = match[1];
  34. const startPos = match.index;
  35. const openBracePos = match.index + match[0].length - 2; // Position of first {{
  36. // Find the matching closing }}
  37. let braceCount = 2; // We start with {{
  38. let pos = openBracePos + 2;
  39. let foundEnd = false;
  40. while (pos < source.length && braceCount > 0) {
  41. if (source[pos] === '{') {
  42. braceCount++;
  43. } else if (source[pos] === '}') {
  44. braceCount--;
  45. if (braceCount === 0) {
  46. foundEnd = true;
  47. break;
  48. }
  49. }
  50. pos++;
  51. }
  52. if (foundEnd) {
  53. replacements.push({
  54. start: startPos,
  55. end: pos + 1, // Include the final }
  56. propName,
  57. });
  58. }
  59. }
  60. // Process replacements in reverse order to maintain indices
  61. for (let i = replacements.length - 1; i >= 0; i--) {
  62. const { start, end, propName } = replacements[i];
  63. try {
  64. // Get the corresponding DocumentNode from args
  65. const docNode = args[propName];
  66. // Verify it's a valid GraphQL DocumentNode
  67. if (!isDocumentNode(docNode)) {
  68. continue;
  69. }
  70. // Convert DocumentNode AST to SDL string
  71. const sdl = print(docNode);
  72. // Format with proper indentation for readability
  73. const formattedSdl = formatSdlForDisplay(sdl);
  74. const replacement = `${propName}={graphql\`\n${formattedSdl}\n \`}`;
  75. // Replace the inline object with the formatted SDL
  76. transformedSource =
  77. transformedSource.substring(0, start) + replacement + transformedSource.substring(end);
  78. } catch (e) {
  79. console.error(`Failed to transform DocumentNode for prop "${propName}":`, e);
  80. }
  81. }
  82. // Also handle variable references like: queryDocument={productQuery}
  83. const varRefPattern = /(\w+)=\{(\w+(?:Query|Document|Mutation|Fragment))\}/g;
  84. transformedSource = transformedSource.replace(
  85. varRefPattern,
  86. (match: string, propName: string, varName: string) => {
  87. try {
  88. // Get the corresponding DocumentNode from args using the prop name
  89. const docNode = args[propName];
  90. // Verify it's a valid GraphQL DocumentNode
  91. if (!isDocumentNode(docNode)) {
  92. return match;
  93. }
  94. // Convert DocumentNode AST to SDL string
  95. const sdl = print(docNode);
  96. // Format with proper indentation for readability
  97. const formattedSdl = formatSdlForDisplay(sdl);
  98. return `${propName}={graphql\`\n${formattedSdl}\n \`}`;
  99. } catch (e) {
  100. console.error(`Failed to transform DocumentNode variable for prop "${propName}":`, e);
  101. return match;
  102. }
  103. },
  104. );
  105. } catch (e) {
  106. console.error('Failed to transform DocumentNode source:', e);
  107. return source;
  108. }
  109. return transformedSource;
  110. }
  111. /**
  112. * Checks if an object is a valid GraphQL DocumentNode
  113. */
  114. function isDocumentNode(obj: any): boolean {
  115. return (
  116. obj &&
  117. typeof obj === 'object' &&
  118. obj.kind === 'Document' &&
  119. Array.isArray(obj.definitions) &&
  120. obj.definitions.length > 0
  121. );
  122. }
  123. /**
  124. * Formats SDL string with proper indentation for display in Storybook docs
  125. */
  126. function formatSdlForDisplay(sdl: string): string {
  127. const lines = sdl.split('\n');
  128. return lines
  129. .map(line => {
  130. // All lines get consistent 4-space indentation
  131. return ' ' + line;
  132. })
  133. .join('\n');
  134. }