Update (write to) an object in a separate JS file using Node - javascript

I'm fairly new to Node, and am wracking my brains on how to achieve the following:
I have a config file that looks something like this:
// various es imports
export default {
input: {
index: 'src/index.ts',
Button: 'src/Button/index.ts',
Spinner: 'src/Spinner/index.ts',
'icons/Notification': 'src/_shared/components/icons/Notification.tsx',
'icons/Heart': 'src/_shared/components/icons/Heart.tsx',
},
//.. other properties
}
From my node script, i need to somehow read this file and do the following:
Delete any entries in the input object that have a key starting
with icons/
Append new entries to the input object.
Write these changes back to the original config file.
Is there a recommended way to do this in Node, i've been looking at a couple of libs, like replace-in-file but none seem to be suited to this particular case.

Just faced the same concern, here is how I solved it :
1. Gets your file content
If it is not a .js file, then use fs.readFileSync (or fs.readFile) like so :
const fs = require('fs');
const path = require('path');
const myObjectAsString = fs.readFileSync(
path.join( process.cwd(), 'my-file.txt' ), // use path.join for cross-platform
'utf-8' // Otherwise you'll get buffer instead of a plain string
);
// process myObjectAsString to get it as something you can manipulate
Here I am using process.cwd(), in case of a CLI app, it will gives you the current working directory path.
If it is a .js file (eg. a JavaScript config file like webpack.config.js for instance), then simply use require native function, as you would do with regular internal file or NPM module, and you will get all module.export content :
const path = require('path');
const myObject = require( path.join( process.cwd(), 'myFile.js') );
2. Modify your object as you want
// ...
myObject.options.foo = 'An awesome option value';
3. Then rewrite it back to the file
You can simply use fs.writeFileSync to achieve that :
// ...
fs.writeFileSync( path.join(process.cwd(), 'my-file.txt', myObject );
If you want to write a plain JavaScript Object, then you can use util.inspect() native method and you may also use fs.appendFileSync :
// ...
// If we wants to adds the 'module.exports = ' part before
fs.writeFileSync( process.cwd() + '/file.js', 'module.exports = ');
// Writes the plain object to the file
fs.appendFileSync( process.cwd() + '/file.js', util.inspect(options));

Related

How do I access a JSON file relative to my calling module in Node?

I'm defining a package, PackageA, that has a function (parseJson) that takes in a file path to a json file to parse. In another package, PackageB, I want to be able to call PackageA using a file I specify with a local path from PackageB. For example, if file.json is in the same directory as packageB, I'd like to be able to call PackageA.parseJson('./file.json'), without any extra code in PackageB. How would I do this? It seems that require requires a path from PackageA to the file, which is not what I want.
Edit: Currently, parseJson looks something like this:
public parseJson(filepath) {
let j = require(filepath);
console.log(j);
}
and PackageB is calling it like this:
let a = new PackageA();
a.parseJson("./file.json");
file.json is in the same directory as PackageB.
CommonJS modules have __dirname variable in their scope, containing a path to directory they reside in.
To get absolute path to RELATIVE_PATH use join(__dirname, RELATIVE_PATH) (join from path module).
example:
// PackageB .js file
const Path = require('path')
const PackageA = require(/* PackageA name or path */)
const PackageB_jsonPathRelative = /* relative path to json file */
// __dirname is directory that contains PackageB .js file
const PackageB_jsonPathAbsolute = Path.join(__dirname, PackageB_jsonPathRelative)
PackageA.parseJson(PackageB_jsonPathAbsolute)
UPDATED
If you can't change PackageB, but you know exactly how PackageA.parseJson is called by PackageB (e.g. directly, or through wrappers, but with known depth), then you can get path to PackageB from stack-trace.
example:
// PackageA .js file
// `npm install stack-trace#0.0.10` if you have `ERR_REQUIRE_ESM` error
const StackTrace = require('stack-trace')
const Path = require('path')
const callerFilename = (skip=0) => StackTrace.get(callerFilename)[skip + 1].getFileName()
module.exports.parseJson = (caller_jsonPathRelative) => {
// we want direct caller of `parseJson` so `skip=0`
// adjust `skip` parameter if caller chain changes
const callerDir = Path.dirname(callerFilename())
// absolute path to json file, from relative to caller file
const jsonPath = Path.join(callerDir, caller_jsonPathRelative)
console.log(jsonPath)
console.log(JSON.parse(require('fs').readFileSync(jsonPath)))
}

Load text as JavaScript object in Node.js

I have a javascript file for node.js:
module.exports = {
someString: 'blblalb'
}
I want to able to read the file as a javascript object, using fs.readFileSync. I can't use require because I am using a variable that may be modified in runtime to load the file.
Is that possible?
You can use eval('JavaScript string') but is highly recommended not to. It is a serious security risk if you cannot 100% trust the source of the text. If a malicious user figures out a way to modify the text they have complete control of your system. It is not a path I would take or recommend.
const text = 'console.log("Hello")';
eval(text);
If I saw that code when I was doing a code review we would definitely be having some words.
it's possible to evaluate a file or string variable as child module in hacky yet valid way.
The problem is that Node.js module environment should be unaware of these operations. Child module may load other modules and contain require(...), it will fail if there is no require function (there is none if it is evaluated with vm) or it uses wrong module relative path (this will happen with eval), also there will be no dedicated module.exports for a module. This can be fixed by wrapping module source with Node.js module wrapper that was rewired to match child module location.
const fs = require('fs');
const Module = require('module');
const path = require('path');
const childModuleAbsPath = path.resolve('./foo/bar.js');
const childModuleBody = fs.readFileSync(childModuleAbsPath);
const childModuleObj = { exports: {} };
const { dir: childModuleDirname, base: childModuleFilename } = path.parse(childModuleAbsPath);
const childRequire = modulePath => module.require(childModuleAbsPath);
require('vm').runInThisContext(Module.wrap(childModuleBody))(
childModuleObj.exports,
childRequire,
childModuleObj,
childModuleDirname,
childModuleFilename
);
In this case childModuleObj.exports.someString === 'blblalb' after bar child module was evaluated.
This is XY problem that should be addressed in another way.
If the intention is to reevaluate a module with new variables, this can be done by invalidating Node module cache by modifying require.cache, e.g. with decache:
decache('./foo/bar');
const reloadedBar = require('./foo/bar');

How to get current filename without path and extension with webpack?

I can get the relative file path with __filename, and sure I could hack it apart to get just the basename with some JS-fu, but I want to do this at compile-time.
DefinePlugin will let me inject some globals like I want, but AFAICT I can't have a "dynamic" global based on the current file.
So how can I do this?
e.g. given I am in the file assets/scripts/lib/components/bpm/RecordAttendancePopup.jsx, how can I get webpack to inject a constant like __basename that evaluates to "RecordAttendancePopup"?
Generic version
If you're using modules or any supported bundler, you can access the pre-existing import.meta.url and manually extract the filename:
const filename = import.meta.url // Get current path
.split(/[\\/]/).pop() // Get current filename
.replace(/\.[^.]+$/, ''); // Drop extension
or you can use a better parser (which may or may not work depending on your config)
const path = require('path');
const filename = path.parse(import.meta.url).name;
Webpack-specific version
This is what I use in my webpack.config.js:
const path = require('path');
const webpack = require('webpack');
module.exports = {
plugins: [
new webpack.DefinePlugin({
__filebasename: webpack.DefinePlugin.runtimeValue(
info => JSON.stringify(path.parse(info.module.resource).name)
)
})
]
};
and then I'll have the __filebasename variable available everywhere in my source. Works in Webpack 4 and 5.
I guess there is no other way than creating your own "DefinePlugin" based on https://github.com/webpack/webpack/blob/master/lib/DefinePlugin.js to get what you want.

my config.js file is not being recognized

In my index.js file, I have const config = require('config'); written as one of the first lines.
And I have a file in my project folder called config.js
But I keep having my console tell my that it Cannot find module 'config'
My config file is this basically:
module.exports = {
'secretKey': 'mySecretCode12232',
'mongoUrl' : 'mongodb://localhost:27017/test'
};
This doesn't make any sense it should be working.
const config = require( path.join(__dirname, 'config'+'.js' ) );
I also have own function which loads atomaticaly from specified subdirectory at it's definition, it saves a lot of time.
When you don't provide any path selector in the require statement (eg. require('./config')), your code will search for the package named config and fail as it cannot find this specific one, as require will assume that it was the package name that was provided (and will start searching e.g. in your node_modules etc. - search path for it is not a trivial topic :) ).
If you want to require the module from another file, you have to provide a correct path to it, so assuming your config.js resides in the same catalog as your other file, the correct statement would be:
const config = require('./config'); // Extension can be omitted

How to get original file path in the script with webpack?

An example code:
//in the file app.module.js
module.exports = framework.module("app", [
require('./api/api.module').name
])
//in the file app/api/api.module.js
module.exports = framework.module("app.api", [
])
Here are two dependent modules named 'app' and 'api'.
Module name is always same as file path to the module file (except module.js part, e.g. for file at app/api/api.module.js module name is 'app.api').
Is it possible to make webpack provide a filename of the included file during compilation, so following can be done?
//in the file app.module.js
module.exports = framework.module(__filename, [
require('./api/api.module').name
])
//in the file app/api/api.module.js
module.exports = framework.module(__filename, [
])
Where __filename is an actual path to the file folder.
It does not really matter what's format of name of the module, but it should be unique (for framework reasons) and lead to the module location (for debug reasons).
Update:
I've solved it for myself - this can be done by custom webpack loader which substitutes a certain placeholder with file path string. But anyway question is still open.
I know you said you resolved this yourself, yet, here's my take on it.
Your solution includes using a custom loader, however, maybe you could have solved it in a different way.
First step, in your webpack.config add these in the config object:
context: __dirname, //set the context of your app to be the project directory
node: {
__dirname: true //Allow use of __dirname in modules, based on context
},
Then, add this in your list of plugins:
new webpack.DefinePlugin({
SEPARATOR: JSON.stringify(path.sep)
})
This will replace all SEPARATOR instances in your modules with whatever the correct path separator is, for the system you are working on (you will have to require('path') in your webpack.config for this to work)
And finally in whatever module you want you can now get its name by doing
var moduleName = __dirname.replace(new RegExp(SEPARATOR, 'g'), '.');
So, for example
//in the file app.module.js
var moduleName = __dirname.replace(new RegExp(SEPARATOR, 'g'), '.');
module.exports = framework.module(moduleName, [
require('./api/api.module').name
])
//in the file app/api/api.module.js
var moduleName = __dirname.replace(new RegExp(SEPARATOR, 'g'), '.');
module.exports = framework.module(moduleName, [
])

Categories