Skip to content
Draft
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
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@
"javascriptreact",
"typescript",
"typescriptreact",
"html"
"html",
"json"
],
"eslint.format.enable": true,
"editor.insertSpaces": false,
Expand Down
5 changes: 3 additions & 2 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { defineConfig, globalIgnores } from "eslint/config";
import { angularEslintConfig, baseEslintConfig, jestEslintConfig, typescriptEslintConfig } from './tools/eslint/config/index.mjs';
import { angularEslintConfig, baseEslintConfig, jestEslintConfig, packageJsonEslintConfig, typescriptEslintConfig } from './tools/eslint/config/index.mjs';

export default defineConfig([
globalIgnores([
Expand All @@ -8,5 +8,6 @@ export default defineConfig([
baseEslintConfig,
typescriptEslintConfig,
angularEslintConfig,
jestEslintConfig
jestEslintConfig,
packageJsonEslintConfig,
]);
288 changes: 167 additions & 121 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@
"esbuild": "^0.24.0",
"eslint": "^9.28.0",
"eslint-plugin-daff-docs": "file:tools/eslint/daff-docs",
"eslint-plugin-daff-packages": "file:tools/eslint/daff-packages",
"eslint-plugin-import": "^2.30.0",
"eslint-plugin-jasmine": "^4.2.1",
"eslint-plugin-jest": "^28.8.3",
Expand All @@ -110,6 +111,7 @@
"jasmine-core": "^4.6.0",
"jasmine-marbles": "^0.9.2",
"jasmine-spec-reporter": "~5.0.0",
"jsonc-eslint-parser": "^3.1.0",
"karma": "^6.4.2",
"karma-chrome-launcher": "~3.1.0",
"karma-coverage": "^2.2.1",
Expand Down
3 changes: 2 additions & 1 deletion tools/eslint/config/index.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export { baseEslintConfig } from './src/base.config.mjs';
export { angularEslintConfig } from './src/angular.config.mjs';
export { jestEslintConfig } from './src/jest.config.mjs';
export { typescriptEslintConfig } from './src/typescript.config.mjs';
export { typescriptEslintConfig } from './src/typescript.config.mjs';
export { packageJsonEslintConfig } from './src/package-json.config.mjs';
16 changes: 16 additions & 0 deletions tools/eslint/config/src/package-json.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { defineConfig } from 'eslint/config';
import * as jsoncParser from 'jsonc-eslint-parser';
import daffPackagesPlugin from 'eslint-plugin-daff-packages';

export const packageJsonEslintConfig = defineConfig([
{
files: ['libs/*/package.json'],
languageOptions: {
parser: jsoncParser,
},
plugins: {
'daff-packages': daffPackagesPlugin,
},
rules: { 'daff-packages/magento-driver-versions': 'error' },
},
]);
5 changes: 5 additions & 0 deletions tools/eslint/daff-packages/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
'use strict';

module.exports.rules = {
'magento-driver-versions': require('./magento-driver-versions'),
};
216 changes: 216 additions & 0 deletions tools/eslint/daff-packages/magento-driver-versions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
/**
* magento-driver-versions.js
*
* Asserts that a @daffodil/* package exposing a Magento driver via
* `exports["./driver/magento/auto"]` declares a `magento-X.Y.Z` condition
* for every versioned driver directory found under `driver/magento/`.
*/
'use strict';

const fs = require('fs');
const path = require('path');

const AUTO_EXPORT_KEY = './driver/magento/auto';
const MAGENTO_KEY_RE = /^magento-\d+\.\d+\.\d+$/;
const VERSION_FROM_KEY_RE = /^magento-(\d+\.\d+\.\d+)$/;
const VERSION_DIR_RE = /^\d+\.\d+\.\d+$/;
Comment on lines +14 to +16
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This currently expects the folder of the versioned Magento driver to be named as driver/magento/<v>.<v>.<v>. However, in the order package, it is named as driver/magento/<v>-<v>-<v>, which deviates from expected pattern


function readPackageVersions(pkgDir) {
Copy link
Copy Markdown
Contributor Author

@joannalauu joannalauu Apr 22, 2026

Choose a reason for hiding this comment

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

Different packages have a different set of Magento versions. For example, order has versions 2-4-0 and 2-4-1 while external-router has versions 2.4.1, 2.4.2, 2.4.3. There is unresolved complexity on whether we should define different versions for different packages, or standardize versions across all packages so that we can set the version once for all packages

const magentoDir = path.join(pkgDir, 'driver', 'magento');
const versions = new Set();
let entries;
try {
entries = fs.readdirSync(magentoDir, { withFileTypes: true });
} catch {
return versions;
}
for (const entry of entries) {
if (entry.isDirectory() && VERSION_DIR_RE.test(entry.name)) {
versions.add(`magento-${entry.name}`);
}
}
return versions;
}

function getPropKeyName(prop) {
const k = prop.key;
if (k && k.type === 'JSONLiteral' && typeof k.value === 'string') {
return k.value;
}
return null;
}

function findProp(objNode, keyName) {
if (!objNode || objNode.type !== 'JSONObjectExpression') {
return null;
}
for (const prop of objNode.properties) {
if (getPropKeyName(prop) === keyName) {
return prop;
}
}
return null;
}

function getStringValue(node) {
if (node && node.type === 'JSONLiteral' && typeof node.value === 'string') {
return node.value;
}
return null;
}

module.exports = {
meta: {
type: 'problem',
docs: {
description:
'Enforce that a @daffodil package exposing a versioned Magento driver declares a magento-X.Y.Z condition under exports["./driver/magento/auto"] for every version directory present under driver/magento/',
},
schema: [],
messages: {
invalidKey:
'Key `{{key}}` is not a valid Magento version condition. Expected `magento-<major>.<minor>.<patch>`.',
missingVersion:
'Missing Magento version condition `{{key}}`. A `driver/magento/{{version}}/` directory exists in this package but has no matching export condition.',
entryNotObject:
'Condition `{{key}}` must be an object with `types` and `default` fields.',
missingField:
'Condition `{{key}}` is missing required field `{{field}}`.',
typesPathMismatch:
'Condition `{{key}}` has a `types` path that does not reference `./driver/magento/{{version}}/`.',
defaultPathMismatch:
'Condition `{{key}}` has a `default` path that does not end with `-magento-{{version}}.mjs`.',
},
},

create(context) {
const filename = context.physicalFilename || context.filename;
if (!filename || path.basename(filename) !== 'package.json') {
return {};
}
const pkgDir = path.dirname(filename);

return {
Program(node) {
const root = node.body?.[0]?.expression;
if (!root || root.type !== 'JSONObjectExpression') {
return;
}

const exportsProp = findProp(root, 'exports');
if (!exportsProp) {
return;
}
const autoProp = findProp(exportsProp.value, AUTO_EXPORT_KEY);
if (!autoProp) {
return;
}
const autoObj = autoProp.value;
if (autoObj.type !== 'JSONObjectExpression') {
return;
}

const expectedVersions = readPackageVersions(pkgDir);
const seenVersions = new Set();

function checkEntryShape(prop, keyName, version) {
if (prop.value.type !== 'JSONObjectExpression') {
context.report({
node: prop.value,
messageId: 'entryNotObject',
data: { key: keyName },
});
return;
}

const typesProp = findProp(prop.value, 'types');
const defaultProp = findProp(prop.value, 'default');

if (!typesProp) {
context.report({
node: prop.value,
messageId: 'missingField',
data: { key: keyName, field: 'types' },
});
} else {
const typesVal = getStringValue(typesProp.value);
if (typesVal === null) {
context.report({
node: typesProp.value,
messageId: 'missingField',
data: { key: keyName, field: 'types' },
});
} else if (version && !typesVal.includes(`./driver/magento/${version}/`)) {
context.report({
node: typesProp.value,
messageId: 'typesPathMismatch',
data: { key: keyName, version },
});
}
}

if (!defaultProp) {
context.report({
node: prop.value,
messageId: 'missingField',
data: { key: keyName, field: 'default' },
});
} else {
const defaultVal = getStringValue(defaultProp.value);
if (defaultVal === null) {
context.report({
node: defaultProp.value,
messageId: 'missingField',
data: { key: keyName, field: 'default' },
});
} else if (version && !defaultVal.endsWith(`-magento-${version}.mjs`)) {
context.report({
node: defaultProp.value,
messageId: 'defaultPathMismatch',
data: { key: keyName, version },
});
}
}
}

for (const prop of autoObj.properties) {
const keyName = getPropKeyName(prop);
if (keyName === null) {
continue;
}

if (keyName === 'default') {
checkEntryShape(prop, keyName, null);
continue;
}

if (!MAGENTO_KEY_RE.test(keyName)) {
context.report({
node: prop.key,
messageId: 'invalidKey',
data: { key: keyName },
});
continue;
}

seenVersions.add(keyName);
const version = keyName.match(VERSION_FROM_KEY_RE)[1];
checkEntryShape(prop, keyName, version);
}

for (const expected of expectedVersions) {
if (!seenVersions.has(expected)) {
context.report({
node: autoProp.key,
messageId: 'missingVersion',
data: {
key: expected,
version: expected.match(VERSION_FROM_KEY_RE)[1],
},
});
}
}
},
};
},
};
10 changes: 10 additions & 0 deletions tools/eslint/daff-packages/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "eslint-plugin-daff-packages",
"version": "0.0.0",
"main": "index.js",
"private": true,
"peerDependencies": {
"eslint": "*",
"jsonc-eslint-parser": "*"
}
}
Loading