Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
b841128f4This file is pinned to the selected release. Download the original to open it in your design tool.
#!/usr/bin/env node
/* eslint max-len: 0 */
const fs = require('fs');
const path = require('path');
const Module = require('module');
const { parseLatestReadmeNotes } = require('./readme_sync');
// Copy necessary properties from 'package.json' to 'src/app/package.json'
const pkg = require('../package.json');
const pkgApp = require('../src/app/package.json');
const RUNTIME_ENTRY_PATHS = [
'src/main.js',
'src/server-cli.js',
'src/server',
'src/electron-app',
'src/app/src/entry-server.tsx',
'src/app/src/AppServer.tsx',
'src/app/src/sentry-config.ts',
];
const SCANNABLE_EXTENSIONS = new Set([
'.js',
'.jsx',
'.ts',
'.tsx',
'.mjs',
'.cjs',
]);
// Tests never ship, and their fixtures routinely contain strings that look
// like imports (a fake plugin bundle, say), which would otherwise be mistaken
// for runtime dependencies.
const isTestPath = (filePath) =>
/(^|[\/])__tests__[\/]/.test(filePath) ||
/\.(test|spec)\.[cm]?[jt]sx?$/.test(filePath);
const INTERNAL_PREFIXES = ['server/', 'app/', 'electron-app/'];
const MANUAL_RUNTIME_DEPS = [
'@electron/remote',
'@sentry/electron',
'@sentry/react',
'@serialport/parser-byte-length',
'@serialport/parser-readline',
'@sienci/avrgirl-arduino',
'electron-updater',
'jsonfile',
'react',
'react-dom',
'sirv',
];
const EXCLUDED_RUNTIME_DEPS = new Set([
'electron',
'electron-reloader',
'vite',
]);
const IMPORT_PATTERNS = [
/import\s+(?:[^'";]+?\s+from\s+)?['"]([^'"]+)['"]/g,
/export\s+(?:[^'";]+?\s+from\s+)?['"]([^'"]+)['"]/g,
/require\(\s*['"]([^'"]+)['"]\s*\)/g,
/import\(\s*['"]([^'"]+)['"]\s*\)/g,
];
function collectSourceFiles(entryPath, acc) {
const absolutePath = path.resolve(__dirname, '..', entryPath);
if (!fs.existsSync(absolutePath)) {
return;
}
const stat = fs.statSync(absolutePath);
if (stat.isFile()) {
if (
SCANNABLE_EXTENSIONS.has(path.extname(absolutePath)) &&
!isTestPath(absolutePath)
) {
acc.push(absolutePath);
}
return;
}
const queue = [absolutePath];
while (queue.length > 0) {
const currentDir = queue.pop();
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(currentDir, entry.name);
if (entry.isDirectory()) {
if (entry.name !== '__tests__') {
queue.push(fullPath);
}
} else if (
SCANNABLE_EXTENSIONS.has(path.extname(entry.name)) &&
!isTestPath(fullPath)
) {
acc.push(fullPath);
}
}
}
}
function normalizeModuleName(specifier) {
if (!specifier) {
return null;
}
if (
specifier.startsWith('.') ||
specifier.startsWith('/') ||
specifier.startsWith('node:')
) {
return null;
}
if (INTERNAL_PREFIXES.some((prefix) => specifier.startsWith(prefix))) {
return null;
}
if (specifier.startsWith('@')) {
const parts = specifier.split('/');
if (parts.length >= 2) {
return `${parts[0]}/${parts[1]}`;
}
}
return specifier.split('/')[0];
}
function extractModuleSpecifiers(source) {
const sourceWithoutComments = source
.replace(/\/\*[\s\S]*?\*\//g, '')
.replace(/(^|[^:])\/\/.*$/gm, '$1');
const matches = new Set();
for (const pattern of IMPORT_PATTERNS) {
pattern.lastIndex = 0;
let match;
while ((match = pattern.exec(sourceWithoutComments)) !== null) {
matches.add(match[1]);
}
}
return Array.from(matches);
}
function getRuntimeDependencyNames() {
const files = [];
for (const entryPath of RUNTIME_ENTRY_PATHS) {
collectSourceFiles(entryPath, files);
}
const builtinModules = new Set(
Module.builtinModules.map((moduleName) =>
moduleName.replace(/^node:/, ''),
),
);
const runtimeDeps = new Set(MANUAL_RUNTIME_DEPS);
const uniqueFiles = Array.from(new Set(files));
for (const file of uniqueFiles) {
const source = fs.readFileSync(file, 'utf8');
const specifiers = extractModuleSpecifiers(source);
for (const specifier of specifiers) {
const moduleName = normalizeModuleName(specifier);
if (!moduleName) {
continue;
}
if (builtinModules.has(moduleName)) {
continue;
}
if (EXCLUDED_RUNTIME_DEPS.has(moduleName)) {
continue;
}
runtimeDeps.add(moduleName);
}
}
return Array.from(runtimeDeps).sort();
}
function selectDependencies(dependencyNames, dependencySources) {
const selected = {};
const missing = [];
for (const name of dependencyNames) {
const version = dependencySources[name];
if (!version) {
missing.push(name);
continue;
}
selected[name] = version;
}
if (missing.length > 0) {
throw new Error(
`Missing runtime dependency versions in root package.json: ${missing.join(', ')}`,
);
}
return selected;
}
const runtimeDependencyNames = getRuntimeDependencyNames();
const dependencySources = pkg.dependencies || {};
//pkgApp.name = pkg.name; // Exclude the name field
pkgApp.version = pkg.version;
pkgApp.homepage = pkg.homepage;
pkgApp.author = pkg.author;
pkgApp.license = pkg.license;
pkgApp.repository = pkg.repository;
// Copy only Node.js dependencies to application package.json
pkgApp.dependencies = selectDependencies(
runtimeDependencyNames,
dependencySources,
);
const target = path.resolve(__dirname, '../src/app/package.json');
const secondTarget = path.resolve(__dirname, '../src/package.json');
const content = JSON.stringify(pkgApp, null, 2);
delete pkgApp.type;
const secondContent = JSON.stringify(pkgApp, null, 2);
fs.writeFileSync(target, content + '\n', 'utf8');
fs.writeFileSync(secondTarget, secondContent + '\n', 'utf8');
// Update readme notes
const readme = fs.readFileSync('README.md', 'utf8');
const notes = parseLatestReadmeNotes(readme);
fs.writeFileSync('./src/server/api/notes.json', JSON.stringify(notes, null, 2));