UNPKG

@needle-tools/engine

Version:

Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development with great integrations into editors like Unity or Blender - and can be deployed onto any device! It is flexible, extensible and networking and XR are built-in.

201 lines (173 loc) • 9.29 kB
// @ts-check import fs from 'fs'; import path from 'path'; /** @typedef {{ version?: number, tags?: {name: string, [key: string]: unknown}[], valueSets?: {name: string, [key: string]: unknown}[], globalAttributes?: unknown[] }} CustomElementData */ /** Known needle-engine tag names that should be updated from the source */ const NEEDLE_TAG_NAMES = ['needle-engine', 'needle-menu', 'needle-button']; /** * Merges needle-engine custom element data into an existing custom-elements.json file. * Preserves user-defined tags while updating needle-engine specific tags. * @param {CustomElementData} sourceData - The needle-engine custom-elements.json data * @param {CustomElementData} targetData - The existing project custom-elements.json data * @returns {CustomElementData} Merged data */ function mergeCustomElementData(sourceData, targetData) { const merged = { ...targetData }; // Ensure basic structure merged.version = sourceData.version || targetData.version || 1.1; merged.tags = merged.tags || []; merged.globalAttributes = merged.globalAttributes || targetData.globalAttributes || []; merged.valueSets = merged.valueSets || targetData.valueSets || []; // Get source tags (needle-engine tags) const sourceTags = sourceData.tags || []; // Update or add needle-engine tags for (const sourceTag of sourceTags) { const existingIndex = merged.tags.findIndex(t => t.name === sourceTag.name); if (existingIndex >= 0) { // Replace existing needle-engine tag with updated version if (NEEDLE_TAG_NAMES.includes(sourceTag.name)) { merged.tags[existingIndex] = sourceTag; } // Otherwise keep existing (user-defined) tag } else { // Add new tag merged.tags.push(sourceTag); } } // Merge valueSets (avoid duplicates by name) const sourceValueSets = sourceData.valueSets || []; for (const sourceSet of sourceValueSets) { const existingIndex = merged.valueSets.findIndex(s => s.name === sourceSet.name); if (existingIndex >= 0) { merged.valueSets[existingIndex] = sourceSet; } else { merged.valueSets.push(sourceSet); } } return merged; } /** * Ensure the repo workspace or .vscode settings include Needle Engine custom HTML data if they exist. * Copies custom-elements.json to the project and merges with existing user content. * - Copies/merges `custom-elements.json` to project root * - Adds `./custom-elements.json` to `.code-workspace settings.html.customData` * - Adds `./custom-elements.json` to `.vscode/settings.json html.customData` * @param {"build" | "serve"} _command * @param {import('../types').needleMeta | null} _config * @param {import('../types').userSettings} userSettings * @returns {import('vite').Plugin | null} */ export function needleCustomElementData(_command, _config, userSettings = {}) { // Allow disabling the workspace updater if (userSettings?.noCustomElementData === true) return null; return { name: 'needle:custom-element-data', configResolved() { try { const cwd = process.cwd(); // Path to source custom-elements.json in node_modules const sourceFile = path.join(cwd, 'node_modules', '@needle-tools', 'engine', 'custom-elements.json'); // Path to target custom-elements.json in project vs code directory const targetFile = path.join(cwd, '.vscode', 'custom-elements.json'); // Copy/merge custom-elements.json to project if (fs.existsSync(sourceFile)) { try { const sourceData = /** @type {CustomElementData} */ (JSON.parse(fs.readFileSync(sourceFile, 'utf8'))); let targetData = /** @type {CustomElementData} */ ({}); if (fs.existsSync(targetFile)) { // Merge with existing file to preserve user content try { targetData = /** @type {CustomElementData} */ (JSON.parse(fs.readFileSync(targetFile, 'utf8'))); } catch { targetData = {}; } } else { // Ensure .vscode directory exists const vscodeDir = path.dirname(targetFile); if (!fs.existsSync(vscodeDir)) { fs.mkdirSync(vscodeDir); } } const mergedData = mergeCustomElementData(sourceData, targetData); const newContent = JSON.stringify(mergedData, null, 2); // Only write if content changed const existingContent = fs.existsSync(targetFile) ? fs.readFileSync(targetFile, 'utf8') : ''; if (newContent !== existingContent) { fs.writeFileSync(targetFile, newContent, 'utf8'); } } catch (err) { // Fallback: just copy the file if merge fails try { fs.copyFileSync(sourceFile, targetFile); } catch { // ignore } } } // Local path for VSCode settings (now points to project root) const localCustomDataPath = './custom-elements.json'; // Old path in node_modules (to remove/replace) const oldNodeModulesPath = './node_modules/@needle-tools/engine/custom-elements.json'; const oldWorkspaceNodeModulesPath = './../node_modules/@needle-tools/engine/custom-elements.json'; // 1) workspace file(s) const files = fs.readdirSync(cwd); const workspaceFiles = files.filter(f => f.endsWith('.code-workspace')); for (const f of workspaceFiles) { const full = path.join(cwd, f); try { const raw = fs.readFileSync(full, 'utf8'); const data = /** @type {{settings?: {'html.customData'?: string[], [key: string]: unknown}}} */ (JSON.parse(raw)); // Ensure settings.html.customData contains the local path data.settings = data.settings || {}; data.settings['html.customData'] = data.settings['html.customData'] || []; // Remove old node_modules path if present const oldIndex = data.settings['html.customData'].indexOf(oldWorkspaceNodeModulesPath); if (oldIndex >= 0) { data.settings['html.customData'].splice(oldIndex, 1); } // Add local path if not present if (!data.settings['html.customData'].includes(localCustomDataPath)) { data.settings['html.customData'].push(localCustomDataPath); const newRaw = JSON.stringify(data, null, 2); fs.writeFileSync(full, newRaw, 'utf8'); } } catch (err) { // ignore } } // 2) .vscode/settings.json const vscodeDir = path.join(cwd, '.vscode'); const settingsFile = path.join(vscodeDir, 'settings.json'); if (fs.existsSync(settingsFile)) { try { const rawSettings = fs.readFileSync(settingsFile, 'utf8'); /** @type {Record<string, unknown>} */ const settings = JSON.parse(rawSettings) || {}; const htmlData = /** @type {string[]} */ (settings['html.customData'] || []); settings['html.customData'] = htmlData; // Remove old node_modules path if present const oldIndex = htmlData.indexOf(oldNodeModulesPath); if (oldIndex >= 0) { htmlData.splice(oldIndex, 1); } // Add local path if not present if (!htmlData.includes(localCustomDataPath)) { htmlData.push(localCustomDataPath); // Write back settings.json if changed const newRawSettings = JSON.stringify(settings, null, 2); fs.writeFileSync(settingsFile, newRawSettings, 'utf8'); } } catch (err) { // ignore } } } catch (err) { // ignore } } } } export default needleCustomElementData;