Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 40 additions & 13 deletions Gruntfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

'use strict';

var path = require('path');
var LIVERELOAD_PORT = 35729;
var lrSnippet = require('connect-livereload')({
port: LIVERELOAD_PORT
Expand All @@ -15,6 +14,39 @@ var mountFolder = function(connect, dir) {
};

var previewMiddleware = require('./lib/preview-middleware').createPreviewMiddleware;
var validationMiddleware = require('./lib/validation-middleware').createValidationMiddleware;

function runValidateTask(grunt, done, soft) {
var validateBundle = require('./lib/validate-bundle').validateBundle;
var printValidationResult = require('./lib/format-validation-output').printValidationResult;
var config = require('./lib/preview-config').readPreviewConfig();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Esta chamada está fora da cadeia de promises e fora de try/catch. Com preview.config.json malformado, ausente ou sem bundleDir/defaultLocale, ela lança de forma síncrona: o grunt aborta a task, done() não é chamado e connect/watch não sobem — sem servidor e sem banner, exatamente o oposto do que validate:soft promete. Todos os outros erros nascem dentro de validateBundle() e o .catch da linha 41 trata; só esta linha escapa.

Vale notar que é uma regressão: a versão anterior rodava a validação em processo separado (spawnSync), então esse erro ficava contido. Envolver em try/catch e, no modo soft, logar e chamar done().


validateBundle(config)
.then(function(result) {
printValidationResult(result, {
suffix: ' — template at template/' + config.bundleDir,
});

if (!result.ok) {
if (soft) {
grunt.log.error('Template validation failed. Fix reported errors.');
done();
return;
}
grunt.fail.fatal('Template validation failed. Fix the bundle before previewing.');
}

done();
})
.catch(function(error) {
if (soft) {
grunt.log.error(error);
done();
return;
}
grunt.fail.fatal(error);
});
}

module.exports = function(grunt) {

Expand All @@ -31,6 +63,7 @@ module.exports = function(grunt) {
middleware: function(connect) {
return [
lrSnippet,
validationMiddleware(),
previewMiddleware(),
mountFolder(connect, 'template'),
mountFolder(connect, 'src')
Expand All @@ -45,7 +78,7 @@ module.exports = function(grunt) {
'template/**/*.{html,css,json,png,jpg,jpeg,webp}',
'lib/**/*.js'
],
tasks: ['validate']
tasks: ['validate:soft']
},
livereload: {
options: {
Expand All @@ -63,21 +96,15 @@ module.exports = function(grunt) {
});

grunt.registerTask('validate', function() {
var done = this.async();
var script = path.join(__dirname, 'scripts', 'validate-reference.js');
var result = require('child_process').spawnSync(process.execPath, [script], {
stdio: 'inherit'
});

if (result.status !== 0) {
grunt.fail.fatal('Template validation failed. Fix the bundle before previewing.');
}
runValidateTask(grunt, this.async(), false);
});

done();
grunt.registerTask('validate:soft', function() {
runValidateTask(grunt, this.async(), true);
});

grunt.registerTask('default', [
'validate',
'validate:soft',
'connect',
'watch'
]);
Expand Down
20 changes: 17 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,31 @@ npm i
grunt
```

`grunt` validates the configured bundle first, then starts the dev server. Fix validation errors before previewing.
`grunt` validates the configured bundle first, then starts the dev server. Validation errors are shown in the page banner and in the terminal; the server still starts so you can preview while fixing issues.

For a strict check (CI or pre-commit), use `npm run validate`.

Open [http://localhost:8080/](http://localhost:8080/).

Re-run validation manually:

```bash
npm run validate:reference
npm run validate
```

Machine-readable output (same `{ ok, errors }` shape as upload validation):

```bash
npm run validate -- --json
```

Each finding includes `rule`, `severity` (`error` or `warning`), `message`, and optional `ref` (`file`, `line`, `column`). Example console line:

```
[error] htmlSafety index.html:2:1 — Forbidden tag <script> in index.html
```

While the dev server runs, saving files under `template/` or `lib/` triggers validation again via Grunt watch.
While the dev server runs, saving files under `template/` or `lib/` triggers validation again via Grunt watch. The checkout shell also loads `/validation.json` and shows findings above the template iframe.

## Template contract

Expand Down
44 changes: 44 additions & 0 deletions lib/build-validation-input.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
'use strict';

const fs = require('fs');
const path = require('path');
const { loadBundleForValidation } = require('./load-bundle');

function readFileEntry(dir, name) {
const filePath = path.join(dir, name);
if (!fs.existsSync(filePath) || fs.statSync(filePath).isDirectory()) {
throw new Error('File not found: ' + name + ' in ' + dir);
}

const buffer = fs.readFileSync(filePath);
return {
name: path.basename(name),
size: buffer.byteLength,
buffer: new Uint8Array(buffer),
};
}

/**
* Maps preview.config.json + bundle folder into ValidationInput for
* @vtex/payment-templates-validator — same shape the upload handler consumes.
*/
function buildValidationInput(config) {
const template = loadBundleForValidation(config.bundlePath, config.defaultLocale);
const input = { template };

if (config.icon) {
const iconName = String(config.icon).replace(/^\.\//, '');
input.icon = readFileEntry(config.bundlePath, iconName);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

path.join(config.bundlePath, iconName) não verifica se o resultado ficou dentro do bundle. Um icon com ../ lê arquivo fora da pasta, e o path.basename(name) da linha 15 esconde o escape na saída. O .replace(/^\.\//, '') remove apenas um ./ inicial e não trata .. nem caminho absoluto.

lib/preview-middleware.js:71-77 faz essa checagem (path.resolve(...).startsWith(normalizedRoot)) para a mesma operação — replicar aqui.

}

if (config.displayName && typeof config.displayName === 'object') {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Um displayName que não seja objeto é descartado em silêncio, então displayNameSafety e displayNameConsistency não rodam e a validação local reporta ok onde o upload falharia. "displayName": "Example Pay" (string) é um erro fácil dada a chave no singular; um array passa por typeof === 'object' e gera erro confuso sobre o locale "0".

Validar o tipo e reportar, ou repassar o valor e deixar o validador emitir a mensagem precisa.

input.displayName = config.displayName;
}

return input;
}

module.exports = {
buildValidationInput,
readFileEntry,
};
87 changes: 87 additions & 0 deletions lib/format-validation-output.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
'use strict';

function formatRef(ref) {
if (!ref || !ref.file) {
return '';
}

if (ref.line == null) {
return ref.file;
}

if (ref.column == null) {
return ref.file + ':' + ref.line;
}

return ref.file + ':' + ref.line + ':' + ref.column;
}

function formatFinding(finding) {
var location = formatRef(finding.ref);
var prefix = '[' + finding.severity + '] ' + finding.rule;
if (location) {
prefix += ' ' + location;
}
return prefix + ' — ' + finding.message;
}

function countBySeverity(findings, severity) {
var count = 0;
for (var i = 0; i < findings.length; i += 1) {
if (findings[i].severity === severity) {
count += 1;
}
}
return count;
}

function printValidationResult(result, options) {
options = options || {};

if (options.json) {
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
return;
}

var errorCount = countBySeverity(result.errors, 'error');
var warningCount = countBySeverity(result.errors, 'warning');
var suffix = options.suffix || '';

if (result.ok) {
var okMessage = 'validate: ok';
if (warningCount) {
okMessage += ' (' + warningCount + ' warning' + (warningCount === 1 ? '' : 's') + ')';
}
console.log(okMessage + suffix);

for (var w = 0; w < result.errors.length; w += 1) {
if (result.errors[w].severity === 'warning') {
console.log(' ' + formatFinding(result.errors[w]));
}
}
return;
}

var summary = 'validate: failed (' + errorCount + ' error' + (errorCount === 1 ? '' : 's');
if (warningCount) {
summary += ', ' + warningCount + ' warning' + (warningCount === 1 ? '' : 's');
}
summary += ')' + suffix;
console.error(summary);

for (var i = 0; i < result.errors.length; i += 1) {
var finding = result.errors[i];
var line = ' ' + formatFinding(finding);
if (finding.severity === 'error') {
console.error(line);
} else {
console.log(line);
}
}
}

module.exports = {
formatFinding,
formatRef,
printValidationResult,
};
20 changes: 20 additions & 0 deletions lib/validate-bundle.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
'use strict';

const { validate } = require('@vtex/payment-templates-validator');
const { readPreviewConfig } = require('./preview-config');
const { buildValidationInput } = require('./build-validation-input');

/**
* Runs the shared validator against the configured preview bundle.
* Returns ValidateResult { ok, errors } — identical to server-side output.
*/
async function validateBundle(config) {
const resolvedConfig = config || readPreviewConfig();
const input = buildValidationInput(resolvedConfig);
return validate(input);
}

module.exports = {
buildValidationInput,
validateBundle,
};
42 changes: 42 additions & 0 deletions lib/validation-middleware.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
'use strict';

const { readPreviewConfig } = require('./preview-config');
const { validateBundle } = require('./validate-bundle');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

validateBundle fica no cache de módulos do processo do servidor, que vive por horas. Ao editar lib/build-validation-input.js ou lib/load-bundle.js — cobertos pelo watch lib/**/*.js — o terminal mostra o resultado novo (o watch roda em processo filho), mas /validation.json e o banner seguem com o código antigo até reiniciar o grunt. Terminal e banner discordam sem indicação do motivo.

lib/preview-middleware.js:10-23 invalida o cache exatamente por isso.


const VALIDATION_PATH = '/validation.json';

function createValidationMiddleware() {
return function validationMiddleware(req, res, next) {
if (!req.url || req.url.split('?')[0] !== VALIDATION_PATH) {
next();
return;
}

let config;
try {
config = readPreviewConfig();
} catch (error) {
res.statusCode = 500;
res.setHeader('Content-Type', 'application/json; charset=utf-8');
res.end(JSON.stringify({ ok: false, errors: [], message: error.message }));
return;
}

validateBundle(config)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Relê o bundle do disco e roda o validador completo em cada requisição, sem cache. O trabalho pesado do validador (parse do HTML e do CSS, varredura de bytes dos assets) é síncrono, então este processo — que também serve o iframe e o livereload — fica bloqueado a cada chamada, e o cliente chama a cada 1,5s por aba aberta (src/assets/libs/template-host.js:121).

Cachear o resultado invalidando por mtime dos arquivos do bundle.

.then(function (result) {
res.setHeader('Content-Type', 'application/json; charset=utf-8');
res.setHeader('Cache-Control', 'no-cache');
res.end(JSON.stringify(result));
})
.catch(function (error) {
res.statusCode = 500;
res.setHeader('Content-Type', 'application/json; charset=utf-8');
res.end(JSON.stringify({ ok: false, errors: [], message: error.message }));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

error.message vai cru para a rede e carrega o caminho absoluto: readFileEntry monta 'File not found: ' + name + ' in ' + dir, onde dir é .../payment-mocker/template/reference sob o home do usuário. Com hostname: '*' no Gruntfile.js:59, qualquer máquina na mesma rede faz GET /validation.json e recebe nome de usuário e diretório.

Devolver mensagem genérica na resposta e deixar o detalhe apenas no terminal. Mesmo caso na linha 21.

});
};
}

module.exports = {
VALIDATION_PATH,
createValidationMiddleware,
};
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"url": "https://github.com/vtex/payment-mocker"
},
"scripts": {
"validate": "node scripts/validate.js",
"validate:reference": "node scripts/validate-reference.js",
"generate:assets": "node scripts/write-png.js"
},
Expand Down
28 changes: 1 addition & 27 deletions scripts/validate-reference.js
Original file line number Diff line number Diff line change
@@ -1,29 +1,3 @@
'use strict';

const { validate } = require('@vtex/payment-templates-validator');
const { loadBundleForValidation } = require('../lib/load-bundle');
const { readPreviewConfig } = require('../lib/preview-config');

async function main() {
const config = readPreviewConfig();
const template = loadBundleForValidation(config.bundlePath, config.defaultLocale);

const result = await validate({ template });

if (result.ok) {
console.log('validate: ok — template at template/' + config.bundleDir + ' passed all applicable rules.');
process.exit(0);
}

console.error('validate: failed');
for (const finding of result.errors) {
const ref = finding.ref ? ' (' + finding.ref.file + ')' : '';
console.error(' [' + finding.rule + '] ' + finding.message + ref);
}
process.exit(1);
}

main().catch(function (error) {
console.error(error);
process.exit(1);
});
require('./validate.js');
23 changes: 23 additions & 0 deletions scripts/validate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
'use strict';

const { validateBundle } = require('../lib/validate-bundle');
const { printValidationResult } = require('../lib/format-validation-output');
const { readPreviewConfig } = require('../lib/preview-config');

async function main() {
const json = process.argv.indexOf('--json') !== -1;
const config = readPreviewConfig();
const result = await validateBundle(config);

printValidationResult(result, {
json: json,
suffix: ' — template at template/' + config.bundleDir,
});

process.exit(result.ok ? 0 : 1);
}

main().catch(function (error) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No modo --json este caminho não imprime nada no stdout — só a pilha no stderr. Um passo de CI com npm run validate -- --json > result.json fica com arquivo vazio para parsear. A flag json é lida na linha 8 e não é consultada aqui.

Imprimir { ok: false, errors: [], message } no stdout, como o middleware já faz.

console.error(error);
process.exit(1);
});
Loading