@k-l-lambda/lilypond-node 2.24.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.d.ts ADDED
@@ -0,0 +1,72 @@
1
+ /**
2
+ * LilyPond Node.js Native Addon
3
+ *
4
+ * TypeScript definitions for the lilypond-node package.
5
+ */
6
+
7
+ /**
8
+ * Options for the engrave function
9
+ */
10
+ export interface EngraveOptions {
11
+ /**
12
+ * Callback invoked when an SVG file is generated
13
+ * @param filename - The name of the SVG file (e.g., "output.svg")
14
+ * @param content - The SVG content as a string
15
+ */
16
+ onSVG?: (filename: string, content: string) => void;
17
+
18
+ /**
19
+ * Callback invoked when a MIDI file is generated
20
+ * @param filename - The name of the MIDI file (e.g., "output.midi")
21
+ * @param data - The MIDI data as an ArrayBuffer
22
+ */
23
+ onMIDI?: (filename: string, data: ArrayBuffer) => void;
24
+
25
+ /**
26
+ * Callback invoked for log messages from LilyPond
27
+ * @param message - The log message
28
+ */
29
+ log?: (message: string) => void;
30
+
31
+ /**
32
+ * Additional include paths for \include commands
33
+ */
34
+ includeFolders?: string[];
35
+ }
36
+
37
+ /**
38
+ * Engrave LilyPond code to SVG and/or MIDI
39
+ *
40
+ * @param lyCode - LilyPond source code
41
+ * @param options - Engrave options
42
+ * @returns Promise resolving to exit code (0 = success, 1 = warning, other = error)
43
+ *
44
+ * @example
45
+ * ```typescript
46
+ * import { engrave } from 'lilypond-node';
47
+ *
48
+ * const lyCode = `\\version "2.24.4"
49
+ * { c' d' e' f' g' }
50
+ * `;
51
+ *
52
+ * const result = await engrave(lyCode, {
53
+ * onSVG: (filename, content) => {
54
+ * console.log('SVG:', filename, content.length, 'bytes');
55
+ * }
56
+ * });
57
+ *
58
+ * console.log('Exit code:', result);
59
+ * ```
60
+ */
61
+ export function engrave(lyCode: string, options?: EngraveOptions): Promise<number>;
62
+
63
+ /**
64
+ * Get the LilyPond data directory path
65
+ * @returns Path to LILYPOND_DATADIR
66
+ */
67
+ export function getDataDir(): string;
68
+
69
+ /**
70
+ * LilyPond version bundled with this package
71
+ */
72
+ export const version: string;
package/lib/index.js ADDED
@@ -0,0 +1,125 @@
1
+ /**
2
+ * LilyPond Node.js Native Addon
3
+ *
4
+ * A Node.js native addon for LilyPond music engraving.
5
+ * Converts LilyPond notation to SVG and MIDI output.
6
+ */
7
+
8
+ const path = require('path');
9
+ const fs = require('fs');
10
+
11
+ // Find the module root directory
12
+ const moduleRoot = path.join(__dirname, '..');
13
+
14
+ // Possible locations for the native addon
15
+ const addonPaths = [
16
+ // Development build location
17
+ path.join(moduleRoot, 'build', 'Release', 'lilypond.node'),
18
+ path.join(moduleRoot, 'build', 'Debug', 'lilypond.node'),
19
+ // cmake-js default output
20
+ path.join(moduleRoot, 'build', 'lilypond.node'),
21
+ // Legacy output location
22
+ path.join(moduleRoot, 'output', 'lilypond.node'),
23
+ ];
24
+
25
+ // Find and load the native addon
26
+ let addon = null;
27
+ let addonPath = null;
28
+
29
+ for (const p of addonPaths) {
30
+ if (fs.existsSync(p)) {
31
+ addonPath = p;
32
+ break;
33
+ }
34
+ }
35
+
36
+ if (!addonPath) {
37
+ throw new Error(
38
+ 'LilyPond native addon not found. ' +
39
+ 'Please run "npm run build" to compile the addon.'
40
+ );
41
+ }
42
+
43
+ // Set up LILYPOND_DATADIR if not already set
44
+ if (!process.env.LILYPOND_DATADIR) {
45
+ const sharePaths = [
46
+ // Output directory has complete build artifacts
47
+ path.join(moduleRoot, 'output', 'share', 'lilypond', 'current'),
48
+ // Package share directory (for npm install)
49
+ path.join(moduleRoot, 'share', 'lilypond', 'current'),
50
+ ];
51
+
52
+ for (const p of sharePaths) {
53
+ if (fs.existsSync(p)) {
54
+ process.env.LILYPOND_DATADIR = p;
55
+ break;
56
+ }
57
+ }
58
+
59
+ if (!process.env.LILYPOND_DATADIR) {
60
+ throw new Error(
61
+ 'LilyPond data directory not found. ' +
62
+ 'Please set LILYPOND_DATADIR or run "npm run postinstall".'
63
+ );
64
+ }
65
+ }
66
+
67
+ // Load the addon
68
+ addon = require(addonPath);
69
+
70
+ /**
71
+ * Engrave LilyPond code to SVG and/or MIDI
72
+ *
73
+ * @param {string} lyCode - LilyPond source code
74
+ * @param {Object} [options] - Engrave options
75
+ * @param {Function} [options.onSVG] - Callback for SVG output: (filename, content) => void
76
+ * @param {Function} [options.onMIDI] - Callback for MIDI output: (filename, data) => void
77
+ * @param {Function} [options.log] - Callback for log messages: (message) => void
78
+ * @param {string[]} [options.includeFolders] - Additional include paths for \include commands
79
+ * @returns {Promise<number>} Promise resolving to exit code (0 = success, 1 = warning, other = error)
80
+ *
81
+ * @example
82
+ * const lilypond = require('lilypond-node');
83
+ *
84
+ * const lyCode = `\\version "2.24.4"
85
+ * { c' d' e' f' g' }
86
+ * `;
87
+ *
88
+ * lilypond.engrave(lyCode, {
89
+ * onSVG: (filename, content) => {
90
+ * console.log('SVG:', filename, content.length, 'bytes');
91
+ * },
92
+ * onMIDI: (filename, data) => {
93
+ * console.log('MIDI:', filename, data.byteLength, 'bytes');
94
+ * },
95
+ * log: (msg) => console.log(msg)
96
+ * }).then((code) => {
97
+ * console.log('Finished with code:', code);
98
+ * });
99
+ */
100
+ function engrave(lyCode, options = {}) {
101
+ if (typeof lyCode !== 'string') {
102
+ return Promise.reject(new TypeError('lyCode must be a string'));
103
+ }
104
+
105
+ return addon.engrave(lyCode, options);
106
+ }
107
+
108
+ /**
109
+ * Get the LilyPond data directory path
110
+ * @returns {string} Path to LILYPOND_DATADIR
111
+ */
112
+ function getDataDir() {
113
+ return process.env.LILYPOND_DATADIR;
114
+ }
115
+
116
+ /**
117
+ * LilyPond version
118
+ */
119
+ const version = '2.24.4';
120
+
121
+ module.exports = {
122
+ engrave,
123
+ getDataDir,
124
+ version,
125
+ };
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@k-l-lambda/lilypond-node",
3
+ "version": "2.24.4",
4
+ "description": "LilyPond music engraving as a Node.js native addon",
5
+ "main": "lib/index.js",
6
+ "types": "lib/index.d.ts",
7
+ "scripts": {
8
+ "install": "node scripts/install.js",
9
+ "build": "cmake-js build",
10
+ "rebuild": "cmake-js rebuild",
11
+ "clean": "cmake-js clean",
12
+ "test": "node test/test.js",
13
+ "postinstall": "node scripts/postinstall.js"
14
+ },
15
+ "keywords": [
16
+ "lilypond",
17
+ "music",
18
+ "engraving",
19
+ "notation",
20
+ "svg",
21
+ "midi",
22
+ "sheet-music"
23
+ ],
24
+ "author": "K.L. Lambda",
25
+ "license": "GPL-3.0",
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "git+https://gitlab.com/k.l.lambda/lilypond.git"
29
+ },
30
+ "bugs": {
31
+ "url": "https://gitlab.com/k.l.lambda/lilypond/-/issues"
32
+ },
33
+ "homepage": "https://gitlab.com/k.l.lambda/lilypond#readme",
34
+ "engines": {
35
+ "node": ">=22.0.0"
36
+ },
37
+ "os": [
38
+ "linux"
39
+ ],
40
+ "cpu": [
41
+ "x64"
42
+ ],
43
+ "dependencies": {},
44
+ "devDependencies": {
45
+ "cmake-js": "^7.3.0"
46
+ },
47
+ "binary": {
48
+ "host": "https://gitlab.com/k.l.lambda/lilypond/-/jobs/artifacts",
49
+ "remote_path": "/{version}/raw",
50
+ "package_name": "lilypond-node-{platform}-{arch}-node{node_abi}.tar.gz"
51
+ },
52
+ "files": [
53
+ "lib/",
54
+ "scripts/"
55
+ ]
56
+ }
@@ -0,0 +1,121 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Install script for lilypond-node
5
+ *
6
+ * Downloads prebuilt binary from GitLab artifacts or falls back to source build.
7
+ */
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const https = require('https');
12
+ const { execSync } = require('child_process');
13
+
14
+ const pkg = require('../package.json');
15
+ const moduleRoot = path.join(__dirname, '..');
16
+
17
+ // Check if prebuilt binary already exists
18
+ const outputDir = path.join(moduleRoot, 'output');
19
+ const addonPath = path.join(outputDir, 'lilypond.node');
20
+
21
+ if (fs.existsSync(addonPath)) {
22
+ console.log('lilypond-node: Prebuilt binary already exists.');
23
+ process.exit(0);
24
+ }
25
+
26
+ // Platform detection
27
+ const platform = process.platform;
28
+ const arch = process.arch;
29
+ const nodeVersion = process.versions.node.split('.')[0];
30
+
31
+ console.log(`lilypond-node: Installing for ${platform}-${arch} (Node ${nodeVersion})`);
32
+
33
+ // Check supported platform
34
+ if (platform !== 'linux' || arch !== 'x64') {
35
+ console.error(`lilypond-node: Unsupported platform: ${platform}-${arch}`);
36
+ console.error('Currently only linux-x64 is supported.');
37
+ console.error('You can try building from source with: npm run build');
38
+ process.exit(1);
39
+ }
40
+
41
+ if (parseInt(nodeVersion) < 22) {
42
+ console.error(`lilypond-node: Node.js ${nodeVersion} is not supported.`);
43
+ console.error('Please use Node.js 22 or later.');
44
+ process.exit(1);
45
+ }
46
+
47
+ // GitLab artifacts URL
48
+ // Format: https://gitlab.com/k.l.lambda/lilypond/-/jobs/artifacts/develop/raw/lilypond-node-linux-x64.tar.gz?job=node-addon-linux-x64
49
+ const gitlabProject = 'k.l.lambda/lilypond';
50
+ const branch = 'develop';
51
+ const artifactName = `lilypond-node-linux-x64.tar.gz`;
52
+ const jobName = 'node-addon-linux-x64';
53
+
54
+ const downloadUrl = `https://gitlab.com/${gitlabProject}/-/jobs/artifacts/${branch}/raw/${artifactName}?job=${jobName}`;
55
+
56
+ console.log(`lilypond-node: Downloading prebuilt binary...`);
57
+ console.log(` URL: ${downloadUrl}`);
58
+
59
+ // Download function with redirect support
60
+ function download(url, dest) {
61
+ return new Promise((resolve, reject) => {
62
+ const file = fs.createWriteStream(dest);
63
+
64
+ function handleResponse(response) {
65
+ if (response.statusCode === 302 || response.statusCode === 301) {
66
+ // Follow redirect
67
+ https.get(response.headers.location, handleResponse).on('error', reject);
68
+ return;
69
+ }
70
+
71
+ if (response.statusCode !== 200) {
72
+ reject(new Error(`Download failed: HTTP ${response.statusCode}`));
73
+ return;
74
+ }
75
+
76
+ response.pipe(file);
77
+ file.on('finish', () => {
78
+ file.close();
79
+ resolve();
80
+ });
81
+ }
82
+
83
+ https.get(url, handleResponse).on('error', reject);
84
+ });
85
+ }
86
+
87
+ async function install() {
88
+ const tarPath = path.join(moduleRoot, artifactName);
89
+
90
+ try {
91
+ // Download
92
+ await download(downloadUrl, tarPath);
93
+ console.log('lilypond-node: Download complete.');
94
+
95
+ // Extract
96
+ console.log('lilypond-node: Extracting...');
97
+ fs.mkdirSync(outputDir, { recursive: true });
98
+
99
+ execSync(`tar xzf "${tarPath}" -C "${moduleRoot}"`, { stdio: 'inherit' });
100
+
101
+ // Clean up
102
+ fs.unlinkSync(tarPath);
103
+
104
+ // Verify
105
+ if (fs.existsSync(addonPath)) {
106
+ console.log('lilypond-node: Installation complete.');
107
+ } else {
108
+ throw new Error('Addon not found after extraction');
109
+ }
110
+ } catch (err) {
111
+ console.error(`lilypond-node: Failed to download prebuilt binary.`);
112
+ console.error(` Error: ${err.message}`);
113
+ console.error('');
114
+ console.error('You can try building from source:');
115
+ console.error(' 1. Clone the full lilypond repo');
116
+ console.error(' 2. cd node-addon && npm run build');
117
+ process.exit(1);
118
+ }
119
+ }
120
+
121
+ install();
@@ -0,0 +1,120 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Postinstall script for lilypond-node
5
+ *
6
+ * This script sets up the LilyPond share directory with required files:
7
+ * - scm/ - Scheme modules (including build-generated files)
8
+ * - ly/ - LilyPond include files
9
+ * - ps/ - PostScript support files
10
+ * - fonts/ - Music fonts (optional, can use system fonts)
11
+ *
12
+ * Note: For development, the output/ directory contains the complete build.
13
+ * For npm install, files should be copied from the build output.
14
+ */
15
+
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+
19
+ const moduleRoot = path.join(__dirname, '..');
20
+ const shareDir = path.join(moduleRoot, 'share', 'lilypond', 'current');
21
+
22
+ // Check if output directory already exists (development build)
23
+ const outputShare = path.join(moduleRoot, 'output', 'share', 'lilypond', 'current');
24
+ if (fs.existsSync(outputShare)) {
25
+ console.log('LilyPond postinstall: Using existing output directory.');
26
+ console.log(` LILYPOND_DATADIR: ${outputShare}`);
27
+ process.exit(0);
28
+ }
29
+
30
+ // Source directories (relative to module root)
31
+ const sourceDirs = ['scm', 'ly', 'ps'];
32
+
33
+ console.log('LilyPond postinstall: Setting up share directory...');
34
+
35
+ // Create share directory structure
36
+ const dirs = ['share', 'share/lilypond', 'share/lilypond/current'];
37
+ for (const dir of dirs) {
38
+ const fullPath = path.join(moduleRoot, dir);
39
+ if (!fs.existsSync(fullPath)) {
40
+ fs.mkdirSync(fullPath, { recursive: true });
41
+ console.log(' Created:', dir);
42
+ }
43
+ }
44
+
45
+ // Copy source directories to share
46
+ for (const dir of sourceDirs) {
47
+ const srcPath = path.join(moduleRoot, dir);
48
+ const destPath = path.join(shareDir, dir);
49
+
50
+ if (!fs.existsSync(srcPath)) {
51
+ console.log(` Warning: Source directory not found: ${dir}`);
52
+ continue;
53
+ }
54
+
55
+ if (fs.existsSync(destPath)) {
56
+ console.log(` Skip: ${dir} (already exists)`);
57
+ continue;
58
+ }
59
+
60
+ // Copy directory recursively
61
+ copyDirSync(srcPath, destPath);
62
+ console.log(` Copied: ${dir}`);
63
+ }
64
+
65
+ // Create scm/lily/ subdirectory for Guile module loading
66
+ // Guile expects modules in (lily xxx) to be at scm/lily/xxx.scm
67
+ const scmSrcDir = path.join(shareDir, 'scm');
68
+ const lilyModuleDir = path.join(scmSrcDir, 'lily');
69
+ if (fs.existsSync(scmSrcDir) && !fs.existsSync(lilyModuleDir)) {
70
+ fs.mkdirSync(lilyModuleDir, { recursive: true });
71
+ // Copy all .scm files from scm/ to scm/lily/
72
+ const scmFiles = fs.readdirSync(scmSrcDir).filter(f => f.endsWith('.scm'));
73
+ for (const file of scmFiles) {
74
+ fs.copyFileSync(path.join(scmSrcDir, file), path.join(lilyModuleDir, file));
75
+ }
76
+ console.log(` Created: scm/lily/ module directory (${scmFiles.length} files)`);
77
+ }
78
+
79
+ // Check for fonts
80
+ const fontsDir = path.join(shareDir, 'fonts');
81
+ if (!fs.existsSync(fontsDir)) {
82
+ console.log('');
83
+ console.log(' Note: Fonts directory not found.');
84
+ console.log(' You may need to copy fonts from a LilyPond installation:');
85
+ console.log(' cp -r /usr/share/lilypond/*/fonts ' + shareDir + '/');
86
+ console.log(' Or set LILYPOND_DATADIR to a system LilyPond installation.');
87
+ }
88
+
89
+ // Check for build-generated files
90
+ const fontEncodings = path.join(lilyModuleDir, 'font-encodings.scm');
91
+ if (!fs.existsSync(fontEncodings)) {
92
+ console.log('');
93
+ console.log(' Warning: font-encodings.scm not found.');
94
+ console.log(' This file is generated during the LilyPond build process.');
95
+ console.log(' For development, run: make -C build');
96
+ console.log(' Then copy: cp -r build/out/share/lilypond/current/* share/lilypond/current/');
97
+ }
98
+
99
+ console.log('');
100
+ console.log('LilyPond postinstall complete.');
101
+
102
+ /**
103
+ * Recursively copy a directory
104
+ */
105
+ function copyDirSync(src, dest) {
106
+ fs.mkdirSync(dest, { recursive: true });
107
+
108
+ const entries = fs.readdirSync(src, { withFileTypes: true });
109
+
110
+ for (const entry of entries) {
111
+ const srcPath = path.join(src, entry.name);
112
+ const destPath = path.join(dest, entry.name);
113
+
114
+ if (entry.isDirectory()) {
115
+ copyDirSync(srcPath, destPath);
116
+ } else {
117
+ fs.copyFileSync(srcPath, destPath);
118
+ }
119
+ }
120
+ }