vite-template-plugin.mjs 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. import fs from 'node:fs';
  2. import path from 'node:path';
  3. const ASSET_MARKER = '<!-- VITE_ASSETS -->';
  4. const DEFAULT_ENTRY = 'ui/frontend/js/frontend.js';
  5. const DEFAULT_TEMPLATES = [
  6. {
  7. source: 'ui/frontend/masterpage_template.tpl',
  8. output: 'ui/frontend/masterpage.tpl',
  9. },
  10. {
  11. source: 'ui/frontend/errors_template.tpl',
  12. output: 'ui/frontend/errors.tpl',
  13. },
  14. ];
  15. /**
  16. * Normalizes a path for comparisons with Vite manifest keys.
  17. *
  18. * @param {string} filePath Path to normalize.
  19. * @returns {string} A slash-separated path.
  20. * @sideEffects None.
  21. */
  22. function normalizeManifestPath(filePath) {
  23. return filePath.replaceAll(path.sep, '/');
  24. }
  25. /**
  26. * Resolves a path relative to the Vite project root.
  27. *
  28. * @param {string} projectRoot Vite project root.
  29. * @param {string} filePath Absolute or project-relative path.
  30. * @returns {string} An absolute path.
  31. * @sideEffects None.
  32. */
  33. function resolveProjectPath(projectRoot, filePath) {
  34. return path.resolve(projectRoot, filePath);
  35. }
  36. /**
  37. * Finds the manifest entry for the configured JavaScript entry.
  38. *
  39. * @param {Record<string, object>} manifest Parsed Vite manifest.
  40. * @param {string} projectRoot Vite project root.
  41. * @param {string} entryPath Configured JavaScript entry path.
  42. * @returns {object} The matching Vite manifest entry.
  43. * @sideEffects None.
  44. */
  45. function findEntry(manifest, projectRoot, entryPath) {
  46. const relativeEntry = normalizeManifestPath(path.relative(projectRoot, entryPath));
  47. const match = Object.entries(manifest).find(([source, asset]) =>
  48. asset.isEntry && (normalizeManifestPath(source) === relativeEntry || asset.src === relativeEntry),
  49. );
  50. if (!match) {
  51. throw new Error(`Vite manifest entry not found for ${relativeEntry}.`);
  52. }
  53. return match[1];
  54. }
  55. /**
  56. * Creates an asset URL from Vite's configured public base.
  57. *
  58. * @param {string} baseUrl Vite public base URL.
  59. * @param {string} assetPath Manifest asset path.
  60. * @returns {string} The public asset URL.
  61. * @sideEffects None.
  62. */
  63. function createAssetUrl(baseUrl, assetPath) {
  64. return `${baseUrl.replace(/\/?$/, '/')}${assetPath.replace(/^\/+/, '')}`;
  65. }
  66. /**
  67. * Builds script and stylesheet tags from a Vite manifest entry.
  68. *
  69. * @param {object} entry Vite manifest entry.
  70. * @param {string} baseUrl Vite public base URL.
  71. * @returns {string} HTML asset tags.
  72. * @sideEffects None.
  73. */
  74. function createAssetTags(entry, baseUrl) {
  75. const cssTags = (entry.css ?? []).map(
  76. assetPath => `\t<link rel="stylesheet" href="${createAssetUrl(baseUrl, assetPath)}">`,
  77. );
  78. const scriptTag = `\t<script type="module" src="${createAssetUrl(baseUrl, entry.file)}"></script>`;
  79. return [...cssTags, scriptTag].join('\n');
  80. }
  81. /**
  82. * Replaces the asset marker in a source template and writes its generated output.
  83. *
  84. * @param {string} projectRoot Vite project root.
  85. * @param {{source: string, output: string}} template Template source and output paths.
  86. * @param {string} assetTags Generated Vite asset tags.
  87. * @returns {void}
  88. * @sideEffects Reads and writes template files.
  89. */
  90. function generateTemplate(projectRoot, template, assetTags) {
  91. const sourcePath = resolveProjectPath(projectRoot, template.source);
  92. const outputPath = resolveProjectPath(projectRoot, template.output);
  93. const source = fs.readFileSync(sourcePath, 'utf8');
  94. const markerCount = source.split(ASSET_MARKER).length - 1;
  95. if (markerCount !== 1) {
  96. throw new Error(`${sourcePath} must contain exactly one ${ASSET_MARKER} marker.`);
  97. }
  98. fs.mkdirSync(path.dirname(outputPath), { recursive: true });
  99. fs.writeFileSync(outputPath, source.replace(ASSET_MARKER, assetTags), 'utf8');
  100. }
  101. /**
  102. * Creates a Vite plugin that generates F3 templates from the build manifest.
  103. *
  104. * @param {{entry?: string, templates?: Array<{source: string, output: string}>}} options Plugin configuration.
  105. * @returns {import('vite').Plugin} Vite plugin instance.
  106. * @sideEffects Writes generated templates after a successful build.
  107. */
  108. export default function viteTemplatePlugin({
  109. entry = DEFAULT_ENTRY,
  110. templates = DEFAULT_TEMPLATES,
  111. } = {}) {
  112. let resolvedConfig;
  113. return {
  114. name: 'vite-template-plugin',
  115. apply: 'build',
  116. /**
  117. * Stores the resolved Vite configuration for post-build path resolution.
  118. *
  119. * @param {import('vite').ResolvedConfig} config Resolved Vite configuration.
  120. * @returns {void}
  121. * @sideEffects Updates plugin state.
  122. */
  123. configResolved(config) {
  124. resolvedConfig = config;
  125. },
  126. /**
  127. * Generates F3 templates after Vite has written the manifest.
  128. *
  129. * @returns {void}
  130. * @sideEffects Reads the manifest and writes generated template files.
  131. */
  132. closeBundle() {
  133. const manifestName = resolvedConfig.build.manifest === true
  134. ? '.vite/manifest.json'
  135. : resolvedConfig.build.manifest;
  136. const manifestPath = resolveProjectPath(resolvedConfig.build.outDir, manifestName);
  137. const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
  138. const entryPath = resolveProjectPath(resolvedConfig.root, entry);
  139. const manifestEntry = findEntry(manifest, resolvedConfig.root, entryPath);
  140. const assetTags = createAssetTags(manifestEntry, resolvedConfig.base);
  141. templates.forEach(template => generateTemplate(resolvedConfig.root, template, assetTags));
  142. },
  143. };
  144. }