messages-modules
Advanced tools
Comparing version 1.0.0 to 1.1.0
@@ -11,4 +11,4 @@ "use strict"; | ||
{ | ||
function: 'getMessages', | ||
module: 'messages-modules', | ||
function: 'getMessages', | ||
}, | ||
@@ -15,0 +15,0 @@ ]; |
@@ -5,2 +5,6 @@ import * as BabelTypes from '@babel/types'; | ||
export declare type Statement = BabelTypes.Statement; | ||
export declare type ExportNamedDeclaration = BabelTypes.ExportNamedDeclaration; | ||
export declare type ExportSpecifier = BabelTypes.ExportSpecifier; | ||
export declare type ExportNamespaceSpecifier = BabelTypes.ExportNamespaceSpecifier; | ||
export declare type ExportDefaultSpecifier = BabelTypes.ExportDefaultSpecifier; | ||
export declare type ImportDeclaration = BabelTypes.ImportDeclaration; | ||
@@ -12,7 +16,16 @@ export declare type ImportSpecifier = BabelTypes.ImportSpecifier; | ||
/** | ||
* Target to hijack. | ||
* A target to hijack. | ||
* | ||
* A target to hijack (inject messages) must be identified by both a `function` and the `module` it's being | ||
* imported from. For example: | ||
* | ||
* `import { getMessages } from 'messages-modules'` | ||
* | ||
* The function is `getMessages` and the module is `messages-modules` | ||
*/ | ||
export declare type HijackTarget = { | ||
/** The name of a function to hijack (e.g., `getMessages`). */ | ||
function: string; | ||
/** The function's module used to import it. */ | ||
module: string; | ||
function: string; | ||
}; | ||
@@ -34,6 +47,6 @@ /** | ||
readonly isInjected = true; | ||
/** A collection of "key/value" objects for for all locales. */ | ||
keyValueObjectCollection: KeyValueObjectCollection; | ||
/** The path of the source file that is invoking `useMessages`. */ | ||
readonly sourceFilePath: string; | ||
/** A collection of "key/value" objects for for all locales. */ | ||
keyValueObjectCollection: KeyValueObjectCollection; | ||
/** | ||
@@ -40,0 +53,0 @@ * The injected localized messages. |
148
lib/index.js
"use strict"; | ||
var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { | ||
if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { | ||
if (ar || !(i in from)) { | ||
if (!ar) ar = Array.prototype.slice.call(from, 0, i); | ||
ar[i] = from[i]; | ||
} | ||
} | ||
return to.concat(ar || Array.prototype.slice.call(from)); | ||
}; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.getMessages = exports.messageModulePlugin = exports.InjectedMessages = void 0; | ||
var fs_1 = require("fs"); | ||
var path_1 = require("path"); | ||
var node_fs_1 = require("node:fs"); | ||
var node_path_1 = require("node:path"); | ||
var template_1 = require("@babel/template"); | ||
var BabelTypes = require("@babel/types"); | ||
var isExportSpecifier = BabelTypes.isExportSpecifier; | ||
var isImportSpecifier = BabelTypes.isImportSpecifier; | ||
var isIdentifier = BabelTypes.isIdentifier; | ||
/** | ||
@@ -17,3 +28,3 @@ * Escapes a regular expression string. | ||
function escapeRegExp(regexp) { | ||
return regexp.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string | ||
return regexp.replace(/[$()*+.?[\\\]^{|}]/g, '\\$&'); // $& means the whole matched string | ||
} | ||
@@ -46,3 +57,3 @@ var InjectedMessages = /** @class */ (function () { | ||
function getInjectedMessages(sourceFilePath, messagesFileExtension, getMessages) { | ||
var parsedSourceFile = (0, path_1.parse)(sourceFilePath); | ||
var parsedSourceFile = (0, node_path_1.parse)(sourceFilePath); | ||
var sourceFileDirectoryPath = parsedSourceFile.dir; | ||
@@ -52,3 +63,3 @@ var sourceFilename = parsedSourceFile.name; | ||
var fileRegExp = new RegExp("^".concat(escapeRegExp(sourceFilename), ".(?<locale>[\\w-]+).").concat(messagesFileExtension, "$")); | ||
(0, fs_1.readdirSync)(sourceFileDirectoryPath, { withFileTypes: true }).forEach(function (directoryEntry) { | ||
(0, node_fs_1.readdirSync)(sourceFileDirectoryPath, { withFileTypes: true }).forEach(function (directoryEntry) { | ||
if (directoryEntry.isFile()) { | ||
@@ -59,3 +70,3 @@ var directoryEntryFilename = directoryEntry.name; | ||
var locale = regExpMatch.groups.locale; | ||
var messagesFilePath = sourceFileDirectoryPath.length | ||
var messagesFilePath = sourceFileDirectoryPath.length > 0 | ||
? "".concat(sourceFileDirectoryPath, "/").concat(directoryEntryFilename) | ||
@@ -71,3 +82,3 @@ : directoryEntryFilename; | ||
/** | ||
* Verify if an import declaration node matches the target module. | ||
* Verify if an import or export statement matches the target module. | ||
* | ||
@@ -77,24 +88,57 @@ * @param nodePath - A node path object. | ||
* | ||
* @returns True is the node matches, otherwise false. | ||
* @returns True is the module matches, otherwise false. | ||
*/ | ||
function isMatchingModule(nodePath, hijackTarget) { | ||
if (!nodePath.isImportDeclaration()) | ||
return false; | ||
if (nodePath.node.source.value !== hijackTarget.module) | ||
return false; | ||
return true; | ||
return !!nodePath.node.source && nodePath.node.source.value === hijackTarget.module; | ||
} | ||
/** | ||
* Verify if a specifier matches the target function. | ||
* Verify if an import or export statement matches the target function. | ||
* | ||
* @param specifier - A specifier object. | ||
* @param nodePath - A node path object. | ||
* @param hijackTarget - The target to hijack. | ||
* | ||
* @returns True is the specifier matches, otherwise false. | ||
* @returns True is the function matches, otherwise false. | ||
*/ | ||
function isMatchingModuleImportName(specifier, hijackTarget) { | ||
return (isImportSpecifier(specifier) && | ||
specifier.imported.name === hijackTarget.function); | ||
function isMatchingFunction(nodePath, hijackTarget) { | ||
return nodePath.node.specifiers.some(function (specifier) { | ||
return ((isImportSpecifier(specifier) && isMatchingImportFunction(specifier, hijackTarget)) || | ||
(isExportSpecifier(specifier) && isMatchingExportFunction(specifier, hijackTarget))); | ||
}); | ||
} | ||
/** | ||
* Verify if an import specifier matches the target function. | ||
* | ||
* @param specifier - An import specifier object. | ||
* @param hijackTarget - The target to hijack. | ||
* | ||
* @returns True is the module matches, otherwise false. | ||
*/ | ||
function isMatchingImportFunction(specifier, hijackTarget) { | ||
return isIdentifier(specifier.imported) && specifier.imported.name === hijackTarget.function; | ||
} | ||
/** | ||
* Verify if an export specifier matches the target function. | ||
* | ||
* @param specifier - An export specifier object. | ||
* @param hijackTarget - The target to hijack. | ||
* | ||
* @returns True is the module matches, otherwise false. | ||
*/ | ||
function isMatchingExportFunction(specifier, hijackTarget) { | ||
return isIdentifier(specifier.local) && specifier.local.name === hijackTarget.function; | ||
} | ||
/** | ||
* Verify if a named export declaration node matches the target module and function. | ||
* | ||
* @param nodePath - A node path object. | ||
* @param hijackTarget - The target to hijack. | ||
* | ||
* @returns True is the node matches, otherwise false. | ||
*/ | ||
function isMatchingNamedExport(nodePath, hijackTarget) { | ||
return (nodePath.isExportNamedDeclaration() && | ||
isMatchingFunction(nodePath, hijackTarget) && | ||
isMatchingModule(nodePath, hijackTarget)); | ||
} | ||
/** | ||
* Verify if an import declaration node matches the target module and function. | ||
@@ -108,6 +152,5 @@ * | ||
function isMatchingNamedImport(nodePath, hijackTarget) { | ||
return (isMatchingModule(nodePath, hijackTarget) && | ||
nodePath.node.specifiers.some(function (specifier) { | ||
return isMatchingModuleImportName(specifier, hijackTarget); | ||
})); | ||
return (nodePath.isImportDeclaration() && | ||
isMatchingFunction(nodePath, hijackTarget) && | ||
isMatchingModule(nodePath, hijackTarget)); | ||
} | ||
@@ -128,3 +171,3 @@ var Messages = /** @class */ (function () { | ||
this.programNodePath = programNodePath; | ||
var leadingPathSeparatorRegExp = new RegExp("^".concat(escapeRegExp(path_1.sep))); | ||
var leadingPathSeparatorRegExp = new RegExp("^".concat(escapeRegExp(node_path_1.sep))); | ||
// this.sourceFilePath = (pluginPass as PluginPass).file.opts.filename | ||
@@ -138,5 +181,5 @@ var pluginPassFilename = (_a = pluginPass.file.opts) === null || _a === void 0 ? void 0 : _a.filename; | ||
.replace(leadingPathSeparatorRegExp, ''); // Remove leading path separator (e.g., '/') if present. | ||
if (path_1.sep !== '/') { | ||
if (node_path_1.sep !== '/') { | ||
// Normalize path separators to `/`. | ||
var separatorRegExp = new RegExp("".concat(escapeRegExp(path_1.sep)), 'g'); | ||
var separatorRegExp = new RegExp("".concat(escapeRegExp(node_path_1.sep)), 'g'); | ||
this.sourceFilePath = this.sourceFilePath.replace(separatorRegExp, '/'); | ||
@@ -191,10 +234,11 @@ } | ||
node.specifiers.forEach(function (specifier) { | ||
if (isMatchingModuleImportName(specifier, hijackTarget)) { | ||
if (isImportSpecifier(specifier) && isMatchingImportFunction(specifier, hijackTarget)) { | ||
// The current function name used in the local scope. | ||
var currentName_1 = specifier.local.name; | ||
// This is the scope-unique variable name that will replace all matching function bindings. | ||
var hijackedFunction_1 = getVariableName(nodePath, hijackTarget, 'Function'); | ||
var currentName_1 = specifier.local.name; | ||
// Rename all bindings with the the new name (this excludes the import declaration). | ||
var binding = nodePath.scope.getBinding(currentName_1); | ||
if (!binding) { | ||
return; // If there is no binding, no need to hijack. | ||
return; // If the function is unused (no binding), no need to hijack. | ||
} | ||
@@ -204,3 +248,3 @@ binding.referencePaths.forEach(function (referencePath) { | ||
}); | ||
// Insert the new "hijacked" namespace variable, with the correct binding. | ||
// Insert the new "hijacked" variable, with the correct binding. | ||
nodePath.insertAfter(template_1.default.ast("const ".concat(hijackedFunction_1, " = ").concat(currentName_1, ".bind(").concat(messages.getVariableName(), ");"))); | ||
@@ -211,2 +255,37 @@ } | ||
/** | ||
* "Hijack" a named export (e.g., `export { useMessages } from`). | ||
* | ||
* For every named export, we will create an import statement to which we will create a new hijacked function | ||
* that will then be re-exported using the original name. If all named exports of a statement are hijacked, the | ||
* export statement will be removed. | ||
* | ||
* @param nodePath - The node path being hijacked. | ||
* @param hijackTarget - The target to hijack. | ||
* @param messages - The object used to conditionally inject messages. | ||
*/ | ||
function hijackNamedExport(nodePath, hijackTarget, messages) { | ||
var node = nodePath.node; | ||
__spreadArray([], node.specifiers, true).reverse().forEach(function (specifier, index, specifiersCopy) { | ||
if (isExportSpecifier(specifier) && | ||
isMatchingExportFunction(specifier, hijackTarget) && | ||
isIdentifier(specifier.exported)) { | ||
// Remove the matching specifier from the export as we will hijack it. | ||
node.specifiers.splice(specifiersCopy.length - 1 - index, 1); | ||
// The current function name used when exporting. | ||
var currentName = specifier.exported.name; | ||
// This is the scope-unique variable name that will be used to perform the hijack. | ||
var hijackedImport = getVariableName(nodePath, hijackTarget, 'Function'); | ||
var hijackedExport = getVariableName(nodePath, hijackTarget, 'Function'); | ||
// Insert new import/exports statement using the new "hijacked" variable, with the correct binding. | ||
nodePath.insertAfter(template_1.default.ast("import { ".concat(hijackTarget.function, " as ").concat(hijackedImport, " } from '").concat(hijackTarget.module, "';") + | ||
"const ".concat(hijackedExport, " = ").concat(hijackedImport, ".bind(").concat(messages.getVariableName(), ");") + | ||
"export { ".concat(hijackedExport, " as ").concat(currentName, " };"))); | ||
} | ||
}); | ||
// If the entire export statement was hijacked (it is now empty), we can remove it. | ||
if (node.specifiers.length === 0) { | ||
nodePath.remove(); | ||
} | ||
} | ||
/** | ||
* Dynamically returns a plugin based on the specified parameters. | ||
@@ -224,5 +303,10 @@ * | ||
hijackTargets.forEach(function (hijackTarget) { | ||
// Try to hijack matching named import statements. | ||
if (isMatchingNamedImport(bodyNodePath, hijackTarget)) { | ||
hijackNamedImport(bodyNodePath, hijackTarget, messages); | ||
} | ||
// Try to hijack matching named export statements. | ||
if (isMatchingNamedExport(bodyNodePath, hijackTarget)) { | ||
hijackNamedExport(bodyNodePath, hijackTarget, messages); | ||
} | ||
}); | ||
@@ -244,2 +328,3 @@ }); | ||
function getMessages(locale) { | ||
var _a; | ||
// @ts-expect-error: `this` is injected using `bind` and will trigger a false compilation error. | ||
@@ -250,5 +335,4 @@ var injectedMessages = this; | ||
} | ||
var messages = injectedMessages.keyValueObjectCollection[locale.toLowerCase()]; | ||
return !messages ? {} : messages; | ||
return (_a = injectedMessages.keyValueObjectCollection[locale.toLowerCase()]) !== null && _a !== void 0 ? _a : {}; | ||
} | ||
exports.getMessages = getMessages; |
{ | ||
"name": "messages-modules", | ||
"version": "1.0.0", | ||
"version": "1.1.0", | ||
"description": "Messages (localized strings) that are scoped locally.", | ||
@@ -24,4 +24,5 @@ "author": "Avansai (https://avansai.com)", | ||
"scripts": { | ||
"build": "rm -Rf ./lib && tsc && npm run lint && npm test", | ||
"lint": "eslint --ext .js --ext .jsx --ext .ts --ext .tsx --fix .", | ||
"build": "npm run prettier && npm run lint-fix && rm -Rf ./lib && tsc && npm test", | ||
"lint-fix": "eslint --ext .js --ext .jsx --ext .ts --ext .tsx --fix .", | ||
"lint-check": "eslint --ext .js --ext .jsx --ext .ts --ext .tsx .", | ||
"lint-print-config": "eslint --print-config ./eslintrc.yaml", | ||
@@ -46,10 +47,10 @@ "prettier": "prettier --write .", | ||
"devDependencies": { | ||
"@babel/cli": "^7.18.9", | ||
"@babel/core": "^7.18.9", | ||
"@babel/cli": "^7.18.10", | ||
"@babel/core": "^7.18.10", | ||
"@release-it/conventional-changelog": "^5.0.0", | ||
"@types/babel__core": "^7.1.19", | ||
"@types/jest": "^28.1.6", | ||
"@types/node": "^18.6.3", | ||
"@typescript-eslint/eslint-plugin": "^5.32.0", | ||
"@typescript-eslint/parser": "^5.32.0", | ||
"@types/node": "^18.6.5", | ||
"@typescript-eslint/eslint-plugin": "^5.33.0", | ||
"@typescript-eslint/parser": "^5.33.0", | ||
"dotenv-cli": "^6.0.0", | ||
@@ -61,8 +62,10 @@ "eslint": "^8.21.0", | ||
"eslint-plugin-import": "^2.26.0", | ||
"eslint-plugin-jest": "^26.7.0", | ||
"eslint-plugin-jest": "^26.8.2", | ||
"eslint-plugin-prettier": "^4.2.1", | ||
"eslint-plugin-unicorn": "^43.0.2", | ||
"jest": "^28.1.3", | ||
"prettier": "^2.7.1", | ||
"prettier-plugin-organize-imports": "^3.0.0", | ||
"release-it": "^15.2.0", | ||
"prettier-plugin-organize-imports": "^3.0.3", | ||
"prettier-plugin-sh": "^0.12.8", | ||
"release-it": "^15.3.0", | ||
"ts-jest": "^28.0.7", | ||
@@ -73,4 +76,7 @@ "ts-node": "^10.9.1", | ||
"dependencies": { | ||
"properties-file": "^2.0.9" | ||
"properties-file": "^2.1.0" | ||
}, | ||
"engines": { | ||
"node": "^14.18.1 || ^16.0.0" | ||
} | ||
} |
@@ -66,25 +66,21 @@ # messages-modules | ||
To keep this simple, the `message-modules` plugins only support **named imports** which means that **namespace imports**, **dynamic imports** and **require imports** are out of scope: | ||
To keep this simple, the `message-modules` plugins only support **named imports** and **named exports**. This means that **namespace imports**, **dynamic imports** and **require imports** are out of scope: | ||
👍 **named imports** | ||
👍 **Supported** | ||
```ts | ||
// Named import | ||
import { getMessages } from 'messages-modules' | ||
// Named export (used for shared messages) | ||
export { getMessages } from 'messages-modules' | ||
``` | ||
👎 **namespace imports** | ||
👎 **Unsupported** | ||
```ts | ||
// Namespace import | ||
import * as messagesModules from 'messages-modules' | ||
``` | ||
👎 **dynamic imports** | ||
```ts | ||
// Dynamic imports | ||
const { getMessages } = await import('messages-modules') | ||
``` | ||
👎 **require imports** | ||
```ts | ||
// Require imports | ||
const messagesModules = require('messages-modules') | ||
@@ -91,0 +87,0 @@ ``` |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
27665
451
0
25
95
Updatedproperties-file@^2.1.0