-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.