From bda2c553ee30bde87c5cffed9761d134a4215264 Mon Sep 17 00:00:00 2001 From: Thomas Date: Sat, 24 Jun 2023 22:58:01 -0400 Subject: [PATCH 1/4] add option to output multipart files as individual parts with/without zipping --- packages/cli/README.md | 8 +++++ packages/cli/cli.js | 46 ++++++++++++++++++++++---- packages/cli/package.json | 3 +- packages/cli/src/generateOutputData.js | 9 ++++- packages/cli/src/parseArgs.js | 8 +++++ 5 files changed, 65 insertions(+), 9 deletions(-) diff --git a/packages/cli/README.md b/packages/cli/README.md index 618933d7d..bf1291680 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -73,6 +73,14 @@ Examples: ```jscad mydesign.js -of amf # -- convert mydesign.js into mydesign.amf``` +For multi-part models, you can pass the parts flag `-p` to output each part as a separate, numbered file: + +```jscad mydesign.js -p # -- convert mydesign.js into mydesign-part-1-of-2.stl and mydesign-part-2-of-2.stl``` + +You may also pass the zip flag `-z` to zip multi-part files into one .zip file: + +```jscad mydesign.js -p -z # -- convert mydesign.js into mydesign.zip which contains: mydesign-part-1-of-2.stl and mydesign-part-2-of-2.stl``` + The '-o' option can be used to control where the output will be placed. While, the '-of' option can be used to control the format of the output. diff --git a/packages/cli/cli.js b/packages/cli/cli.js index adc7c4160..3d839a518 100644 --- a/packages/cli/cli.js +++ b/packages/cli/cli.js @@ -22,6 +22,7 @@ // jscad name_plate.jscad --name "Just Me" --title "CEO" -o amf test.amf // const fs = require('fs') +const JSZip = require('jszip') const { formats } = require('@jscad/io/formats') @@ -33,7 +34,7 @@ const parseArgs = require('./src/parseArgs') // handle arguments (inputs, outputs, etc) const args = process.argv.splice(2) -let { inputFile, inputFormat, outputFile, outputFormat, params, addMetaData, inputIsDirectory } = parseArgs(args) +let { inputFile, inputFormat, outputFile, outputFormat, parts, zip, params, addMetaData, inputIsDirectory } = parseArgs(args) // outputs const output = determineOutputNameAndFormat(outputFormat, outputFile, inputFile) @@ -48,18 +49,49 @@ const clicolors = { black: '\u{1b}[0m' } -console.log(`${clicolors.blue}JSCAD: generating output ${clicolors.red} - from: ${clicolors.green} ${inputFile} ${clicolors.red} - to: ${clicolors.green} ${outputFile} ${clicolors.yellow}(${formats[outputFormat].description}) ${clicolors.black} -`) +const logFileOutput = (outputFile) => { + console.log(`${clicolors.blue}JSCAD: generating output ${clicolors.red} + from: ${clicolors.green} ${inputFile} ${clicolors.red} + to: ${clicolors.green} ${outputFile} ${clicolors.yellow}(${formats[outputFormat].description}) ${clicolors.black} + `) +} // read input data const src = fs.readFileSync(inputFile, inputFile.match(/\.stl$/i) ? 'binary' : 'UTF8') // -- convert from JSCAD script into the desired output format // -- and write it to disk -generateOutputData(src, params, { outputFile, outputFormat, inputFile, inputFormat, version, addMetaData, inputIsDirectory }) - .then((outputData) => writeOutput(outputFile, outputData)) +generateOutputData(src, params, { outputFile, outputFormat, inputFile, inputFormat, parts, version, addMetaData, inputIsDirectory }) + .then((outputData) => { + if (outputData instanceof Array) { + if (zip) { + const zip = new JSZip() + for (let i = 0; i < outputData.length; i++) { + const filename = outputFile.replace(/\.(\w+)$/, `-part-${i + 1}-of-${outputData.length}.$1`) + zip.file(filename, outputData[i].asBuffer()) + } + zip.generateAsync({ type: 'nodebuffer' }).then((content) => { + const zipFilename = outputFile.replace(/\.(\w+)$/, '.zip') + fs.writeFile(zipFilename, content, (err) => { + if (err) { + console.log('err', err) + } else { + logFileOutput(zipFilename) + } + }) + }) + } else { + for (let i = 0; i < outputData.length; i++) { + const filename = outputFile.replace(/\.(\w+)$/, `-part-${i + 1}-of-${outputData.length}.$1`) + logFileOutput(filename) + writeOutput(filename, outputData[i]) + } + } + } else { + logFileOutput(outputFile) + writeOutput(outputFile, outputData) + } + }) .catch((error) => { console.error(error) process.exit(1) diff --git a/packages/cli/package.json b/packages/cli/package.json index 1172999a7..8df1624e2 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -38,7 +38,8 @@ "@jscad/array-utils": "2.1.4", "@jscad/core": "2.6.6", "@jscad/io": "2.4.5", - "@jscad/modeling": "2.11.1" + "@jscad/modeling": "2.11.1", + "jszip": "^3.10.1" }, "devDependencies": { "ava": "3.15.0", diff --git a/packages/cli/src/generateOutputData.js b/packages/cli/src/generateOutputData.js index 329b0685b..00bed9fc0 100644 --- a/packages/cli/src/generateOutputData.js +++ b/packages/cli/src/generateOutputData.js @@ -22,7 +22,7 @@ const generateOutputData = (source, params, options) => { addMetaData: true } options = Object.assign({}, defaults, options) - const { outputFormat, inputFile, inputFormat } = options + const { outputFormat, inputFile, inputFormat, parts } = options options.filename = inputFile // for deserializers @@ -62,6 +62,13 @@ const generateOutputData = (source, params, options) => { }) .then((solids) => { const serializerOptions = Object.assign({ format: outputFormat }, params) + if (parts) { + let blobs = [] + for (let i = 0; i < solids.length; i++) { + blobs.push(solidsAsBlob(solids[i], serializerOptions)) + } + return blobs + } return solidsAsBlob(solids, serializerOptions) }) } diff --git a/packages/cli/src/parseArgs.js b/packages/cli/src/parseArgs.js index c2dc9d4fa..08c7b37cd 100644 --- a/packages/cli/src/parseArgs.js +++ b/packages/cli/src/parseArgs.js @@ -24,6 +24,8 @@ const parseArgs = (args) => { let inputFormat let outputFile let outputFormat + let parts = false + let zip = false const params = {} // parameters to feed the script if applicable let addMetaData = false // wether to add metadata to outputs or not : ie version info, timestamp etc let inputIsDirectory = false // did we pass in a folder or a file ? @@ -41,6 +43,10 @@ const parseArgs = (args) => { for (let i = 0; i < args.length; i++) { if (args[i] === '-of') { // -of outputFormat = args[++i] + } else if (args[i] === '-p') { + parts = true + } else if (args[i] === '-z') { + zip = true } else if (args[i].match(/^-o(\S.+)/)) { // -o outputFile = args[i] outputFile = outputFile.replace(/^-o(\S+)$/, '$1') @@ -92,6 +98,8 @@ const parseArgs = (args) => { inputFormat, outputFile, outputFormat, + parts, + zip, params, addMetaData, inputIsDirectory From 1b50b1aa3cbf8f660e3e0765cd80ae84e9c163b4 Mon Sep 17 00:00:00 2001 From: Thomas Date: Sat, 24 Jun 2023 23:30:01 -0400 Subject: [PATCH 2/4] add tests, update readme --- packages/cli/README.md | 2 +- packages/cli/cli.parameters.test.js | 63 ++++++++++++++++++++++++++++- 2 files changed, 62 insertions(+), 3 deletions(-) diff --git a/packages/cli/README.md b/packages/cli/README.md index bf1291680..cad69ed61 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -77,7 +77,7 @@ For multi-part models, you can pass the parts flag `-p` to output each part as a ```jscad mydesign.js -p # -- convert mydesign.js into mydesign-part-1-of-2.stl and mydesign-part-2-of-2.stl``` -You may also pass the zip flag `-z` to zip multi-part files into one .zip file: +If you pass `-p` you may also pass the zip flag `-z` to zip multi-part files into one .zip file: ```jscad mydesign.js -p -z # -- convert mydesign.js into mydesign.zip which contains: mydesign-part-1-of-2.stl and mydesign-part-2-of-2.stl``` diff --git a/packages/cli/cli.parameters.test.js b/packages/cli/cli.parameters.test.js index 504acc281..605cd4bac 100644 --- a/packages/cli/cli.parameters.test.js +++ b/packages/cli/cli.parameters.test.js @@ -13,6 +13,12 @@ test.afterEach.always((t) => { try { if (t.context.outputPath) fs.unlinkSync(t.context.outputPath) } catch (err) {} + try { + if (t.context.outputPath2) fs.unlinkSync(t.context.outputPath2) + } catch (err) {} + try { + if (t.context.outputPath3) fs.unlinkSync(t.context.outputPath3) + } catch (err) {} try { if (t.context.folderPath) fs.rmdirSync(t.context.folderPath, { recursive: false }) @@ -32,7 +38,7 @@ test.beforeEach((t) => { // create a simple JSCAD script for input // the script should produce ALL geometry types -const createJscad = (id) => { +const createJscad = (id, multipart = false) => { const jscadScript = `// test script ${id} const { primitives } = require('@jscad/modeling') @@ -51,7 +57,7 @@ const main = (params) => { let ageom2 = primitives.ellipse() let ageom3 = primitives.ellipsoid() - return [apath2, ageom2, ageom3] + ${multipart ? `return [ageom3, ageom3, ageom3]` : `return [apath2, ageom2, ageom3]`} } module.exports = { main, getParameterDefinitions } @@ -207,3 +213,56 @@ test('cli (single input file, invalid jscad)', (t) => { execSync(cmd, { stdio: [0, 1, 2] }) }) }) + + +test('cli (single input file, multiple output files)', (t) => { + const testID = 7 + + const inputPath = createJscad(testID, true) + t.true(fs.existsSync(inputPath)) + + t.context.inputPath = inputPath + + const outputName = (partNum) => `./test${testID}-part-${partNum}-of-3.stl` + const outputPath1 = path.resolve(__dirname, outputName(1)) + const outputPath2 = path.resolve(__dirname, outputName(2)) + const outputPath3 = path.resolve(__dirname, outputName(3)) + t.false(fs.existsSync(outputPath1)) + t.false(fs.existsSync(outputPath2)) + t.false(fs.existsSync(outputPath3)) + + t.context.outputPath = outputPath1 + t.context.outputPath2 = outputPath2 + t.context.outputPath3 = outputPath3 + + const cliPath = t.context.cliPath + + const cmd = `node ${cliPath} ${inputPath} -p` + execSync(cmd, { stdio: [0,1,2] }) + t.true(fs.existsSync(outputPath1)) + t.true(fs.existsSync(outputPath2)) + t.true(fs.existsSync(outputPath3)) +}) + +test('cli (single input file, zipped output file)', (t) => { + const testID = 8 + + const inputPath = createJscad(testID, true) + t.true(fs.existsSync(inputPath)) + + t.context.inputPath = inputPath + + + const outputName = `./test${testID}.zip` + const outputPath = path.resolve(__dirname, outputName) + + t.false(fs.existsSync(outputPath)) + + t.context.outputPath = outputPath + + const cliPath = t.context.cliPath + + const cmd = `node ${cliPath} ${inputPath} -p -z` + execSync(cmd, { stdio: [0,1,2] }) + t.true(fs.existsSync(outputPath)) +}) From 1898eaf7230e3df29567c46f3a55231ac8ca4e4e Mon Sep 17 00:00:00 2001 From: Thomas Date: Sun, 25 Jun 2023 12:42:22 -0400 Subject: [PATCH 3/4] rename `parts` to `generateParts`, use `-gp` flag --- packages/cli/README.md | 8 ++++---- packages/cli/cli.js | 4 ++-- packages/cli/cli.parameters.test.js | 4 ++-- packages/cli/src/generateOutputData.js | 7 ++++--- packages/cli/src/parseArgs.js | 8 ++++---- 5 files changed, 16 insertions(+), 15 deletions(-) diff --git a/packages/cli/README.md b/packages/cli/README.md index cad69ed61..a5b64ba74 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -73,13 +73,13 @@ Examples: ```jscad mydesign.js -of amf # -- convert mydesign.js into mydesign.amf``` -For multi-part models, you can pass the parts flag `-p` to output each part as a separate, numbered file: +For multi-part models, you can pass the `generateParts` flag `-gp` to output each part as a separate, numbered file: -```jscad mydesign.js -p # -- convert mydesign.js into mydesign-part-1-of-2.stl and mydesign-part-2-of-2.stl``` +```jscad mydesign.js -gp # -- convert mydesign.js into mydesign-part-1-of-2.stl and mydesign-part-2-of-2.stl``` -If you pass `-p` you may also pass the zip flag `-z` to zip multi-part files into one .zip file: +If you pass `-gp` you may also pass the `zip` flag `-z` to zip multi-part files into one .zip file: -```jscad mydesign.js -p -z # -- convert mydesign.js into mydesign.zip which contains: mydesign-part-1-of-2.stl and mydesign-part-2-of-2.stl``` +```jscad mydesign.js -gp -z # -- convert mydesign.js into mydesign.zip which contains: mydesign-part-1-of-2.stl and mydesign-part-2-of-2.stl``` The '-o' option can be used to control where the output will be placed. While, the '-of' option can be used to control the format of the output. diff --git a/packages/cli/cli.js b/packages/cli/cli.js index 3d839a518..5ccf789c8 100644 --- a/packages/cli/cli.js +++ b/packages/cli/cli.js @@ -34,7 +34,7 @@ const parseArgs = require('./src/parseArgs') // handle arguments (inputs, outputs, etc) const args = process.argv.splice(2) -let { inputFile, inputFormat, outputFile, outputFormat, parts, zip, params, addMetaData, inputIsDirectory } = parseArgs(args) +let { inputFile, inputFormat, outputFile, outputFormat, generateParts, zip, params, addMetaData, inputIsDirectory } = parseArgs(args) // outputs const output = determineOutputNameAndFormat(outputFormat, outputFile, inputFile) @@ -61,7 +61,7 @@ const src = fs.readFileSync(inputFile, inputFile.match(/\.stl$/i) ? 'binary' : ' // -- convert from JSCAD script into the desired output format // -- and write it to disk -generateOutputData(src, params, { outputFile, outputFormat, inputFile, inputFormat, parts, version, addMetaData, inputIsDirectory }) +generateOutputData(src, params, { outputFile, outputFormat, inputFile, inputFormat, generateParts, version, addMetaData, inputIsDirectory }) .then((outputData) => { if (outputData instanceof Array) { if (zip) { diff --git a/packages/cli/cli.parameters.test.js b/packages/cli/cli.parameters.test.js index 605cd4bac..1172d3f88 100644 --- a/packages/cli/cli.parameters.test.js +++ b/packages/cli/cli.parameters.test.js @@ -237,7 +237,7 @@ test('cli (single input file, multiple output files)', (t) => { const cliPath = t.context.cliPath - const cmd = `node ${cliPath} ${inputPath} -p` + const cmd = `node ${cliPath} ${inputPath} -gp` execSync(cmd, { stdio: [0,1,2] }) t.true(fs.existsSync(outputPath1)) t.true(fs.existsSync(outputPath2)) @@ -262,7 +262,7 @@ test('cli (single input file, zipped output file)', (t) => { const cliPath = t.context.cliPath - const cmd = `node ${cliPath} ${inputPath} -p -z` + const cmd = `node ${cliPath} ${inputPath} -gp -z` execSync(cmd, { stdio: [0,1,2] }) t.true(fs.existsSync(outputPath)) }) diff --git a/packages/cli/src/generateOutputData.js b/packages/cli/src/generateOutputData.js index 00bed9fc0..0990bd379 100644 --- a/packages/cli/src/generateOutputData.js +++ b/packages/cli/src/generateOutputData.js @@ -19,10 +19,11 @@ const generateOutputData = (source, params, options) => { outputFormat: 'stl', inputFile: '', version: '', - addMetaData: true + addMetaData: true, + generateParts: false } options = Object.assign({}, defaults, options) - const { outputFormat, inputFile, inputFormat, parts } = options + const { outputFormat, inputFile, inputFormat, generateParts } = options options.filename = inputFile // for deserializers @@ -62,7 +63,7 @@ const generateOutputData = (source, params, options) => { }) .then((solids) => { const serializerOptions = Object.assign({ format: outputFormat }, params) - if (parts) { + if (generateParts) { let blobs = [] for (let i = 0; i < solids.length; i++) { blobs.push(solidsAsBlob(solids[i], serializerOptions)) diff --git a/packages/cli/src/parseArgs.js b/packages/cli/src/parseArgs.js index 08c7b37cd..577a6bb70 100644 --- a/packages/cli/src/parseArgs.js +++ b/packages/cli/src/parseArgs.js @@ -24,7 +24,7 @@ const parseArgs = (args) => { let inputFormat let outputFile let outputFormat - let parts = false + let generateParts = false let zip = false const params = {} // parameters to feed the script if applicable let addMetaData = false // wether to add metadata to outputs or not : ie version info, timestamp etc @@ -43,8 +43,8 @@ const parseArgs = (args) => { for (let i = 0; i < args.length; i++) { if (args[i] === '-of') { // -of outputFormat = args[++i] - } else if (args[i] === '-p') { - parts = true + } else if (args[i] === '-gp') { + generateParts = true } else if (args[i] === '-z') { zip = true } else if (args[i].match(/^-o(\S.+)/)) { // -o @@ -98,7 +98,7 @@ const parseArgs = (args) => { inputFormat, outputFile, outputFormat, - parts, + generateParts, zip, params, addMetaData, From f4609875bfa509c53af25c5330fe73ff5437b678 Mon Sep 17 00:00:00 2001 From: Thomas Date: Sun, 25 Jun 2023 13:12:43 -0400 Subject: [PATCH 4/4] add zip functionality to all exports - allow zipping single file exports - add test for single file export as zip - refactor zip tests to check actual zip contents --- packages/cli/README.md | 4 ++- packages/cli/cli.js | 21 +++++++++++++--- packages/cli/cli.parameters.test.js | 38 +++++++++++++++++++++++++++-- 3 files changed, 57 insertions(+), 6 deletions(-) diff --git a/packages/cli/README.md b/packages/cli/README.md index a5b64ba74..8eeb8646e 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -77,7 +77,9 @@ For multi-part models, you can pass the `generateParts` flag `-gp` to output eac ```jscad mydesign.js -gp # -- convert mydesign.js into mydesign-part-1-of-2.stl and mydesign-part-2-of-2.stl``` -If you pass `-gp` you may also pass the `zip` flag `-z` to zip multi-part files into one .zip file: +You may also pass the `zip` flag `-z` to zip generated files into one .zip file: + +```jscad mydesign.js -z # -- convert mydesign.js into mydesign.zip which contains: mydesign.stl``` ```jscad mydesign.js -gp -z # -- convert mydesign.js into mydesign.zip which contains: mydesign-part-1-of-2.stl and mydesign-part-2-of-2.stl``` diff --git a/packages/cli/cli.js b/packages/cli/cli.js index 5ccf789c8..f6c305240 100644 --- a/packages/cli/cli.js +++ b/packages/cli/cli.js @@ -74,7 +74,7 @@ generateOutputData(src, params, { outputFile, outputFormat, inputFile, inputForm const zipFilename = outputFile.replace(/\.(\w+)$/, '.zip') fs.writeFile(zipFilename, content, (err) => { if (err) { - console.log('err', err) + console.error(err) } else { logFileOutput(zipFilename) } @@ -88,8 +88,23 @@ generateOutputData(src, params, { outputFile, outputFormat, inputFile, inputForm } } } else { - logFileOutput(outputFile) - writeOutput(outputFile, outputData) + if (zip) { + const zip = new JSZip() + zip.file(outputFile, outputData.asBuffer()) + zip.generateAsync({ type: 'nodebuffer' }).then((content) => { + const zipFilename = outputFile.replace(/\.(\w+)$/, '.zip') + fs.writeFile(zipFilename, content, (err) => { + if (err) { + console.error(err) + } else { + logFileOutput(zipFilename) + } + }) + }) + } else { + logFileOutput(outputFile) + writeOutput(outputFile, outputData) + } } }) .catch((error) => { diff --git a/packages/cli/cli.parameters.test.js b/packages/cli/cli.parameters.test.js index 1172d3f88..0b6108992 100644 --- a/packages/cli/cli.parameters.test.js +++ b/packages/cli/cli.parameters.test.js @@ -1,4 +1,5 @@ const test = require('ava') +const JSZip = require('jszip') const path = require('path') const { execSync } = require('child_process') @@ -244,7 +245,7 @@ test('cli (single input file, multiple output files)', (t) => { t.true(fs.existsSync(outputPath3)) }) -test('cli (single input file, zipped output file)', (t) => { +test('cli (single multipart input file, zipped output file)', async (t) => { const testID = 8 const inputPath = createJscad(testID, true) @@ -252,7 +253,6 @@ test('cli (single input file, zipped output file)', (t) => { t.context.inputPath = inputPath - const outputName = `./test${testID}.zip` const outputPath = path.resolve(__dirname, outputName) @@ -265,4 +265,38 @@ test('cli (single input file, zipped output file)', (t) => { const cmd = `node ${cliPath} ${inputPath} -gp -z` execSync(cmd, { stdio: [0,1,2] }) t.true(fs.existsSync(outputPath)) + + // check contents of zip file + const data = await fs.promises.readFile(outputPath) + const content = await JSZip.loadAsync(data) + t.true(content.files[path.resolve(__dirname, `./test${testID}-part-1-of-3.stl`)] !== undefined) + t.true(content.files[path.resolve(__dirname, `./test${testID}-part-2-of-3.stl`)] !== undefined) + t.true(content.files[path.resolve(__dirname, `./test${testID}-part-3-of-3.stl`)] !== undefined) +}) + +test('cli (single input file, zipped output file)', async (t) => { + const testID = 9 + + const inputPath = createJscad(testID, true) + t.true(fs.existsSync(inputPath)) + + t.context.inputPath = inputPath + + const outputName = `./test${testID}.zip` + const outputPath = path.resolve(__dirname, outputName) + + t.false(fs.existsSync(outputPath)) + + t.context.outputPath = outputPath + + const cliPath = t.context.cliPath + + const cmd = `node ${cliPath} ${inputPath} -z` + execSync(cmd, { stdio: [0,1,2] }) + t.true(fs.existsSync(outputPath)) + + // check contents of zip file + const data = await fs.promises.readFile(outputPath) + const content = await JSZip.loadAsync(data) + t.true(content.files[path.resolve(__dirname, `./test${testID}.stl`)] !== undefined) })