Skip to content

Instantly share code, notes, and snippets.

@spikyjt
Created August 28, 2020 12:54
Show Gist options
  • Save spikyjt/5431daf5710f98b3c7eb77975c893563 to your computer and use it in GitHub Desktop.
Save spikyjt/5431daf5710f98b3c7eb77975c893563 to your computer and use it in GitHub Desktop.
A script to parse Vue SFCs and output to the Typescript part to a .ts file. See comments at the top for dependencies.
/**
* A script to parse Vue SFCs and output to the Typescript part to a .ts file.
* Requires @vue/compiler-src and yargs to run (and typescript to do the checking)
*
* Takes options for src/path, quiet (defaults to verbose) and commands for parse (default) or clean
*
* Example use case would be (assuming all files in src dir):
* node parse-vue.js && tsc --noEmit; node parse-vue.js clean
*/
const compiler = require('@vue/compiler-sfc');
const fs = require('fs');
const path = require('path');
const yargs = require('yargs');
/**
* Whether to output logging info
* @type {boolean}
*/
let verbose = true;
/**
* Log a message to stdout, if verbose mode is on
* @param {string} message
*/
const log = (message) =>
{
if (verbose)
{
console.log(message);
}
};
/**
* Log a message to stderr, if verbose mode is on
* @param {string} message
*/
const logError = (message) =>
{
if (verbose)
{
console.error(message);
}
};
/**
* Get the content of a file.
* Basically just turns `fs.readFile` into promise format.
* @see fs.readFile
* @param {string} filePath Full path to file to read
* @return {Promise<string>} Promise for the file content
*/
const getFileContents = async (filePath) =>
{
return new Promise((resolve, reject) =>
{
fs.readFile(filePath, 'utf8', (err, data) =>
{
if (err)
{
logError(`Error reading SFC contents for ${filePath}`);
reject(err);
}
else
{
log(`Read ${filePath}`);
resolve(data);
}
});
});
};
/**
* Write a file.
* Basically just turns `fs.writeFile` into promise format.
* @see fs.writeFile
* @param {string} filePath Full path to file to write
* @param {string} content The content to write to the file
* @return {Promise<void>}
*/
const writeFile = async (filePath, content) =>
{
return new Promise((resolve, reject) =>
{
fs.writeFile(filePath, content, 'utf8', err =>
{
if (err)
{
logError(`Error writing SFC script file ${filePath}`);
reject(err);
}
else
{
log(`Wrote ${filePath}`);
resolve();
}
});
});
};
/**
* Delete a file.
* Basically just turns `fs.unlink` into promise format.
* @see fs.unlink
* @param {string} filePath Full path to file to delete
* @return {Promise<void>}
*/
const delFile = async (filePath) =>
{
return new Promise((resolve, reject) =>
{
fs.unlink(filePath, err =>
{
if (err)
{
logError(`Error deleting SFC script file ${filePath}`);
reject(err);
}
else
{
log(`Deleted ${filePath}`);
resolve();
}
});
});
};
/**
* Parse a vue file (SFC) and extract the script part, if it is Typescript
* @param {string} filePath Full path to the vue file
* @return {Promise<void>}
*/
const parseVue = async (filePath) =>
{
const rawSfc = await getFileContents(filePath);
const sfc = compiler.parse(rawSfc);
if (sfc.errors && sfc.errors.length > 0)
{
return Promise.reject(sfc.errors);
}
if (!sfc.descriptor.script || sfc.descriptor.script.lang !== 'ts')
{
return;
}
return writeFile(
`${filePath}.ts`,
sfc.descriptor.script.content,
);
};
/**
* Job callback for file processing
* @callback Job
* @param {string} filePath Full path to the file to process
* @return {Promise<void>}
*/
/**
* Recurse directory and its children, processing a job
* for each file found that matches a given extension
* @param {string} dirPath Full path to the directory to recurse
* @param {string} ext File extension to match
* @param {Job} job Async job to process on the file
* @return {Promise<void>}
*/
const recurseDir = async (dirPath, ext, job) =>
{
return new Promise(
(resolve, reject) =>
{
fs.readdir(dirPath, { withFileTypes: true }, (err, files) =>
{
if (err)
{
reject(err);
}
else
{
const jobs = [];
files.forEach(f =>
{
const filePath = path.join(dirPath, f.name);
if (f.isFile())
{
if (!f.name.endsWith(ext))
{
return;
}
jobs.push(job(filePath));
}
else if (f.isDirectory())
{
jobs.push(recurseDir(filePath, ext, job));
}
});
Promise.all(jobs).then(_v =>
{
resolve();
});
}
});
},
);
};
/**
* @typedef {Object} Args
* @property {boolean} quiet Whether to silence logging
* @property {string} src Path to source files (can be relative to CWD)
*/
/**
* Process CLI args and return the full source path
* @param {Args} argv Parsed CLI args
* @return {string} Resolved source path
*/
const processArgs = (argv) =>
{
verbose = !argv.quiet;
return path.resolve(argv.src);
}
/**
* Parse SFCs and write Typescript files alongside them
* @param {Args} argv Parsed CLI args
*/
const parse = (argv) =>
{
log('Parsing vue files...');
const fullPath = processArgs(argv);
recurseDir(fullPath, '.vue', parseVue).catch(reason =>
{
console.error(reason);
});
};
/**
* Clean generated files
* @param {Args} argv Parsed CLI args
*/
const clean = (argv) =>
{
log('Cleaning up vue Typescript files...');
const fullPath = processArgs(argv);
recurseDir(fullPath, '.vue.ts', delFile).catch(reason =>
{
console.error(reason);
});
};
/**
* Process CLI args
*/
yargs
.option('quiet', {
alias: [
'silent',
'q',
's',
],
describe: 'Disable logging',
type: 'boolean',
default: false,
})
.option('src', {
alias: [
'path',
'p',
],
describe: 'The path to the source files (can be relative to CWD)',
type: 'string',
default: 'src',
})
.command({
command: 'clean',
describe: 'Clean generated Typescript files',
handler: clean,
})
.command({
command: '*',
aliases: [ 'parse' ],
describe: 'Parse .vue SFC files for Typescript sections',
handler: parse,
})
.help()
.argv;
@lewishowles
Copy link

Ah, I think I've misunderstood what's causing it to 'not fail' anyway. It seems like tsc is 'passing' even with errors, as it isn't triggering a pre-push git hook. That's a shame.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment