diff --git a/package-lock.json b/package-lock.json index 20427552..db79d412 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,6 +39,7 @@ "oxlint": "^1.51.0", "semver": "^5.0.1", "tinybench": "^6.0.0", + "tstyche": "^6.2.0", "typescript": "^5.9.3", "uglify-js": "^3.19.3", "vitest": "^4.0.18" @@ -13211,6 +13212,30 @@ "integrity": "sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA==", "dev": true }, + "node_modules/tstyche": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tstyche/-/tstyche-6.2.0.tgz", + "integrity": "sha512-WBiB6fGGsmQCFFRGwYaKq528pCLJ0CUUdrIdUm2rnVJ1Ineskbrv9uQADWsD3uHndEV/Hesdx1Gj8PdkBahxlQ==", + "dev": true, + "license": "MIT", + "bin": { + "tstyche": "dist/bin.js" + }, + "engines": { + "node": ">=22.12" + }, + "funding": { + "url": "https://github.com/tstyche/tstyche?sponsor=1" + }, + "peerDependencies": { + "typescript": ">=5.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -13244,6 +13269,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -22876,6 +22902,13 @@ "integrity": "sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA==", "dev": true }, + "tstyche": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tstyche/-/tstyche-6.2.0.tgz", + "integrity": "sha512-WBiB6fGGsmQCFFRGwYaKq528pCLJ0CUUdrIdUm2rnVJ1Ineskbrv9uQADWsD3uHndEV/Hesdx1Gj8PdkBahxlQ==", + "dev": true, + "requires": {} + }, "type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -22899,7 +22932,8 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true + "dev": true, + "peer": true }, "uglify-js": { "version": "3.19.3", diff --git a/package.json b/package.json index 5c910e2b..e3851062 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "lint": "npm run lint:oxlint && npm run lint:format && npm run lint:types && npm run lint:compat", "lint:oxlint": "oxlint --max-warnings 0 .", "lint:format": "oxfmt --check .", - "lint:types": "tsc --noEmit --project types", + "lint:types": "tsc --noEmit --project types && tstyche", "lint:compat": "eslint", "test": "npm run build && vitest run --project node --project tasks --project rspack --coverage", "test:browser": "vitest run --project browser", @@ -85,6 +85,7 @@ "oxlint": "^1.51.0", "semver": "^5.0.1", "tinybench": "^6.0.0", + "tstyche": "^6.2.0", "typescript": "^5.9.3", "uglify-js": "^3.19.3", "vitest": "^4.0.18" diff --git a/spec/source-map.js b/spec/source-map.js index 0a428069..4565c3f0 100644 --- a/spec/source-map.js +++ b/spec/source-map.js @@ -25,7 +25,7 @@ describe('source-map', function () { expect(template.map).toBeTruthy(); } }); - it('should map source properly', function () { + it('should map source properly', async function () { var templateSource = ' b{{hello}} \n {{bar}}a {{#block arg hash=(subex 1 subval)}}{{/block}}', template = Handlebars.precompile(templateSource, { @@ -34,7 +34,7 @@ describe('source-map', function () { }); if (template.map) { - var consumer = new SourceMapConsumer(template.map), + var consumer = await new SourceMapConsumer(template.map), lines = template.code.split('\n'), srcLines = templateSource.split('\n'), generated = grepLine('" b"', lines), @@ -43,6 +43,7 @@ describe('source-map', function () { var mapped = consumer.originalPositionFor(generated); expect(mapped.line).toBe(source.line); expect(mapped.column).toBe(source.column); + consumer.destroy(); } }); }); diff --git a/tasks/tests/bin.test.js b/tasks/tests/bin.test.js deleted file mode 100644 index 36f3986b..00000000 --- a/tasks/tests/bin.test.js +++ /dev/null @@ -1,256 +0,0 @@ -const childProcess = require('child_process'); -const fs = require('fs'); -const os = require('os'); -const path = require('path'); - -const testCases = [ - { - binInputParameters: ['-a', 'spec/artifacts/empty.handlebars'], - outputLocation: 'stdout', - expectedOutputSpec: './spec/expected/empty.amd.js', - }, - { - binInputParameters: [ - '-a', - '-f', - 'TEST_OUTPUT', - 'spec/artifacts/empty.handlebars', - ], - outputLocation: 'TEST_OUTPUT', - expectedOutputSpec: './spec/expected/empty.amd.js', - }, - { - binInputParameters: [ - '-a', - '-n', - 'CustomNamespace.templates', - 'spec/artifacts/empty.handlebars', - ], - outputLocation: 'stdout', - expectedOutputSpec: './spec/expected/empty.amd.namespace.js', - }, - { - binInputParameters: [ - '-a', - '--namespace', - 'CustomNamespace.templates', - 'spec/artifacts/empty.handlebars', - ], - outputLocation: 'stdout', - expectedOutputSpec: './spec/expected/empty.amd.namespace.js', - }, - { - binInputParameters: ['-a', '-s', 'spec/artifacts/empty.handlebars'], - outputLocation: 'stdout', - expectedOutputSpec: './spec/expected/empty.amd.simple.js', - }, - { - binInputParameters: ['-a', '-m', 'spec/artifacts/empty.handlebars'], - outputLocation: 'stdout', - expectedOutputSpec: './spec/expected/empty.amd.min.js', - }, - { - binInputParameters: [ - 'spec/artifacts/known.helpers.handlebars', - '-a', - '-k', - 'someHelper', - '-k', - 'anotherHelper', - '-o', - ], - outputLocation: 'stdout', - expectedOutputSpec: './spec/expected/non.empty.amd.known.helper.js', - }, - { - binInputParameters: ['--help'], - outputLocation: 'stdout', - expectedOutputSpec: './spec/expected/help.menu.txt', - }, - { - binInputParameters: ['-v'], - outputLocation: 'stdout', - expectedOutput: require('../../lib').VERSION, - }, - { - binInputParameters: [ - '-a', - '-e', - 'hbs', - './spec/artifacts/non.default.extension.hbs', - ], - outputLocation: 'stdout', - expectedOutputSpec: './spec/expected/non.default.extension.amd.js', - }, - { - binInputParameters: [ - '-a', - '-p', - './spec/artifacts/partial.template.handlebars', - ], - outputLocation: 'stdout', - expectedOutputSpec: './spec/expected/partial.template.js', - }, - { - binInputParameters: ['spec/artifacts/empty.handlebars', '-c'], - outputLocation: 'stdout', - expectedOutputSpec: './spec/expected/empty.common.js', - }, - { - binInputParameters: [ - 'spec/artifacts/empty.handlebars', - 'spec/artifacts/empty.handlebars', - '-a', - '-n', - 'someNameSpace', - ], - outputLocation: 'stdout', - expectedOutputSpec: './spec/expected/namespace.amd.js', - }, - { - binInputParameters: [ - 'spec/artifacts/empty.handlebars', - '-h', - 'some-path/', - '-a', - ], - outputLocation: 'stdout', - expectedOutputSpec: './spec/expected/handlebar.path.amd.js', - }, - { - binInputParameters: [ - 'spec/artifacts/partial.template.handlebars', - '-r', - 'spec', - '-a', - ], - outputLocation: 'stdout', - expectedOutputSpec: './spec/expected/empty.root.amd.js', - }, - { - binInputParameters: [ - '-i', - '
1
', - '-i', - '
2
', - '-N', - 'firstTemplate', - '-N', - 'secondTemplate', - '-a', - ], - outputLocation: 'stdout', - expectedOutputSpec: './spec/expected/empty.name.amd.js', - }, - { - binInputParameters: [ - '-i', - '
1
', - '-a', - '-m', - '-N', - 'test', - '--map', - './spec/tmp/source.map.amd.txt', - ], - outputLocation: 'stdout', - expectedOutputSpec: './spec/expected/source.map.amd.js', - }, - { - binInputParameters: ['./spec/artifacts/bom.handlebars', '-b', '-a'], - outputLocation: 'stdout', - expectedOutputSpec: './spec/expected/bom.amd.js', - }, - // Issue #1673 - { - binInputParameters: [ - '--amd', - '--no-amd', - 'spec/artifacts/empty.handlebars', - ], - outputLocation: 'stdout', - expectedOutputSpec: './spec/expected/empty.common.js', - }, -]; - -expect.extend({ - toEqualWithRelaxedSpace(received, expected) { - const normalize = (str) => - typeof str === 'string' - ? str - .replace(/\r\n/g, '\n') - .split('\n') - .map((line) => line.replace(/\s+/g, ' ').trim()) - .filter((line) => line.length > 0) - .join('\n') - .trim() - : str; - - const normalizedReceived = normalize(received); - const normalizedExpected = normalize(expected); - const pass = normalizedReceived === normalizedExpected; - - return { - pass, - message: () => - `Expected output to match with relaxed whitespace.\n\n` + - `Expected:\n${normalizedExpected}\n\nReceived:\n${normalizedReceived}`, - }; - }, -}); - -describe('bin/handlebars', function () { - testCases.forEach( - ( - { - binInputParameters, - outputLocation, - expectedOutputSpec, - expectedOutput, - }, - index - ) => { - it(`test case ${index}: handlebars ${binInputParameters.join( - ' ' - )}`, function () { - const stdout = executeBinHandlebars(...binInputParameters); - - if (!expectedOutput && expectedOutputSpec) { - expectedOutput = fs.readFileSync(expectedOutputSpec, 'utf-8'); - } - - const useStdout = outputLocation === 'stdout'; - const actualOutput = useStdout - ? stdout - : fs.readFileSync(outputLocation, 'utf-8'); - - if (!useStdout) { - fs.unlinkSync(outputLocation); - } - - expect(actualOutput).toEqualWithRelaxedSpace(expectedOutput); - }); - } - ); -}); - -// helper functions - -function executeBinHandlebars(...args) { - if (os.platform() === 'win32') { - // On Windows, the executable handlebars.js file cannot be run directly - const nodeJs = process.argv[0]; - return execFilesSyncUtf8(nodeJs, ['./bin/handlebars.js'].concat(args)); - } - return execFilesSyncUtf8('./bin/handlebars.js', args); -} - -function execFilesSyncUtf8(command, args) { - const env = process.env; - env.PATH = addPathToNodeJs(env.PATH); - return childProcess.execFileSync(command, args, { encoding: 'utf-8', env }); -} - -function addPathToNodeJs(pathEnvironment) { - return path.dirname(process.argv0) + path.delimiter + pathEnvironment; -} diff --git a/tasks/tests/cli.test.js b/tasks/tests/cli.test.js index e2c718b3..28934d4f 100644 --- a/tasks/tests/cli.test.js +++ b/tasks/tests/cli.test.js @@ -1,87 +1,128 @@ +const fs = require('fs'); const { execCommand, FileTestHelper } = require('cli-testlab'); const Handlebars = require('../../lib'); const cli = 'node ./bin/handlebars.js'; -describe('bin/handlebars (cli-testlab)', function () { +expect.extend({ + toEqualWithRelaxedSpace(received, expected) { + const normalize = (str) => + typeof str === 'string' + ? str + .replace(/\r\n/g, '\n') + .split('\n') + .map((line) => line.replace(/\s+/g, ' ').trim()) + .filter((line) => line.length > 0) + .join('\n') + .trim() + : str; + + const normalizedReceived = normalize(received); + const normalizedExpected = normalize(expected); + const pass = normalizedReceived === normalizedExpected; + + return { + pass, + message: () => + `Expected output to match with relaxed whitespace.\n\n` + + `Expected:\n${normalizedExpected}\n\nReceived:\n${normalizedReceived}`, + }; + }, +}); + +function expectedFile(specPath) { + return fs.readFileSync(specPath, 'utf-8'); +} + +describe('bin/handlebars', function () { describe('help and version', function () { - it('should display help menu with --help', async function () { - await execCommand(`${cli} --help`, { - expectedOutput: [ - 'Precompile handlebar templates.', - 'Usage:', - '--output', - '--amd', - '--commonjs', - '--partial', - '--extension', - '--bom', - ], - }); + it('--help displays help menu', async function () { + const result = await execCommand(`${cli} --help`); + expect(result.stdout).toEqualWithRelaxedSpace( + expectedFile('./spec/expected/help.menu.txt') + ); }); - it('should display version with -v', async function () { + it('no arguments displays help menu', async function () { + const result = await execCommand(`${cli}`); + expect(result.stdout).toEqualWithRelaxedSpace( + expectedFile('./spec/expected/help.menu.txt') + ); + }); + + it('-v prints the compiler version', async function () { await execCommand(`${cli} -v`, { expectedOutput: Handlebars.VERSION, }); }); - - it('should display help when no arguments are provided', async function () { - await execCommand(`${cli}`, { - expectedOutput: 'Precompile handlebar templates.', - }); - }); }); - describe('precompilation output modes', function () { - it('should precompile a template in AMD mode', async function () { + describe('AMD output', function () { + it('-a produces AMD output', async function () { const result = await execCommand( `${cli} -a spec/artifacts/empty.handlebars` ); - expect(result.stdout).toContain("define(['handlebars.runtime']"); - expect(result.stdout).toContain("templates['empty']"); - }); - - it('should precompile a template in CommonJS mode', async function () { - const result = await execCommand( - `${cli} spec/artifacts/empty.handlebars -c` + expect(result.stdout).toEqualWithRelaxedSpace( + expectedFile('./spec/expected/empty.amd.js') ); - expect(result.stdout).toContain('Handlebars.template'); - expect(result.stdout).not.toContain("define(['handlebars.runtime']"); }); - it('should precompile in simple mode', async function () { + it('-a -s produces simple AMD output', async function () { const result = await execCommand( `${cli} -a -s spec/artifacts/empty.handlebars` ); - expect(result.stdout).toContain('"compiler"'); - expect(result.stdout).toContain('"main"'); - expect(result.stdout).not.toContain("templates['empty']"); + expect(result.stdout).toEqualWithRelaxedSpace( + expectedFile('./spec/expected/empty.amd.simple.js') + ); }); - it('should precompile with minification', async function () { + it('-a -m produces minified AMD output', async function () { const result = await execCommand( `${cli} -a -m spec/artifacts/empty.handlebars` ); - expect(result.stdout).toContain('define('); - expect(result.stdout).toContain('handlebars.runtime'); + expect(result.stdout).toEqualWithRelaxedSpace( + expectedFile('./spec/expected/empty.amd.min.js') + ); }); }); - describe('custom namespace', function () { - it('should use custom namespace with -n', async function () { + describe('CommonJS output', function () { + it('-c produces CommonJS output', async function () { + const result = await execCommand( + `${cli} spec/artifacts/empty.handlebars -c` + ); + expect(result.stdout).toEqualWithRelaxedSpace( + expectedFile('./spec/expected/empty.common.js') + ); + }); + }); + + describe('namespace', function () { + it('-n sets custom namespace', async function () { const result = await execCommand( `${cli} -a -n CustomNamespace.templates spec/artifacts/empty.handlebars` ); - expect(result.stdout).toContain('CustomNamespace.templates'); - expect(result.stdout).not.toContain('Handlebars.templates'); + expect(result.stdout).toEqualWithRelaxedSpace( + expectedFile('./spec/expected/empty.amd.namespace.js') + ); }); - it('should use custom namespace with --namespace', async function () { + it('--namespace sets custom namespace', async function () { const result = await execCommand( `${cli} -a --namespace CustomNamespace.templates spec/artifacts/empty.handlebars` ); - expect(result.stdout).toContain('CustomNamespace.templates'); + expect(result.stdout).toEqualWithRelaxedSpace( + expectedFile('./spec/expected/empty.amd.namespace.js') + ); + }); + + it('multiple files share a namespace', async function () { + const result = await execCommand( + `${cli} spec/artifacts/empty.handlebars spec/artifacts/empty.handlebars -a -n someNameSpace` + ); + expect(result.stdout).toEqualWithRelaxedSpace( + expectedFile('./spec/expected/namespace.amd.js') + ); }); }); @@ -97,8 +138,8 @@ describe('bin/handlebars (cli-testlab)', function () { files.cleanup(); }); - it('should write output to a file with -f', async function () { - const outputFile = 'tmp/cli-testlab-output.js'; + it('-f writes output to a file', async function () { + const outputFile = 'tmp/cli-test-output.js'; files.registerForCleanup(outputFile); await execCommand( @@ -107,21 +148,22 @@ describe('bin/handlebars (cli-testlab)', function () { expect(files.fileExists(outputFile)).toBe(true); const content = files.getFileTextContent(outputFile); - expect(content).toContain("define(['handlebars.runtime']"); - expect(content).toContain("templates['empty']"); + expect(content).toEqualWithRelaxedSpace( + expectedFile('./spec/expected/empty.amd.js') + ); }); - it('should generate source map file with --map', async function () { - const mapFile = 'tmp/cli-testlab-source.map'; + it('--map writes source map and appends sourceMappingURL', async function () { + const mapFile = 'tmp/cli-test-source.map'; files.registerForCleanup(mapFile); - await execCommand( + const result = await execCommand( `${cli} -i "
1
" -a -m -N test --map ${mapFile}` ); + expect(result.stdout).toContain('sourceMappingURL='); expect(files.fileExists(mapFile)).toBe(true); - const mapContent = files.getFileTextContent(mapFile); - const parsed = JSON.parse(mapContent); + const parsed = JSON.parse(files.getFileTextContent(mapFile)); expect(parsed).toHaveProperty('version', 3); expect(parsed).toHaveProperty('sources'); expect(parsed).toHaveProperty('mappings'); @@ -129,39 +171,63 @@ describe('bin/handlebars (cli-testlab)', function () { }); describe('template options', function () { - it('should support custom extension with -e', async function () { + it('-e sets custom extension', async function () { const result = await execCommand( `${cli} -a -e hbs ./spec/artifacts/non.default.extension.hbs` ); - expect(result.stdout).toContain("define(['handlebars.runtime']"); - expect(result.stdout).toContain("templates['non.default.extension']"); + expect(result.stdout).toEqualWithRelaxedSpace( + expectedFile('./spec/expected/non.default.extension.amd.js') + ); }); - it('should compile as partial with -p', async function () { + it('-p compiles as partial', async function () { const result = await execCommand( `${cli} -a -p ./spec/artifacts/partial.template.handlebars` ); - expect(result.stdout).toContain('Handlebars.partials'); + expect(result.stdout).toEqualWithRelaxedSpace( + expectedFile('./spec/expected/partial.template.js') + ); }); - it('should strip root from template names with -r', async function () { + it('-r strips root from template names', async function () { const result = await execCommand( `${cli} spec/artifacts/partial.template.handlebars -r spec -a` ); - expect(result.stdout).not.toContain("templates['spec/"); + expect(result.stdout).toEqualWithRelaxedSpace( + expectedFile('./spec/expected/empty.root.amd.js') + ); }); - it('should strip BOM with -b', async function () { + it('-b strips BOM', async function () { const result = await execCommand( `${cli} ./spec/artifacts/bom.handlebars -b -a` ); - expect(result.stdout).not.toContain('\uFEFF'); - expect(result.stdout).toContain("define(['handlebars.runtime']"); + expect(result.stdout).toEqualWithRelaxedSpace( + expectedFile('./spec/expected/bom.amd.js') + ); + }); + + it('-h sets custom handlebar path', async function () { + const result = await execCommand( + `${cli} spec/artifacts/empty.handlebars -h some-path/ -a` + ); + expect(result.stdout).toEqualWithRelaxedSpace( + expectedFile('./spec/expected/handlebar.path.amd.js') + ); + }); + + it('-k -o sets known helpers only', async function () { + const result = await execCommand( + `${cli} spec/artifacts/known.helpers.handlebars -a -k someHelper -k anotherHelper -o` + ); + expect(result.stdout).toEqualWithRelaxedSpace( + expectedFile('./spec/expected/non.empty.amd.known.helper.js') + ); }); }); describe('inline templates', function () { - it('should compile inline template with -i', async function () { + it('-i compiles inline template', async function () { const result = await execCommand( `${cli} -i "
hello
" -a -N myTemplate` ); @@ -169,52 +235,24 @@ describe('bin/handlebars (cli-testlab)', function () { expect(result.stdout).toContain("templates['myTemplate']"); }); - it('should compile multiple inline templates', async function () { + it('-i compiles multiple inline templates with -N', async function () { const result = await execCommand( - `${cli} -i "
1
" -i "
2
" -N first -N second -a` + `${cli} -i "
1
" -i "
2
" -N firstTemplate -N secondTemplate -a` ); - expect(result.stdout).toContain("templates['first']"); - expect(result.stdout).toContain("templates['second']"); - }); - }); - - describe('known helpers', function () { - it('should accept known helpers with -k', async function () { - const result = await execCommand( - `${cli} spec/artifacts/known.helpers.handlebars -a -k someHelper -k anotherHelper -o` - ); - expect(result.stdout).toContain("define(['handlebars.runtime']"); - }); - }); - - describe('handlebar path', function () { - it('should accept custom handlebar path with -h', async function () { - const result = await execCommand( - `${cli} spec/artifacts/empty.handlebars -h some-path/ -a` - ); - expect(result.stdout).toContain( - "define(['some-path/handlebars.runtime']" + expect(result.stdout).toEqualWithRelaxedSpace( + expectedFile('./spec/expected/empty.name.amd.js') ); }); }); describe('negated boolean flags', function () { - it('should support --no-amd to negate --amd (issue #1673)', async function () { + it('--no-amd negates --amd (issue #1673)', async function () { const result = await execCommand( `${cli} --amd --no-amd spec/artifacts/empty.handlebars` ); - expect(result.stdout).not.toContain("define(['handlebars.runtime']"); - expect(result.stdout).toContain('Handlebars.template'); - }); - }); - - describe('multiple files', function () { - it('should precompile multiple files into a single output', async function () { - const result = await execCommand( - `${cli} spec/artifacts/empty.handlebars spec/artifacts/empty.handlebars -a -n someNameSpace` + expect(result.stdout).toEqualWithRelaxedSpace( + expectedFile('./spec/expected/empty.common.js') ); - expect(result.stdout).toContain('someNameSpace'); - expect(result.stdout).toContain("define(['handlebars.runtime']"); }); }); }); diff --git a/tstyche.config.json b/tstyche.config.json new file mode 100644 index 00000000..c497ad0d --- /dev/null +++ b/tstyche.config.json @@ -0,0 +1,4 @@ +{ + "testFileMatch": ["types/__typetests__/**/*.tst.ts"], + "tsconfig": "./types/tsconfig.json" +} diff --git a/types/__typetests__/handlebars.tst.ts b/types/__typetests__/handlebars.tst.ts new file mode 100644 index 00000000..05835f0d --- /dev/null +++ b/types/__typetests__/handlebars.tst.ts @@ -0,0 +1,645 @@ +import { expect, test, describe } from 'tstyche'; +import Handlebars from 'handlebars'; +import { + HandlebarsTemplatable, + HandlebarsTemplateDelegate, + HandlebarsTemplates, + TemplateSpecification, + CompileOptions, + PrecompileOptions, + RuntimeOptions, + KnownHelpers, + BuiltinHelperName, + CustomHelperName, + Logger, + CompilerInfo, + hbs, +} from 'handlebars'; + +// --------------------------------------------------------------------------- +// Handlebars.compile +// --------------------------------------------------------------------------- +describe('Handlebars.compile', () => { + test('returns a HandlebarsTemplateDelegate', () => { + expect(Handlebars.compile('{{name}}')).type.toBe< + HandlebarsTemplateDelegate + >(); + }); + + test('returns a typed delegate when generic is specified', () => { + expect(Handlebars.compile<{ name: string }>('{{name}}')).type.toBe< + HandlebarsTemplateDelegate<{ name: string }> + >(); + }); + + test('compiled template returns string', () => { + const template = Handlebars.compile('{{name}}'); + expect(template({})).type.toBe(); + }); + + test('accepts CompileOptions', () => { + expect( + Handlebars.compile('test', { strict: true, noEscape: true }) + ).type.not.toRaiseError(); + }); + + test('accepts knownHelpers with built-in and custom names', () => { + expect( + Handlebars.compile('test', { + knownHelpers: { each: true, customHelper: true }, + }) + ).type.not.toRaiseError(); + }); +}); + +// --------------------------------------------------------------------------- +// Handlebars.precompile +// --------------------------------------------------------------------------- +describe('Handlebars.precompile', () => { + test('returns TemplateSpecification', () => { + expect( + Handlebars.precompile('{{name}}') + ).type.toBe(); + }); + + test('accepts PrecompileOptions', () => { + expect( + Handlebars.precompile('test', { srcName: 'a.hbs', destName: 'a.js' }) + ).type.not.toRaiseError(); + }); +}); + +// --------------------------------------------------------------------------- +// Handlebars.template +// --------------------------------------------------------------------------- +describe('Handlebars.template', () => { + test('returns HandlebarsTemplateDelegate', () => { + const spec = {} as TemplateSpecification; + expect(Handlebars.template(spec)).type.toBe< + HandlebarsTemplateDelegate + >(); + }); +}); + +// --------------------------------------------------------------------------- +// Handlebars.registerHelper / unregisterHelper +// --------------------------------------------------------------------------- +describe('Handlebars.registerHelper', () => { + test('accepts name and function', () => { + expect( + Handlebars.registerHelper('myHelper', (ctx: any) => 'result') + ).type.toBe(); + }); + + test('accepts a spec object', () => { + expect( + Handlebars.registerHelper({ myHelper: () => 'result' }) + ).type.toBe(); + }); +}); + +describe('Handlebars.unregisterHelper', () => { + test('accepts a name', () => { + expect(Handlebars.unregisterHelper('myHelper')).type.toBe(); + }); +}); + +// --------------------------------------------------------------------------- +// Handlebars.registerPartial / unregisterPartial +// --------------------------------------------------------------------------- +describe('Handlebars.registerPartial', () => { + test('accepts name and string', () => { + expect( + Handlebars.registerPartial('myPartial', '
{{name}}
') + ).type.toBe(); + }); + + test('accepts spec object', () => { + expect( + Handlebars.registerPartial({ myPartial: '
{{name}}
' }) + ).type.toBe(); + }); +}); + +describe('Handlebars.unregisterPartial', () => { + test('accepts a name', () => { + expect(Handlebars.unregisterPartial('myPartial')).type.toBe(); + }); +}); + +// --------------------------------------------------------------------------- +// Handlebars.registerDecorator / unregisterDecorator +// --------------------------------------------------------------------------- +describe('Handlebars.registerDecorator', () => { + test('accepts name and function', () => { + expect( + Handlebars.registerDecorator('myDecorator', () => {}) + ).type.toBe(); + }); +}); + +describe('Handlebars.unregisterDecorator', () => { + test('accepts a name', () => { + expect(Handlebars.unregisterDecorator('myDecorator')).type.toBe(); + }); +}); + +// --------------------------------------------------------------------------- +// Handlebars utility functions +// --------------------------------------------------------------------------- +describe('Handlebars utility functions', () => { + test('K returns void', () => { + expect(Handlebars.K()).type.toBe(); + }); + + test('createFrame returns any', () => { + expect(Handlebars.createFrame({})).type.toBe(); + }); + + test('blockParams returns any[]', () => { + expect(Handlebars.blockParams([], [])).type.toBe(); + }); + + test('log returns void', () => { + expect(Handlebars.log(1, 'message')).type.toBe(); + }); + + test('noConflict returns typeof Handlebars', () => { + expect(Handlebars.noConflict()).type.toBe(); + }); + + test('escapeExpression is exported at top level', () => { + expect(Handlebars.escapeExpression('test')).type.toBe(); + }); +}); + +// --------------------------------------------------------------------------- +// Handlebars.HelperOptions +// --------------------------------------------------------------------------- +describe('Handlebars.HelperOptions', () => { + test('fn and inverse are callable TemplateDelegates', () => { + const options = {} as Handlebars.HelperOptions; + expect(options.fn({})).type.toBe(); + expect(options.inverse({})).type.toBe(); + }); + + test('hash is a Record', () => { + const options = {} as Handlebars.HelperOptions; + expect(options.hash).type.toBe>(); + }); +}); + +// --------------------------------------------------------------------------- +// Handlebars.SafeString +// --------------------------------------------------------------------------- +describe('Handlebars.SafeString', () => { + test('is constructable with string', () => { + expect( + new Handlebars.SafeString('bold') + ).type.toBe(); + }); + + test('toString returns string', () => { + const safe = new Handlebars.SafeString('test'); + expect(safe.toString()).type.toBe(); + }); + + test('toHTML returns string', () => { + const safe = new Handlebars.SafeString('test'); + expect(safe.toHTML()).type.toBe(); + }); +}); + +// --------------------------------------------------------------------------- +// Handlebars.Exception +// --------------------------------------------------------------------------- +describe('Handlebars.Exception', () => { + test('is constructable with message only', () => { + expect(new Handlebars.Exception('error')).type.toBe(); + }); + + test('is constructable with message and node', () => { + expect( + new Handlebars.Exception('error', { + type: 'MustacheStatement', + loc: { + source: 'source', + start: { line: 1, column: 0 }, + end: { line: 1, column: 10 }, + }, + }) + ).type.not.toRaiseError(); + }); + + test('has all expected fields', () => { + const ex = new Handlebars.Exception('msg'); + expect(ex.message).type.toBe(); + expect(ex.name).type.toBe(); + expect(ex.description).type.toBe(); + expect(ex.fileName).type.toBe(); + expect(ex.number).type.toBe(); + expect(ex.lineNumber).type.toBe(); + expect(ex.endLineNumber).type.toBe(); + expect(ex.column).type.toBe(); + expect(ex.endColumn).type.toBe(); + expect(ex.stack).type.toBe(); + }); +}); + +// --------------------------------------------------------------------------- +// Handlebars.Utils +// --------------------------------------------------------------------------- +describe('Handlebars.Utils', () => { + test('escapeExpression returns string', () => { + expect(Handlebars.Utils.escapeExpression('" -); - -Handlebars.helpers !== undefined; - -const parsedTmpl = Handlebars.parse('

Hello, my name is {{name}}.

', { - srcName: '/foo/bar/baz.hbs', - ignoreStandalone: true, -}); - -const parsedTmplWithoutOptions = Handlebars.parse( - '

Hello, my name is {{name}}.

' -); - -// Custom partial resolution. -const originalResolvePartial = Handlebars.VM.resolvePartial; -Handlebars.VM.resolvePartial = ( - partial: HandlebarsTemplateDelegate | undefined, - context: any, - options: Handlebars.ResolvePartialOptions -): HandlebarsTemplateDelegate => { - const name = options.name.replace(/my/, 'your'); - // transform name. - options.name = name; - return originalResolvePartial(partial, context, options); -}; - -// #1544, allow custom helpers in knownHelpers -Handlebars.compile('test', { - knownHelpers: { - each: true, - customHelper: true, - }, -}); - -Handlebars.compile('test')({}, { allowCallsToHelperMissing: true }); - -Handlebars.compile('test')({}, {}); - -const allthings = {} as - | hbs.AST.MustacheStatement - | hbs.AST.BlockStatement - | hbs.AST.PartialStatement - | hbs.AST.PartialBlockStatement - | hbs.AST.ContentStatement - | hbs.AST.CommentStatement - | hbs.AST.SubExpression - | hbs.AST.PathExpression - | hbs.AST.StringLiteral - | hbs.AST.BooleanLiteral - | hbs.AST.NumberLiteral - | hbs.AST.UndefinedLiteral - | hbs.AST.NullLiteral - | hbs.AST.Hash - | hbs.AST.HashPair; - -switch (allthings.type) { - case 'MustacheStatement': - let mustacheStatement: hbs.AST.MustacheStatement; - mustacheStatement = allthings; - break; - case 'BlockStatement': - let blockStatement: hbs.AST.BlockStatement; - blockStatement = allthings; - break; - case 'PartialStatement': - let partialStatement: hbs.AST.PartialStatement; - partialStatement = allthings; - break; - case 'PartialBlockStatement': - let partialBlockStatement: hbs.AST.PartialBlockStatement; - partialBlockStatement = allthings; - break; - case 'ContentStatement': - let ContentStatement: hbs.AST.ContentStatement; - ContentStatement = allthings; - break; - case 'CommentStatement': - let CommentStatement: hbs.AST.CommentStatement; - CommentStatement = allthings; - break; - case 'SubExpression': - let SubExpression: hbs.AST.SubExpression; - SubExpression = allthings; - break; - case 'PathExpression': - let PathExpression: hbs.AST.PathExpression; - PathExpression = allthings; - break; - case 'StringLiteral': - let StringLiteral: hbs.AST.StringLiteral; - StringLiteral = allthings; - break; - case 'BooleanLiteral': - let BooleanLiteral: hbs.AST.BooleanLiteral; - BooleanLiteral = allthings; - break; - case 'NumberLiteral': - let NumberLiteral: hbs.AST.NumberLiteral; - NumberLiteral = allthings; - break; - case 'UndefinedLiteral': - let UndefinedLiteral: hbs.AST.UndefinedLiteral; - UndefinedLiteral = allthings; - break; - case 'NullLiteral': - let NullLiteral: hbs.AST.NullLiteral; - NullLiteral = allthings; - break; - case 'Hash': - let Hash: hbs.AST.Hash; - Hash = allthings; - break; - case 'HashPair': - let HashPair: hbs.AST.HashPair; - HashPair = allthings; - break; - default: - break; -} - -function testParseWithoutProcessing() { - const parsedTemplate: hbs.AST.Program = Handlebars.parseWithoutProcessing( - '

Hello, my name is {{name}}.

', - { - srcName: '/foo/bar/baz.hbs', - } - ); - - const parsedTemplateWithoutOptions: hbs.AST.Program = - Handlebars.parseWithoutProcessing('

Hello, my name is {{name}}.

'); -} - -function testExceptionTypings() { - // Test exception constructor with a single argument - message. - let exception: Handlebars.Exception = new Handlebars.Exception('message'); - - // Fields - let message: string = exception.message; - let lineNumber: number = exception.lineNumber; - let column: number = exception.column; - let endLineNumber: number = exception.endLineNumber; - let endColumn: number = exception.endColumn; - let description = exception.description; - let name: string = exception.name; - let fileName: string = exception.fileName; - let stack: string | undefined = exception.stack; -} - -function testExceptionWithNodeTypings() { - // Test exception constructor with both arguments. - const exception: Handlebars.Exception = new Handlebars.Exception('message', { - type: 'MustacheStatement', - loc: { - source: 'source', - start: { line: 1, column: 5 }, - end: { line: 10, column: 2 }, - }, - }); - - // Fields - let message: string = exception.message; - let lineNumber: number = exception.lineNumber; - let column: number = exception.column; - let endLineNumber: number = exception.endLineNumber; - let endColumn: number = exception.endColumn; - let description = exception.description; - let name: string = exception.name; - let fileName: string = exception.fileName; - let stack: string | undefined = exception.stack; -} - -function testProtoAccessControlControlOptions() { - Handlebars.compile('test')( - {}, - { - allowedProtoMethods: { allowedMethod: true, forbiddenMethod: false }, - allowedProtoProperties: { - allowedProperty: true, - forbiddenProperty: false, - }, - allowProtoMethodsByDefault: true, - allowProtoPropertiesByDefault: false, - partials: { - link: '{{name}}', - }, - } - ); -} - -function testHandlebarsVersion() { - let version: string = Handlebars.VERSION; -}