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
45 changes: 44 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

Expand Down Expand Up @@ -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` 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

Contributions are welcomed! Read the [Contributing Guide](CONTRIBUTING.md) for more information.
Expand Down
44 changes: 44 additions & 0 deletions lib/gatherFilesInNodeEnvironment.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/***************************************************************************************
* (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
*
* 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.
//
// 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');
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;
Copy link
Member

Choose a reason for hiding this comment

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

web-manifest.test.js and edge-manifest.test.js do cover this code, but there are no direct tests verifying:

  • Recursive directory traversal
  • Handling of symlinks, empty directories, or special characters in filenames
  • Path separator output on different OS

Maybe consider if we need unit tests for these cases.

136 changes: 10 additions & 126 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
};
28 changes: 28 additions & 0 deletions lib/validate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/***************************************************************************************
* (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
*
* 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;
};
89 changes: 89 additions & 0 deletions lib/validateFiles.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/***************************************************************************************
* (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
*
* 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, '/');
Copy link
Member

Choose a reason for hiding this comment

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

Should probably fix this to work for Windows as well.

gatherFilesInNodeEnvironment uses pathUtil.relative() which returns backslash-separated paths on windows. But, validateFiles.js constructs lookup paths using forward slashes via joinPath().

On windows, the fileSet would contain src\view\config.html but the validator would look for src/view/config.html causing false validation failures.

Looks like the old code avoided this because it used pathUtil.resolve() everywhere.

In gatherFilesInNodeEnvironment we could normalize separators to /...
result.push(relPath.replace(/\\/g, '/'));

...or normalize the file list in validateFiles.js when building the Set.

};

var validateViewBasePath = function(extensionDescriptor, fileSet, fileList) {
var viewBasePath = extensionDescriptor.viewBasePath;
if (!viewBasePath) return;

var viewBaseNorm = viewBasePath.replace(/\/+$/, '');
if (fileSet.has(viewBaseNorm)) {
Copy link
Member

Choose a reason for hiding this comment

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

FYI - AI found a non-urgent edge case...

This checks if viewBaseNorm appears as an exact file in the set. If it does, it's assumed to be a file (not a directory). This is a reasonable heuristic for real-world extension packages. However, the redundant p === viewBaseNorm check at line 38 inside the some() can never succeed at that point (if the path were in the set as a file, we'd have already returned an error). Not harmful, but slightly confusing.

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 'The referenced viewBasePath ' + viewBasePath + ' is either not a directory or is empty.';
}
};

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 error = validateViewBasePath(extensionDescriptor, fileSet, fileList);
if (error) return error;
error = validateFileList(extensionDescriptor, fileSet);
if (error) return error;
};
Loading