From 24b5db527a6d296cacd763e8c81dadd522037ab3 Mon Sep 17 00:00:00 2001 From: Brent Hosie Date: Thu, 5 Mar 2026 16:07:04 -0700 Subject: [PATCH 1/5] pdcl-14485: Refactor validator into portable modules with subpath export Split validation into schema, file checks, and Node-only file gathering. The default export is unchanged for Node consumers. Add a ./validatesubpath that takes (descriptor, fileList) so browser consumers can run full validation without Node APIs. - @adobe/reactor-validator/schema (schema validation only) - @adobe/reactor-validator/files (file validation with caller-provided file list). --- lib/gatherFilesInNodeEnvironment.js | 36 ++++++++ lib/index.js | 136 ++-------------------------- lib/validate.js | 28 ++++++ lib/validateFiles.js | 80 ++++++++++++++++ lib/validateSchema.js | 43 +++++++++ package-lock.json | 82 +++++++++-------- package.json | 6 +- tests/validate/validate.test.js | 68 ++++++++++++++ 8 files changed, 312 insertions(+), 167 deletions(-) create mode 100644 lib/gatherFilesInNodeEnvironment.js create mode 100644 lib/validate.js create mode 100644 lib/validateFiles.js create mode 100644 lib/validateSchema.js create mode 100644 tests/validate/validate.test.js diff --git a/lib/gatherFilesInNodeEnvironment.js b/lib/gatherFilesInNodeEnvironment.js new file mode 100644 index 0000000..c218681 --- /dev/null +++ b/lib/gatherFilesInNodeEnvironment.js @@ -0,0 +1,36 @@ +/*************************************************************************************** + * (c) 2017 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + ****************************************************************************************/ + +// Recursively scans a directory and returns an array of relative file path strings. +// Node-only -- uses fs and path. Not suitable for browser environments. + +'use strict'; +var fs = require('fs'); +var pathUtil = require('path'); + +var gatherFilesInNodeEnvironment = function(dir, root, result) { + root = root || dir; + result = result || []; + var entries = fs.readdirSync(dir); + entries.forEach(function(entry) { + var fullPath = pathUtil.join(dir, entry); + var relPath = pathUtil.relative(root, fullPath); + if (fs.statSync(fullPath).isDirectory()) { + gatherFilesInNodeEnvironment(fullPath, root, result); + } else { + result.push(relPath); + } + }); + return result; +}; + +module.exports = gatherFilesInNodeEnvironment; diff --git a/lib/index.js b/lib/index.js index 4910385..f9c0ea7 100644 --- a/lib/index.js +++ b/lib/index.js @@ -10,134 +10,18 @@ * governing permissions and limitations under the License. ****************************************************************************************/ -'use strict'; - -var fs = require('fs'); -var pathUtil = require('path'); -var Ajv = require("ajv-draft-04") -var addAJVFormats = require("ajv-formats") - -var isFile = function(path) { - try { - return fs.statSync(path).isFile(); - } catch (e) { - return false; - } -}; - -var isDir = function(path) { - try { - return fs.statSync(path).isDirectory(); - } catch (e) { - return false; - } -}; - -var stripQueryAndAnchor = function(path) { - path = path.split('?').shift(); - path = path.split('#').shift(); - return path; -}; - -var validateJsonStructure = function(extensionDescriptor) { - var platform = extensionDescriptor.platform; - - if (!platform) { - return 'the required property "platform" is missing.'; - } - - var extensionDescriptorSchema = - require('@adobe/reactor-turbine-schemas/schemas/extension-package-' + platform + '.json'); - - var ajv = new Ajv({ - schemaId: 'auto', - strict: false - }); - addAJVFormats(ajv); - - if (!ajv.validate(extensionDescriptorSchema, extensionDescriptor)) { - return ajv.errorsText(); - } -}; - -var validateViewBasePath = function(extensionDescriptor) { - var absViewBasePath = pathUtil.resolve( - process.cwd(), - extensionDescriptor.viewBasePath - ); - - if (!isDir(absViewBasePath)) { - return absViewBasePath + ' is not a directory.'; - } -}; - -var validateFiles = function(extensionDescriptor) { - var paths = []; - var platform = extensionDescriptor.platform; - - if (!platform) { - return 'the required property "platform" is missing.'; - } - - if (extensionDescriptor.configuration) { - paths.push(pathUtil.resolve( - process.cwd(), - extensionDescriptor.viewBasePath, - stripQueryAndAnchor(extensionDescriptor.configuration.viewPath) - )); - } - - if (extensionDescriptor.main) { - paths.push(pathUtil.resolve( - process.cwd(), - extensionDescriptor.main - )); - } - - ['events', 'conditions', 'actions', 'dataElements'].forEach(function(featureType) { - var features = extensionDescriptor[featureType]; - - if (features) { - features.forEach(function(feature) { - if (feature.viewPath) { - paths.push(pathUtil.resolve( - process.cwd(), - extensionDescriptor.viewBasePath, - stripQueryAndAnchor(feature.viewPath) - )); - } - - if (platform === 'web') { - paths.push(pathUtil.resolve( - process.cwd(), - feature.libPath - )); - } - }); - } - }); - - for (var i = 0; i < paths.length; i++) { - var path = paths[i]; - if (!isFile(path)) { - return path + ' is not a file.'; - } - } -}; +// Main entry point for Node consumers. Scans the current working directory for files, +// then delegates to the portable validate function for schema and file validation. +// Returns undefined if valid, or an error string if not. +'use strict'; +var validate = require('./validate'); +var gatherFilesInNodeEnvironment = require('./gatherFilesInNodeEnvironment'); module.exports = function(extensionDescriptor) { - var validators = [ - validateJsonStructure, - validateViewBasePath, - validateFiles - ]; - - for (var i = 0; i < validators.length; i++) { - var error = validators[i](extensionDescriptor); - - if (error) { - return 'An error was found in your extension.json: ' + error; - } + var fileList = gatherFilesInNodeEnvironment(process.cwd()); + var error = validate(extensionDescriptor, fileList); + if (error) { + return 'An error was found in your extension.json: ' + error; } }; diff --git a/lib/validate.js b/lib/validate.js new file mode 100644 index 0000000..39c09a8 --- /dev/null +++ b/lib/validate.js @@ -0,0 +1,28 @@ +/*************************************************************************************** + * (c) 2017 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + ****************************************************************************************/ + +// Validates an extension descriptor against its JSON schema and a provided file list. +// Portable -- no Node-specific APIs. Safe to use in browser environments. +// Accepts (extensionDescriptor, fileList) where fileList is an array of relative path strings. +// Returns undefined if valid, or an error string on the first problem encountered. + +'use strict'; +var validateSchema = require('./validateSchema'); +var validateFiles = require('./validateFiles'); + +module.exports = function(extensionDescriptor, fileList) { + var error = validateSchema(extensionDescriptor); + if (error) return error; + + error = validateFiles(extensionDescriptor, fileList); + if (error) return error; +}; diff --git a/lib/validateFiles.js b/lib/validateFiles.js new file mode 100644 index 0000000..5a1947f --- /dev/null +++ b/lib/validateFiles.js @@ -0,0 +1,80 @@ +/*************************************************************************************** + * (c) 2017 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + ****************************************************************************************/ + +// Validates that files referenced by an extension descriptor exist in a provided file list. +// Portable -- no Node-specific APIs. Safe to use in browser environments. +// Accepts (extensionDescriptor, fileList) where fileList is an array of relative path strings. +// Returns undefined if valid, or an error string if not. + +'use strict'; + +var stripQueryAndAnchor = function(path) { + return path.split('?').shift().split('#').shift(); +}; + +var joinPath = function() { + return [].slice.call(arguments).filter(Boolean).join('/').replace(/\/+/g, '/'); +}; + +var validateViewBasePath = function(extensionDescriptor, fileSet) { + var viewBasePath = extensionDescriptor.viewBasePath; + if (viewBasePath && fileSet.has(viewBasePath.replace(/\/+$/, ''))) { + return viewBasePath + ' is not a directory.'; + } +}; + +var validateFileList = function(extensionDescriptor, fileSet) { + var paths = []; + var platform = extensionDescriptor.platform; + var viewBase = extensionDescriptor.viewBasePath; + + if (!platform) { + return 'the required property "platform" is missing.'; + } + + if (extensionDescriptor.main) { + paths.push(extensionDescriptor.main); + } + + if (extensionDescriptor.configuration) { + paths.push(joinPath(viewBase, stripQueryAndAnchor(extensionDescriptor.configuration.viewPath))); + } + + ['events', 'conditions', 'actions', 'dataElements'].forEach(function(type) { + var features = extensionDescriptor[type]; + if (features) { + features.forEach(function(feature) { + if (feature.viewPath) { + paths.push(joinPath(viewBase, stripQueryAndAnchor(feature.viewPath))); + } + if (platform === 'web') { + paths.push(feature.libPath); + } + }); + } + }); + + for (var i = 0; i < paths.length; i++) { + if (!fileSet.has(paths[i])) { + return paths[i] + ' is not a file.'; + } + } +}; + +module.exports = function(extensionDescriptor, fileList) { + var fileSet = new Set(fileList); + var validators = [validateViewBasePath, validateFileList]; + for (var i = 0; i < validators.length; i++) { + var error = validators[i](extensionDescriptor, fileSet); + if (error) return error; + } +}; diff --git a/lib/validateSchema.js b/lib/validateSchema.js new file mode 100644 index 0000000..01ee3db --- /dev/null +++ b/lib/validateSchema.js @@ -0,0 +1,43 @@ +/*************************************************************************************** + * (c) 2017 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + ****************************************************************************************/ + +// Validates an extension descriptor object against the JSON schema for its platform. +// Portable -- no Node-specific APIs. Safe to use in browser environments. +// Returns undefined if valid, or an error string if not. + +'use strict'; +var Ajv = require("ajv-draft-04"); +var addAJVFormats = require("ajv-formats"); + +var schemas = { + web: require('@adobe/reactor-turbine-schemas/schemas/extension-package-web.json'), + edge: require('@adobe/reactor-turbine-schemas/schemas/extension-package-edge.json'), + mobile: require('@adobe/reactor-turbine-schemas/schemas/extension-package-mobile.json') +}; + +var validateJsonStructure = function(extensionDescriptor) { + var platform = extensionDescriptor.platform; + if (!platform) { + return 'the required property "platform" is missing.'; + } + var extensionDescriptorSchema = schemas[platform]; + if (!extensionDescriptorSchema) { + return 'unknown platform "' + platform + '".'; + } + var ajv = new Ajv({ schemaId: 'auto', strict: false }); + addAJVFormats(ajv); + if (!ajv.validate(extensionDescriptorSchema, extensionDescriptor)) { + return ajv.errorsText(); + } +}; + +module.exports = validateJsonStructure; diff --git a/package-lock.json b/package-lock.json index 811df17..74c8678 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@adobe/reactor-validator", - "version": "2.5.0", + "version": "3.0.0-beta.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@adobe/reactor-validator", - "version": "2.5.0", + "version": "3.0.0-beta.1", "license": "Apache-2.0", "dependencies": { "@adobe/reactor-turbine-schemas": "^10.8.0", @@ -1462,14 +1462,15 @@ ] }, "node_modules/ajv": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "license": "MIT", "dependencies": { - "fast-deep-equal": "^3.1.1", + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" + "require-from-string": "^2.0.2" }, "funding": { "type": "github", @@ -2190,6 +2191,22 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/fb-watchman": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", @@ -2310,9 +2327,10 @@ } }, "node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, "license": "ISC", "dependencies": { @@ -2365,9 +2383,9 @@ } }, "node_modules/immutable": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.3.tgz", - "integrity": "sha512-+chQdDfvscSF1SJqv2gn4SRO2ZyS3xL3r7IW/wWEEzrzLisnOlKiQu5ytC/BVNcS15C39WT2Hg/bjKjDMcu+zg==", + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.5.tgz", + "integrity": "sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==", "dev": true, "license": "MIT" }, @@ -3191,9 +3209,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "dev": true, "license": "MIT", "dependencies": { @@ -3353,13 +3371,13 @@ } }, "node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -3677,14 +3695,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/punycode": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", - "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", - "engines": { - "node": ">=6" - } - }, "node_modules/pure-rand": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz", @@ -4099,9 +4109,9 @@ } }, "node_modules/test-exclude/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -4235,14 +4245,6 @@ "browserslist": ">= 4.21.0" } }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dependencies": { - "punycode": "^2.1.0" - } - }, "node_modules/v8-to-istanbul": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", diff --git a/package.json b/package.json index c528f6f..5c10cb4 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,12 @@ { "name": "@adobe/reactor-validator", - "version": "2.5.0", + "version": "3.0.0-beta.1", "description": "Validates a Tags extension package.", "main": "lib/index.js", + "exports": { + ".": "./lib/index.js", + "./validate": "./lib/validate.js" + }, "bin": { "reactor-validator": "bin/index.js" }, diff --git a/tests/validate/validate.test.js b/tests/validate/validate.test.js new file mode 100644 index 0000000..69f9288 --- /dev/null +++ b/tests/validate/validate.test.js @@ -0,0 +1,68 @@ +/*************************************************************************************** + * (c) 2017 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + ****************************************************************************************/ + +// Tests for the portable validate subpath export (validate.js). + +const validate = require('../../lib/validate'); +const { fromJS } = require('immutable'); +const webManifest = require('../web/extension.json'); +const edgeManifest = require('../edge/extension.json'); + +describe('portable validate', () => { + describe('schema validation', () => { + it('returns undefined for a valid web descriptor', () => { + var fileList = ['src/view/.gitkeep']; + expect(validate(fromJS(webManifest).toJS(), fileList)).toBeUndefined(); + }); + + it('returns undefined for a valid edge descriptor', () => { + var fileList = ['src/view/.gitkeep']; + expect(validate(fromJS(edgeManifest).toJS(), fileList)).toBeUndefined(); + }); + + it('returns an error for an invalid descriptor', () => { + var bad = fromJS(webManifest).toJS(); + delete bad.name; + var fileList = ['src/view/.gitkeep']; + expect(typeof validate(bad, fileList)).toBe('string'); + }); + + it('returns an error for unknown platform', () => { + var bad = fromJS(webManifest).toJS(); + bad.platform = 'unknown'; + var fileList = ['src/view/.gitkeep']; + expect(validate(bad, fileList)).toBe('unknown platform "unknown".'); + }); + }); + + describe('file validation', () => { + it('returns undefined when viewBasePath is a directory (has files under it)', () => { + var manifest = fromJS(webManifest).toJS(); + var fileList = ['src/view/.gitkeep']; + expect(validate(manifest, fileList)).toBeUndefined(); + }); + + it('returns an error when viewBasePath is a file', () => { + var manifest = fromJS(webManifest).toJS(); + manifest.viewBasePath = 'src/view'; + var fileList = ['src/view']; + expect(validate(manifest, fileList)).toContain('is not a directory'); + }); + + it('returns an error when a required file is missing', () => { + var manifest = fromJS(webManifest).toJS(); + manifest.main = 'src/lib/main.js'; + var fileList = ['src/view/.gitkeep']; + expect(validate(manifest, fileList)).toContain('is not a file'); + }); + }); +}); From a2fdb646a9e1729ab85d0ed94dde80f02a828558 Mon Sep 17 00:00:00 2001 From: Brent Hosie Date: Thu, 5 Mar 2026 16:22:00 -0700 Subject: [PATCH 2/5] 3.0.0-beta.2 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 74c8678..adda998 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@adobe/reactor-validator", - "version": "3.0.0-beta.1", + "version": "3.0.0-beta.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@adobe/reactor-validator", - "version": "3.0.0-beta.1", + "version": "3.0.0-beta.2", "license": "Apache-2.0", "dependencies": { "@adobe/reactor-turbine-schemas": "^10.8.0", diff --git a/package.json b/package.json index 5c10cb4..d166245 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/reactor-validator", - "version": "3.0.0-beta.1", + "version": "3.0.0-beta.2", "description": "Validates a Tags extension package.", "main": "lib/index.js", "exports": { From 43fe0c79ca21521bb1d4e925b481d9e3cd3d746a Mon Sep 17 00:00:00 2001 From: Brent Hosie Date: Thu, 5 Mar 2026 18:18:45 -0700 Subject: [PATCH 3/5] pdcl-14485: due to an isDirectory() check not supported between all consumers, be a little more restrictive with the viewBasePath. --- README.md | 45 ++++++++++++++++++++++++++++++++- lib/validateFiles.js | 23 ++++++++++++----- package-lock.json | 7 +++-- package.json | 2 +- tests/validate/validate.test.js | 6 +++++ 5 files changed, 72 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 3ade02b..e86f3c0 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Adobe Experience Platform Tags is a next-generation tag management solution enab The extension validator helps extension developers validate that an extension package is well-structured. Namely, it verifies that: 1. The extension has a [manifest](https://experienceleague.adobe.com/docs/experience-platform/tags/extension-dev/manifest.html?lang=en) (`extension.json`) matching the expected structure. -2. All referenced directories and files exist at the specified locations within the extension directory. +2. All referenced directories and files exist at the specified locations within the extension directory. The manifest's `viewBasePath` must point to an existing directory (not a file, and not a missing path); at least one file must exist under that path. For more information about developing an extension for Tags, please visit our [extension development guide](https://experienceleague.adobe.com/docs/experience-platform/tags/extension-dev/overview.html?lang=en). @@ -45,6 +45,49 @@ const error = validate(require('./extension.json')); console.log(error); ``` +### Using the validator in a browser or bundled app + +Browser (or other non-Node) consumers should use the `@adobe/reactor-validator/validate` subpath so they do not pull in Node-only code. The default export (`@adobe/reactor-validator`) scans the filesystem and is for Node only. + +The `validate` function has the signature `(extensionDescriptor, fileList)`: + +- **extensionDescriptor:** The parsed extension manifest (e.g. from `extension.json`). +- **fileList:** An array of relative path strings for every file in the extension package (e.g. from a folder picker or zip). The caller is responsible for gathering this list; the validator only checks that required paths are present. +- **Returns:** `undefined` if valid, or an error string on the first problem found. + +```javascript +import validate from '@adobe/reactor-validator/validate'; + +const error = validate(manifestJson, Array.from(filesMap.keys())); +if (error) { + console.error(error); +} +``` + +**Webpack alias for `ajv`:** The `validate` module depends on `ajv-draft-04`, which in turn requires `ajv` and resolves internal paths (e.g. `ajv/dist/core`). If your app does not list `ajv` as a direct dependency, add a resolve alias so the bundler uses the validator's `ajv`: + +- Resolve via an **exported** path (e.g. `@adobe/reactor-validator/validate`), not `package.json`, to avoid `ERR_PACKAGE_PATH_NOT_EXPORTED`. +- Alias to the **package directory** of `ajv`, not to `ajv.js`, so subpaths resolve and Watchpack ENOTDIR errors are avoided. + +In your webpack config: + +```javascript +const path = require('path'); + +// Inside module.exports or your config object: +resolve: { + alias: { + 'ajv': path.join( + path.dirname(path.dirname(require.resolve('@adobe/reactor-validator/validate'))), + 'node_modules', + 'ajv' + ) + } +} +``` + +Alternatively, add `ajv@^8.12.0` as a direct dependency in your project so the alias is unnecessary. + ## Contributing Contributions are welcomed! Read the [Contributing Guide](CONTRIBUTING.md) for more information. diff --git a/lib/validateFiles.js b/lib/validateFiles.js index 5a1947f..f67703b 100644 --- a/lib/validateFiles.js +++ b/lib/validateFiles.js @@ -25,9 +25,19 @@ var joinPath = function() { return [].slice.call(arguments).filter(Boolean).join('/').replace(/\/+/g, '/'); }; -var validateViewBasePath = function(extensionDescriptor, fileSet) { +var validateViewBasePath = function(extensionDescriptor, fileSet, fileList) { var viewBasePath = extensionDescriptor.viewBasePath; - if (viewBasePath && fileSet.has(viewBasePath.replace(/\/+$/, ''))) { + if (!viewBasePath) return; + + var viewBaseNorm = viewBasePath.replace(/\/+$/, ''); + if (fileSet.has(viewBaseNorm)) { + return viewBasePath + ' is not a directory.'; + } + + var hasFileUnderPath = fileList.some(function(p) { + return p === viewBaseNorm || p.indexOf(viewBaseNorm + '/') === 0; + }); + if (!hasFileUnderPath) { return viewBasePath + ' is not a directory.'; } }; @@ -72,9 +82,8 @@ var validateFileList = function(extensionDescriptor, fileSet) { module.exports = function(extensionDescriptor, fileList) { var fileSet = new Set(fileList); - var validators = [validateViewBasePath, validateFileList]; - for (var i = 0; i < validators.length; i++) { - var error = validators[i](extensionDescriptor, fileSet); - if (error) return error; - } + var error = validateViewBasePath(extensionDescriptor, fileSet, fileList); + if (error) return error; + error = validateFileList(extensionDescriptor, fileSet); + if (error) return error; }; diff --git a/package-lock.json b/package-lock.json index adda998..98d5741 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@adobe/reactor-validator", - "version": "3.0.0-beta.2", + "version": "3.0.0-beta.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@adobe/reactor-validator", - "version": "3.0.0-beta.2", + "version": "3.0.0-beta.3", "license": "Apache-2.0", "dependencies": { "@adobe/reactor-turbine-schemas": "^10.8.0", @@ -79,6 +79,7 @@ "integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -1466,6 +1467,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -1723,6 +1725,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001735", "electron-to-chromium": "^1.5.204", diff --git a/package.json b/package.json index d166245..a89fbf1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/reactor-validator", - "version": "3.0.0-beta.2", + "version": "3.0.0-beta.3", "description": "Validates a Tags extension package.", "main": "lib/index.js", "exports": { diff --git a/tests/validate/validate.test.js b/tests/validate/validate.test.js index 69f9288..03c43b9 100644 --- a/tests/validate/validate.test.js +++ b/tests/validate/validate.test.js @@ -64,5 +64,11 @@ describe('portable validate', () => { var fileList = ['src/view/.gitkeep']; expect(validate(manifest, fileList)).toContain('is not a file'); }); + + it('returns an error when viewBasePath is set but no files exist under it', () => { + var manifest = fromJS(webManifest).toJS(); + var fileList = ['extension.json']; + expect(validate(manifest, fileList)).toContain('is not a directory'); + }); }); }); From a216969965ed98f936e8803bcf65dd81fd135deb Mon Sep 17 00:00:00 2001 From: Brent Hosie Date: Thu, 5 Mar 2026 18:25:55 -0700 Subject: [PATCH 4/5] pdcl-14485: make the directory check error more clear. --- lib/validateFiles.js | 4 ++-- package-lock.json | 4 ++-- package.json | 2 +- tests/validate/validate.test.js | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/validateFiles.js b/lib/validateFiles.js index f67703b..df4f365 100644 --- a/lib/validateFiles.js +++ b/lib/validateFiles.js @@ -31,14 +31,14 @@ var validateViewBasePath = function(extensionDescriptor, fileSet, fileList) { var viewBaseNorm = viewBasePath.replace(/\/+$/, ''); if (fileSet.has(viewBaseNorm)) { - return viewBasePath + ' is not a directory.'; + return 'The referenced viewBasePath ' + viewBasePath + ' is either not a directory or is empty.'; } var hasFileUnderPath = fileList.some(function(p) { return p === viewBaseNorm || p.indexOf(viewBaseNorm + '/') === 0; }); if (!hasFileUnderPath) { - return viewBasePath + ' is not a directory.'; + return 'The referenced viewBasePath ' + viewBasePath + ' is either not a directory or is empty.'; } }; diff --git a/package-lock.json b/package-lock.json index 98d5741..502df20 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@adobe/reactor-validator", - "version": "3.0.0-beta.3", + "version": "3.0.0-beta.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@adobe/reactor-validator", - "version": "3.0.0-beta.3", + "version": "3.0.0-beta.4", "license": "Apache-2.0", "dependencies": { "@adobe/reactor-turbine-schemas": "^10.8.0", diff --git a/package.json b/package.json index a89fbf1..b271de9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/reactor-validator", - "version": "3.0.0-beta.3", + "version": "3.0.0-beta.4", "description": "Validates a Tags extension package.", "main": "lib/index.js", "exports": { diff --git a/tests/validate/validate.test.js b/tests/validate/validate.test.js index 03c43b9..aa26242 100644 --- a/tests/validate/validate.test.js +++ b/tests/validate/validate.test.js @@ -55,7 +55,7 @@ describe('portable validate', () => { var manifest = fromJS(webManifest).toJS(); manifest.viewBasePath = 'src/view'; var fileList = ['src/view']; - expect(validate(manifest, fileList)).toContain('is not a directory'); + expect(validate(manifest, fileList)).toContain('The referenced viewBasePath'); }); it('returns an error when a required file is missing', () => { @@ -68,7 +68,7 @@ describe('portable validate', () => { it('returns an error when viewBasePath is set but no files exist under it', () => { var manifest = fromJS(webManifest).toJS(); var fileList = ['extension.json']; - expect(validate(manifest, fileList)).toContain('is not a directory'); + expect(validate(manifest, fileList)).toContain('The referenced viewBasePath'); }); }); }); From f86848bc02ed18002b0636ee02c7c4d3015bd05f Mon Sep 17 00:00:00 2001 From: Brent Hosie Date: Fri, 6 Mar 2026 10:34:19 -0700 Subject: [PATCH 5/5] pdcl-14485: clarifying comments about certain functions. enhance a test case. 3.0.0-beta.5 --- README.md | 2 +- lib/gatherFilesInNodeEnvironment.js | 10 +++++++++- lib/validate.js | 2 +- lib/validateFiles.js | 2 +- package-lock.json | 4 ++-- package.json | 2 +- tests/validate/validate.test.js | 8 ++++---- 7 files changed, 19 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index e86f3c0..e50d9ea 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,7 @@ resolve: { } ``` -Alternatively, add `ajv@^8.12.0` as a direct dependency in your project so the alias is unnecessary. +Alternatively, add `ajv` as a direct dependency in your project with the current version used here so the alias is unnecessary. However, this is very brittle. This project reserves the right to use a different version of `ajv` at any time. ## Contributing diff --git a/lib/gatherFilesInNodeEnvironment.js b/lib/gatherFilesInNodeEnvironment.js index c218681..d9cb15c 100644 --- a/lib/gatherFilesInNodeEnvironment.js +++ b/lib/gatherFilesInNodeEnvironment.js @@ -1,5 +1,5 @@ /*************************************************************************************** - * (c) 2017 Adobe. All rights reserved. + * (c) 2026 Adobe. All rights reserved. * This file is licensed to you under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. You may obtain a copy * of the License at http://www.apache.org/licenses/LICENSE-2.0 @@ -12,6 +12,14 @@ // Recursively scans a directory and returns an array of relative file path strings. // Node-only -- uses fs and path. Not suitable for browser environments. +// +// Example: for an extension layout like +// extension.json +// src/view/configuration.html +// src/view/events/click.html +// src/lib/main.js +// the returned array is e.g. +// ['extension.json', 'src/view/configuration.html', 'src/view/events/click.html', 'src/lib/main.js'] 'use strict'; var fs = require('fs'); diff --git a/lib/validate.js b/lib/validate.js index 39c09a8..ee02cd4 100644 --- a/lib/validate.js +++ b/lib/validate.js @@ -1,5 +1,5 @@ /*************************************************************************************** - * (c) 2017 Adobe. All rights reserved. + * (c) 2026 Adobe. All rights reserved. * This file is licensed to you under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. You may obtain a copy * of the License at http://www.apache.org/licenses/LICENSE-2.0 diff --git a/lib/validateFiles.js b/lib/validateFiles.js index df4f365..8a9d945 100644 --- a/lib/validateFiles.js +++ b/lib/validateFiles.js @@ -1,5 +1,5 @@ /*************************************************************************************** - * (c) 2017 Adobe. All rights reserved. + * (c) 2026 Adobe. All rights reserved. * This file is licensed to you under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. You may obtain a copy * of the License at http://www.apache.org/licenses/LICENSE-2.0 diff --git a/package-lock.json b/package-lock.json index 502df20..d4ea8ac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@adobe/reactor-validator", - "version": "3.0.0-beta.4", + "version": "3.0.0-beta.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@adobe/reactor-validator", - "version": "3.0.0-beta.4", + "version": "3.0.0-beta.5", "license": "Apache-2.0", "dependencies": { "@adobe/reactor-turbine-schemas": "^10.8.0", diff --git a/package.json b/package.json index b271de9..8a83363 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/reactor-validator", - "version": "3.0.0-beta.4", + "version": "3.0.0-beta.5", "description": "Validates a Tags extension package.", "main": "lib/index.js", "exports": { diff --git a/tests/validate/validate.test.js b/tests/validate/validate.test.js index aa26242..290426e 100644 --- a/tests/validate/validate.test.js +++ b/tests/validate/validate.test.js @@ -53,9 +53,9 @@ describe('portable validate', () => { it('returns an error when viewBasePath is a file', () => { var manifest = fromJS(webManifest).toJS(); - manifest.viewBasePath = 'src/view'; - var fileList = ['src/view']; - expect(validate(manifest, fileList)).toContain('The referenced viewBasePath'); + manifest.viewBasePath = 'src/view/config.html'; + var fileList = ['src/view/config.html']; + expect(validate(manifest, fileList)).toBe('The referenced viewBasePath src/view/config.html is either not a directory or is empty.'); }); it('returns an error when a required file is missing', () => { @@ -68,7 +68,7 @@ describe('portable validate', () => { it('returns an error when viewBasePath is set but no files exist under it', () => { var manifest = fromJS(webManifest).toJS(); var fileList = ['extension.json']; - expect(validate(manifest, fileList)).toContain('The referenced viewBasePath'); + expect(validate(manifest, fileList)).toBe('The referenced viewBasePath src/view/ is either not a directory or is empty.'); }); }); });