From d40e17f8ed7fd2037096855148797be12e5153a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20=C5=81=C4=85giewka?= Date: Sat, 28 Mar 2026 14:29:15 +0100 Subject: [PATCH 1/6] refactor: drop walk and minimatch This commit drops the combined use of walk and minimatch by using fs.readdir and path.matchesGlob. The baseline for this commit is node v22.5. --- lib/compile.js | 42 +++-- package-lock.json | 22 +-- package.json | 4 +- test/test.compile.js | 358 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 382 insertions(+), 44 deletions(-) create mode 100644 test/test.compile.js diff --git a/lib/compile.js b/lib/compile.js index 7f4154b7..e36c19d4 100644 --- a/lib/compile.js +++ b/lib/compile.js @@ -1,6 +1,5 @@ const FS = require('fs'); -const minimatch = require('minimatch'); -const WALK = require('walk'); +const {matchesGlob} = require('node:path'); const Twig = require('..'); const PATHS = require('./paths'); @@ -8,7 +7,7 @@ const {twig} = Twig; exports.defaults = { compress: false, - pattern: '*\\.twig', + pattern: '*.twig', recursive: false }; @@ -38,28 +37,25 @@ exports.compile = function (options, files) { function parseTemplateFolder(directory, pattern) { directory = PATHS.stripSlash(directory); - // Get the files in the directory - // Walker options - const walker = WALK.walk(directory, {followLinks: false}); - const files = []; - - walker.on('file', (root, stat, next) => { - // Normalize (remove / from end if present) - root = PATHS.stripSlash(root); - - // Match against file pattern - const {name} = stat; - const file = root + '/' + name; - if (minimatch(name, pattern)) { - parseTemplateFile(file, directory); - files.push(file); + FS.readdir(directory, {recursive: true, withFileTypes: true}, (err, entries) => { + if (err) { + console.error('ERROR ' + directory + ': Unable to read directory'); + return; } - next(); - }); - - walker.on('end', () => { - // Console.log(files); + for (const entry of entries) { + if (!entry.isFile()) { + continue; + } + + // Match against file pattern + const {name} = entry; + if (matchesGlob(name, pattern)) { + const root = PATHS.stripSlash(entry.parentPath || entry.path); + const file = root + '/' + name; + parseTemplateFile(file, directory); + } + } }); } diff --git a/package-lock.json b/package-lock.json index 5ca2a8cc..239f561d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,9 +10,7 @@ "license": "BSD-2-Clause", "dependencies": { "@babel/runtime": "^7.8.4", - "locutus": "^3.0.9", - "minimatch": "^10", - "walk": "2.3.x" + "locutus": "^3.0.9" }, "bin": { "twigjs": "bin/twigjs" @@ -2356,6 +2354,7 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, "license": "MIT", "engines": { "node": "18 || 20 || >=22" @@ -2378,6 +2377,7 @@ "version": "5.0.5", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" @@ -2991,12 +2991,6 @@ "dev": true, "license": "ISC" }, - "node_modules/foreachasync": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/foreachasync/-/foreachasync-3.0.0.tgz", - "integrity": "sha512-J+ler7Ta54FwwNcx6wQRDhTIbNeyDcARMkOcguEqnEdtm0jKvN3Li3PDAb2Du3ubJYEWfYL83XMROXdsXAXycw==", - "license": "Apache2" - }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -3524,6 +3518,7 @@ "version": "10.2.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "brace-expansion": "^5.0.2" @@ -4532,15 +4527,6 @@ "dev": true, "license": "MIT" }, - "node_modules/walk": { - "version": "2.3.15", - "resolved": "https://registry.npmjs.org/walk/-/walk-2.3.15.tgz", - "integrity": "sha512-4eRTBZljBfIISK1Vnt69Gvr2w/wc3U6Vtrw7qiN5iqYJPH7LElcYh/iU4XWhdCy2dZqv1ToMyYlybDylfG/5Vg==", - "license": "(MIT OR Apache-2.0)", - "dependencies": { - "foreachasync": "^3.0.0" - } - }, "node_modules/watchpack": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", diff --git a/package.json b/package.json index 1a46443c..74fe779d 100644 --- a/package.json +++ b/package.json @@ -33,9 +33,7 @@ }, "dependencies": { "@babel/runtime": "^7.8.4", - "locutus": "^3.0.9", - "minimatch": "^10", - "walk": "2.3.x" + "locutus": "^3.0.9" }, "devDependencies": { "@babel/core": "^7.8.4", diff --git a/test/test.compile.js b/test/test.compile.js new file mode 100644 index 00000000..4bd5b43c --- /dev/null +++ b/test/test.compile.js @@ -0,0 +1,358 @@ +const fs = require('fs'); +const path = require('path'); +const sinon = require('sinon'); +const {setTimeout: delay} = require('node:timers/promises'); +require('should-sinon'); + +const Twig = require('..'); +const compileModule = require('../lib/compile'); +const PATHS = require('../lib/paths'); + +describe('lib/compile ->', function () { + describe('defaults ->', function () { + it('should have compress set to false', function () { + compileModule.defaults.compress.should.equal(false); + }); + + it('should have pattern set to *.twig', function () { + compileModule.defaults.pattern.should.equal('*.twig'); + }); + + it('should have recursive set to false', function () { + compileModule.defaults.recursive.should.equal(false); + }); + }); + + describe('compile ->', function () { + let mkdirStub; + let writeFileStub; + + beforeEach(function () { + // Disable Twig template caching so the same template IDs can + // be registered across tests without colliding. + Twig.cache(false); + mkdirStub = sinon.stub(PATHS, 'mkdir'); + writeFileStub = sinon.stub(fs, 'writeFile'); + }); + + afterEach(function () { + sinon.restore(); + Twig.cache(true); + }); + + describe('output directory ->', function () { + it('should create output directory when output option is provided', function () { + compileModule.compile({output: 'dist/templates'}, []); + mkdirStub.should.be.calledWith('dist/templates'); + }); + + it('should not create output directory when output option is not provided', function () { + compileModule.compile({}, []); + mkdirStub.should.not.be.called(); + }); + }); + + describe('fs.stat handling ->', function () { + it('should call fs.stat for each file', function () { + const statStub = sinon.stub(fs, 'stat'); + compileModule.compile({}, ['file1.twig', 'file2.twig', 'file3.twig']); + + statStub.should.be.calledThrice(); + statStub.firstCall.args[0].should.equal('file1.twig'); + statStub.secondCall.args[0].should.equal('file2.twig'); + statStub.thirdCall.args[0].should.equal('file3.twig'); + }); + + it('should log error when fs.stat returns an error', function () { + const consoleStub = sinon.stub(console, 'error'); + sinon.stub(fs, 'stat').callsFake((file, cb) => { + cb(new Error('ENOENT')); + }); + + compileModule.compile({}, ['missing.twig']); + + consoleStub.should.be.calledOnce(); + consoleStub.firstCall.args[0].should.containEql('missing.twig'); + consoleStub.firstCall.args[0].should.containEql('Unable to stat file'); + }); + + it('should log error for unknown file types', function () { + const consoleStub = sinon.stub(console, 'log'); + sinon.stub(fs, 'stat').callsFake((file, cb) => { + cb(null, { + isDirectory() { + return false; + }, + isFile() { + return false; + } + }); + }); + + compileModule.compile({}, ['unknown_type']); + + consoleStub.should.be.calledOnce(); + consoleStub.firstCall.args[0].should.containEql('ERROR'); + consoleStub.firstCall.args[0].should.containEql('unknown_type'); + consoleStub.firstCall.args[0].should.containEql('Unknown file information'); + }); + }); + + describe('single file compilation ->', function () { + it('should compile a template file and write output with .js extension', async function () { + const testFile = path.join(__dirname, 'compiler', 'test.twig'); + const written = new Promise(resolve => { + writeFileStub.callsFake((outputFile, output, encoding, cb) => { + cb(null); + resolve({outputFile, output, encoding}); + }); + }); + + compileModule.compile({}, [testFile]); + const {outputFile, output, encoding} = await written; + + outputFile.should.equal(testFile + '.js'); + encoding.should.equal('utf8'); + output.should.be.a.String(); + output.should.containEql('precompiled: true'); + }); + + it('should use the file path as the template id when no output directory is specified', async function () { + const testFile = path.join(__dirname, 'compiler', 'test.twig'); + const written = new Promise(resolve => { + writeFileStub.callsFake((outputFile, output, encoding, cb) => { + cb(null); + resolve(output); + }); + }); + + compileModule.compile({}, [testFile]); + const output = await written; + + output.should.containEql('id:"' + testFile + '"'); + }); + + it('should write compiled output to the output directory when specified', async function () { + const testFile = path.join(__dirname, 'compiler', 'test.twig'); + const outputDir = path.join(__dirname, 'compiler', 'output'); + const written = new Promise(resolve => { + writeFileStub.callsFake((outputFile, output, encoding, cb) => { + cb(null); + resolve({outputFile, output}); + }); + }); + + compileModule.compile({output: outputDir}, [testFile]); + const {outputFile, output} = await written; + + outputFile.should.equal(outputDir + '/test.twig.js'); + output.should.be.a.String(); + output.should.containEql('precompiled: true'); + }); + + it('should use the output directory in the template id when output is specified', async function () { + const testFile = path.join(__dirname, 'compiler', 'test.twig'); + const outputDir = path.join(__dirname, 'compiler', 'output'); + const written = new Promise(resolve => { + writeFileStub.callsFake((outputFile, output, encoding, cb) => { + cb(null); + resolve(output); + }); + }); + + compileModule.compile({output: outputDir}, [testFile]); + const output = await written; + + output.should.containEql('id:"' + outputDir + '/test.twig"'); + }); + + it('should log success message when compilation succeeds', async function () { + const consoleStub = sinon.stub(console, 'log'); + const testFile = path.join(__dirname, 'compiler', 'test.twig'); + const written = new Promise(resolve => { + writeFileStub.callsFake((outputFile, output, encoding, cb) => { + cb(null); + resolve(); + }); + }); + + compileModule.compile({}, [testFile]); + await written; + + consoleStub.should.be.calledOnce(); + consoleStub.firstCall.args[0].should.containEql('Compiled'); + consoleStub.firstCall.args[0].should.containEql(testFile); + consoleStub.firstCall.args[0].should.containEql(testFile + '.js'); + }); + + it('should log error when writeFile fails', async function () { + const consoleStub = sinon.stub(console, 'log'); + const testFile = path.join(__dirname, 'compiler', 'test.twig'); + const written = new Promise(resolve => { + writeFileStub.callsFake((outputFile, output, encoding, cb) => { + cb(new Error('disk full')); + resolve(); + }); + }); + + compileModule.compile({}, [testFile]); + await written; + + consoleStub.should.be.calledOnce(); + consoleStub.firstCall.args[0].should.containEql('Unable to compile'); + consoleStub.firstCall.args[0].should.containEql(testFile); + }); + + it('should pass options through to template.compile using default wrap format', async function () { + const testFile = path.join(__dirname, 'compiler', 'test.twig'); + const written = new Promise(resolve => { + writeFileStub.callsFake((outputFile, output, encoding, cb) => { + cb(null); + resolve(output); + }); + }); + + compileModule.compile({}, [testFile]); + const output = await written; + + output.should.startWith('twig({'); + output.should.containEql('precompiled: true'); + }); + }); + + describe('directory compilation ->', function () { + it('should walk a directory and compile all matching .twig files', async function () { + this.timeout(5000); + const srcDir = path.join(__dirname, 'compiler', 'src'); + const compiledFiles = []; + + writeFileStub.callsFake((outputFile, output, encoding, cb) => { + compiledFiles.push(outputFile); + output.should.be.a.String(); + output.should.containEql('precompiled: true'); + cb(null); + }); + + compileModule.compile({pattern: '*.twig'}, [srcDir]); + await delay(2000); + + // src/ contains dir_test.twig and sub/sub.twig + compiledFiles.length.should.equal(2); + compiledFiles.sort(); + + const dirTestFile = path.join(srcDir, 'dir_test.twig.js'); + const subFile = path.join(srcDir, 'sub', 'sub.twig.js'); + + compiledFiles.should.containEql(dirTestFile); + compiledFiles.should.containEql(subFile); + }); + + it('should only compile files matching the given pattern', async function () { + this.timeout(5000); + const srcDir = path.join(__dirname, 'compiler'); + const compiledFiles = []; + + writeFileStub.callsFake((outputFile, output, encoding, cb) => { + compiledFiles.push(path.basename(outputFile)); + cb(null); + }); + + compileModule.compile({pattern: '*.twig'}, [srcDir]); + await delay(2000); + + // Only .twig files should match, not test.html + compiledFiles.length.should.be.aboveOrEqual(1); + compiledFiles.forEach(file => { + file.should.endWith('.twig.js'); + }); + compiledFiles.should.not.containEql('test.html.js'); + }); + + it('should compile directory files into output directory', async function () { + this.timeout(5000); + const srcDir = path.join(__dirname, 'compiler', 'src'); + const outputDir = path.join(__dirname, 'compiler', 'build_output'); + const compiledFiles = []; + + writeFileStub.callsFake((outputFile, output, encoding, cb) => { + compiledFiles.push(outputFile); + outputFile.should.startWith(outputDir + '/'); + outputFile.should.endWith('.twig.js'); + cb(null); + }); + + compileModule.compile({output: outputDir, pattern: '*.twig'}, [srcDir]); + await delay(2000); + + compiledFiles.length.should.equal(2); + // mkdir is called once for the output dir and once for each + // subdirectory structure under it + mkdirStub.should.be.called(); + mkdirStub.firstCall.args[0].should.equal(outputDir); + }); + + it('should strip trailing slash from directory path', async function () { + this.timeout(5000); + const srcDir = path.join(__dirname, 'compiler', 'src') + '/'; + const compiledFiles = []; + + writeFileStub.callsFake((outputFile, output, encoding, cb) => { + compiledFiles.push(outputFile); + outputFile.should.not.containEql('//'); + cb(null); + }); + + compileModule.compile({pattern: '*.twig'}, [srcDir]); + await delay(2000); + + compiledFiles.length.should.be.aboveOrEqual(1); + }); + }); + + describe('using defaults ->', function () { + it('should use the default pattern to match only .twig files when compiling a directory', async function () { + this.timeout(5000); + // The compiler directory contains test.twig, test.html, and + // subdirectories with more .twig files — the default pattern + // *.twig should match .twig files and skip test.html. + const srcDir = path.join(__dirname, 'compiler'); + const compiledFiles = []; + + writeFileStub.callsFake((outputFile, output, encoding, cb) => { + compiledFiles.push(path.basename(outputFile)); + cb(null); + }); + + // Pass defaults directly, as the CLI does + compileModule.compile(compileModule.defaults, [srcDir]); + await delay(2000); + + compiledFiles.length.should.be.aboveOrEqual(1); + compiledFiles.forEach(file => { + file.should.endWith('.twig.js'); + }); + compiledFiles.should.not.containEql('test.html.js'); + }); + }); + + describe('mixed files and directories ->', function () { + it('should handle a mix of file and directory inputs', async function () { + this.timeout(5000); + const testFile = path.join(__dirname, 'compiler', 'test.twig'); + const srcDir = path.join(__dirname, 'compiler', 'src'); + const compiledFiles = []; + + writeFileStub.callsFake((outputFile, output, encoding, cb) => { + compiledFiles.push(outputFile); + cb(null); + }); + + compileModule.compile({pattern: '*.twig'}, [testFile, srcDir]); + await delay(2000); + + // test.twig (single file) + dir_test.twig + sub/sub.twig (from directory walk) + compiledFiles.length.should.equal(3); + }); + }); + }); +}); From 4adbf52cb0281f676a13b9d4838a101d12a9772e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20=C5=81=C4=85giewka?= Date: Tue, 12 May 2026 09:51:07 +0200 Subject: [PATCH 2/6] refactor: make everything in compile full sync --- lib/compile.js | 51 +++++++++++++++++++++----------------------- test/test.compile.js | 49 ++++++++++++++++++++---------------------- 2 files changed, 47 insertions(+), 53 deletions(-) diff --git a/lib/compile.js b/lib/compile.js index e36c19d4..8b93c11d 100644 --- a/lib/compile.js +++ b/lib/compile.js @@ -18,45 +18,41 @@ exports.compile = function (options, files) { } files.forEach(file => { - FS.stat(file, (err, stats) => { - if (err) { - console.error('ERROR ' + file + ': Unable to stat file'); - return; - } + const stats = FS.statSync(file); - if (stats.isDirectory()) { - parseTemplateFolder(file, options.pattern); - } else if (stats.isFile()) { - parseTemplateFile(file); - } else { - console.log('ERROR ' + file + ': Unknown file information'); - } - }); + if (stats.isDirectory()) { + parseTemplateFolder(file, options.pattern); + } else if (stats.isFile()) { + parseTemplateFile(file); + } else { + console.log('ERROR ' + file + ': Unknown file information'); + } }); function parseTemplateFolder(directory, pattern) { directory = PATHS.stripSlash(directory); - FS.readdir(directory, {recursive: true, withFileTypes: true}, (err, entries) => { + const entries = FS.readdirSync(directory, {recursive: true, withFileTypes: true}, (err, entries) => { if (err) { console.error('ERROR ' + directory + ': Unable to read directory'); return; } - for (const entry of entries) { - if (!entry.isFile()) { - continue; - } - - // Match against file pattern - const {name} = entry; - if (matchesGlob(name, pattern)) { - const root = PATHS.stripSlash(entry.parentPath || entry.path); - const file = root + '/' + name; - parseTemplateFile(file, directory); - } - } }); + + for (const entry of entries) { + if (!entry.isFile()) { + continue; + } + + // Match against file pattern + const {name} = entry; + if (matchesGlob(name, pattern)) { + const root = PATHS.stripSlash(entry.parentPath || entry.path); + const file = root + '/' + name; + parseTemplateFile(file, directory); + } + } } function parseTemplateFile(file, base) { @@ -89,6 +85,7 @@ exports.compile = function (options, files) { twig({ id: outputId, path: file, + async: false, load(template) { // Compile! const output = template.compile(options); diff --git a/test/test.compile.js b/test/test.compile.js index 4bd5b43c..08ac8ed4 100644 --- a/test/test.compile.js +++ b/test/test.compile.js @@ -52,9 +52,17 @@ describe('lib/compile ->', function () { }); }); - describe('fs.stat handling ->', function () { - it('should call fs.stat for each file', function () { - const statStub = sinon.stub(fs, 'stat'); + describe('fs.statSync handling ->', function () { + it('should call fs.statSync for each file', function () { + sinon.stub(console, 'log'); + const statStub = sinon.stub(fs, 'statSync').returns({ + isDirectory() { + return false; + }, + isFile() { + return false; + } + }); compileModule.compile({}, ['file1.twig', 'file2.twig', 'file3.twig']); statStub.should.be.calledThrice(); @@ -63,30 +71,23 @@ describe('lib/compile ->', function () { statStub.thirdCall.args[0].should.equal('file3.twig'); }); - it('should log error when fs.stat returns an error', function () { - const consoleStub = sinon.stub(console, 'error'); - sinon.stub(fs, 'stat').callsFake((file, cb) => { - cb(new Error('ENOENT')); - }); - - compileModule.compile({}, ['missing.twig']); + it('should throw when fs.statSync fails', function () { + sinon.stub(fs, 'statSync').throws(new Error('ENOENT')); - consoleStub.should.be.calledOnce(); - consoleStub.firstCall.args[0].should.containEql('missing.twig'); - consoleStub.firstCall.args[0].should.containEql('Unable to stat file'); + (function () { + compileModule.compile({}, ['missing.twig']); + }).should.throw('ENOENT'); }); it('should log error for unknown file types', function () { const consoleStub = sinon.stub(console, 'log'); - sinon.stub(fs, 'stat').callsFake((file, cb) => { - cb(null, { - isDirectory() { - return false; - }, - isFile() { - return false; - } - }); + sinon.stub(fs, 'statSync').returns({ + isDirectory() { + return false; + }, + isFile() { + return false; + } }); compileModule.compile({}, ['unknown_type']); @@ -222,7 +223,6 @@ describe('lib/compile ->', function () { describe('directory compilation ->', function () { it('should walk a directory and compile all matching .twig files', async function () { - this.timeout(5000); const srcDir = path.join(__dirname, 'compiler', 'src'); const compiledFiles = []; @@ -234,7 +234,6 @@ describe('lib/compile ->', function () { }); compileModule.compile({pattern: '*.twig'}, [srcDir]); - await delay(2000); // src/ contains dir_test.twig and sub/sub.twig compiledFiles.length.should.equal(2); @@ -248,7 +247,6 @@ describe('lib/compile ->', function () { }); it('should only compile files matching the given pattern', async function () { - this.timeout(5000); const srcDir = path.join(__dirname, 'compiler'); const compiledFiles = []; @@ -258,7 +256,6 @@ describe('lib/compile ->', function () { }); compileModule.compile({pattern: '*.twig'}, [srcDir]); - await delay(2000); // Only .twig files should match, not test.html compiledFiles.length.should.be.aboveOrEqual(1); From 092d98ecc21c306f97e2d0a9868a59ea652f788f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20=C5=81=C4=85giewka?= Date: Tue, 12 May 2026 09:57:29 +0200 Subject: [PATCH 3/6] test: drop remaining delays - run in full sync --- test/test.compile.js | 9 --------- 1 file changed, 9 deletions(-) diff --git a/test/test.compile.js b/test/test.compile.js index 08ac8ed4..b93f9b7f 100644 --- a/test/test.compile.js +++ b/test/test.compile.js @@ -1,7 +1,6 @@ const fs = require('fs'); const path = require('path'); const sinon = require('sinon'); -const {setTimeout: delay} = require('node:timers/promises'); require('should-sinon'); const Twig = require('..'); @@ -266,7 +265,6 @@ describe('lib/compile ->', function () { }); it('should compile directory files into output directory', async function () { - this.timeout(5000); const srcDir = path.join(__dirname, 'compiler', 'src'); const outputDir = path.join(__dirname, 'compiler', 'build_output'); const compiledFiles = []; @@ -279,7 +277,6 @@ describe('lib/compile ->', function () { }); compileModule.compile({output: outputDir, pattern: '*.twig'}, [srcDir]); - await delay(2000); compiledFiles.length.should.equal(2); // mkdir is called once for the output dir and once for each @@ -289,7 +286,6 @@ describe('lib/compile ->', function () { }); it('should strip trailing slash from directory path', async function () { - this.timeout(5000); const srcDir = path.join(__dirname, 'compiler', 'src') + '/'; const compiledFiles = []; @@ -300,7 +296,6 @@ describe('lib/compile ->', function () { }); compileModule.compile({pattern: '*.twig'}, [srcDir]); - await delay(2000); compiledFiles.length.should.be.aboveOrEqual(1); }); @@ -308,7 +303,6 @@ describe('lib/compile ->', function () { describe('using defaults ->', function () { it('should use the default pattern to match only .twig files when compiling a directory', async function () { - this.timeout(5000); // The compiler directory contains test.twig, test.html, and // subdirectories with more .twig files — the default pattern // *.twig should match .twig files and skip test.html. @@ -322,7 +316,6 @@ describe('lib/compile ->', function () { // Pass defaults directly, as the CLI does compileModule.compile(compileModule.defaults, [srcDir]); - await delay(2000); compiledFiles.length.should.be.aboveOrEqual(1); compiledFiles.forEach(file => { @@ -334,7 +327,6 @@ describe('lib/compile ->', function () { describe('mixed files and directories ->', function () { it('should handle a mix of file and directory inputs', async function () { - this.timeout(5000); const testFile = path.join(__dirname, 'compiler', 'test.twig'); const srcDir = path.join(__dirname, 'compiler', 'src'); const compiledFiles = []; @@ -345,7 +337,6 @@ describe('lib/compile ->', function () { }); compileModule.compile({pattern: '*.twig'}, [testFile, srcDir]); - await delay(2000); // test.twig (single file) + dir_test.twig + sub/sub.twig (from directory walk) compiledFiles.length.should.equal(3); From d20428c337044422ae4c73939c4b96f44b2f0590 Mon Sep 17 00:00:00 2001 From: Will Rowe Date: Tue, 12 May 2026 08:38:09 -0400 Subject: [PATCH 4/6] Remove reliance on async --- test/test.compile.js | 125 ++++++++++++++++--------------------------- 1 file changed, 45 insertions(+), 80 deletions(-) diff --git a/test/test.compile.js b/test/test.compile.js index b93f9b7f..61d7ea7b 100644 --- a/test/test.compile.js +++ b/test/test.compile.js @@ -99,129 +99,94 @@ describe('lib/compile ->', function () { }); describe('single file compilation ->', function () { - it('should compile a template file and write output with .js extension', async function () { + it('should compile a template file and write output with .js extension', function () { const testFile = path.join(__dirname, 'compiler', 'test.twig'); - const written = new Promise(resolve => { - writeFileStub.callsFake((outputFile, output, encoding, cb) => { - cb(null); - resolve({outputFile, output, encoding}); - }); + writeFileStub.callsFake((outputFile, output, encoding, cb) => { + cb(null); + outputFile.should.equal(testFile + '.js'); + encoding.should.equal('utf8'); + output.should.be.a.String(); + output.should.containEql('precompiled: true'); }); compileModule.compile({}, [testFile]); - const {outputFile, output, encoding} = await written; - - outputFile.should.equal(testFile + '.js'); - encoding.should.equal('utf8'); - output.should.be.a.String(); - output.should.containEql('precompiled: true'); }); - it('should use the file path as the template id when no output directory is specified', async function () { + it('should use the file path as the template id when no output directory is specified', function () { const testFile = path.join(__dirname, 'compiler', 'test.twig'); - const written = new Promise(resolve => { - writeFileStub.callsFake((outputFile, output, encoding, cb) => { - cb(null); - resolve(output); - }); + writeFileStub.callsFake((outputFile, output, encoding, cb) => { + cb(null); + output.should.containEql('id:"' + testFile + '"'); }); compileModule.compile({}, [testFile]); - const output = await written; - - output.should.containEql('id:"' + testFile + '"'); }); - it('should write compiled output to the output directory when specified', async function () { + it('should write compiled output to the output directory when specified', function () { const testFile = path.join(__dirname, 'compiler', 'test.twig'); const outputDir = path.join(__dirname, 'compiler', 'output'); - const written = new Promise(resolve => { - writeFileStub.callsFake((outputFile, output, encoding, cb) => { - cb(null); - resolve({outputFile, output}); - }); + writeFileStub.callsFake((outputFile, output, encoding, cb) => { + cb(null); + outputFile.should.equal(outputDir + '/test.twig.js'); + output.should.be.a.String(); + output.should.containEql('precompiled: true'); }); compileModule.compile({output: outputDir}, [testFile]); - const {outputFile, output} = await written; - - outputFile.should.equal(outputDir + '/test.twig.js'); - output.should.be.a.String(); - output.should.containEql('precompiled: true'); }); - it('should use the output directory in the template id when output is specified', async function () { + it('should use the output directory in the template id when output is specified', function () { const testFile = path.join(__dirname, 'compiler', 'test.twig'); const outputDir = path.join(__dirname, 'compiler', 'output'); - const written = new Promise(resolve => { - writeFileStub.callsFake((outputFile, output, encoding, cb) => { - cb(null); - resolve(output); - }); + writeFileStub.callsFake((outputFile, output, encoding, cb) => { + cb(null); + output.should.containEql('id:"' + outputDir + '/test.twig"'); }); compileModule.compile({output: outputDir}, [testFile]); - const output = await written; - - output.should.containEql('id:"' + outputDir + '/test.twig"'); }); - it('should log success message when compilation succeeds', async function () { + it('should log success message when compilation succeeds', function () { const consoleStub = sinon.stub(console, 'log'); const testFile = path.join(__dirname, 'compiler', 'test.twig'); - const written = new Promise(resolve => { - writeFileStub.callsFake((outputFile, output, encoding, cb) => { - cb(null); - resolve(); - }); + writeFileStub.callsFake((outputFile, output, encoding, cb) => { + cb(null); + consoleStub.should.be.calledOnce(); + consoleStub.firstCall.args[0].should.containEql('Compiled'); + consoleStub.firstCall.args[0].should.containEql(testFile); + consoleStub.firstCall.args[0].should.containEql(testFile + '.js'); }); compileModule.compile({}, [testFile]); - await written; - - consoleStub.should.be.calledOnce(); - consoleStub.firstCall.args[0].should.containEql('Compiled'); - consoleStub.firstCall.args[0].should.containEql(testFile); - consoleStub.firstCall.args[0].should.containEql(testFile + '.js'); }); - it('should log error when writeFile fails', async function () { + it('should log error when writeFile fails', function () { const consoleStub = sinon.stub(console, 'log'); const testFile = path.join(__dirname, 'compiler', 'test.twig'); - const written = new Promise(resolve => { - writeFileStub.callsFake((outputFile, output, encoding, cb) => { - cb(new Error('disk full')); - resolve(); - }); + writeFileStub.callsFake((outputFile, output, encoding, cb) => { + cb(new Error('disk full')); + consoleStub.should.be.calledOnce(); + consoleStub.firstCall.args[0].should.containEql('Unable to compile'); + consoleStub.firstCall.args[0].should.containEql(testFile); }); compileModule.compile({}, [testFile]); - await written; - - consoleStub.should.be.calledOnce(); - consoleStub.firstCall.args[0].should.containEql('Unable to compile'); - consoleStub.firstCall.args[0].should.containEql(testFile); }); - it('should pass options through to template.compile using default wrap format', async function () { + it('should pass options through to template.compile using default wrap format', function () { const testFile = path.join(__dirname, 'compiler', 'test.twig'); - const written = new Promise(resolve => { - writeFileStub.callsFake((outputFile, output, encoding, cb) => { - cb(null); - resolve(output); - }); + writeFileStub.callsFake((outputFile, output, encoding, cb) => { + cb(null); + output.should.startWith('twig({'); + output.should.containEql('precompiled: true'); }); compileModule.compile({}, [testFile]); - const output = await written; - - output.should.startWith('twig({'); - output.should.containEql('precompiled: true'); }); }); describe('directory compilation ->', function () { - it('should walk a directory and compile all matching .twig files', async function () { + it('should walk a directory and compile all matching .twig files', function () { const srcDir = path.join(__dirname, 'compiler', 'src'); const compiledFiles = []; @@ -245,7 +210,7 @@ describe('lib/compile ->', function () { compiledFiles.should.containEql(subFile); }); - it('should only compile files matching the given pattern', async function () { + it('should only compile files matching the given pattern', function () { const srcDir = path.join(__dirname, 'compiler'); const compiledFiles = []; @@ -264,7 +229,7 @@ describe('lib/compile ->', function () { compiledFiles.should.not.containEql('test.html.js'); }); - it('should compile directory files into output directory', async function () { + it('should compile directory files into output directory', function () { const srcDir = path.join(__dirname, 'compiler', 'src'); const outputDir = path.join(__dirname, 'compiler', 'build_output'); const compiledFiles = []; @@ -285,7 +250,7 @@ describe('lib/compile ->', function () { mkdirStub.firstCall.args[0].should.equal(outputDir); }); - it('should strip trailing slash from directory path', async function () { + it('should strip trailing slash from directory path', function () { const srcDir = path.join(__dirname, 'compiler', 'src') + '/'; const compiledFiles = []; @@ -302,7 +267,7 @@ describe('lib/compile ->', function () { }); describe('using defaults ->', function () { - it('should use the default pattern to match only .twig files when compiling a directory', async function () { + it('should use the default pattern to match only .twig files when compiling a directory', function () { // The compiler directory contains test.twig, test.html, and // subdirectories with more .twig files — the default pattern // *.twig should match .twig files and skip test.html. @@ -326,7 +291,7 @@ describe('lib/compile ->', function () { }); describe('mixed files and directories ->', function () { - it('should handle a mix of file and directory inputs', async function () { + it('should handle a mix of file and directory inputs', function () { const testFile = path.join(__dirname, 'compiler', 'test.twig'); const srcDir = path.join(__dirname, 'compiler', 'src'); const compiledFiles = []; From 80cd6e04d33cecbce75a685b39d3b41ce77859a8 Mon Sep 17 00:00:00 2001 From: Will Rowe Date: Tue, 12 May 2026 08:39:29 -0400 Subject: [PATCH 5/6] Remove invalid argument - `readdirSync` does not have a callback argument. --- lib/compile.js | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/lib/compile.js b/lib/compile.js index 8b93c11d..9818ff5f 100644 --- a/lib/compile.js +++ b/lib/compile.js @@ -32,13 +32,7 @@ exports.compile = function (options, files) { function parseTemplateFolder(directory, pattern) { directory = PATHS.stripSlash(directory); - const entries = FS.readdirSync(directory, {recursive: true, withFileTypes: true}, (err, entries) => { - if (err) { - console.error('ERROR ' + directory + ': Unable to read directory'); - return; - } - - }); + const entries = FS.readdirSync(directory, {recursive: true, withFileTypes: true}); for (const entry of entries) { if (!entry.isFile()) { From ab24fc3f6a7c45057edcc2a1939427edd4695931 Mon Sep 17 00:00:00 2001 From: Will Rowe Date: Tue, 12 May 2026 08:47:14 -0400 Subject: [PATCH 6/6] Remove unused 'recursive' option - This was leftover from the CLI version of the package before automatic directory walking was added. --- lib/compile.js | 1 - test/test.compile.js | 4 ---- 2 files changed, 5 deletions(-) diff --git a/lib/compile.js b/lib/compile.js index 9818ff5f..b5e42882 100644 --- a/lib/compile.js +++ b/lib/compile.js @@ -8,7 +8,6 @@ const {twig} = Twig; exports.defaults = { compress: false, pattern: '*.twig', - recursive: false }; exports.compile = function (options, files) { diff --git a/test/test.compile.js b/test/test.compile.js index 61d7ea7b..6a157afe 100644 --- a/test/test.compile.js +++ b/test/test.compile.js @@ -16,10 +16,6 @@ describe('lib/compile ->', function () { it('should have pattern set to *.twig', function () { compileModule.defaults.pattern.should.equal('*.twig'); }); - - it('should have recursive set to false', function () { - compileModule.defaults.recursive.should.equal(false); - }); }); describe('compile ->', function () {