Skip to content

Instantly share code, notes, and snippets.

@Cheny-chen
Last active February 24, 2020 09:16
Show Gist options
  • Save Cheny-chen/a35e3d50b898513a0230fa8e19e8ed40 to your computer and use it in GitHub Desktop.
Save Cheny-chen/a35e3d50b898513a0230fa8e19e8ed40 to your computer and use it in GitHub Desktop.
/**
* Author: ChenyChen, JCloudYu
* Create: 2020/01/29
**/
(async()=>{
"use strict";
const START_SIG = "//@export";
const START_SIG_LEN = START_SIG.length;
const END_SIG = "//@endexport";
const END_SIG_LEN = END_SIG.length;
const fs = require('fs');
const path = require('path');
const stream = require('stream');
const readline = require('readline');
const _env_conf = {source_dirs: [], skipped_paths:[], tmpl: null, output: null};
const working_root = process.cwd();
let line_sum=0;
// Read incoming arguments
{
const [, , basic_dir, tmpl_path, ...exec_args] = process.argv;
exec_args.reverse();
if( !basic_dir ){
console.error("Source directory is not assigned!");
process.exit(1);
return;
}
_env_conf.source_dirs.push(path.resolve(working_root, basic_dir));
if( !tmpl_path ){
console.error("Template script is not assigned!");
process.exit(1);
return;
}
_env_conf.tmpl = path.resolve(working_root, tmpl_path);
while( exec_args.length > 0 ){
const opt = exec_args.pop();
switch( opt ){
case "--dir":
case "-d":{
const dpath = exec_args.pop() || '';
if( dpath ){
_env_conf.source_dirs.push(path.resolve(working_root, dpath));
}
break;
}
case '--skip': {
const tpath = exec_args.pop() || '';
if ( tpath ) {
_env_conf.skipped_paths.push(path.resolve(working_root, tpath));
}
break;
}
case "--output":
case "-o":{
const fpath = exec_args.pop() || '';
if( fpath ){
_env_conf.output = path.resolve(working_root, fpath);
}
break;
}
default:{
console.error(`Unknown option \`${ opt }\``);
process.exit(1);
return;
}
}
}
}
// Expose collected arguments
const {tmpl, source_dirs, output, skipped_paths} = _env_conf;
// Fetch template
global.BuildTemplate = TemplateResolver;
const template = (()=>{
try{
const script = require(tmpl);
if( !(script instanceof TemplateResolver) ){
console.error("Given tmpl is not a valid template descriptor!");
process.exit(1);
return null;
}
return script;
} catch(e){
if( e.code === 'ENOENT' ){
console.error(`Template path \`${ tmpl }\` doesn't exist!`);
process.exit(1);
return null;
}
throw e;
}
})();
// Check source directories
{
for( const dir_path of source_dirs ){
try{
const dir_stat = fs.statSync(dir_path);
if( !dir_stat.isDirectory() ){
console.error(`Source path \`${ dir_path }\` is not a directory!`);
process.exit(1);
return;
}
} catch(e){
if( e.code === "ENOENT" ){
console.error(`Source path \`${ dir_path }\` is not a directory!`);
process.exit(1);
return;
}
throw e;
}
try{
fs.accessSync(dir_path, fs.constants.R_OK | fs.constants.X_OK);
} catch(e){
console.error(`Current user has no privilege to access directory \`${ dir_path }\`!`);
process.exit(1);
return;
}
}
}
// Create output stream
const write_stream = (()=>{
if( !output ) return process.stdout;
try{
return fs.createWriteStream(output, {mode: 0o644, flags: 'w'});
} catch(e){
return null;
}
})();
if( !write_stream ){
console.error(`Cannot create output stream to file \`${ output }\`!`);
process.exit(1);
return;
}
// Collect and parse all js script of the target folders
const named_map = {}, unnamed_pool = [];
for ( const dir_path of source_dirs ){
const arrayFiles = ReadMainFolder(dir_path);
for ( const file of arrayFiles ) {
if ( skipped_paths.indexOf(file) >= 0 ) continue;
console.error(`Parsing ${file}...`);
await ParseFile(file, named_map, unnamed_pool);
}
}
console.error(`\nRendering...`);
// Output generated script base on template script
{
const {statics, dynamics} = template;
await WriteToStream(write_stream, statics[0]);
for( let i = 1; i < statics.length; i++ ){
const dynamic_content = dynamics[i - 1];
if( typeof dynamic_content !== "string" ) {
await WriteToStream(write_stream, '' + dynamic_content);
await WriteToStream(write_stream, statics[i]);
continue;
}
if ( dynamic_content === "@unnamed" ) {
for( const code_segment of unnamed_pool ){
await WriteToStream(write_stream, code_segment);
}
await WriteToStream(write_stream, statics[i]);
continue;
}
const important = dynamic_content[0] === '!';
const optional = dynamic_content[0] === '?';
const key = (important || optional) ? dynamic_content.substring(1) : dynamic_content;
const seg_pool = named_map[key];
if( seg_pool !== undefined ){
for( const code_segment of seg_pool ) {
await WriteToStream(write_stream, code_segment);
}
}
else if( important ){
console.error(` Missing required named block \`${ key }\``);
process.exit(1);
}
else{
if( !optional ){
console.error(` Miss matching named block \`${ key }\``);
}
await WriteToStream(write_stream, '');
}
await WriteToStream(write_stream, statics[i]);
}
await CloseStream(write_stream);
}
function TemplateResolver(strings, ...dynamics) {
if ( !(this instanceof TemplateResolver) ) {
return new TemplateResolver(strings, ...dynamics);
}
this.statics = strings;
this.dynamics = dynamics;
}
function WriteToStream(stream, data){
return new Promise((resolve, reject)=>{
stream.write(data, ()=>resolve(true));
// if( !should_wait ){
// reject(false);
// }
});
}
function CloseStream(stream){
return new Promise((resolve, reject)=>stream.end(resolve));
}
function ReadMainFolder(folder, arrayFiles = [], arrayFolder = []) {
fs.readdirSync(folder, {withFileTypes: true}).forEach(filename=>{
if( filename.isDirectory() && filename.name.indexOf('.')!==0 ){
arrayFolder.push(path.join(folder, filename.name));
return;
}
if ( filename.name.indexOf('.js') > 0 ) {
arrayFiles.push(path.join(folder, filename.name));
}
});
if( arrayFolder.length > 0 ){
for (const foldername of arrayFolder) {
if ( skipped_paths.indexOf(foldername) >= 0 ) continue;
const temp = ReadMainFolder(foldername, arrayFiles);
arrayFiles.concat(temp);
}
}
return arrayFiles;
}
function GenerateLineReader(filePath) {
return new Promise((resolve)=>{
let lineNumer=0;
const output = new stream.PassThrough({objectMode:true, highWaterMark:1000});
const readInterface = readline.createInterface({
input: fs.createReadStream(filePath),
});
readInterface.on("line", line => {
output.write(line);
lineNumer++;
});
readInterface.on("close", () => {
output.push(null);
resolve( {readInterface:output, lineNumer:lineNumer} );
});
});
}
async function ParseFile(filePath, named, unnamed){
let idx1, idx2;
let name_key=null;
let collect_mode=false;
let collect_buffer='';
let line_count=0;
let line_queue=[];
const { readInterface, lineNumer} = await GenerateLineReader(filePath);
for await(const new_line of readInterface) {
line_sum++;
line_queue.push(new_line);
ProcessLineQueue();
if(lineNumer===line_sum) {
line_sum=0;
return;
}
}
// Do remaining cleanup on boundary conditions
if ( collect_mode && collect_buffer !== '' ) {
console.error(` Unterminated region detected!`);
if ( name_key !== null ) {
named[name_key] = named[name_key] || [];
named[name_key].push(collect_buffer);
}
else {
unnamed.push(collect_buffer);
}
}
async function ProcessLineQueue() {
while(line_queue.length > 0) {
let line = line_queue.shift();
line_count++;
// Search for //@export
if ( !collect_mode ) {
if ( (idx1 = line.indexOf(START_SIG)) < 0 ) continue;
// Parse //@export=[key]
collect_mode = true;
if ( line[idx1+START_SIG_LEN] === '=' ) {
name_key = line.substring(idx1+START_SIG_LEN+1).trim();
// Purge key identifiers
if (name_key[0] === '!' || name_key[0] === '@' || name_key === '?') {
name_key = name_key.substring(1);
}
}
// Ignore the whole line since that contents before //@export is defined to be discarded
continue;
}
// Search for paired //@endexport
const has_terminator = ((idx1 = line.indexOf(END_SIG)) >= 0);
const has_starter = ((idx2 = line.indexOf(START_SIG)) >= 0);
if ( !has_starter && !has_terminator ) {
collect_buffer += line + "\n";
continue;
}
if ( has_starter ) {
// console.log('has_starter', line_queue);
if ( !has_terminator || idx2 < idx1 ) {
console.error(` ${line_count}:\n A paired //@endexport token must be presented before //@export token!`);
process.exit(1);
return;
}
// The token //@endexport is presented at right position!
line_queue.push(line.substring(idx1+END_SIG_LEN));
line_count--;
}
collect_buffer += line.substring(0, idx1);
if ( name_key !== null ) {
named[name_key] = named[name_key] || [];
named[name_key].push(collect_buffer);
}
else {
unnamed.push(collect_buffer);
}
// Clear state
collect_mode = false;
collect_buffer = '';
}
}
}
})()
.catch((e)=>setTimeout(()=>{throw e;}));
@Cheny-chen
Copy link
Author

Cheny-chen commented Jan 28, 2020

node embedded-script-packer.js <folder name> <template filename> -d <destination filename>

@JCloudYu
Copy link

node embedded-script-packer.js <folder name> <template filename> -d <destination filename>

打錯了喔!是
node embedded-script-packer.js <source folder path> <template file path> [-d <additional source folder path>] [-o <output file path>]

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