diff --git a/README.md b/README.md index 8453701..493cbb7 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![Static Badge](https://img.shields.io/badge/license-Free_(Restricted)-blue)](https://github.com/nasriyasoftware/PostBuild?tab=License-1-ov-file) ![Repository Size](https://img.shields.io/github/repo-size/nasriyasoftware/PostBuild.svg) ![Last Commit](https://img.shields.io/github/last-commit/nasriyasoftware/PostBuild.svg) [![Status](https://img.shields.io/badge/Status-Stable-green.svg)](link-to-your-status-page) ##### Visit us at [www.nasriya.net](https://nasriya.net). -PostBuild is a utility pacakge for **TypeScript** run useful tasks after transpiling TypeScript into **ESM** and **CJS** JavaScript file. +PostBuild is a utility pacakge for **TypeScript** that runs useful tasks after transpiling TypeScript into **ESM** and **CJS** JavaScript file. Made with ❤️ in **Palestine** 🇵🇸 ___ @@ -25,15 +25,16 @@ npm run postbuild-init ##### Config File Content The above comand will generate a file with all the features set to their recommended values. This table below explains them in details. -| Property | Description | Posible values | Default value | -| ------------------- | ------------------------------------------------------------------------ | ----------------------- | ------------- | -| `esmDir` | The directory of the generated `ESM` folder. | `auto` or the directory | `auto` | -| `cjsDir` | The directory of the generated `CJS` folder. | `auto` or the directory | `auto` | -| `verbose` | An option to enable logging extra details . | `true` or `false` | `true` | -| `addExtensions` | Appending `.js` to all import statements. | `true` or `false` | `true` | -| `copyFiles` | An options object to copy assets to the `dist` folder after transpiling. | `object` or `undefined` | Notice below | -| `copyFiles.from` | The directory where you want to copy the assets to. | directory | `src` | -| `copyFiles.exclude` | An array of file extensions to exclude. | `string[]` | `['.ts']` | +| Property | Description | Posible values | Default value | +| ------------------- | ------------------------------------------------------------------------ | ------------------------ | ------------- | +| `esmDir` | The directory of the generated `ESM` folder. | `auto` or the directory | `auto` | +| `cjsDir` | The directory of the generated `CJS` folder. | `auto` or the directory | `auto` | +| `verbose` | An option to enable logging extra details . | `true` or `false` | `true` | +| `addExtensions` | Appending `.js` to all import statements. | `true` or `false` | `true` | +| `copyFiles` | An options object to copy assets to the `dist` folder after transpiling. | `object` or `undefined` | Notice below | +| `copyFiles.from` | The directory where you want to copy the assets to. | directory | `src` | +| `copyFiles.exclude` | An array of file extensions to exclude. | `string[]` | `['.ts']` | +| `aliases` | Define aliases to your imports | `Record` | Nothing | The default configurations works well if your project is structured like this: ``` @@ -65,7 +66,19 @@ The best way to use this package is to integrate it with your build process by a } ``` -**Note:** +#### Defining aliases +In `postbuild.config.json`, you can add your aliases as such: + +```json +{ + "aliases": { + "my-module": "/modules/my-module", + "@elements/*": "/elements/" + } +} +``` + +#### Using `__dirname` All `__dirname` matches in `ESM` will be replaced with `import.meta.dirname`, for example: ```ts diff --git a/main.js b/main.js index 5cd1455..a72e906 100644 --- a/main.js +++ b/main.js @@ -87,9 +87,23 @@ class Main { } else { this.#_config.addExtensions = false; } + + if ('aliases' in this.#_configFile) { + const aliases = this.#_configFile.aliases; + if (!utils.is.realObject(aliases)) { throw new Error(`The "aliases" option is expecting a real object, isntead got ${typeof aliases}`) } + + this.#_config.aliases = {} + for (const prop in aliases) { + if (typeof aliases[prop] === 'string') { + this.#_config.aliases[prop] = aliases[prop]; + } else { + throw new TypeError(`One of the defined aliases (${aliases[prop]}) is not a string`) + } + } + } } catch (error) { if (error instanceof Error) { - error.message = `Unable to read postbuild.this.#_configFile.json: ${error.message}`; + error.message = `Unable to read postbuild configFile: ${error.message}`; } throw error; @@ -263,6 +277,98 @@ class Main { fs.writeFileSync(fullPath, content, 'utf8'); } }); + }, + aliases: { + regex: { + /** + * Create a regular expression for catching imports + * @param {string} pattern + * @returns {RegExp} + */ + createCatch: (pattern) => { + const escapedPattern = pattern.replace(/[-\/\\^$+?.()|[\]{}]/g, '\\$&').replace(/\*/g, '.*'); + return new RegExp(`^${escapedPattern}$`) + }, + /** + * Create a regular expression for catching imports + * @param {string} pattern + * @returns {RegExp} + */ + createExact: (pattern) => { + const escapedPattern = pattern.replace(/[-\/\\^$+?.()|[\]{}]/g, '\\$&'); + return new RegExp(`^${escapedPattern}`) + } + }, + getPatterns: () => { + const aliases = this.#_config.aliases; + return Object.keys(aliases).map(alias => { + const catchPattern = this.#_helpers.aliases.regex.createCatch(alias); + const exactPattern = this.#_helpers.aliases.regex.createExact(alias); + + return { + alias, + catchPattern, + exactPattern, + resolvedPath: aliases[alias] + }; + }); + }, + matchImportPath: (importPath, aliasPatterns) => { + for (const { catchPattern, exactPattern, resolvedPath } of aliasPatterns) { + // Check if the import path starts with the alias pattern + const match = importPath.match(catchPattern); + if (match) { + const newMatch = match[0].replace(exactPattern, resolvedPath) + const newImportPath = importPath.replace(match[0], newMatch); + + // Construct the new path by combining the resolved path with the remaining path + return importPath.replace(match, newImportPath) + } + } + + return importPath; // No match found + }, + replaceImports: (filePath, aliasPatterns) => { + let fileContent = fs.readFileSync(filePath, 'utf8'); + + // Replace import paths based on alias patterns + fileContent = fileContent.replace(/from ['"]([^'"]+)['"]/g, (match, importPath) => { + // Match the import path against alias patterns + const resolvedPath = this.#_helpers.aliases.matchImportPath(importPath, aliasPatterns); + + // Only replace if the alias was matched, else return the original import path + if (resolvedPath !== importPath) { + // Preserve the specific path segments after the alias + const remainingPath = importPath.replace(/^.*\/[^\/]+/, ''); // Strip alias part + return `from '${resolvedPath}${remainingPath}${remainingPath.endsWith('.js') ? '' : '.js'}'`; + } + + return match; // No change if alias was not matched + }); + + fs.writeFileSync(filePath, fileContent, 'utf8'); + }, + processFiles: (directory, aliasPatterns) => { + fs.readdirSync(directory).forEach(file => { + const fullPath = path.join(directory, file); + + if (fs.lstatSync(fullPath).isDirectory()) { + this.#_helpers.aliases.processFiles(fullPath, aliasPatterns); + } else if (file.endsWith('.js')) { + this.#_helpers.aliases.replaceImports(fullPath, aliasPatterns); + } + }); + }, + check: () => { + const aliasPatterns = this.#_helpers.aliases.getPatterns(); + if (this.#_config.esmDir) { + this.#_helpers.aliases.processFiles(this.#_config.esmDir, aliasPatterns); + } + + if (this.#_config.cjsDir) { + this.#_helpers.aliases.processFiles(this.#_config.cjsDir, aliasPatterns); + } + } } } @@ -274,10 +380,11 @@ class Main { this.#_helpers.create.packages(); this.#_helpers.copy.run(); this.#_helpers.extensions.run(); + this.#_helpers.aliases.check(); const et = Date.now(); const duration = et - st; - this.#_helpers.print(`PostBuild finishes in ${duration} milliseconds`); + this.#_helpers.print(`PostBuild finished in ${duration} milliseconds`); } } diff --git a/package.json b/package.json index 59cf7c7..8b65bb7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@nasriya/postbuild", - "version": "1.0.6", + "version": "1.1.0", "description": "A package that does some tasks after compilation", "main": "main.js", "type": "module",