Recently I was called in to help get a product ready for production that has been through a few different developers and has hundreds of files with no rhyme or reason to its use of camelCase.js
, TitleCase.js
, and snake_case.js
.
And while, it may seem like something that doesn’t matter that much - it does.
Consistency makes a project easier to navigate, to think around, and to organize for testing.
(think of it like broken window theory, but for your project - it’s a quick win and it cleans up the head space)
Just use dash-case
dash-case
or kebab-case
, as some call it, is the most pragmatic naming convention for files for three reasons:
- Many file systems are case sensitive - so always use lowercase!
- It’s easier to read separators than it is to read cases.
- The decades old Linux convention of dashes is easier to read than underscores in word processors and browsers, which sometimes use underlines for file or URL paths.
I’ve been around a number of times when a project is being switched from development on Mac or Windows to deployment and something subtle goes wrong related to naming conventions and the filesystem. Fortunately those are usually pretty easy to fix.
Although more rare, I’ve also seen the opposite happen - a “Project.js” and “project.js” in the same folder, causing problems. Or a “foobar.js” and “foo-bar.js” in the same folder, which doesn’t cause a technical problem, but is mentally confusing.
Converting imports and requires
Here’s a little script I cooked up for the occasion. I’m posting it here so that I have it the next time I need it - which will probably won’t be never.
Usage
The node script only handles one file at a time. It could be made more efficient by walking the filesystem from within node, but I found it easier to test and use by combining it with a little bash:
#!/bin/bash
set -e
set -u
# walk the project directory and call dashify.js on each file
# also output the `git mv` commands into their own script
find . -type f -exec node dashify.js {} \; | tee git-mv.sh
# this was generated by dashify.js to rename files as needed
bash git-mv.sh
# Use prettier to solve any line-length or whitespace issues with the auto-
# replacement, and make sure we didn't cause any syntax errors.
prettier --write './**/*.{js,css,html,json,md}'
To Dash Case
Here’s the function for converting file names from other most other casing systems to dash case.
You could make a shorter version of this with more complex RegExp, but I prefer the clarity of several that are easier to read.
// toDashCase for file names
function toDashCase(str) {
return (
str
// catches CAPS-only
// FOOBAR => foobar
.replace(/^([A-Z]+)$/g, function (m) {
return m.toLowerCase();
})
// catches CAPS acronyms
// APIErrorFOOBarBazQux => api-error-foo-barBazQux
.replace(/([A-Z0-9]+)([A-Z][a-z0-9])/g, function (_, a, b) {
return a.toLowerCase() + '-' + b.toLowerCase();
})
// catches starts with a Cap
// UserName => userName
.replace(/^([A-Z]+)/g, function (m) {
return m.toLowerCase();
})
// catches camelCase
// api-error-foo-barBazQux => api-error-foo-bar-baz-qux
.replace(/([A-Z]+)/g, function (m) {
return '-' + m.toLowerCase();
})
// catches underscores
// foo_bar => foo-bar
.replace(/_/g, '-')
// catches accidental leading dashes
// /-foobar => /foobar
.replace(/\/-/g, '/')
// -foobar => /foobar
.replace(/^-/g, '')
);
}
Convert and Disambiguate
In this example I’m only handling require
, but you could change it slightly to handle import
as well.
Also, rather that doing a child_process.exec
or spawn
, this prints out the git mv
commands to be run after all changes have been made.
(this only handles one file at a time)
'use strict';
var path = require('path');
var fsSync = require('fs');
var fs = require('fs').promises;
async function dashify(oldFilename) {
if (!oldFilename) {
console.error('#error: no filename given');
return;
}
if (!['.cjs', '.mjs', '.js'].includes(path.extname(oldFilename))) {
console.warn("#warn: skipping non-js file '%s'", oldFilename);
return;
}
var filename = path.resolve(oldFilename);
var oldTxt = await fs.readFile(filename, 'utf8');
// We can use a simple regex rather than a full JS parser because all of the
// project files are already prettified, so we know exactly what the requires
// will look like: require('./path/to/foobar')
var txt = oldTxt.replace(/(require\('\.)(.*)('\))/g, function (_, a, b, c) {
// a = "require('."
// b = /path/to/oldFile
// c = "')"
var oldRequire = a + b + c;
// x = /path/to/old-file
var x = toDashCase(b);
var pathname = path.resolve(path.dirname(filename), '.' + b);
x = disambiguate(pathname, x);
var newRequire = a + x + c;
if (newRequire != oldRequire) {
// log the change as a comment
console.log('#', result);
}
//console.log('#', result, '=>', path.relative(path.dirname(filename), '.' + x));
return result;
});
// if we need to replace other strings that are filename-dependent, we can
// do that too
//.replace(/modelClass: '(\w+)'/g, function (_, name) {
// var x = toDashCase(name);
// var result = "modelClass: './" + x + ".js'";
// console.log('# ' + result);
// return result;
//});
// write the changes back to the OLD file
await fs.writeFile(oldFilename, txt);
var newFilename = toDashCase(oldFilename);
if (newFilename != oldFilename) {
// rather than moving the files in-process, or spawning out to 'git mv',
// we just output the 'git mv' so that it can be written to a file
console.info('mkdir -p', path.dirname(newFilename), ';');
console.info('git mv', oldFilename, newFilename, ';');
console.info('git add', newFilename, ';');
// For other use cases we could move the files directly
//await fs.mkdir(path.dirname(newFilename), { recursive: true });
//await fs.unlink(oldFilename);
} else if (txt != oldTxt) {
// it may be that the file was updated, but does not need to be moved
console.info('git add', oldFilename, ';');
}
}
// change require('./path/to/foobar') into either
// require('./path/to/foobar.js')
// or
// require('./path/to/foobar/')
// Note that files with .js take precedence over directories
function disambiguate(pathname, x) {
// ./path/to/foobar.js is not ambigious
if ('.js' === path.extname(x)) {
return x;
}
// ./path/to/foobar/ is not ambigious
if ('/' === x[x.length - 1]) {
return x;
}
// ./path/to/foobar may be refer to foobar.js or foobar/index.js
// foobar.js takes precedence over foobar/
var stat;
try {
stat = fsSync.statSync(pathname + '.js');
} catch (e) {
// ignore
}
if (stat) {
// found 'foobar.js'
x += '.js';
return x;
}
// must be 'foobar/index.js'
if (!/\/$/.test(x)) {
x += '/';
}
return x;
}
module.exports = dashify;
if (require.main == module) {
var oldFilename = process.argv[2];
if (!oldFilename) {
console.error('Usage: node ./dashify.js ./path/to/file.js');
process.exit();
}
dashify(oldFilename).catch(function (err) {
console.error(oldFilename, err);
});
}