@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
JavaScript
// @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;