Node program/module reading its own package.json: is that possible?
Asked Answered
J

2

2

I have a Node CLI program with some dependencies and various of its own modules imported via relative file paths. I would like for the code to be able to read its own package.json file, but I can't figure out how to "find" it when the program is installed (that is, when its package is installed with npm install or npm install -g).

Reading and parsing package.json is easy — when running it from its own source directory. Of course that's true, because the file path "./package.json" works.

I don't mind reading the file and parsing it, but I don't know of a mechanism to resolve "."-relative paths that works the same way as

import foo from "./lib/foo.mjs";

works. Such imports work when the program (well the package) has been installed anywhere on the system if the proper node_modules/.bin directory is in my PATH, regardless of the working directory I'm using at the time.

Maybe this is impossible, but wanting to load package.json doesn't seem that weird.

Jorgenson answered 27/7, 2023 at 15:52 Comment(1)
I realize that when importing from JSON works, that should do it, but afaik it doesn't yet.Jorgenson
D
2

You can try to parse process.argv[1] which points to the path of the executed script (could be a directory name with index.js):

import fs from 'fs';
const path = process.argv[1].split('/');
let found;

while (path.length) {
    const dirname = path.join('/');
    if (fs.statSync(dirname || '/').isDirectory) {
        const filename = dirname + '/package.json';
        if (fs.existsSync(filename)) {
            found = filename;
            break;
        }

    }
    path.pop();
}

if (!found) {
    // we are silly not to found     
}

const json = JSON.parse(fs.readFileSync(found));

console.log(JSON.stringify(json));

Output:

{"name":"package-json","version":"1.0.0","description":"","main":"index.js","type":"module","scripts":{"test":"echo \"Error: no test specified\" && exit 1"},"author":"","license":"ISC"}
Dynel answered 27/7, 2023 at 16:19 Comment(1)
I'm going to accept your answer because it works, and also because I really wanted to believe that there's a better way to do it, and it was very motivating :)Jorgenson
J
3

This is something I found in the wild and it's still making my head spin, but it works. It's basically what Mr Nenashev's code does in his answer, but it leverages built-in tools (that I did not know existed).

Step one is to import the createRequire() function from the "module" module:

import { createRequire } from "module";

That function returns a function that acts like the CommonJS require(), starting from a given path or file URL as an argument. Now, that's super useful, but we need the starting directory, which is the root of my problem (and which Mr Nenashev's answer solves). That brings us to new-to-me tool number two, import.meta. That's an object with one property, "url", and the value of that is a "file://" URL for the module from which it's called.

Thus, armed with those two things, I can write:

const require = createRequire(import.meta.url);

const PACKAGE = require("./package.json");

This will work even if (for example) I install my package globally and cd to any random directory. So long as my Node program's launcher is in my PATH, from anywhere I run it that "file://" URL will be the absolute path to the real directory where the package files live.

So to sum up, a very quick way to get package.json is:

import { createRequire } from "module";
const PACKAGE = createRequire(import.meta.url)("./package.json");

And it's synchronous, yay.

Jorgenson answered 27/7, 2023 at 15:52 Comment(1)
What a great solution !Wilhite
D
2

You can try to parse process.argv[1] which points to the path of the executed script (could be a directory name with index.js):

import fs from 'fs';
const path = process.argv[1].split('/');
let found;

while (path.length) {
    const dirname = path.join('/');
    if (fs.statSync(dirname || '/').isDirectory) {
        const filename = dirname + '/package.json';
        if (fs.existsSync(filename)) {
            found = filename;
            break;
        }

    }
    path.pop();
}

if (!found) {
    // we are silly not to found     
}

const json = JSON.parse(fs.readFileSync(found));

console.log(JSON.stringify(json));

Output:

{"name":"package-json","version":"1.0.0","description":"","main":"index.js","type":"module","scripts":{"test":"echo \"Error: no test specified\" && exit 1"},"author":"","license":"ISC"}
Dynel answered 27/7, 2023 at 16:19 Comment(1)
I'm going to accept your answer because it works, and also because I really wanted to believe that there's a better way to do it, and it was very motivating :)Jorgenson

© 2022 - 2024 — McMap. All rights reserved.